@angular/build 19.0.0-next.9 → 19.0.0-rc.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (90) hide show
  1. package/package.json +21 -19
  2. package/src/builders/application/build-action.js +22 -10
  3. package/src/builders/application/chunk-optimizer.js +1 -4
  4. package/src/builders/application/execute-build.js +59 -24
  5. package/src/builders/application/execute-post-bundle.js +28 -5
  6. package/src/builders/application/index.d.ts +0 -16
  7. package/src/builders/application/index.js +15 -10
  8. package/src/builders/application/options.d.ts +14 -2
  9. package/src/builders/application/options.js +25 -10
  10. package/src/builders/application/results.d.ts +5 -3
  11. package/src/builders/application/schema.d.ts +86 -0
  12. package/src/builders/application/schema.js +19 -1
  13. package/src/builders/application/schema.json +73 -4
  14. package/src/builders/application/setup-bundling.d.ts +6 -1
  15. package/src/builders/application/setup-bundling.js +47 -13
  16. package/src/builders/dev-server/options.d.ts +2 -2
  17. package/src/builders/dev-server/options.js +2 -2
  18. package/src/builders/dev-server/schema.d.ts +2 -1
  19. package/src/builders/dev-server/schema.json +1 -2
  20. package/src/builders/dev-server/vite-server.d.ts +3 -2
  21. package/src/builders/dev-server/vite-server.js +123 -61
  22. package/src/index.d.ts +1 -0
  23. package/src/tools/angular/angular-host.d.ts +1 -1
  24. package/src/tools/angular/angular-host.js +14 -6
  25. package/src/tools/angular/compilation/angular-compilation.d.ts +1 -0
  26. package/src/tools/angular/compilation/aot-compilation.d.ts +1 -0
  27. package/src/tools/angular/compilation/aot-compilation.js +39 -0
  28. package/src/tools/angular/compilation/parallel-compilation.js +2 -2
  29. package/src/tools/angular/compilation/parallel-worker.d.ts +1 -0
  30. package/src/tools/angular/compilation/parallel-worker.js +5 -2
  31. package/src/tools/esbuild/angular/compiler-plugin.d.ts +3 -4
  32. package/src/tools/esbuild/angular/compiler-plugin.js +58 -33
  33. package/src/tools/esbuild/angular/component-stylesheets.d.ts +18 -18
  34. package/src/tools/esbuild/angular/component-stylesheets.js +66 -38
  35. package/src/tools/esbuild/angular/jit-plugin-callbacks.d.ts +1 -1
  36. package/src/tools/esbuild/angular/jit-plugin-callbacks.js +11 -3
  37. package/src/tools/esbuild/angular/source-file-cache.d.ts +1 -1
  38. package/src/tools/esbuild/angular/source-file-cache.js +6 -2
  39. package/src/tools/esbuild/application-code-bundle.d.ts +7 -5
  40. package/src/tools/esbuild/application-code-bundle.js +280 -249
  41. package/src/tools/esbuild/bundler-context.d.ts +2 -1
  42. package/src/tools/esbuild/bundler-context.js +10 -12
  43. package/src/tools/esbuild/bundler-execution-result.d.ts +14 -3
  44. package/src/tools/esbuild/bundler-execution-result.js +15 -8
  45. package/src/tools/esbuild/commonjs-checker.js +2 -2
  46. package/src/tools/esbuild/compiler-plugin-options.d.ts +2 -4
  47. package/src/tools/esbuild/compiler-plugin-options.js +15 -37
  48. package/src/tools/esbuild/global-scripts.js +1 -1
  49. package/src/tools/esbuild/global-styles.js +4 -1
  50. package/src/tools/esbuild/index-html-generator.js +8 -0
  51. package/src/tools/esbuild/javascript-transformer.js +3 -0
  52. package/src/tools/esbuild/server-bundle-metadata-plugin.d.ts +22 -0
  53. package/src/tools/esbuild/server-bundle-metadata-plugin.js +36 -0
  54. package/src/tools/esbuild/stylesheets/bundle-options.d.ts +2 -0
  55. package/src/tools/esbuild/stylesheets/bundle-options.js +2 -1
  56. package/src/tools/esbuild/stylesheets/sass-language.js +4 -0
  57. package/src/tools/esbuild/stylesheets/stylesheet-plugin-factory.d.ts +9 -0
  58. package/src/tools/esbuild/utils.js +13 -31
  59. package/src/tools/sass/worker.js +19 -0
  60. package/src/tools/vite/middlewares/assets-middleware.d.ts +6 -1
  61. package/src/tools/vite/middlewares/assets-middleware.js +42 -22
  62. package/src/tools/vite/middlewares/component-middleware.d.ts +9 -0
  63. package/src/tools/vite/middlewares/component-middleware.js +33 -0
  64. package/src/tools/vite/middlewares/index.d.ts +2 -1
  65. package/src/tools/vite/middlewares/index.js +3 -1
  66. package/src/tools/vite/middlewares/ssr-middleware.js +11 -8
  67. package/src/tools/vite/plugins/angular-memory-plugin.d.ts +1 -0
  68. package/src/tools/vite/plugins/angular-memory-plugin.js +5 -13
  69. package/src/tools/vite/plugins/setup-middlewares-plugin.d.ts +3 -1
  70. package/src/tools/vite/plugins/setup-middlewares-plugin.js +12 -3
  71. package/src/tools/vite/utils.d.ts +1 -0
  72. package/src/typings.d.ts +1 -1
  73. package/src/utils/environment-options.d.ts +1 -0
  74. package/src/utils/environment-options.js +4 -2
  75. package/src/utils/index-file/auto-csp.d.ts +23 -0
  76. package/src/utils/index-file/auto-csp.js +283 -0
  77. package/src/utils/index-file/html-rewriting-stream.d.ts +5 -1
  78. package/src/utils/index-file/index-html-generator.d.ts +4 -0
  79. package/src/utils/index-file/index-html-generator.js +11 -0
  80. package/src/utils/index-file/inline-critical-css.js +17 -18
  81. package/src/utils/normalize-cache.js +1 -1
  82. package/src/utils/server-rendering/esm-in-memory-loader/utils.d.ts +8 -0
  83. package/src/utils/server-rendering/esm-in-memory-loader/utils.js +13 -0
  84. package/src/utils/server-rendering/launch-server.js +5 -5
  85. package/src/utils/server-rendering/load-esm-from-memory.d.ts +1 -1
  86. package/src/utils/server-rendering/manifest.d.ts +9 -8
  87. package/src/utils/server-rendering/manifest.js +17 -23
  88. package/src/utils/server-rendering/prerender.js +30 -19
  89. package/src/utils/server-rendering/render-worker.js +4 -2
  90. package/src/utils/supported-browsers.js +1 -0
