Hacktoberfest 2026: le issue che i maintainer hanno segnato per ottobre, aperte e adatte ai principianti. Sfoglia le issue Hacktoberfest

dev-server: with SSR, stale component HMR update re-applied on every page load

Chiusa
#34,163 0 commenti 0 reazioni 0 assegnatari Vedi su GitHub

I maintainer di solito rispondono entro 1 giorno

Nessuno ha ancora preso questa issue.

Valutazione

Difficoltà
4/5
Tempo stimato
3-5 giorni
Idoneità per principianti
75/100
Tipo di issue
Bug
Chiarezza
Specificata chiaramente
Stato di attività
Attiva
Stack tecnologico
angular, sass, typescript

Direzione di ricerca

Start with packages/angular/build/src/tools/vite/middlewares/ssr-middleware.ts and plugins/setup-middlewares-plugin.ts, then compare their SSR request flow with the existing reset path in the Angular index HTML middleware. Reproduce the Sass partial case with SSR and verify both internal and external SSR paths clear stale component updates after a rendered document while component HMR still works.

Scritto dal modello di indicizzazione a partire dal testo della issue.

Descrizione

area: @angular/build gemini-triaged severity5: regression
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

No response

Description

With SSR enabled, once a component has received an HMR update (Component update sent to client(s).), later changes that rebuild as a page reload never show in the browser for that component. Typical trigger: editing a Sass partial @used by the component stylesheet. Server output (/, main.js) is fresh; the browser keeps the old styles through any number of reloads, until the component's own file is saved again or ng serve restarts.

Cause, read from @angular/build 22.2.0 source:

  1. src/builders/dev-server/vite/index.js: a ComponentUpdate result stores its content in templateUpdates. The map is cleared only for ResultKind.Full. The dev-server sets incrementalResults = true, so every rebuild after the first is Incremental and never clears it.
  2. The other clear path, resetComponentUpdates, is passed only to createAngularIndexHtmlMiddleware (src/tools/vite/plugins/setup-middlewares-plugin.js). With SSR the SSR middleware answers the page request first, so the reset never runs.
  3. On load, each component's HMR init fetches /@ng/component?c=<id>&t=<now>. component-middleware.js returns the stored, now stale, update and the runtime applies it over the fresh bundle.

Without SSR the same steps work: the index HTML middleware resets the map on reload.

Expected: a page load after a later rebuild uses current styles. A fix is proposed at the end of this issue.

Minimal Reproduction
  1. ng new hmr-repro-ssr --defaults --style=scss --ssr
  2. src/app/_tokens.scss:
    $gap: 10px;
    
  3. src/app/app.scss:
    @use "tokens";
    
    h1 {
      margin-top: tokens.$gap;
      color: red;
    }
    
  4. src/app/app.html: <h1>Hello</h1>
  5. ng serve, open http://localhost:4200/. h1: margin-top: 10px, red.
  6. In app.scss change red to blue. Log: Component update sent to client(s). h1 turns blue. Correct.
  7. In _tokens.scss change 10px to 40px. Log: Page reload sent to client(s).
  8. Actual: h1 keeps margin-top: 10px, also after a manual reload or a new navigation.
    • curl http://localhost:4200/ and curl http://localhost:4200/main.js both carry margin-top: 40px.
    • curl 'http://localhost:4200/@ng/component?c=src%2Fapp%2Fapp.ts%40App&t=1' still returns margin-top: 10px.
  9. Save app.scss again, or restart ng serve: 40px appears.

Control: the same steps in an app created with --ssr=false show 40px at step 8.

Workaround: NG_HMR_TEMPLATES=0 ng serve, or ng serve --no-hmr.

Exception or Error

Your Environment
Angular CLI       : 22.2.0
Angular           : 22.2.0
Node.js           : 26.7.0
Package Manager   : pnpm 10.30.2
Operating System  : darwin arm64

@angular/build            22.2.0
@angular/cli              22.2.0
@angular/common           22.2.0
@angular/compiler         22.2.0
@angular/compiler-cli     22.2.0
@angular/core             22.2.0
@angular/platform-browser 22.2.0
@angular/platform-server  22.2.0
@angular/router           22.2.0
@angular/ssr              22.2.0
rxjs                      7.8.2
typescript                6.0.3
Anything else relevant?

Not browser specific: the stale content is served by the dev server (tested in Chrome). First seen on 22.2.0-rc.0 in a larger SSR app, then confirmed on a clean 22.2.0 app with the steps above.

Proposed fix (verified locally)

CSR already resets the map on a page load: createAngularIndexHtmlMiddleware calls resetComponentUpdates() ("A request for the index indicates a full page reload request."). The SSR middlewares never receive that callback. Pass it to both SSR middleware factories and call it in the html:transform:pre hook, which runs once per rendered document:

