Skip to content

Commit bad4bdc

Browse files
committed
fix(@angular/build): reset component updates on dev-server SSR page request
When server-side rendering is enabled (either internal or external mode), page reload requests are handled by the SSR middleware before reaching the index HTML middleware. As a result, `resetComponentUpdates` was never invoked on page reloads, causing stale component HMR updates to persist in `templateUpdates` and be re-applied over freshly rendered pages. We now pass `resetComponentUpdates` to `createAngularSsrInternalMiddleware` and `createAngularSsrExternalMiddleware`, and trigger it when an SSR HTML transform occurs (`html:transform:pre`). Fixes #34163
1 parent d968259 commit bad4bdc

4 files changed

Lines changed: 139 additions & 4 deletions

File tree

‎packages/angular/build/BUILD.bazel‎

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -148,6 +148,7 @@ ts_project(
148148
":node_modules/@babel/core",
149149
":node_modules/oxc-parser",
150150
"//:node_modules/@angular/compiler-cli",
151+
"//:node_modules/@angular/router",
151152
"//:node_modules/@types/jasmine",
152153
"//:node_modules/esbuild",
153154
"//:node_modules/prettier",

‎packages/angular/build/src/tools/vite/middlewares/ssr-middleware.ts‎

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,7 @@ import {
2121

2222
export function createAngularSsrInternalMiddleware(
2323
server: ViteDevServer,
24+
resetComponentUpdates: () => void,
2425
indexHtmlTransformer?: (content: string) => Promise<string>,
2526
): Connect.NextHandleFunction {
2627
let cachedAngularServerApp: ReturnType<typeof getOrCreateAngularServerApp> | undefined;
@@ -53,6 +54,7 @@ export function createAngularSsrInternalMiddleware(
5354
// Only Add the transform hook only if it's a different instance.
5455
if (cachedAngularServerApp !== angularServerApp) {
5556
angularServerApp.hooks.on('html:transform:pre', async ({ html, url }) => {
57+
resetComponentUpdates();
5658
const processedHtml = await server.transformIndexHtml(url.pathname, html);
5759

5860
return indexHtmlTransformer?.(processedHtml) ?? processedHtml;
@@ -76,13 +78,13 @@ export function createAngularSsrInternalMiddleware(
7678

7779
export async function createAngularSsrExternalMiddleware(
7880
server: ViteDevServer,
81+
resetComponentUpdates: () => void,
7982
indexHtmlTransformer?: (content: string) => Promise<string>,
8083
): Promise<Connect.NextHandleFunction> {
8184
let fallbackWarningShown = false;
8285
let cachedAngularAppEngine: typeof SSRAngularAppEngine | undefined;
8386
let angularSsrInternalMiddleware:
84-
| ReturnType<typeof createAngularSsrInternalMiddleware>
85-
| undefined;
87+
ReturnType<typeof createAngularSsrInternalMiddleware> | undefined;
8688

8789
// Load the compiler because `@angular/ssr/node` depends on `@angular/` packages,
8890
// which must be processed by the runtime linker, even if they are not used.
@@ -120,6 +122,7 @@ export async function createAngularSsrExternalMiddleware(
120122

121123
angularSsrInternalMiddleware ??= createAngularSsrInternalMiddleware(
122124
server,
125+
resetComponentUpdates,
123126
indexHtmlTransformer,
124127
);
125128

@@ -132,6 +135,7 @@ export async function createAngularSsrExternalMiddleware(
132135
AngularAppEngine.ɵdisableAllowedHostsCheck = disableAllowedHostsCheck;
133136
AngularAppEngine.ɵallowStaticRouteRender = true;
134137
AngularAppEngine.ɵhooks.on('html:transform:pre', async ({ html, url }) => {
138+
resetComponentUpdates();
135139
const processedHtml = await server.transformIndexHtml(url.pathname, html);
136140

137141
return indexHtmlTransformer?.(processedHtml) ?? processedHtml;
Lines changed: 122 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,122 @@
1+
/**
2+
* @license
3+
* Copyright Google LLC All Rights Reserved.
4+
*
5+
* Use of this source code is governed by an MIT-style license that can be
6+
* found in the LICENSE file at https://angular.dev/license
7+
*/
8+
9+
import type { IncomingMessage, ServerResponse } from 'node:http';
10+
import { setTimeout } from 'node:timers/promises';
11+
import {
12+
createAngularSsrExternalMiddleware,
13+
createAngularSsrInternalMiddleware,
14+
} from './ssr-middleware';
15+
16+
type ViteDevServer = Parameters<typeof createAngularSsrInternalMiddleware>[0];
17+
18+
describe('SSR dev-server middleware', () => {
19+
function createMockReqRes(): { req: IncomingMessage; res: ServerResponse } {
20+
const req = {
21+
url: '/',
22+
method: 'GET',
23+
headers: { host: 'localhost' },
24+
} as unknown as IncomingMessage;
25+
26+
const res = {
27+
statusCode: 200,
28+
setHeader: jasmine.createSpy('setHeader'),
29+
writeHead: jasmine.createSpy('writeHead'),
30+
end: jasmine.createSpy('end'),
31+
} as unknown as ServerResponse;
32+
33+
return { req, res };
34+
}
35+
36+
it('resets component updates when html:transform:pre executes in internal SSR middleware', async () => {
37+
let hookCallback: ((args: { html: string; url: URL }) => Promise<string>) | undefined;
38+
const resetComponentUpdates = jasmine.createSpy('resetComponentUpdates');
39+
40+
const fakeAngularServerApp = {
41+
hooks: {
42+
on: (_name: string, cb: typeof hookCallback) => {
43+
hookCallback = cb;
44+
},
45+
},
46+
handle: jasmine.createSpy('handle').and.resolveTo(new Response('rendered')),
47+
};
48+
49+
const server = {
50+
config: { server: { allowedHosts: true }, base: '/' },
51+
ssrLoadModule: jasmine.createSpy('ssrLoadModule').and.resolveTo({
52+
ɵgetOrCreateAngularServerApp: () => fakeAngularServerApp,
53+
}),
54+
transformIndexHtml: jasmine
55+
.createSpy('transformIndexHtml')
56+
.and.callFake(async (_path: string, html: string) => html),
57+
} as unknown as ViteDevServer;
58+
59+
const middleware = createAngularSsrInternalMiddleware(
60+
server,
61+
resetComponentUpdates,
62+
async (html) => html + '<!-- custom -->',
63+
);
64+
65+
const { req, res } = createMockReqRes();
66+
middleware(req, res, () => {});
67+
68+
// Allow internal async import/setup to execute
69+
await setTimeout(50);
70+
71+
expect(resetComponentUpdates).not.toHaveBeenCalled();
72+
expect(hookCallback).toBeDefined();
73+
const result = await hookCallback?.({
74+
html: '<div>test</div>',
75+
url: new URL('http://localhost/'),
76+
});
77+
expect(resetComponentUpdates).toHaveBeenCalledTimes(1);
78+
expect(result).toBe('<div>test</div><!-- custom -->');
79+
});
80+
81+
it('resets component updates when html:transform:pre executes in external SSR middleware', async () => {
82+
let hookCallback: ((args: { html: string; url: URL }) => Promise<string>) | undefined;
83+
const resetComponentUpdates = jasmine.createSpy('resetComponentUpdates');
84+
85+
class FakeAngularAppEngine {
86+
static ɵhooks = {
87+
on: (_name: string, cb: typeof hookCallback) => {
88+
hookCallback = cb;
89+
},
90+
};
91+
static ɵdisableAllowedHostsCheck: boolean;
92+
static ɵallowStaticRouteRender: boolean;
93+
}
94+
95+
const server = {
96+
config: { server: { allowedHosts: true }, base: '/' },
97+
ssrLoadModule: jasmine.createSpy('ssrLoadModule').and.resolveTo({
98+
reqHandler: Object.assign(() => new Response('ok'), { __ng_request_handler__: true }),
99+
AngularAppEngine: FakeAngularAppEngine,
100+
}),
101+
transformIndexHtml: jasmine
102+
.createSpy('transformIndexHtml')
103+
.and.callFake(async (_path: string, html: string) => html + '<!-- transformed -->'),
104+
} as unknown as ViteDevServer;
105+
106+
const middleware = await createAngularSsrExternalMiddleware(server, resetComponentUpdates);
107+
108+
const { req, res } = createMockReqRes();
109+
middleware(req, res, () => {});
110+
111+
await setTimeout(50);
112+
113+
expect(resetComponentUpdates).not.toHaveBeenCalled();
114+
expect(hookCallback).toBeDefined();
115+
const result = await hookCallback?.({
116+
html: '<div>test</div>',
117+
url: new URL('http://localhost/'),
118+
});
119+
expect(resetComponentUpdates).toHaveBeenCalledTimes(1);
120+
expect(result).toBe('<div>test</div><!-- transformed -->');
121+
});
122+
});

‎packages/angular/build/src/tools/vite/plugins/setup-middlewares-plugin.ts‎

Lines changed: 10 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -117,13 +117,21 @@ export function createAngularSetupMiddlewaresPlugin(
117117

118118
if (ssrMode === ServerSsrMode.ExternalSsrMiddleware) {
119119
patchBaseMiddleware(server.middlewares, server.config.base);
120-
middlewares.use(await createAngularSsrExternalMiddleware(server, indexHtmlTransformer));
120+
middlewares.use(
121+
await createAngularSsrExternalMiddleware(
122+
server,
123+
resetComponentUpdates,
124+
indexHtmlTransformer,
125+
),
126+
);
121127

122128
return;
123129
}
124130

125131
if (ssrMode === ServerSsrMode.InternalSsrMiddleware) {
126-
middlewares.use(createAngularSsrInternalMiddleware(server, indexHtmlTransformer));
132+
middlewares.use(
133+
createAngularSsrInternalMiddleware(server, resetComponentUpdates, indexHtmlTransformer),
134+
);
127135
}
128136

129137
middlewares.use(angularHtmlFallbackMiddleware);

0 commit comments

Comments
 (0)