Skip to content

Commit 0376167

Browse files
committed
refactor(@angular/build): add component update middleware to development server
An additional development server middleware has been added that responds to client `/@ng/component` requests from the Angular framework for hot component template updates. These client requests are made by the framework after being triggered by the development server sending a `angular:component-update` WebSocket event. The Angular compiler's new internal `_enableHmr` option will emit template replacement and reloading code that uses this new development server update middleware. Within the development server, the component update build result will be used to indicate that an event should be sent to active clients. The build system does not yet generate component update results.
1 parent 29855bf commit 0376167

File tree

6 files changed

+127
-6
lines changed

6 files changed

+127
-6
lines changed

packages/angular/build/src/builders/application/results.ts

+5-3
Original file line numberDiff line numberDiff line change
@@ -68,7 +68,9 @@ export interface ResultMessage {
6868

6969
export interface ComponentUpdateResult extends BaseResult {
7070
kind: ResultKind.ComponentUpdate;
71-
id: string;
72-
type: 'style' | 'template';
73-
content: string;
71+
updates: {
72+
id: string;
73+
type: 'style' | 'template';
74+
content: string;
75+
}[];
7476
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
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 { executeDevServer } from '../../index';
10+
import { executeOnceAndFetch } from '../execute-fetch';
11+
import { describeServeBuilder } from '../jasmine-helpers';
12+
import { BASE_OPTIONS, DEV_SERVER_BUILDER_INFO } from '../setup';
13+
14+
describeServeBuilder(executeDevServer, DEV_SERVER_BUILDER_INFO, (harness, setupTarget) => {
15+
describe('Behavior: "Component updates"', () => {
16+
beforeEach(async () => {
17+
setupTarget(harness, {});
18+
19+
// Application code is not needed for these tests
20+
await harness.writeFile('src/main.ts', 'console.log("foo");');
21+
});
22+
23+
it('responds with a 400 status if no request component query is present', async () => {
24+
harness.useTarget('serve', {
25+
...BASE_OPTIONS,
26+
});
27+
28+
const { result, response } = await executeOnceAndFetch(harness, '/@ng/component');
29+
30+
expect(result?.success).toBeTrue();
31+
expect(response?.status).toBe(400);
32+
});
33+
34+
it('responds with an empty JS file when no component update is available', async () => {
35+
harness.useTarget('serve', {
36+
...BASE_OPTIONS,
37+
});
38+
const { result, response } = await executeOnceAndFetch(
39+
harness,
40+
'/@ng/component?c=src%2Fapp%2Fapp.component.ts%40AppComponent',
41+
);
42+
43+
expect(result?.success).toBeTrue();
44+
expect(response?.status).toBe(200);
45+
const output = await response?.text();
46+
expect(response?.headers.get('Content-Type')).toEqual('text/javascript');
47+
expect(response?.headers.get('Cache-Control')).toEqual('no-cache');
48+
expect(output).toBe('');
49+
});
50+
});
51+
});

packages/angular/build/src/builders/dev-server/vite-server.ts

+24-3
Original file line numberDiff line numberDiff line change
@@ -167,6 +167,7 @@ export async function* serveWithVite(
167167
explicitServer: [],
168168
};
169169
const usedComponentStyles = new Map<string, string[]>();
170+
const templateUpdates = new Map<string, string>();
170171

171172
// Add cleanup logic via a builder teardown.
172173
let deferred: () => void;
@@ -211,6 +212,9 @@ export async function* serveWithVite(
211212
assetFiles.set('/' + normalizePath(outputPath), normalizePath(file.inputPath));
212213
}
213214
}
215+
// Clear stale template updates on a code rebuilds
216+
templateUpdates.clear();
217+
214218
// Analyze result files for changes
215219
analyzeResultFiles(normalizePath, htmlIndexPath, result.files, generatedFiles);
216220
break;
@@ -220,8 +224,22 @@ export async function* serveWithVite(
220224
break;
221225
case ResultKind.ComponentUpdate:
222226
assert(serverOptions.hmr, 'Component updates are only supported with HMR enabled.');
223-
// TODO: Implement support -- application builder currently does not use
224-
break;
227+
assert(
228+
server,
229+
'Builder must provide an initial full build before component update results.',
230+
);
231+
232+
for (const componentUpdate of result.updates) {
233+
if (componentUpdate.type === 'template') {
234+
templateUpdates.set(componentUpdate.id, componentUpdate.content);
235+
server.ws.send('angular:component-update', {
236+
id: componentUpdate.id,
237+
timestamp: Date.now(),
238+
});
239+
}
240+
}
241+
context.logger.info('Component update sent to client(s).');
242+
continue;
225243
default:
226244
context.logger.warn(`Unknown result kind [${(result as Result).kind}] provided by build.`);
227245
continue;
@@ -353,6 +371,7 @@ export async function* serveWithVite(
353371
target,
354372
isZonelessApp(polyfills),
355373
usedComponentStyles,
374+
templateUpdates,
356375
browserOptions.loader as EsbuildLoaderOption | undefined,
357376
extensions?.middleware,
358377
transformers?.indexHtml,
@@ -460,7 +479,7 @@ async function handleUpdate(
460479
}
461480

462481
return {
463-
type: 'css-update',
482+
type: 'css-update' as const,
464483
timestamp,
465484
path: filePath,
466485
acceptedPath: filePath,
@@ -564,6 +583,7 @@ export async function setupServer(
564583
target: string[],
565584
zoneless: boolean,
566585
usedComponentStyles: Map<string, string[]>,
586+
templateUpdates: Map<string, string>,
567587
prebundleLoaderExtensions: EsbuildLoaderOption | undefined,
568588
extensionMiddleware?: Connect.NextHandleFunction[],
569589
indexHtmlTransformer?: (content: string) => Promise<string>,
@@ -671,6 +691,7 @@ export async function setupServer(
671691
indexHtmlTransformer,
672692
extensionMiddleware,
673693
usedComponentStyles,
694+
templateUpdates,
674695
ssrMode,
675696
}),
676697
createRemoveIdPrefixPlugin(externalMetadata.explicitBrowser),
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,42 @@
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 { Connect } from 'vite';
10+
11+
const ANGULAR_COMPONENT_PREFIX = '/@ng/component';
12+
13+
export function createAngularComponentMiddleware(
14+
templateUpdates: ReadonlyMap<string, string>,
15+
): Connect.NextHandleFunction {
16+
return function angularComponentMiddleware(req, res, next) {
17+
if (req.url === undefined || res.writableEnded) {
18+
return;
19+
}
20+
21+
if (!req.url.startsWith(ANGULAR_COMPONENT_PREFIX)) {
22+
next();
23+
24+
return;
25+
}
26+
27+
const requestUrl = new URL(req.url, 'http://localhost');
28+
const componentId = requestUrl.searchParams.get('c');
29+
if (!componentId) {
30+
res.statusCode = 400;
31+
res.end();
32+
33+
return;
34+
}
35+
36+
const updateCode = templateUpdates.get(componentId) ?? '';
37+
38+
res.setHeader('Content-Type', 'text/javascript');
39+
res.setHeader('Cache-Control', 'no-cache');
40+
res.end(updateCode);
41+
};
42+
}

packages/angular/build/src/tools/vite/middlewares/index.ts

+1
Original file line numberDiff line numberDiff line change
@@ -14,3 +14,4 @@ export {
1414
createAngularSsrInternalMiddleware,
1515
} from './ssr-middleware';
1616
export { createAngularHeadersMiddleware } from './headers-middleware';
17+
export { createAngularComponentMiddleware } from './component-middleware';

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

+4
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,7 @@ import type { Connect, Plugin } from 'vite';
1010
import {
1111
angularHtmlFallbackMiddleware,
1212
createAngularAssetsMiddleware,
13+
createAngularComponentMiddleware,
1314
createAngularHeadersMiddleware,
1415
createAngularIndexHtmlMiddleware,
1516
createAngularSsrExternalMiddleware,
@@ -48,6 +49,7 @@ interface AngularSetupMiddlewaresPluginOptions {
4849
extensionMiddleware?: Connect.NextHandleFunction[];
4950
indexHtmlTransformer?: (content: string) => Promise<string>;
5051
usedComponentStyles: Map<string, string[]>;
52+
templateUpdates: Map<string, string>;
5153
ssrMode: ServerSsrMode;
5254
}
5355

@@ -64,11 +66,13 @@ export function createAngularSetupMiddlewaresPlugin(
6466
extensionMiddleware,
6567
assets,
6668
usedComponentStyles,
69+
templateUpdates,
6770
ssrMode,
6871
} = options;
6972

7073
// Headers, assets and resources get handled first
7174
server.middlewares.use(createAngularHeadersMiddleware(server));
75+
server.middlewares.use(createAngularComponentMiddleware(templateUpdates));
7276
server.middlewares.use(
7377
createAngularAssetsMiddleware(server, assets, outputFiles, usedComponentStyles),
7478
);

0 commit comments

Comments
 (0)