@angular/build 19.0.0-next.7 → 19.0.0-next.8

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 (71) hide show
  1. package/package.json +9 -9
  2. package/src/builders/application/execute-build.js +19 -2
  3. package/src/builders/application/execute-post-bundle.d.ts +2 -2
  4. package/src/builders/application/execute-post-bundle.js +30 -11
  5. package/src/builders/application/i18n.d.ts +2 -2
  6. package/src/builders/application/i18n.js +4 -5
  7. package/src/builders/application/index.js +8 -5
  8. package/src/builders/application/options.d.ts +18 -1
  9. package/src/builders/application/options.js +30 -2
  10. package/src/builders/application/schema.d.ts +15 -0
  11. package/src/builders/application/schema.js +11 -1
  12. package/src/builders/application/schema.json +5 -0
  13. package/src/builders/application/setup-bundling.js +6 -3
  14. package/src/builders/dev-server/vite-server.d.ts +2 -1
  15. package/src/builders/dev-server/vite-server.js +64 -47
  16. package/src/builders/extract-i18n/application-extraction.js +3 -3
  17. package/src/tools/angular/angular-host.d.ts +2 -1
  18. package/src/tools/angular/angular-host.js +20 -1
  19. package/src/tools/angular/compilation/angular-compilation.d.ts +1 -0
  20. package/src/tools/angular/compilation/aot-compilation.d.ts +1 -0
  21. package/src/tools/angular/compilation/aot-compilation.js +9 -1
  22. package/src/tools/angular/compilation/parallel-compilation.d.ts +1 -0
  23. package/src/tools/angular/compilation/parallel-worker.d.ts +1 -0
  24. package/src/tools/angular/compilation/parallel-worker.js +2 -1
  25. package/src/tools/esbuild/angular/compiler-plugin.d.ts +1 -0
  26. package/src/tools/esbuild/angular/compiler-plugin.js +42 -3
  27. package/src/tools/esbuild/angular/component-stylesheets.d.ts +8 -3
  28. package/src/tools/esbuild/angular/component-stylesheets.js +46 -11
  29. package/src/tools/esbuild/application-code-bundle.d.ts +1 -0
  30. package/src/tools/esbuild/application-code-bundle.js +109 -2
  31. package/src/tools/esbuild/budget-stats.js +1 -1
  32. package/src/tools/esbuild/bundler-context.d.ts +4 -3
  33. package/src/tools/esbuild/bundler-context.js +8 -4
  34. package/src/tools/esbuild/bundler-execution-result.d.ts +5 -2
  35. package/src/tools/esbuild/bundler-execution-result.js +7 -3
  36. package/src/tools/esbuild/cache.d.ts +5 -0
  37. package/src/tools/esbuild/cache.js +7 -0
  38. package/src/tools/esbuild/compiler-plugin-options.js +2 -1
  39. package/src/tools/esbuild/i18n-inliner.js +2 -1
  40. package/src/tools/esbuild/utils.js +7 -3
  41. package/src/tools/vite/middlewares/assets-middleware.js +2 -5
  42. package/src/tools/vite/middlewares/html-fallback-middleware.js +22 -6
  43. package/src/tools/vite/middlewares/index.d.ts +1 -1
  44. package/src/tools/vite/middlewares/index.js +3 -2
  45. package/src/tools/vite/middlewares/ssr-middleware.d.ts +2 -1
  46. package/src/tools/vite/middlewares/ssr-middleware.js +61 -15
  47. package/src/tools/vite/plugins/angular-memory-plugin.d.ts +16 -0
  48. package/src/tools/vite/{angular-memory-plugin.js → plugins/angular-memory-plugin.js} +19 -41
  49. package/src/tools/vite/{i18n-locale-plugin.d.ts → plugins/i18n-locale-plugin.d.ts} +0 -4
  50. package/src/tools/vite/{i18n-locale-plugin.js → plugins/i18n-locale-plugin.js} +2 -3
  51. package/src/tools/vite/plugins/index.d.ts +12 -0
  52. package/src/tools/vite/plugins/index.js +21 -0
  53. package/src/tools/vite/plugins/setup-middlewares-plugin.d.ts +41 -0
  54. package/src/tools/vite/plugins/setup-middlewares-plugin.js +62 -0
  55. package/src/tools/vite/plugins/ssr-transform-plugin.d.ts +9 -0
  56. package/src/tools/vite/plugins/ssr-transform-plugin.js +38 -0
  57. package/src/utils/environment-options.d.ts +2 -0
  58. package/src/utils/environment-options.js +5 -1
  59. package/src/utils/index-file/valid-self-closing-tags.js +1 -0
  60. package/src/utils/normalize-cache.js +1 -1
  61. package/src/utils/server-rendering/manifest.d.ts +8 -2
  62. package/src/utils/server-rendering/manifest.js +61 -12
  63. package/src/utils/server-rendering/models.d.ts +27 -0
  64. package/src/utils/server-rendering/models.js +22 -0
  65. package/src/utils/server-rendering/prerender.d.ts +6 -10
  66. package/src/utils/server-rendering/prerender.js +100 -63
  67. package/src/utils/server-rendering/routes-extractor-worker.d.ts +5 -10
  68. package/src/utils/server-rendering/routes-extractor-worker.js +3 -4
  69. package/src/tools/vite/angular-memory-plugin.d.ts +0 -22
  70. /package/src/tools/vite/{id-prefix-plugin.d.ts → plugins/id-prefix-plugin.d.ts} +0 -0
  71. /package/src/tools/vite/{id-prefix-plugin.js → plugins/id-prefix-plugin.js} +0 -0
