dev-server: with SSR, stale component HMR update re-applied on every page load
Les mainteneurs répondent en général sous 1 jour
Personne n'a encore pris cette issue.
Évaluation
- Difficulté
- 4/5
- Temps estimé
- 3-5 jours
- Accessibilité débutants
- 75/100
- Type d'issue
- Bug
- Clarté
- Clairement spécifiée
- Activité
- Active
- Stack technique
- angular, sass, typescript
- Domaine
- build-system, devtools
Piste de recherche
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.
Rédigé par le modèle d'indexation à partir du texte de l'issue.
Description
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:
src/builders/dev-server/vite/index.js: aComponentUpdateresult stores its content intemplateUpdates. The map is cleared only forResultKind.Full. The dev-server setsincrementalResults = true, so every rebuild after the first isIncrementaland never clears it.- The other clear path,
resetComponentUpdates, is passed only tocreateAngularIndexHtmlMiddleware(src/tools/vite/plugins/setup-middlewares-plugin.js). With SSR the SSR middleware answers the page request first, so the reset never runs. - On load, each component's HMR init fetches
/@ng/component?c=<id>&t=<now>.component-middleware.jsreturns 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
ng new hmr-repro-ssr --defaults --style=scss --ssrsrc/app/_tokens.scss:$gap: 10px;src/app/app.scss:@use "tokens"; h1 { margin-top: tokens.$gap; color: red; }src/app/app.html:<h1>Hello</h1>ng serve, open http://localhost:4200/. h1:margin-top: 10px, red.- In
app.scsschangeredtoblue. Log:Component update sent to client(s).h1 turns blue. Correct. - In
_tokens.scsschange10pxto40px. Log:Page reload sent to client(s). - Actual: h1 keeps
margin-top: 10px, also after a manual reload or a new navigation.curl http://localhost:4200/andcurl http://localhost:4200/main.jsboth carrymargin-top: 40px.curl 'http://localhost:4200/@ng/component?c=src%2Fapp%2Fapp.ts%40App&t=1'still returnsmargin-top: 10px.
- Save
app.scssagain, or restartng serve:40pxappears.
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/componentstill 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.
- Langage dominant
- TypeScript
- Étoiles
- 27k
- Forks
- 11.8k
- Merge moyen
- 21 h 48 min
- PR mergées (30 j)
- 164
Préparer son environnement
Par où commencer
- Lisez l'issue en entier, puis le guide de contribution du projet.
- Signalez en commentaire que vous la prenez — cela évite que deux personnes fassent le même travail.
- Forkez le dépôt et travaillez sur une branche.
- Ouvrez une pull request qui référence le numéro de l'issue.
Autres issues de angular/angular-cli
-
Can't use an array of hostnames in --allowedHosts cli parameter in @angular/build:dev-serverOuvertearea: @angular/build gemini-triaged
Difficulté 2/5 1-3 heures Accessibilité débutants 74/100
angular/angular-cli#33955 ·
Les mainteneurs répondent en général sous 1 jour
-
area: @angular/cli gemini-triaged
Difficulté 2/5 1-3 heures Accessibilité débutants 72/100
angular/angular-cli#33055 · 1 commentaire · 3 réactions ·
Les mainteneurs répondent en général sous 1 jour
-
area: @angular/build gemini-triaged
Difficulté 3/5 1-2 jours Accessibilité débutants 70/100
angular/angular-cli#34185 ·
Les mainteneurs répondent en général sous 1 jour
-
area: @angular/build gemini-triaged
Difficulté 3/5 1-2 jours Accessibilité débutants 68/100
angular/angular-cli#34166 · 1 commentaire ·
Les mainteneurs répondent en général sous 1 jour
-
library builder: tslib is not added to the output package.jsonPeut-être pris @alan-agius4 l’a pris il y a 7 jours. Ouverteangular/build:library area: @angular/build gemini-triaged
angular/angular-cli#34131 · 1 personne assignée ·
Les mainteneurs répondent en général sous 1 jour
Toutes les issues de angular/angular-cli
Issues similaires
-
bug
Difficulté 1/5 Moins d'une heure Accessibilité débutants 88/100
StabilityNexus/Fate-EVM-Frontend#153 ·
Les mainteneurs répondent en général sous 1 jour
-
Difficulté 2/5 1-3 heures Accessibilité débutants 78/100
code-yeongyu/oh-my-openagent#9039 ·
Les mainteneurs répondent en général sous 1 jour
-
bug
Difficulté 2/5 1-3 heures Accessibilité débutants 84/100
Tencent/teamai-cli#862 ·
Les mainteneurs répondent en général sous 1 jour
-
bug good first issue hacktoberfest redis
Difficulté 2/5 1-3 heures Accessibilité débutants 88/100
libredb/libredb-studio#1164 ·
Les mainteneurs répondent en général sous 1 jour
-
flake
Difficulté 2/5 1-3 heures Accessibilité débutants 85/100
coder/xum#4920 · 2 commentaires ·
Les mainteneurs répondent en général sous 1 jour