Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Since 19.1: SSR Apps with urlMatcher in route config are breaking due to commit 6edb908 #29384

Open
1 task done
denisyilmaz opened this issue Jan 17, 2025 · 6 comments
Open
1 task done
Labels
needs: more info Reporter must clarify the issue

Comments

@denisyilmaz
Copy link

Command

serve

Is this a regression?

  • Yes, this behavior used to work in the previous version

The previous version in which this bug was not present was

19.0

Description

With 19.0 it was possible to use UrlMatcher functions within SSR applications. 19.1 introduced a commit 6edb908 which was meant to fix a incompatibality of UrlMatcher with SSR applications. I wasn't able to find any documentation that indicated UrlMatcher and SSR are not working together. Was this a issue that needed to be solved? The only issue i was able to find on the web was in combination with the @angular-architects/module-federation library (https://dev.to/michaeljota/how-to-add-module-federation-into-your-angular-micro-frontend-apps-jmh)

Before we start refactoring our project (which requires SSR and also UrlMatcher) or downgrading to 19.0 I would like to know what the implications were for this "fix". Happy for any insights.

Minimal Reproduction

  1. create project with ssr enabled with 19.1+
  2. add UrlMatcher function to route
  3. open project with ng serve

Exception or Error

Error: Error(s) occurred while extracting routes:
- The route 'en/example' uses a route matcher which is not supported.
- The route 'en/example' uses a route matcher which is not supported.
- The route 'en/example-2' uses a route matcher which is not supported.
- The route 'en/example-2' uses a route matcher which is not supported.
- The route 'example' uses a route matcher which is not supported.
- The route 'example' uses a route matcher which is not supported.
- The route 'example-2' uses a route matcher which is not supported.
- The route 'example-2' uses a route matcher which is not supported.
    at eval (/../angular/.angular/cache/19.1.1/main/vite/deps_ssr/chunk-FOZQYQAA.js:10655:15)
    at _ZoneDelegate.invoke (/../angular/.angular/vite-root/main/node_modules/zone.js/fesm2015/zone-node.js:369:28)
    at ZoneImpl.run (/../angular/.angular/vite-root/main/node_modules/zone.js/fesm2015/zone-node.js:111:43)
    at eval (/../angular/.angular/vite-root/main/node_modules/zone.js/fesm2015/zone-node.js:1221:40)
    at _ZoneDelegate.invokeTask (/../angular/.angular/vite-root/main/node_modules/zone.js/fesm2015/zone-node.js:402:33)
    at ZoneImpl.runTask (/../angular/.angular/vite-root/main/node_modules/zone.js/fesm2015/zone-node.js:159:47)
    at drainMicroTaskQueue (/../angular/.angular/vite-root/main/node_modules/zone.js/fesm2015/zone-node.js:581:35)
    at invokeTask (/../angular/.angular/vite-root/main/node_modules/zone.js/fesm2015/zone-node.js:487:21)
    at Timeout.ZoneTask.invoke (/../angular/.angular/vite-root/main/node_modules/zone.js/fesm2015/zone-node.js:472:48)
    at Timeout.data.args.<computed> (/../angular/.angular/vite-root/main/node_modules/zone.js/fesm2015/zone-node.js:2260:32)

Your Environment