@@ -39,10 +39,9 @@ const node_assert_1 = __importDefault(require("node:assert"));
39
39
  const promises_1 = require("node:fs/promises");
40
40
  const node_module_1 = require("node:module");
41
41
  const node_path_1 = require("node:path");
42
- const angular_memory_plugin_1 = require("../../tools/vite/angular-memory-plugin");
43
- const i18n_locale_plugin_1 = require("../../tools/vite/i18n-locale-plugin");
44
- const id_prefix_plugin_1 = require("../../tools/vite/id-prefix-plugin");
42
+ const plugins_1 = require("../../tools/vite/plugins");
45
43
  const utils_1 = require("../../utils");
44
+ const environment_options_1 = require("../../utils/environment-options");
46
45
  const load_esm_1 = require("../../utils/load-esm");
47
46
  const results_1 = require("../application/results");
48
47
  const internal_1 = require("./internal");
@@ -66,17 +65,16 @@ async function* serveWithVite(serverOptions, builderName, builderAction, context
66
65
  }
67
66
  // TODO: Adjust architect to not force a JsonObject derived return type
68
67
  const browserOptions = (await context.validateOptions(rawBrowserOptions, builderName));
69
- if (browserOptions.prerender || browserOptions.ssr) {
68
+ if (browserOptions.prerender) {
70
69
  // Disable prerendering if enabled and force SSR.
71
70
  // This is so instead of prerendering all the routes for every change, the page is "prerendered" when it is requested.
72
71
  browserOptions.prerender = false;
73
- // Avoid bundling and processing the ssr entry-point as this is not used by the dev-server.
74
- browserOptions.ssr = true;
75
- // https://nodejs.org/api/process.html#processsetsourcemapsenabledval
76
- process.setSourceMapsEnabled(true);
77
72
  }
78
73
  // Set all packages as external to support Vite's prebundle caching
79
74
  browserOptions.externalPackages = serverOptions.prebundle;
75
+ // Disable generating a full manifest with routes.
76
+ // This is done during runtime when using the dev-server.
77
+ browserOptions.partialSSRBuild = true;
80
78
  // The development server currently only supports a single locale when localizing.
81
79
  // This matches the behavior of the Webpack-based development server but could be expanded in the future.
82
80
  if (browserOptions.localize === true ||
@@ -88,7 +86,13 @@ async function* serveWithVite(serverOptions, builderName, builderAction, context
88
86
  // When localization is enabled with a single locale, force a flat path to maintain behavior with the existing Webpack-based dev server.
89
87
  browserOptions.forceI18nFlatOutput = true;
90
88
  }
91
- const { vendor: thirdPartySourcemaps } = (0, utils_1.normalizeSourceMaps)(browserOptions.sourceMap ?? false);
89
+ const { vendor: thirdPartySourcemaps, scripts: scriptsSourcemaps } = (0, utils_1.normalizeSourceMaps)(browserOptions.sourceMap ?? false);
90
+ if (scriptsSourcemaps && browserOptions.server) {
91
+ // https://nodejs.org/api/process.html#processsetsourcemapsenabledval
92
+ process.setSourceMapsEnabled(true);
93
+ }
94
+ // TODO: Enable by default once full support across CLI and FW is integrated
95
+ browserOptions.externalRuntimeStyles = environment_options_1.useComponentStyleHmr;
92
96
  // Setup the prebundling transformer that will be shared across Vite prebundling requests
93
97
  const prebundleTransformer = new internal_1.JavaScriptTransformer(
94
98
  // Always enable JIT linking to support applications built with and without AOT.
@@ -180,8 +184,8 @@ async function* serveWithVite(serverOptions, builderName, builderAction, context
180
184
  let requiresServerRestart = false;
181
185
  if (result.detail?.['externalMetadata']) {
182
186
  const { implicitBrowser, implicitServer, explicit } = result.detail['externalMetadata'];
183
- const implicitServerFiltered = implicitServer.filter((m) => removeNodeJsBuiltinModules(m) && removeAbsoluteUrls(m));
184
- const implicitBrowserFiltered = implicitBrowser.filter(removeAbsoluteUrls);
187
+ const implicitServerFiltered = implicitServer.filter((m) => !(0, node_module_1.isBuiltin)(m) && !isAbsoluteUrl(m));
188
+ const implicitBrowserFiltered = implicitBrowser.filter((m) => !isAbsoluteUrl(m));
185
189
  if (browserOptions.ssr && serverOptions.prebundle !== false) {
186
190
  const previousImplicitServer = new Set(externalMetadata.implicitServer);
187
191
  // Restart the server to force SSR dep re-optimization when a dependency has been added.
@@ -194,7 +198,7 @@ async function* serveWithVite(serverOptions, builderName, builderAction, context
194
198
  externalMetadata.implicitServer.length = 0;
195
199
  externalMetadata.implicitBrowser.length = 0;
196
200
  externalMetadata.explicitBrowser.push(...explicit);
197
- externalMetadata.explicitServer.push(...explicit, ...nodeJsBuiltinModules);
201
+ externalMetadata.explicitServer.push(...explicit, ...node_module_1.builtinModules);
198
202
  externalMetadata.implicitServer.push(...implicitServerFiltered);
199
203
  externalMetadata.implicitBrowser.push(...implicitBrowserFiltered);
200
204
  // The below needs to be sorted as Vite uses these options are part of the hashing invalidation algorithm.
@@ -238,15 +242,19 @@ async function* serveWithVite(serverOptions, builderName, builderAction, context
238
242
  const polyfills = Array.isArray((browserOptions.polyfills ??= []))
239
243
  ? browserOptions.polyfills
240
244
  : [browserOptions.polyfills];
245
+ let ssrMode = plugins_1.ServerSsrMode.NoSsr;
246
+ if (browserOptions.outputMode &&
247
+ typeof browserOptions.ssr === 'object' &&
248
+ browserOptions.ssr.entry) {
249
+ ssrMode = plugins_1.ServerSsrMode.ExternalSsrMiddleware;
250
+ }
251
+ else if (browserOptions.server) {
252
+ ssrMode = plugins_1.ServerSsrMode.InternalSsrMiddleware;
253
+ }
241
254
  // Setup server and start listening
242
- const serverConfiguration = await setupServer(serverOptions, generatedFiles, assetFiles, browserOptions.preserveSymlinks, externalMetadata, !!browserOptions.ssr, prebundleTransformer, target, (0, internal_1.isZonelessApp)(polyfills), usedComponentStyles, browserOptions.loader, extensions?.middleware, transformers?.indexHtml, thirdPartySourcemaps);
255
+ 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);
243
256
  server = await createServer(serverConfiguration);
244
257
  await server.listen();
245
- if (browserOptions.ssr && serverOptions.prebundle !== false) {
246
- // Warm up the SSR request and begin optimizing dependencies.
247
- // Without this, Vite will only start optimizing SSR modules when the first request is made.
248
- void server.warmupRequest('./main.server.mjs', { ssr: true });
249
- }
250
258
  const urls = server.resolvedUrls;
251
259
  if (urls && (urls.local.length || urls.network.length)) {
252
260
  serverUrl = new URL(urls.local[0] ?? urls.network[0]);
@@ -280,24 +288,26 @@ async function* serveWithVite(serverOptions, builderName, builderAction, context
280
288
  }
281
289
  async function handleUpdate(normalizePath, generatedFiles, server, serverOptions, logger, usedComponentStyles) {
282
290
  const updatedFiles = [];
283
- let isServerFileUpdated = false;
291
+ let destroyAngularServerAppCalled = false;
284
292
  // Invalidate any updated files
285
- for (const [file, record] of generatedFiles) {
286
- if (record.updated) {
287
- updatedFiles.push(file);
288
- isServerFileUpdated ||= record.type === internal_1.BuildOutputFileType.Server;
289
- const updatedModules = server.moduleGraph.getModulesByFile(normalizePath((0, node_path_1.join)(server.config.root, file)));
290
- updatedModules?.forEach((m) => server?.moduleGraph.invalidateModule(m));
293
+ for (const [file, { updated, type }] of generatedFiles) {
294
+ if (!updated) {
295
+ continue;
296
+ }
297
+ if (type === internal_1.BuildOutputFileType.ServerApplication && !destroyAngularServerAppCalled) {
298
+ // Clear the server app cache
299
+ // This must be done before module invalidation.
300
+ const { ɵdestroyAngularServerApp } = (await server.ssrLoadModule('/main.server.mjs'));
301
+ ɵdestroyAngularServerApp();
302
+ destroyAngularServerAppCalled = true;
291
303
  }
304
+ updatedFiles.push(file);
305
+ const updatedModules = server.moduleGraph.getModulesByFile(normalizePath((0, node_path_1.join)(server.config.root, file)));
306
+ updatedModules?.forEach((m) => server.moduleGraph.invalidateModule(m));
292
307
  }
293
308
  if (!updatedFiles.length) {
294
309
  return;
295
310
  }
296
- // clean server apps cache
297
- if (isServerFileUpdated) {
298
- const { ɵdestroyAngularServerApp } = (await server.ssrLoadModule('/main.server.mjs'));
299
- ɵdestroyAngularServerApp();
300
- }
301
311
  if (serverOptions.liveReload || serverOptions.hmr) {
302
312
  if (updatedFiles.every((f) => f.endsWith('.css'))) {
303
313
  const timestamp = Date.now();
@@ -392,7 +402,7 @@ function analyzeResultFiles(normalizePath, htmlIndexPath, resultFiles, generated
392
402
  }
393
403
  }
394
404
  }
395
- async function setupServer(serverOptions, outputFiles, assets, preserveSymlinks, externalMetadata, ssr, prebundleTransformer, target, zoneless, usedComponentStyles, prebundleLoaderExtensions, extensionMiddleware, indexHtmlTransformer, thirdPartySourcemaps = false) {
405
+ async function setupServer(serverOptions, outputFiles, assets, preserveSymlinks, externalMetadata, ssrMode, prebundleTransformer, target, zoneless, usedComponentStyles, prebundleLoaderExtensions, extensionMiddleware, indexHtmlTransformer, thirdPartySourcemaps = false) {
396
406
  const proxy = await (0, utils_1.loadProxyConfiguration)(serverOptions.workspaceRoot, serverOptions.proxyConfig);
397
407
  // dynamically import Vite for ESM compatibility
398
408
  const { normalizePath } = await (0, load_esm_1.loadEsmModule)('vite');
@@ -427,6 +437,9 @@ async function setupServer(serverOptions, outputFiles, assets, preserveSymlinks,
427
437
  preserveSymlinks,
428
438
  },
429
439
  server: {
440
+ warmup: {
441
+ ssrFiles: ['./main.server.mjs', './server.mjs'],
442
+ },
430
443
  port: serverOptions.port,
431
444
  strictPort: true,
432
445
  host: serverOptions.host,
@@ -475,20 +488,22 @@ async function setupServer(serverOptions, outputFiles, assets, preserveSymlinks,
475
488
  }),
476
489
  },
477
490
  plugins: [
478
- (0, i18n_locale_plugin_1.createAngularLocaleDataPlugin)(),
479
- (0, angular_memory_plugin_1.createAngularMemoryPlugin)({
480
- workspaceRoot: serverOptions.workspaceRoot,
481
- virtualProjectRoot,
491
+ (0, plugins_1.createAngularLocaleDataPlugin)(),
492
+ (0, plugins_1.createAngularSetupMiddlewaresPlugin)({
482
493
  outputFiles,
483
494
  assets,
484
- ssr,
485
- external: externalMetadata.explicitBrowser,
486
495
  indexHtmlTransformer,
487
496
  extensionMiddleware,
488
- normalizePath,
489
497
  usedComponentStyles,
498
+ ssrMode,
499
+ }),
500
+ (0, plugins_1.createRemoveIdPrefixPlugin)(externalMetadata.explicitBrowser),
501
+ await (0, plugins_1.createAngularSsrTransformPlugin)(serverOptions.workspaceRoot),
502
+ await (0, plugins_1.createAngularMemoryPlugin)({
503
+ virtualProjectRoot,
504
+ outputFiles,
505
+ external: externalMetadata.explicitBrowser,
490
506
  }),
491
- (0, id_prefix_plugin_1.createRemoveIdPrefixPlugin)(externalMetadata.explicitBrowser),
492
507
  ],
493
508
  // Browser only optimizeDeps. (This does not run for SSR dependencies).
494
509
  optimizeDeps: getDepOptimizationConfig({
@@ -556,12 +571,14 @@ function getDepOptimizationConfig({ disabled, exclude, include, target, zoneless
556
571
  },
557
572
  };
558
573
  }
559
- const nodeJsBuiltinModules = new Set(node_module_1.builtinModules);
560
- /** Remove any Node.js builtin modules to avoid Vite's prebundling from processing them as files. */
561
- function removeNodeJsBuiltinModules(value) {
562
- return !nodeJsBuiltinModules.has(value);
563
- }
564
- /** Remove any absolute URLs (http://, https://, //) to avoid Vite's prebundling from processing them as files. */
565
- function removeAbsoluteUrls(value) {
566
- return !/^(?:https?:)?\/\//.test(value);
574
+ /**
575
+ * Checks if the given value is an absolute URL.
576
+ *
577
+ * This function helps in avoiding Vite's prebundling from processing absolute URLs (http://, https://, //) as files.
578
+ *
579
+ * @param value - The URL or path to check.
580
+ * @returns `true` if the value is not an absolute URL; otherwise, `false`.
581
+ */
582
+ function isAbsoluteUrl(value) {
583
+ return /^(?:https?:)?\/\//.test(value);
567
584
  }
@@ -15,6 +15,7 @@ const node_fs_1 = require("node:fs");
15
15
  const node_path_1 = __importDefault(require("node:path"));
16
16
  const application_1 = require("../application");
17
17
  const results_1 = require("../application/results");
18
+ const schema_1 = require("../application/schema");
18
19
  async function extractMessages(options, builderName, context, extractorConstructor, extensions) {
19
20
  const messages = [];
20
21
  // Setup the build options for the application based on the buildTarget option
@@ -25,9 +26,8 @@ async function extractMessages(options, builderName, context, extractorConstruct
25
26
  buildOptions.budgets = undefined;
26
27
  buildOptions.index = false;
27
28
  buildOptions.serviceWorker = false;
28
- buildOptions.ssr = false;
29
- buildOptions.appShell = false;
30
- buildOptions.prerender = false;
29
+ buildOptions.outputMode = schema_1.OutputMode.Static;
30
+ buildOptions.server = undefined;
31
31
  // Build the application with the build options
32
32
  const builderResult = await first((0, application_1.buildApplicationInternal)(buildOptions, context, extensions));
33
33
  let success = false;
@@ -13,7 +13,8 @@ export interface AngularHostOptions {
13
13
  fileReplacements?: Record<string, string>;
14
14
  sourceFileCache?: Map<string, ts.SourceFile>;
15
15
  modifiedFiles?: Set<string>;
16
- transformStylesheet(data: string, containingFile: string, stylesheetFile?: string): Promise<string | null>;
16
+ externalStylesheets?: Map<string, string>;
17
+ transformStylesheet(data: string, containingFile: string, stylesheetFile?: string, order?: number): Promise<string | null>;
17
18
  processWebWorker(workerFile: string, containingFile: string): string;
18
19
  }
19
20
  /**
@@ -12,6 +12,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
13
  exports.ensureSourceFileVersions = ensureSourceFileVersions;
14
14
  exports.createAngularCompilerHost = createAngularCompilerHost;
15
+ const node_assert_1 = __importDefault(require("node:assert"));
15
16
  const node_crypto_1 = require("node:crypto");
16
17
  const node_path_1 = __importDefault(require("node:path"));
17
18
  /**
@@ -107,13 +108,31 @@ function createAngularCompilerHost(typescript, compilerOptions, hostOptions) {
107
108
  if (context.type !== 'style') {
108
109
  return null;
109
110
  }
111
+ (0, node_assert_1.default)(!context.resourceFile || !hostOptions.externalStylesheets?.has(context.resourceFile), 'External runtime stylesheets should not be transformed: ' + context.resourceFile);
110
112
  // No transformation required if the resource is empty
111
113
  if (data.trim().length === 0) {
112
114
  return { content: '' };
113
115
  }
114
- const result = await hostOptions.transformStylesheet(data, context.containingFile, context.resourceFile ?? undefined);
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);
115
120
  return typeof result === 'string' ? { content: result } : null;
116
121
  };
122
+ host.resourceNameToFileName = function (resourceName, containingFile) {
123
+ 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) {
126
+ return resolvedPath;
127
+ }
128
+ // For external stylesheets, create a unique identifier and store the mapping
129
+ let externalId = hostOptions.externalStylesheets.get(resolvedPath);
130
+ if (externalId === undefined) {
131
+ externalId = (0, node_crypto_1.createHash)('sha256').update(resolvedPath).digest('hex');
132
+ hostOptions.externalStylesheets.set(resolvedPath, externalId);
133
+ }
134
+ return externalId + '.css';
135
+ };
117
136
  // Allow the AOT compiler to request the set of changed templates and styles
118
137
  host.getModifiedResourceFiles = function () {
119
138
  return hostOptions.modifiedFiles;
@@ -30,6 +30,7 @@ export declare abstract class AngularCompilation {
30
30
  affectedFiles: ReadonlySet<ts.SourceFile>;
31
31
  compilerOptions: ng.CompilerOptions;
32
32
  referencedFiles: readonly string[];
33
+ externalStylesheets?: ReadonlyMap<string, string>;
33
34
  }>;
34
35
  abstract emitAffectedFiles(): Iterable<EmitFileResult> | Promise<Iterable<EmitFileResult>>;
35
36
  protected abstract collectDiagnostics(modes: DiagnosticModes): Iterable<ts.Diagnostic> | Promise<Iterable<ts.Diagnostic>>;
@@ -15,6 +15,7 @@ export declare class AotCompilation extends AngularCompilation {
15
15
  affectedFiles: ReadonlySet<ts.SourceFile>;
16
16
  compilerOptions: ng.CompilerOptions;
17
17
  referencedFiles: readonly string[];
18
+ externalStylesheets?: ReadonlyMap<string, string>;
18
19
  }>;
19
20
  collectDiagnostics(modes: DiagnosticModes): Iterable<ts.Diagnostic>;
20
21
  emitAffectedFiles(): Iterable<EmitFileResult>;
@@ -47,6 +47,9 @@ class AotCompilation extends angular_compilation_1.AngularCompilation {
47
47
  // Load the compiler configuration and transform as needed
48
48
  const { options: originalCompilerOptions, rootNames, errors: configurationDiagnostics, } = await this.loadConfiguration(tsconfig);
49
49
  const compilerOptions = compilerOptionsTransformer?.(originalCompilerOptions) ?? originalCompilerOptions;
50
+ if (compilerOptions.externalRuntimeStyles) {
51
+ hostOptions.externalStylesheets ??= new Map();
52
+ }
50
53
  // Create Angular compiler host
51
54
  const host = (0, angular_host_1.createAngularCompilerHost)(typescript_1.default, compilerOptions, hostOptions);
52
55
  // Create the Angular specific program that contains the Angular compiler
@@ -82,7 +85,12 @@ class AotCompilation extends angular_compilation_1.AngularCompilation {
82
85
  return [sourceFile.fileName, ...resourceDependencies];
83
86
  });
84
87
  this.#state = new AngularCompilationState(angularProgram, host, typeScriptProgram, affectedFiles, affectedFiles.size === 1 ? OptimizeFor.SingleFile : OptimizeFor.WholeProgram, (0, web_worker_transformer_1.createWorkerTransformer)(hostOptions.processWebWorker.bind(hostOptions)), this.#state?.diagnosticCache);
85
- return { affectedFiles, compilerOptions, referencedFiles };
88
+ return {
89
+ affectedFiles,
90
+ compilerOptions,
91
+ referencedFiles,
92
+ externalStylesheets: hostOptions.externalStylesheets,
93
+ };
86
94
  }
87
95
  *collectDiagnostics(modes) {
88
96
  (0, node_assert_1.default)(this.#state, 'Angular compilation must be initialized prior to collecting diagnostics.');
@@ -26,6 +26,7 @@ export declare class ParallelCompilation extends AngularCompilation {
26
26
  affectedFiles: ReadonlySet<SourceFile>;
27
27
  compilerOptions: CompilerOptions;
28
28
  referencedFiles: readonly string[];
29
+ externalStylesheets?: ReadonlyMap<string, string>;
29
30
  }>;
30
31
  /**
31
32
  * This is not needed with this compilation type since the worker will already send a response
@@ -19,6 +19,7 @@ export interface InitRequest {
19
19
  webWorkerSignal: Int32Array;
20
20
  }
21
21
  export declare function initialize(request: InitRequest): Promise<{
22
+ externalStylesheets: ReadonlyMap<string, string> | undefined;
22
23
  referencedFiles: readonly string[];
23
24
  compilerOptions: {
24
25
  allowJs: boolean | undefined;
@@ -33,7 +33,7 @@ async function initialize(request) {
33
33
  stylesheetRequests.get(requestId)?.[0](value);
34
34
  }
35
35
  });
36
- const { compilerOptions, referencedFiles } = await compilation.initialize(request.tsconfig, {
36
+ const { compilerOptions, referencedFiles, externalStylesheets } = await compilation.initialize(request.tsconfig, {
37
37
  fileReplacements: request.fileReplacements,
38
38
  sourceFileCache,
39
39
  modifiedFiles: sourceFileCache.modifiedFiles,
@@ -69,6 +69,7 @@ async function initialize(request) {
69
69
  return result?.transformedOptions ?? compilerOptions;
70
70
  });
71
71
  return {
72
+ externalStylesheets,
72
73
  referencedFiles,
73
74
  // TODO: Expand? `allowJs`, `isolatedModules`, `sourceMap`, `inlineSourceMap` are the only fields needed currently.
74
75
  compilerOptions: {
@@ -21,6 +21,7 @@ export interface CompilerPluginOptions {
21
21
  sourceFileCache?: SourceFileCache;
22
22
  loadResultCache?: LoadResultCache;
23
23
  incremental: boolean;
24
+ externalRuntimeStyles?: boolean;
24
25
  }
25
26
  export declare function createCompilerPlugin(pluginOptions: CompilerPluginOptions, styleOptions: BundleStylesheetOptions & {
26
27
  inlineStyleLanguage: string;
@@ -35,6 +35,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.createCompilerPlugin = createCompilerPlugin;
37
37
  const node_assert_1 = __importDefault(require("node:assert"));
38
+ const node_crypto_1 = require("node:crypto");
38
39
  const path = __importStar(require("node:path"));
39
40
  const environment_options_1 = require("../../../utils/environment-options");
40
41
  const compilation_1 = require("../../angular/compilation");
@@ -120,13 +121,14 @@ function createCompilerPlugin(pluginOptions, styleOptions) {
120
121
  // Angular compiler which does not have direct knowledge of transitive resource
121
122
  // dependencies or web worker processing.
122
123
  let modifiedFiles;
124
+ let invalidatedStylesheetEntries;
123
125
  if (pluginOptions.sourceFileCache?.modifiedFiles.size &&
124
126
  referencedFileTracker &&
125
127
  !pluginOptions.noopTypeScriptCompilation) {
126
128
  // TODO: Differentiate between changed input files and stale output files
127
129
  modifiedFiles = referencedFileTracker.update(pluginOptions.sourceFileCache.modifiedFiles);
128
130
  pluginOptions.sourceFileCache.invalidate(modifiedFiles);
129
- stylesheetBundler.invalidate(modifiedFiles);
131
+ invalidatedStylesheetEntries = stylesheetBundler.invalidate(modifiedFiles);
130
132
  }
131
133
  if (!pluginOptions.noopTypeScriptCompilation &&
132
134
  compilation.update &&
@@ -138,7 +140,7 @@ function createCompilerPlugin(pluginOptions, styleOptions) {
138
140
  fileReplacements: pluginOptions.fileReplacements,
139
141
  modifiedFiles,
140
142
  sourceFileCache: pluginOptions.sourceFileCache,
141
- async transformStylesheet(data, containingFile, stylesheetFile) {
143
+ async transformStylesheet(data, containingFile, stylesheetFile, order) {
142
144
  let stylesheetResult;
143
145
  // Stylesheet file only exists for external stylesheets
144
146
  if (stylesheetFile) {
@@ -147,7 +149,17 @@ function createCompilerPlugin(pluginOptions, styleOptions) {
147
149
  else {
148
150
  stylesheetResult = await stylesheetBundler.bundleInline(data, containingFile,
149
151
  // Inline stylesheets from a template style element are always CSS
150
- containingFile.endsWith('.html') ? 'css' : styleOptions.inlineStyleLanguage);
152
+ containingFile.endsWith('.html') ? 'css' : styleOptions.inlineStyleLanguage,
153
+ // When external runtime styles are enabled, an identifier for the style that does not change
154
+ // based on the content is required to avoid emitted JS code changes. Any JS code changes will
155
+ // invalid the output and force a full page reload for HMR cases. The containing file and order
156
+ // of the style within the containing file is used.
157
+ pluginOptions.externalRuntimeStyles
158
+ ? (0, node_crypto_1.createHash)('sha-256')
159
+ .update(containingFile)
160
+ .update((order ?? 0).toString())
161
+ .digest('hex')
162
+ : undefined);
151
163
  }
152
164
  const { contents, outputFiles, metafile, referencedFiles, errors, warnings } = stylesheetResult;
153
165
  if (errors) {
@@ -201,6 +213,7 @@ function createCompilerPlugin(pluginOptions, styleOptions) {
201
213
  // Initialize the Angular compilation for the current build.
202
214
  // In watch mode, previous build state will be reused.
203
215
  let referencedFiles;
216
+ let externalStylesheets;
204
217
  try {
205
218
  const initializationResult = await compilation.initialize(pluginOptions.tsconfig, hostOptions, createCompilerOptionsTransformer(setupWarnings, pluginOptions, preserveSymlinks));
206
219
  shouldTsIgnoreJs = !initializationResult.compilerOptions.allowJs;
@@ -211,6 +224,7 @@ function createCompilerPlugin(pluginOptions, styleOptions) {
211
224
  !!initializationResult.compilerOptions.sourceMap ||
212
225
  !!initializationResult.compilerOptions.inlineSourceMap;
213
226
  referencedFiles = initializationResult.referencedFiles;
227
+ externalStylesheets = initializationResult.externalStylesheets;
214
228
  }
215
229
  catch (error) {
216
230
  (result.errors ??= []).push({
@@ -231,6 +245,19 @@ function createCompilerPlugin(pluginOptions, styleOptions) {
231
245
  hasCompilationErrors = await sharedTSCompilationState.waitUntilReady;
232
246
  return result;
233
247
  }
248
+ if (externalStylesheets) {
249
+ // Process any new external stylesheets
250
+ for (const [stylesheetFile, externalId] of externalStylesheets) {
251
+ await bundleExternalStylesheet(stylesheetBundler, stylesheetFile, externalId, result, additionalResults);
252
+ }
253
+ // Process any updated stylesheets
254
+ if (invalidatedStylesheetEntries) {
255
+ for (const stylesheetFile of invalidatedStylesheetEntries) {
256
+ // externalId is already linked in the bundler context so only enabling is required here
257
+ await bundleExternalStylesheet(stylesheetBundler, stylesheetFile, true, result, additionalResults);
258
+ }
259
+ }
260
+ }
234
261
  // Update TypeScript file output cache for all affected files
235
262
  try {
236
263
  await (0, profiling_1.profileAsync)('NG_EMIT_TS', async () => {
@@ -376,6 +403,17 @@ function createCompilerPlugin(pluginOptions, styleOptions) {
376
403
  },
377
404
  };
378
405
  }
406
+ async function bundleExternalStylesheet(stylesheetBundler, stylesheetFile, externalId, result, additionalResults) {
407
+ const { outputFiles, metafile, errors, warnings } = await stylesheetBundler.bundleFile(stylesheetFile, externalId);
408
+ if (errors) {
409
+ (result.errors ??= []).push(...errors);
410
+ }
411
+ (result.warnings ??= []).push(...warnings);
412
+ additionalResults.set(stylesheetFile, {
413
+ outputFiles,
414
+ metafile,
415
+ });
416
+ }
379
417
  function createCompilerOptionsTransformer(setupWarnings, pluginOptions, preserveSymlinks) {
380
418
  return (compilerOptions) => {
381
419
  // target of 9 is ES2022 (using the number avoids an expensive import of typescript just for an enum)
@@ -433,6 +471,7 @@ function createCompilerOptionsTransformer(setupWarnings, pluginOptions, preserve
433
471
  mapRoot: undefined,
434
472
  sourceRoot: undefined,
435
473
  preserveSymlinks,
474
+ externalRuntimeStyles: pluginOptions.externalRuntimeStyles,
436
475
  };
437
476
  };
438
477
  }
@@ -22,7 +22,7 @@ export declare class ComponentStylesheetBundler {
22
22
  * @param cache A load result cache to use when bundling.
23
23
  */
24
24
  constructor(options: BundleStylesheetOptions, incremental: boolean);
25
- bundleFile(entry: string): Promise<{
25
+ bundleFile(entry: string, externalId?: string | boolean): Promise<{
26
26
  errors: import("esbuild").Message[] | undefined;
27
27
  warnings: import("esbuild").Message[];
28
28
  contents: string;
@@ -30,7 +30,7 @@ export declare class ComponentStylesheetBundler {
30
30
  metafile: import("esbuild").Metafile | undefined;
31
31
  referencedFiles: Set<string> | undefined;
32
32
  }>;
33
- bundleInline(data: string, filename: string, language: string): Promise<{
33
+ bundleInline(data: string, filename: string, language: string, externalId?: string): Promise<{
34
34
  errors: import("esbuild").Message[] | undefined;
35
35
  warnings: import("esbuild").Message[];
36
36
  contents: string;
@@ -38,7 +38,12 @@ export declare class ComponentStylesheetBundler {
38
38
  metafile: import("esbuild").Metafile | undefined;
39
39
  referencedFiles: Set<string> | undefined;
40
40
  }>;
41
- invalidate(files: Iterable<string>): void;
41
+ /**
42
+ * Invalidates both file and inline based component style bundling state for a set of modified files.
43
+ * @param files The group of files that have been modified
44
+ * @returns An array of file based stylesheet entries if any were invalidated; otherwise, undefined.
45
+ */
46
+ invalidate(files: Iterable<string>): string[] | undefined;
42
47
  dispose(): Promise<void>;
43
48
  private extractResult;
44
49
  }
@@ -11,6 +11,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
11
11
  };
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
13
  exports.ComponentStylesheetBundler = void 0;
14
+ const node_assert_1 = __importDefault(require("node:assert"));
14
15
  const node_crypto_1 = require("node:crypto");
15
16
  const node_path_1 = __importDefault(require("node:path"));
16
17
  const bundler_context_1 = require("../bundler-context");
@@ -35,21 +36,31 @@ class ComponentStylesheetBundler {
35
36
  this.options = options;
36
37
  this.incremental = incremental;
37
38
  }
38
- async bundleFile(entry) {
39
+ async bundleFile(entry, externalId) {
39
40
  const bundlerContext = await this.#fileContexts.getOrCreate(entry, () => {
40
41
  return new bundler_context_1.BundlerContext(this.options.workspaceRoot, this.incremental, (loadCache) => {
41
42
  const buildOptions = (0, bundle_options_1.createStylesheetBundleOptions)(this.options, loadCache);
42
- buildOptions.entryPoints = [entry];
43
+ if (externalId) {
44
+ (0, node_assert_1.default)(typeof externalId === 'string', 'Initial external component stylesheets must have a string identifier');
45
+ buildOptions.entryPoints = { [externalId]: entry };
46
+ delete buildOptions.publicPath;
47
+ }
48
+ else {
49
+ buildOptions.entryPoints = [entry];
50
+ }
43
51
  return buildOptions;
44
52
  });
45
53
  });
46
- return this.extractResult(await bundlerContext.bundle(), bundlerContext.watchFiles);
54
+ return this.extractResult(await bundlerContext.bundle(), bundlerContext.watchFiles, !!externalId);
47
55
  }
48
- async bundleInline(data, filename, language) {
56
+ async bundleInline(data, filename, language, externalId) {
49
57
  // Use a hash of the inline stylesheet content to ensure a consistent identifier. External stylesheets will resolve
50
58
  // to the actual stylesheet file path.
51
59
  // TODO: Consider xxhash instead for hashing
52
- const id = (0, node_crypto_1.createHash)('sha256').update(data).digest('hex');
60
+ const id = (0, node_crypto_1.createHash)('sha256')
61
+ .update(data)
62
+ .update(externalId ?? '')
63
+ .digest('hex');
53
64
  const entry = [language, id, filename].join(';');
54
65
  const bundlerContext = await this.#inlineContexts.getOrCreate(entry, () => {
55
66
  const namespace = 'angular:styles/component';
@@ -57,7 +68,13 @@ class ComponentStylesheetBundler {
57
68
  const buildOptions = (0, bundle_options_1.createStylesheetBundleOptions)(this.options, loadCache, {
58
69
  [entry]: data,
59
70
  });
60
- buildOptions.entryPoints = [`${namespace};${entry}`];
71
+ if (externalId) {
72
+ buildOptions.entryPoints = { [externalId]: `${namespace};${entry}` };
73
+ delete buildOptions.publicPath;
74
+ }
75
+ else {
76
+ buildOptions.entryPoints = [`${namespace};${entry}`];
77
+ }
61
78
  buildOptions.plugins.push({
62
79
  name: 'angular-component-styles',
63
80
  setup(build) {
@@ -83,19 +100,29 @@ class ComponentStylesheetBundler {
83
100
  });
84
101
  });
85
102
  // Extract the result of the bundling from the output files
86
- return this.extractResult(await bundlerContext.bundle(), bundlerContext.watchFiles);
103
+ return this.extractResult(await bundlerContext.bundle(), bundlerContext.watchFiles, !!externalId);
87
104
  }
105
+ /**
106
+ * Invalidates both file and inline based component style bundling state for a set of modified files.
107
+ * @param files The group of files that have been modified
108
+ * @returns An array of file based stylesheet entries if any were invalidated; otherwise, undefined.
109
+ */
88
110
  invalidate(files) {
89
111
  if (!this.incremental) {
90
112
  return;
91
113
  }
92
114
  const normalizedFiles = [...files].map(node_path_1.default.normalize);
93
- for (const bundler of this.#fileContexts.values()) {
94
- bundler.invalidate(normalizedFiles);
115
+ let entries;
116
+ for (const [entry, bundler] of this.#fileContexts.entries()) {
117
+ if (bundler.invalidate(normalizedFiles)) {
118
+ entries ??= [];
119
+ entries.push(entry);
120
+ }
95
121
  }
96
122
  for (const bundler of this.#inlineContexts.values()) {
97
123
  bundler.invalidate(normalizedFiles);
98
124
  }
125
+ return entries;
99
126
  }
100
127
  async dispose() {
101
128
  const contexts = [...this.#fileContexts.values(), ...this.#inlineContexts.values()];
@@ -103,7 +130,7 @@ class ComponentStylesheetBundler {
103
130
  this.#inlineContexts.clear();
104
131
  await Promise.allSettled(contexts.map((context) => context.dispose()));
105
132
  }
106
- extractResult(result, referencedFiles) {
133
+ extractResult(result, referencedFiles, external) {
107
134
  let contents = '';
108
135
  let metafile;
109
136
  const outputFiles = [];
@@ -122,7 +149,15 @@ class ComponentStylesheetBundler {
122
149
  outputFiles.push(clonedOutputFile);
123
150
  }
124
151
  else if (filename.endsWith('.css')) {
125
- contents = outputFile.text;
152
+ if (external) {
153
+ const clonedOutputFile = outputFile.clone();
154
+ clonedOutputFile.path = node_path_1.default.join(this.options.workspaceRoot, outputFile.path);
155
+ outputFiles.push(clonedOutputFile);
156
+ contents = node_path_1.default.posix.join(this.options.publicPath ?? '', filename);
157
+ }
158
+ else {
159
+ contents = outputFile.text;
160
+ }
126
161
  }
127
162
  else {
128
163
  throw new Error(`Unexpected non CSS/Media file "${filename}" outputted during component stylesheet processing.`);
@@ -13,3 +13,4 @@ export declare function createBrowserCodeBundleOptions(options: NormalizedApplic
13
13
  export declare function createBrowserPolyfillBundleOptions(options: NormalizedApplicationBuildOptions, target: string[], sourceFileCache?: SourceFileCache): BuildOptions | BundlerOptionsFactory | undefined;
14
14
  export declare function createServerPolyfillBundleOptions(options: NormalizedApplicationBuildOptions, target: string[], sourceFileCache?: SourceFileCache): BundlerOptionsFactory | undefined;
15
15
  export declare function createServerMainCodeBundleOptions(options: NormalizedApplicationBuildOptions, target: string[], sourceFileCache: SourceFileCache): BuildOptions;
16
+ export declare function createSsrEntryCodeBundleOptions(options: NormalizedApplicationBuildOptions, target: string[], sourceFileCache: SourceFileCache): BuildOptions;