--- a/packages/angular/build/src/tools/vite/middlewares/ssr-middleware.ts
+++ b/packages/angular/build/src/tools/vite/middlewares/ssr-middleware.ts
@@ -22,6 +22,7 @@
 export function createAngularSsrInternalMiddleware(
   server: ViteDevServer,
   indexHtmlTransformer?: (content: string) => Promise<string>,
+  resetComponentUpdates?: () => void,
 ): Connect.NextHandleFunction {
   let cachedAngularServerApp: ReturnType<typeof getOrCreateAngularServerApp> | undefined;
 
@@ -53,6 +54,9 @@
       // Only Add the transform hook only if it's a different instance.
       if (cachedAngularServerApp !== angularServerApp) {
         angularServerApp.hooks.on('html:transform:pre', async ({ html, url }) => {
+          // A rendered document indicates a full page reload request.
+          resetComponentUpdates?.();
+
           const processedHtml = await server.transformIndexHtml(url.pathname, html);
 
           return indexHtmlTransformer?.(processedHtml) ?? processedHtml;
@@ -77,6 +81,7 @@
 export async function createAngularSsrExternalMiddleware(
   server: ViteDevServer,
   indexHtmlTransformer?: (content: string) => Promise<string>,
+  resetComponentUpdates?: () => void,
 ): Promise<Connect.NextHandleFunction> {
   let fallbackWarningShown = false;
   let cachedAngularAppEngine: typeof SSRAngularAppEngine | undefined;
@@ -121,6 +126,7 @@
         angularSsrInternalMiddleware ??= createAngularSsrInternalMiddleware(
           server,
           indexHtmlTransformer,
+          resetComponentUpdates,
         );
 
         angularSsrInternalMiddleware(req, res, next);
@@ -132,6 +138,9 @@
         AngularAppEngine.ɵdisableAllowedHostsCheck = disableAllowedHostsCheck;
         AngularAppEngine.ɵallowStaticRouteRender = true;
         AngularAppEngine.ɵhooks.on('html:transform:pre', async ({ html, url }) => {
+          // A rendered document indicates a full page reload request.
+          resetComponentUpdates?.();
+
           const processedHtml = await server.transformIndexHtml(url.pathname, html);
 
           return indexHtmlTransformer?.(processedHtml) ?? processedHtml;
--- a/packages/angular/build/src/tools/vite/plugins/setup-middlewares-plugin.ts
+++ b/packages/angular/build/src/tools/vite/plugins/setup-middlewares-plugin.ts
@@ -117,13 +117,13 @@
 
         if (ssrMode === ServerSsrMode.ExternalSsrMiddleware) {
           patchBaseMiddleware(server.middlewares, server.config.base);
-          middlewares.use(await createAngularSsrExternalMiddleware(server, indexHtmlTransformer));
+          middlewares.use(await createAngularSsrExternalMiddleware(server, indexHtmlTransformer, resetComponentUpdates));
 
           return;
         }
 
         if (ssrMode === ServerSsrMode.InternalSsrMiddleware) {
-          middlewares.use(createAngularSsrInternalMiddleware(server, indexHtmlTransformer));
+          middlewares.use(createAngularSsrInternalMiddleware(server, indexHtmlTransformer, resetComponentUpdates));
         }
 
         middlewares.use(angularHtmlFallbackMiddleware);

Verified by applying the equivalent change to the compiled @angular/build 22.2.0 in the reproduction above (external SSR middleware, default ng new --ssr server.ts):

  • Before: after steps 6 and 7 plus one page request, /@ng/component still returns the stored update (4839 bytes, margin-top: 10px).
  • After: the same request returns an empty body. The browser shows the new partial value after the reload, and component HMR still hot-swaps in place (step 6 applies without a page reload).

Not exercised: the internal SSR middleware path (the same change is applied there). The diff has not been run through prettier.

Lingua principale
TypeScript
Stelle
27k
Fork
11.8k
Merge medio
20h 47m
PR unite (30g)
178

Preparare l'ambiente

Come iniziare

  1. Leggi tutta la issue e poi la guida ai contributi del progetto.
  2. Commenta sulla issue per dire che te ne occupi tu — evita che due persone facciano lo stesso lavoro.
  3. Fai un fork del repository e lavora su un branch.
  4. Apri una pull request che faccia riferimento al numero della issue.

Altre issue di angular/angular-cli

Tutte le issue di angular/angular-cli

Issue simili

Altre issue su TypeScript

Ricevi le nuove issue nella tua casella

Un breve riepilogo di issue GitHub adatte ai principianti.