_                      _                 ____ _     ___
    / \   _ __   __ _ _   _| | __ _ _ __     / ___| |   |_ _|
   / △ \ | '_ \ / _` | | | | |/ _` | '__|   | |   | |    | |
  / ___ \| | | | (_| | |_| | | (_| | |      | |___| |___ | |
 /_/   \_\_| |_|\__, |\__,_|_|\__,_|_|       \____|_____|___|
                |___/
    

Angular CLI: 19.1.1
Node: 20.18.1
Package Manager: npm 10.8.2
OS: darwin arm64

Angular: 19.1.1
... animations, cli, common, compiler, compiler-cli, core, forms
... platform-browser, platform-browser-dynamic, platform-server
... router, ssr

Package                         Version
---------------------------------------------------------
@angular-devkit/architect       0.1901.1
@angular-devkit/build-angular   19.1.1
@angular-devkit/core            19.1.1 (cli-only)
@angular-devkit/schematics      19.1.1
@angular/cdk                    19.1.0
@schematics/angular             19.1.1
ng-packagr                      19.0.1
rxjs                            7.8.1
typescript                      5.6.3
zone.js                         0.15.0

Anything else relevant?

No response

@alan-agius4
Copy link
Collaborator

alan-agius4 commented Jan 17, 2025

Before this change, the URL matchers did not function correctly at runtime. With this update, an error is shown during build time instead. For more details, refer to: #29284.

I’m interested in understanding the specific use cases where route matchers are used. Could you share yours, along with a code snippet demonstrating your implementation?

@denisyilmaz
Copy link
Author

denisyilmaz commented Jan 17, 2025

@alan-agius4 thanks for the input. You are right, on local development the matcher is not working. But on our staging server with a production build the routes with urlMatcher are rendered correctly on the server.

happy to share our use case:

We have a route that lists subpages of various levels in a chronical order. The pages can have related events to it which will have a nested permalink

for simplicity here are the path created by the backend:

/overview
/overview/page-1
/overview/page-1/page-2/page-3
/overview/page-1/events/event-1
/overview/page-1/page-2/events/event-2

as you can see the "events" urlSegments indicate that the last element is a event. As Angulars router config does not allow wildcards before url segements like **/events/:slug the only way to get this to work was using the UrlMatcher function. As mentioned above, on the production build it was working without any issues.

Current implementation (which was working in 19.0):

export default [
  {
    path: '',
    resolve: {
      nav: navigationResolver,
    },
    children: [
      {
        path: '',
        loadComponent: () =>
          import('./overview/overview.component'),
        resolve: {
          page: overviewResolver,
        },
      },
      {
        path: 'past',
        loadComponent: () => import('./archive/archive.component'),
        resolve: {
          page: archiveResolver,
        },
      },
      {
        matcher: relatedEvent,
        loadComponent: () =>
          import('../events/event-detail/event-detail.component'),
      },
      {
        matcher: defaultPage,
        loadComponent: () => import('./default-page/default-page.component'),
        resolve: {
            page: defaultPageResolver,
        },
      },
    ],
  },
] satisfies Route[];

relatedEvent matcher function

export function relatedEvent(url: UrlSegment[]): UrlMatchResult | null {
  const isEventDetail = url.length > 2 && url.at(-2)?.path === 'events';
  const slug = url.at(-1);
  return isEventDetail && slug ? { consumed: url, posParams: { slug } } : null;
}

defaultPage matcher function

export function defaultPage(url: UrlSegment[]): UrlMatchResult | null {
  if (url.length >= 1) {
    const isNotEvent = !(
      url.length >= 2 && url.at(-2)?.path === 'events'
    );
    const slug = url.at(-1);
    return isNotEvent && slug ? { consumed: url, posParams: { slug } } : null;
  }
  return null;
}

Ideal implementation if Angular route config would allow wildcards (not working in any version of Angular):

export default [
  {
    path: '',
    resolve: {
      nav: navigationResolver,
    },
    children: [
      {
        path: '',
        loadComponent: () =>
          import('./overview/overview.component'),
        resolve: {
          page: overviewResolver,
        },
      },
      {
        path: 'past',
        loadComponent: () => import('./archive/archive.component'),
        resolve: {
          page: archiveResolver,
        },
      },
      {
        path: '**/:slug',
        loadComponent: () => import('./default-page/default-page.component'),
        resolve: {
            page: defaultPageResolver,
        },
        children: [
            {
                path: 'events/:event-slug',
                loadComponent: () => import('../events/event-detail/event-detail.component'),
            }
        ]
      },
    ],
  },
] satisfies Route[];

Maybe there is another way to have n-wildcard-urlSegments before a routerParameter is defined, but i'm not aware of such a solution. The urlMatcher function was giving me exactly this feature. Happy for any help here.

@alan-agius4
Copy link
Collaborator

Based on the provided URL structure, it seems unnecessary to create a custom matcher function. Instead, you can use Angular's built-in matcher and define nested routes with multiple levels or parameters as required.

For example:

import { Routes } from '@angular/router';

export const routes: Routes = [
  {
    path: 'overview',
    children: [
      {
        path: '',
        loadComponent: () => import('./overview/overview.component'),
      },
      {
        path: 'past',
        loadComponent: () => import('./past/past.component'),
      },
      {
        path: ':event',
        children: [
          {
            path: '',
            loadComponent: () => import('./events/events.component'),
          },
          {
            path: 'past',
            loadComponent: () => import('./past/past.component'),
          },
          {
            path: ':event-detail',
            loadComponent: () =>
              import('./events-details/events-details.component'),
          },
        ],
      },
    ],
  },
];

@denisyilmaz
Copy link
Author

denisyilmaz commented Jan 17, 2025

@alan-agius4 true, but this would mean i need to create several child routes and cannot guarantee a "n-deep" hierachy.

import { Routes } from '@angular/router';

export const routes: Routes = [
  {
    path: 'overview',
    children: [
      {
        path: '',
        loadComponent: () => import('./overview/overview.component'),
      },
      {
        path: 'past',
        loadComponent: () => import('./past/past.component'),
      },
      {
        path: ':event',
        children: [
          {
            path: '',
            loadComponent: () => import('./events/events.component'),
          },
          {
            path: ':sub-event',
            children: [
              {
                path: '',
                loadComponent: () => import('./../events/events.component'),
              },
              {
                path: ':sub-sub-event',
                children: [
                  {
                    path: '',
                    loadComponent: () => import('./../../events/events.component'),
                  },
                  {
                      children: [
                         ... // this would require manual repitition depending on the allowed levels (currently there should not be any limit to the editors)
                      ]
                  },
                  {
                    path: 'events/:event-detail',
                    loadComponent: () =>
                      import('./../../events-details/events-details.component'),
                  },
                ],
              },
              {
                path: 'events/:event-detail',
                loadComponent: () =>
                  import('./../events-details/events-details.component'),
              },
            ],
          },
          {
            path: 'events/:event-detail',
            loadComponent: () =>
              import('./events-details/events-details.component'),
          },
        ],
      },
    ],
  },
];

@alan-agius4
Copy link
Collaborator

Oh I missed that sorry

@denisyilmaz
Copy link
Author

denisyilmaz commented Jan 17, 2025

@alan-agius4 do you think the urlMatcher will work together with SSR anytime soon? until the new project is launched (we are in the alpha phase) we could disable SSR so we can still update to 19.1

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
needs: more info Reporter must clarify the issue
Projects
None yet
Development

No branches or pull requests

2 participants