@angular/ssr: page under nested empty-path routes is prerendered without the modulepreload hints of its lazy chunks (regression since 19.1.6)
Maintainer thường phản hồi trong vòng 1 ngày
Đánh giá
Issue này chưa được đánh giá.
Mô tả
Command
build
Is this a regression?
Yes, the previous version in which this bug was not present was: 19.1.5
Description
A prerendered page reached through an empty-path route nested under another empty-path route (a componentless '' route whose '' child is a loadChildren, whose own '' route is a loadComponent) is emitted without any <link rel="modulepreload"> hint for its own lazy chunks. A sibling page reached through a non-empty path with exactly the same lazy depth ('pricing' → loadChildren → '' loadComponent) gets both of its hints. The page itself renders correctly; only the hints are missing.
Measured with 22.2.2, outputMode: "static" (index.csr.html has 0 hints):
| Routes | / |
/pricing |
|---|---|---|
'' (componentless) → '' loadChildren → '' loadComponent |
0 (expected 2: home.routes, home-page) |
2 (pricing.routes, pricing-page) |
home flattened to a top-level '' loadComponent |
1 (home-page, its only chunk) |
2 |
Same nested routes on 19.1.5 vs 19.1.6 (both builds share 2 initial chunks, which index.csr.html also has):
| Version | / |
/pricing |
|---|---|---|
| 19.1.5 | 4 (2 shared + home.routes + home-page) |
4 |
| 19.1.6 | 2 (shared only) | 4 |
@angular/ssr 19.1.5 with @angular/build 19.1.6 |
4 | 4 |
The chunk hashes are identical across these builds; only @angular/ssr makes the difference.
Likely cause (packages/angular/ssr/src/routes/ng-routes.ts): traverseRoutesConfig keys an empty-path route as addTrailingSlash(currentRoutePath). Under a non-empty parent the parent and its '' child get different keys (/pricing and /pricing/), which RouteTree.insert later merges with the last entry winning, the leaf that carries the accumulated preload. Under an empty-path parent both get the same key (/, or /es/ for es → '' → ''), and getRoutesFromAngularRouterConfig keeps only the first result per key (seenRoutes, added in 5bf5e5fd2 "fix(@angular/ssr): prioritize the first matching route over subsequent ones", #29539). The first result is the outer route, yielded before handleRoute appends its loadChildren preloads and before any descendant's loadComponent preloads, so the leaf's metadata with the preloads is dropped. That dedupe is right for competing sibling routes with the same path; here the later entry is a descendant in the same match chain, the route the router actually activates.
Expected: the prerendered page carries the hints of the route chain that renders it, as on 19.1.5 (for example, let a later result replace the kept one, or merge its preload, when it is a descendant of it rather than a sibling).
Real-world impact: our home page had 3 modulepreload hints against 12 on /pricing; flattening the home route to a top-level loadComponent gave 9.
Minimal Reproduction
npx -p @angular/[email protected] ng new repro --ssr --routing --style=css --skip-git --skip-tests --defaults --ai-config=none --package-manager=npm
cd repro
# angular.json: set "outputMode": "static"
# src/app/app.html: replace its content with <router-outlet /> (optional, only trims the placeholder)
# app.routes.server.ts stays as generated: [{ path: '**', renderMode: RenderMode.Prerender }]
src/app/app.routes.ts
import { Routes } from '@angular/router';
export const routes: Routes = [
{
path: '',
children: [
{ path: '', pathMatch: 'full', loadChildren: () => import('./home/home.routes') },
{ path: 'pricing', loadChildren: () => import('./pricing/pricing.routes') },
],
},
];
src/app/home/home.routes.ts (and src/app/pricing/pricing.routes.ts, the same with ./pricing-page)
import { Routes } from '@angular/router';
export default [{ path: '', loadComponent: () => import('./home-page') }] satisfies Routes;
src/app/home/home-page.ts (and src/app/pricing/pricing-page.ts, the same with PricingPage)
import { Component } from '@angular/core';
@Component({ template: '<h1>Home page</h1>' })
export default class HomePage {}
npx ng build
grep -o '<link rel="modulepreload"[^>]*>' dist/repro/browser/index.html # nothing
grep -o '<link rel="modulepreload"[^>]*>' dist/repro/browser/pricing/index.html # 2 links
Workaround that brings / back to its hint, the page as a top-level route:
export const routes: Routes = [
{ path: '', pathMatch: 'full', loadComponent: () => import('./home/home-page') },
{ path: 'pricing', loadChildren: () => import('./pricing/pricing.routes') },
];
Exception or Error
None. The build succeeds ("Prerendered 2 static routes.") and both pages render their content; only the modulepreload hints of `/` are missing.
Your Environment
Angular CLI : 22.2.2
Angular : 22.2.2
Node.js : 24.21.0
Package Manager : npm 11.19.0
Operating System : darwin arm64
@angular/build 22.2.2
@angular/cli 22.2.2
@angular/common 22.2.2
@angular/compiler 22.2.2
@angular/compiler-cli 22.2.2
@angular/core 22.2.2
@angular/forms 22.2.2
@angular/platform-browser 22.2.2
@angular/platform-server 22.2.2
@angular/router 22.2.2
@angular/ssr 22.2.2
rxjs 7.8.2
typescript 6.0.3
Anything else relevant?
- At the root, one empty-path level under another is enough: a top-level
{ path: '', pathMatch: 'full', loadChildren: () => import('./home/home.routes') }with no componentless wrapper also gives/0 hints. So removing only the wrapper is not a workaround; the page itself must be the top-level''route. - Not specific to the root:
{ path: 'es', children: [{ path: '', pathMatch: 'full', loadChildren: ... }, { path: 'pricing', loadChildren: ... }] }gives/es0 hints and/es/pricing2. - The per-route
preloadfeature itself worked in 19.1.0–19.1.5; theseenRoutesdedupe appears in 19.1.6 and is still there in 22.2.2.
- Ngôn ngữ chính
- TypeScript
- Star
- 27k
- Fork
- 11.8k
- Merge trung bình
- 1 ngày 2 giờ
- Pull request đã merge (30 ngày)
- 156
Chuẩn bị môi trường
- Không có Dockerfile hay tệp Docker Compose
- Có mẫu pull request
- Đọc hướng dẫn đóng góp
Bắt đầu từ đâu
- Đọc hết issue, rồi đọc hướng dẫn đóng góp của dự án.
- Bình luận trên issue rằng bạn sẽ nhận — tránh hai người làm cùng một việc.
- Fork repository và làm thay đổi trên một nhánh.
- Mở pull request có tham chiếu số hiệu của issue.
Issue khác của angular/angular-cli
-
area: @angular/cli
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
angular/angular-cli#34307 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Can't use an array of hostnames in --allowedHosts cli parameter in @angular/build:dev-serverĐang mởarea: @angular/build gemini-triaged
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 74/100
angular/angular-cli#33955 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
area: @angular/cli gemini-triaged
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 72/100
angular/angular-cli#33055 · 1 bình luận · 3 reaction ·
Maintainer thường phản hồi trong vòng 1 ngày
-
area: @angular/build
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 48/100
angular/angular-cli#34312 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
area: @angular/build
Độ khó 4/5 3-5 ngày Mức phù hợp với người mới 25/100
angular/angular-cli#34292 ·
Maintainer thường phản hồi trong vòng 1 ngày
Tất cả issue của angular/angular-cli
Issue tương tự
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
db-ux-design-system/core-web#8461 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 78/100
Maintainer thường phản hồi trong vòng 1 ngày
-
Cannot run an integration on windows when power shell is selected as the default profile terminalĐang mởPriority/Blocker Type/Bug
Độ khó 2/5 1-3 giờ Mức phù hợp với người mới 76/100
wso2/product-integrator#2767 ·
Maintainer thường phản hồi trong vòng 1 ngày
-
Độ khó 1/5 Dưới một giờ Mức phù hợp với người mới 85/100
-
severity:low
Độ khó 1/5 1-3 giờ Mức phù hợp với người mới 84/100
Maintainer thường phản hồi trong vòng 1 ngày