@@ -65,10 +65,10 @@ async function* serveWithVite(serverOptions, builderName, builderAction, context
65
65
  }
66
66
  // TODO: Adjust architect to not force a JsonObject derived return type
67
67
  const browserOptions = (await context.validateOptions(rawBrowserOptions, builderName));
68
- if (browserOptions.prerender) {
68
+ if (browserOptions.prerender || (browserOptions.outputMode && browserOptions.server)) {
69
69
  // Disable prerendering if enabled and force SSR.
70
70
  // This is so instead of prerendering all the routes for every change, the page is "prerendered" when it is requested.
71
- browserOptions.prerender = false;
71
+ browserOptions.prerender = undefined;
72
72
  browserOptions.ssr ||= true;
73
73
  }
74
74
  // Set all packages as external to support Vite's prebundle caching
@@ -92,8 +92,14 @@ async function* serveWithVite(serverOptions, builderName, builderAction, context
92
92
  // https://nodejs.org/api/process.html#processsetsourcemapsenabledval
93
93
  process.setSourceMapsEnabled(true);
94
94
  }
95
- // TODO: Enable by default once full support across CLI and FW is integrated
96
- browserOptions.externalRuntimeStyles = environment_options_1.useComponentStyleHmr;
95
+ // Enable to support component style hot reloading (`NG_HMR_CSTYLES=0` can be used to disable selectively)
96
+ browserOptions.externalRuntimeStyles =
97
+ serverOptions.liveReload && serverOptions.hmr && environment_options_1.useComponentStyleHmr;
98
+ // Enable to support component template hot replacement (`NG_HMR_TEMPLATE=1` can be used to enable)
99
+ browserOptions.templateUpdates = !!serverOptions.liveReload && environment_options_1.useComponentTemplateHmr;
100
+ if (browserOptions.templateUpdates) {
101
+ context.logger.warn('Experimental support for component template hot replacement has been enabled via the "NG_HMR_TEMPLATE" environment variable.');
102
+ }
97
103
  // Setup the prebundling transformer that will be shared across Vite prebundling requests
98
104
  const prebundleTransformer = new internal_1.JavaScriptTransformer(
99
105
  // Always enable JIT linking to support applications built with and without AOT.
@@ -115,7 +121,8 @@ async function* serveWithVite(serverOptions, builderName, builderAction, context
115
121
  explicitBrowser: [],
116
122
  explicitServer: [],
117
123
  };
118
- const usedComponentStyles = new Map();
124
+ const componentStyles = new Map();
125
+ const templateUpdates = new Map();
119
126
  // Add cleanup logic via a builder teardown.
120
127
  let deferred;
121
128
  context.addTeardown(async () => {
@@ -125,20 +132,30 @@ async function* serveWithVite(serverOptions, builderName, builderAction, context
125
132
  });
126
133
  // TODO: Switch this to an architect schedule call when infrastructure settings are supported
127
134
  for await (const result of builderAction(browserOptions, context, extensions?.buildPlugins)) {
135
+ if (result.kind === results_1.ResultKind.Failure) {
136
+ if (result.errors.length && server) {
137
+ hadError = true;
138
+ server.ws.send({
139
+ type: 'error',
140
+ err: {
141
+ message: result.errors[0].text,
142
+ stack: '',
143
+ loc: result.errors[0].location ?? undefined,
144
+ },
145
+ });
146
+ }
147
+ continue;
148
+ }
149
+ // Clear existing error overlay on successful result
150
+ if (hadError && server) {
151
+ hadError = false;
152
+ // Send an empty update to clear the error overlay
153
+ server.ws.send({
154
+ 'type': 'update',
155
+ updates: [],
156
+ });
157
+ }
128
158
  switch (result.kind) {
129
- case results_1.ResultKind.Failure:
130
- if (result.errors.length && server) {
131
- hadError = true;
132
- server.ws.send({
133
- type: 'error',
134
- err: {
135
- message: result.errors[0].text,
136
- stack: '',
137
- loc: result.errors[0].location ?? undefined,
138
- },
139
- });
140
- }
141
- continue;
142
159
  case results_1.ResultKind.Full:
143
160
  if (result.detail?.['htmlIndexPath']) {
144
161
  htmlIndexPath = result.detail['htmlIndexPath'];
@@ -157,8 +174,10 @@ async function* serveWithVite(serverOptions, builderName, builderAction, context
157
174
  assetFiles.set('/' + normalizePath(outputPath), normalizePath(file.inputPath));
158
175
  }
159
176
  }
177
+ // Clear stale template updates on code rebuilds
178
+ templateUpdates.clear();
160
179
  // Analyze result files for changes
161
- analyzeResultFiles(normalizePath, htmlIndexPath, result.files, generatedFiles);
180
+ analyzeResultFiles(normalizePath, htmlIndexPath, result.files, generatedFiles, componentStyles);
162
181
  break;
163
182
  case results_1.ResultKind.Incremental:
164
183
  (0, node_assert_1.default)(server, 'Builder must provide an initial full build before incremental results.');
@@ -166,21 +185,22 @@ async function* serveWithVite(serverOptions, builderName, builderAction, context
166
185
  break;
167
186
  case results_1.ResultKind.ComponentUpdate:
168
187
  (0, node_assert_1.default)(serverOptions.hmr, 'Component updates are only supported with HMR enabled.');
169
- // TODO: Implement support -- application builder currently does not use
170
- break;
188
+ (0, node_assert_1.default)(server, 'Builder must provide an initial full build before component update results.');
189
+ for (const componentUpdate of result.updates) {
190
+ if (componentUpdate.type === 'template') {
191
+ templateUpdates.set(componentUpdate.id, componentUpdate.content);
192
+ server.ws.send('angular:component-update', {
193
+ id: componentUpdate.id,
194
+ timestamp: Date.now(),
195
+ });
196
+ }
197
+ }
198
+ context.logger.info('Component update sent to client(s).');
199
+ continue;
171
200
  default:
172
201
  context.logger.warn(`Unknown result kind [${result.kind}] provided by build.`);
173
202
  continue;
174
203
  }
175
- // Clear existing error overlay on successful result
176
- if (hadError && server) {
177
- hadError = false;
178
- // Send an empty update to clear the error overlay
179
- server.ws.send({
180
- 'type': 'update',
181
- updates: [],
182
- });
183
- }
184
204
  // To avoid disconnecting the array objects from the option, these arrays need to be mutated instead of replaced.
185
205
  let requiresServerRestart = false;
186
206
  if (result.detail?.['externalMetadata']) {
@@ -220,7 +240,7 @@ async function* serveWithVite(serverOptions, builderName, builderAction, context
220
240
  await server.restart();
221
241
  }
222
242
  else {
223
- await handleUpdate(normalizePath, generatedFiles, server, serverOptions, context.logger, usedComponentStyles);
243
+ await handleUpdate(normalizePath, generatedFiles, server, serverOptions, context.logger, componentStyles);
224
244
  }
225
245
  }
226
246
  else {
@@ -249,11 +269,17 @@ async function* serveWithVite(serverOptions, builderName, builderAction, context
249
269
  browserOptions.ssr.entry) {
250
270
  ssrMode = plugins_1.ServerSsrMode.ExternalSsrMiddleware;
251
271
  }
252
- else if (browserOptions.server) {
272
+ else if (browserOptions.ssr) {
253
273
  ssrMode = plugins_1.ServerSsrMode.InternalSsrMiddleware;
254
274
  }
275
+ if (browserOptions.progress !== false && ssrMode !== plugins_1.ServerSsrMode.NoSsr) {
276
+ // This is a workaround for https://github.com/angular/angular-cli/issues/28336, which is caused by the interaction between `zone.js` and `listr2`.
277
+ process.once('SIGINT', () => {
278
+ process.kill(process.pid);
279
+ });
280
+ }
255
281
  // Setup server and start listening
256
- const serverConfiguration = await setupServer(serverOptions, generatedFiles, assetFiles, browserOptions.preserveSymlinks, externalMetadata, ssrMode, prebundleTransformer, target, (0, internal_1.isZonelessApp)(polyfills), usedComponentStyles, browserOptions.loader, extensions?.middleware, transformers?.indexHtml, thirdPartySourcemaps);
282
+ const serverConfiguration = await setupServer(serverOptions, generatedFiles, assetFiles, browserOptions.preserveSymlinks, externalMetadata, ssrMode, prebundleTransformer, target, (0, internal_1.isZonelessApp)(polyfills), componentStyles, templateUpdates, browserOptions.loader, extensions?.middleware, transformers?.indexHtml, thirdPartySourcemaps);
257
283
  server = await createServer(serverConfiguration);
258
284
  await server.listen();
259
285
  const urls = server.resolvedUrls;
@@ -269,6 +295,7 @@ async function* serveWithVite(serverOptions, builderName, builderAction, context
269
295
  key: 'r',
270
296
  description: 'force reload browser',
271
297
  action(server) {
298
+ componentStyles.forEach((record) => record.used?.clear());
272
299
  server.ws.send({
273
300
  type: 'full-reload',
274
301
  path: '*',
@@ -287,7 +314,7 @@ async function* serveWithVite(serverOptions, builderName, builderAction, context
287
314
  }
288
315
  await new Promise((resolve) => (deferred = resolve));
289
316
  }
290
- async function handleUpdate(normalizePath, generatedFiles, server, serverOptions, logger, usedComponentStyles) {
317
+ async function handleUpdate(normalizePath, generatedFiles, server, serverOptions, logger, componentStyles) {
291
318
  const updatedFiles = [];
292
319
  let destroyAngularServerAppCalled = false;
293
320
  // Invalidate any updated files
@@ -309,40 +336,54 @@ async function handleUpdate(normalizePath, generatedFiles, server, serverOptions
309
336
  if (!updatedFiles.length) {
310
337
  return;
311
338
  }
312
- if (serverOptions.liveReload || serverOptions.hmr) {
339
+ if (serverOptions.hmr) {
313
340
  if (updatedFiles.every((f) => f.endsWith('.css'))) {
341
+ let requiresReload = false;
314
342
  const timestamp = Date.now();
315
- server.ws.send({
316
- type: 'update',
317
- updates: updatedFiles.flatMap((filePath) => {
318
- // For component styles, an HMR update must be sent for each one with the corresponding
319
- // component identifier search parameter (`ngcomp`). The Vite client code will not keep
320
- // the existing search parameters when it performs an update and each one must be
321
- // specified explicitly. Typically, there is only one each though as specific style files
322
- // are not typically reused across components.
323
- const componentIds = usedComponentStyles.get(filePath);
324
- if (componentIds) {
325
- return componentIds.map((id) => ({
343
+ const updates = updatedFiles.flatMap((filePath) => {
344
+ // For component styles, an HMR update must be sent for each one with the corresponding
345
+ // component identifier search parameter (`ngcomp`). The Vite client code will not keep
346
+ // the existing search parameters when it performs an update and each one must be
347
+ // specified explicitly. Typically, there is only one each though as specific style files
348
+ // are not typically reused across components.
349
+ const record = componentStyles.get(filePath);
350
+ if (record) {
351
+ if (record.reload) {
352
+ // Shadow DOM components currently require a full reload.
353
+ // Vite's CSS hot replacement does not support shadow root searching.
354
+ requiresReload = true;
355
+ return [];
356
+ }
357
+ return Array.from(record.used ?? []).map((id) => {
358
+ return {
326
359
  type: 'css-update',
327
360
  timestamp,
328
- path: `${filePath}?ngcomp` + (id ? `=${id}` : ''),
361
+ path: `${filePath}?ngcomp` + (typeof id === 'string' ? `=${id}` : ''),
329
362
  acceptedPath: filePath,
330
- }));
331
- }
332
- return {
333
- type: 'css-update',
334
- timestamp,
335
- path: filePath,
336
- acceptedPath: filePath,
337
- };
338
- }),
363
+ };
364
+ });
365
+ }
366
+ return {
367
+ type: 'css-update',
368
+ timestamp,
369
+ path: filePath,
370
+ acceptedPath: filePath,
371
+ };
339
372
  });
340
- logger.info('HMR update sent to client(s).');
341
- return;
373
+ if (!requiresReload) {
374
+ server.ws.send({
375
+ type: 'update',
376
+ updates,
377
+ });
378
+ logger.info('HMR update sent to client(s).');
379
+ return;
380
+ }
342
381
  }
343
382
  }
344
383
  // Send reload command to clients
345
384
  if (serverOptions.liveReload) {
385
+ // Clear used component tracking on full reload
386
+ componentStyles.forEach((record) => record.used?.clear());
346
387
  server.ws.send({
347
388
  type: 'full-reload',
348
389
  path: '*',
@@ -350,7 +391,7 @@ async function handleUpdate(normalizePath, generatedFiles, server, serverOptions
350
391
  logger.info('Page reload sent to client(s).');
351
392
  }
352
393
  }
353
- function analyzeResultFiles(normalizePath, htmlIndexPath, resultFiles, generatedFiles) {
394
+ function analyzeResultFiles(normalizePath, htmlIndexPath, resultFiles, generatedFiles, componentStyles) {
354
395
  const seen = new Set(['/index.html']);
355
396
  for (const [outputPath, file] of Object.entries(resultFiles)) {
356
397
  if (file.origin === 'disk') {
@@ -373,6 +414,7 @@ function analyzeResultFiles(normalizePath, htmlIndexPath, resultFiles, generated
373
414
  contents: file.contents,
374
415
  servable,
375
416
  size: file.contents.byteLength,
417
+ hash: file.hash,
376
418
  type: file.type,
377
419
  updated: false,
378
420
  });
@@ -395,15 +437,28 @@ function analyzeResultFiles(normalizePath, htmlIndexPath, resultFiles, generated
395
437
  type: file.type,
396
438
  servable,
397
439
  });
440
+ // Record any external component styles
441
+ if (filePath.endsWith('.css') && /^\/[a-f0-9]{64}\.css$/.test(filePath)) {
442
+ const componentStyle = componentStyles.get(filePath);
443
+ if (componentStyle) {
444
+ componentStyle.rawContent = file.contents;
445
+ }
446
+ else {
447
+ componentStyles.set(filePath, {
448
+ rawContent: file.contents,
449
+ });
450
+ }
451
+ }
398
452
  }
399
453
  // Clear stale output files
400
454
  for (const file of generatedFiles.keys()) {
401
455
  if (!seen.has(file)) {
402
456
  generatedFiles.delete(file);
457
+ componentStyles.delete(file);
403
458
  }
404
459
  }
405
460
  }
406
- async function setupServer(serverOptions, outputFiles, assets, preserveSymlinks, externalMetadata, ssrMode, prebundleTransformer, target, zoneless, usedComponentStyles, prebundleLoaderExtensions, extensionMiddleware, indexHtmlTransformer, thirdPartySourcemaps = false) {
461
+ async function setupServer(serverOptions, outputFiles, assets, preserveSymlinks, externalMetadata, ssrMode, prebundleTransformer, target, zoneless, componentStyles, templateUpdates, prebundleLoaderExtensions, extensionMiddleware, indexHtmlTransformer, thirdPartySourcemaps = false) {
407
462
  const proxy = await (0, utils_1.loadProxyConfiguration)(serverOptions.workspaceRoot, serverOptions.proxyConfig);
408
463
  // dynamically import Vite for ESM compatibility
409
464
  const { normalizePath } = await (0, load_esm_1.loadEsmModule)('vite');
@@ -446,6 +501,8 @@ async function setupServer(serverOptions, outputFiles, assets, preserveSymlinks,
446
501
  host: serverOptions.host,
447
502
  open: serverOptions.open,
448
503
  headers: serverOptions.headers,
504
+ // Disable the websocket if live reload is disabled (false/undefined are the only valid values)
505
+ ws: serverOptions.liveReload === false && serverOptions.hmr === false ? false : undefined,
449
506
  proxy,
450
507
  cors: {
451
508
  // Allow preflight requests to be proxied.
@@ -495,7 +552,8 @@ async function setupServer(serverOptions, outputFiles, assets, preserveSymlinks,
495
552
  assets,
496
553
  indexHtmlTransformer,
497
554
  extensionMiddleware,
498
- usedComponentStyles,
555
+ componentStyles,
556
+ templateUpdates,
499
557
  ssrMode,
500
558
  }),
501
559
  (0, plugins_1.createRemoveIdPrefixPlugin)(externalMetadata.explicitBrowser),
@@ -504,6 +562,7 @@ async function setupServer(serverOptions, outputFiles, assets, preserveSymlinks,
504
562
  virtualProjectRoot,
505
563
  outputFiles,
506
564
  external: externalMetadata.explicitBrowser,
565
+ skipViteClient: serverOptions.liveReload === false && serverOptions.hmr === false,
507
566
  }),
508
567
  ],
509
568
  // Browser only optimizeDeps. (This does not run for SSR dependencies).
@@ -568,6 +627,9 @@ function getDepOptimizationConfig({ disabled, exclude, include, target, zoneless
568
627
  supported: (0, internal_1.getFeatureSupport)(target, zoneless),
569
628
  plugins,
570
629
  loader,
630
+ define: {
631
+ 'ngServerMode': `${ssr}`,
632
+ },
571
633
  resolveExtensions: ['.mjs', '.js', '.cjs'],
572
634
  },
573
635
  };
package/src/index.d.ts CHANGED
@@ -6,6 +6,7 @@
6
6
  * found in the LICENSE file at https://angular.dev/license
7
7
  */
8
8
  export { buildApplication, type ApplicationBuilderOptions, type ApplicationBuilderOutput, } from './builders/application';
9
+ export type { ApplicationBuilderExtensions } from './builders/application/options';
9
10
  export { type BuildOutputFile, BuildOutputFileType } from './tools/esbuild/bundler-context';
10
11
  export type { BuildOutputAsset } from './tools/esbuild/bundler-execution-result';
11
12
  export { executeDevServerBuilder, type DevServerBuilderOptions, type DevServerBuilderOutput, } from './builders/dev-server';
@@ -14,7 +14,7 @@ export interface AngularHostOptions {
14
14
  sourceFileCache?: Map<string, ts.SourceFile>;
15
15
  modifiedFiles?: Set<string>;
16
16
  externalStylesheets?: Map<string, string>;
17
- transformStylesheet(data: string, containingFile: string, stylesheetFile?: string, order?: number): Promise<string | null>;
17
+ transformStylesheet(data: string, containingFile: string, stylesheetFile?: string, order?: number, className?: string): Promise<string | null>;
18
18
  processWebWorker(workerFile: string, containingFile: string): string;
19
19
  }
20
20
  /**
@@ -113,16 +113,14 @@ function createAngularCompilerHost(typescript, compilerOptions, hostOptions) {
113
113
  if (data.trim().length === 0) {
114
114
  return { content: '' };
115
115
  }
116
- const result = await hostOptions.transformStylesheet(data, context.containingFile, context.resourceFile ?? undefined,
117
- // TODO: Remove once available in compiler-cli types
118
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
119
- context.order);
116
+ const result = await hostOptions.transformStylesheet(data, context.containingFile, context.resourceFile ?? undefined, context.order, context.className);
120
117
  return typeof result === 'string' ? { content: result } : null;
121
118
  };
122
119
  host.resourceNameToFileName = function (resourceName, containingFile) {
123
120
  const resolvedPath = node_path_1.default.join(node_path_1.default.dirname(containingFile), resourceName);
124
- // All resource names that have HTML file extensions are assumed to be templates
125
- if (resourceName.endsWith('.html') || !hostOptions.externalStylesheets) {
121
+ // All resource names that have template file extensions are assumed to be templates
122
+ // TODO: Update compiler to provide the resource type to avoid extension matching here.
123
+ if (!hostOptions.externalStylesheets || hasTemplateExtension(resolvedPath)) {
126
124
  return resolvedPath;
127
125
  }
128
126
  // For external stylesheets, create a unique identifier and store the mapping
@@ -150,3 +148,13 @@ function createAngularCompilerHost(typescript, compilerOptions, hostOptions) {
150
148
  }
151
149
  return host;
152
150
  }
151
+ function hasTemplateExtension(file) {
152
+ const extension = node_path_1.default.extname(file).toLowerCase();
153
+ switch (extension) {
154
+ case '.htm':
155
+ case '.html':
156
+ case '.svg':
157
+ return true;
158
+ }
159
+ return false;
160
+ }
@@ -31,6 +31,7 @@ export declare abstract class AngularCompilation {
31
31
  compilerOptions: ng.CompilerOptions;
32
32
  referencedFiles: readonly string[];
33
33
  externalStylesheets?: ReadonlyMap<string, string>;
34
+ templateUpdates?: ReadonlyMap<string, string>;
34
35
  }>;
35
36
  abstract emitAffectedFiles(): Iterable<EmitFileResult> | Promise<Iterable<EmitFileResult>>;
36
37
  protected abstract collectDiagnostics(modes: DiagnosticModes): Iterable<ts.Diagnostic> | Promise<Iterable<ts.Diagnostic>>;
@@ -16,6 +16,7 @@ export declare class AotCompilation extends AngularCompilation {
16
16
  compilerOptions: ng.CompilerOptions;
17
17
  referencedFiles: readonly string[];
18
18
  externalStylesheets?: ReadonlyMap<string, string>;
19
+ templateUpdates?: ReadonlyMap<string, string>;
19
20
  }>;
20
21
  collectDiagnostics(modes: DiagnosticModes): Iterable<ts.Diagnostic>;
21
22
  emitAffectedFiles(): Iterable<EmitFileResult>;
@@ -12,6 +12,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
13
  exports.AotCompilation = void 0;
14
14
  const node_assert_1 = __importDefault(require("node:assert"));
15
+ const node_path_1 = require("node:path");
15
16
  const typescript_1 = __importDefault(require("typescript"));
16
17
  const profiling_1 = require("../../esbuild/profiling");
17
18
  const angular_host_1 = require("../angular-host");
@@ -65,6 +66,33 @@ class AotCompilation extends angular_compilation_1.AngularCompilation {
65
66
  }
66
67
  const typeScriptProgram = typescript_1.default.createEmitAndSemanticDiagnosticsBuilderProgram(angularTypeScriptProgram, host, oldProgram, configurationDiagnostics);
67
68
  await (0, profiling_1.profileAsync)('NG_ANALYZE_PROGRAM', () => angularCompiler.analyzeAsync());
69
+ let templateUpdates;
70
+ if (compilerOptions['_enableHmr'] &&
71
+ hostOptions.modifiedFiles &&
72
+ hasOnlyTemplates(hostOptions.modifiedFiles)) {
73
+ const componentNodes = [...hostOptions.modifiedFiles].flatMap((file) => [
74
+ ...angularCompiler.getComponentsWithTemplateFile(file),
75
+ ]);
76
+ for (const node of componentNodes) {
77
+ if (!typescript_1.default.isClassDeclaration(node)) {
78
+ continue;
79
+ }
80
+ const componentFilename = node.getSourceFile().fileName;
81
+ let relativePath = (0, node_path_1.relative)(host.getCurrentDirectory(), componentFilename);
82
+ if (relativePath.startsWith('..')) {
83
+ relativePath = componentFilename;
84
+ }
85
+ const updateId = encodeURIComponent(`${host.getCanonicalFileName(relativePath)}@${node.name?.text}`);
86
+ const updateText = angularCompiler.emitHmrUpdateModule(node);
87
+ if (updateText === null) {
88
+ // Build is needed if a template cannot be updated
89
+ templateUpdates = undefined;
90
+ break;
91
+ }
92
+ templateUpdates ??= new Map();
93
+ templateUpdates.set(updateId, updateText);
94
+ }
95
+ }
68
96
  const affectedFiles = (0, profiling_1.profileSync)('NG_FIND_AFFECTED', () => findAffectedFiles(typeScriptProgram, angularCompiler, usingBuildInfo));
69
97
  // Get all files referenced in the TypeScript/Angular program including component resources
70
98
  const referencedFiles = typeScriptProgram
@@ -90,6 +118,7 @@ class AotCompilation extends angular_compilation_1.AngularCompilation {
90
118
  compilerOptions,
91
119
  referencedFiles,
92
120
  externalStylesheets: hostOptions.externalStylesheets,
121
+ templateUpdates,
93
122
  };
94
123
  }
95
124
  *collectDiagnostics(modes) {
@@ -267,3 +296,13 @@ function findAffectedFiles(builder, { ignoreForDiagnostics }, includeTTC) {
267
296
  }
268
297
  return affectedFiles;
269
298
  }
299
+ function hasOnlyTemplates(modifiedFiles) {
300
+ for (const file of modifiedFiles) {
301
+ const lowerFile = file.toLowerCase();
302
+ if (lowerFile.endsWith('.html') || lowerFile.endsWith('.svg')) {
303
+ continue;
304
+ }
305
+ return false;
306
+ }
307
+ return true;
308
+ }
@@ -37,9 +37,9 @@ class ParallelCompilation extends angular_compilation_1.AngularCompilation {
37
37
  initialize(tsconfig, hostOptions, compilerOptionsTransformer) {
38
38
  const stylesheetChannel = new node_worker_threads_1.MessageChannel();
39
39
  // The request identifier is required because Angular can issue multiple concurrent requests
40
- stylesheetChannel.port1.on('message', ({ requestId, data, containingFile, stylesheetFile }) => {
40
+ stylesheetChannel.port1.on('message', ({ requestId, data, containingFile, stylesheetFile, order, className }) => {
41
41
  hostOptions
42
- .transformStylesheet(data, containingFile, stylesheetFile)
42
+ .transformStylesheet(data, containingFile, stylesheetFile, order, className)
43
43
  .then((value) => stylesheetChannel.port1.postMessage({ requestId, value }))
44
44
  .catch((error) => stylesheetChannel.port1.postMessage({ requestId, error }));
45
45
  });
@@ -20,6 +20,7 @@ export interface InitRequest {
20
20
  }
21
21
  export declare function initialize(request: InitRequest): Promise<{
22
22
  externalStylesheets: ReadonlyMap<string, string> | undefined;
23
+ templateUpdates: ReadonlyMap<string, string> | undefined;
23
24
  referencedFiles: readonly string[];
24
25
  compilerOptions: {
25
26
  allowJs: boolean | undefined;
@@ -33,11 +33,11 @@ async function initialize(request) {
33
33
  stylesheetRequests.get(requestId)?.[0](value);
34
34
  }
35
35
  });
36
- const { compilerOptions, referencedFiles, externalStylesheets } = await compilation.initialize(request.tsconfig, {
36
+ const { compilerOptions, referencedFiles, externalStylesheets, templateUpdates } = await compilation.initialize(request.tsconfig, {
37
37
  fileReplacements: request.fileReplacements,
38
38
  sourceFileCache,
39
39
  modifiedFiles: sourceFileCache.modifiedFiles,
40
- transformStylesheet(data, containingFile, stylesheetFile) {
40
+ transformStylesheet(data, containingFile, stylesheetFile, order, className) {
41
41
  const requestId = (0, node_crypto_1.randomUUID)();
42
42
  const resultPromise = new Promise((resolve, reject) => stylesheetRequests.set(requestId, [resolve, reject]));
43
43
  request.stylesheetPort.postMessage({
@@ -45,6 +45,8 @@ async function initialize(request) {
45
45
  data,
46
46
  containingFile,
47
47
  stylesheetFile,
48
+ order,
49
+ className,
48
50
  });
49
51
  return resultPromise;
50
52
  },
@@ -70,6 +72,7 @@ async function initialize(request) {
70
72
  });
71
73
  return {
72
74
  externalStylesheets,
75
+ templateUpdates,
73
76
  referencedFiles,
74
77
  // TODO: Expand? `allowJs`, `isolatedModules`, `sourceMap`, `inlineSourceMap` are the only fields needed currently.
75
78
  compilerOptions: {
@@ -7,7 +7,7 @@
7
7
  */
8
8
  import type { Plugin } from 'esbuild';
9
9
  import { LoadResultCache } from '../load-result-cache';
10
- import { BundleStylesheetOptions } from '../stylesheets/bundle-options';
10
+ import { ComponentStylesheetBundler } from './component-stylesheets';
11
11
  import { SourceFileCache } from './source-file-cache';
12
12
  export interface CompilerPluginOptions {
13
13
  sourcemap: boolean | 'external';
@@ -23,7 +23,6 @@ export interface CompilerPluginOptions {
23
23
  incremental: boolean;
24
24
  externalRuntimeStyles?: boolean;
25
25
  instrumentForCoverage?: (request: string) => boolean;
26
+ templateUpdates?: Map<string, string>;
26
27
  }
27
- export declare function createCompilerPlugin(pluginOptions: CompilerPluginOptions, styleOptions: BundleStylesheetOptions & {
28
- inlineStyleLanguage: string;
29
- }): Plugin;
28
+ export declare function createCompilerPlugin(pluginOptions: CompilerPluginOptions, stylesheetBundler: ComponentStylesheetBundler): Plugin;