@angular/build 19.0.0-next.1 → 19.0.0-next.3

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 (38) hide show
  1. package/package.json +10 -10
  2. package/src/builders/application/execute-post-bundle.js +29 -18
  3. package/src/builders/application/i18n.js +2 -11
  4. package/src/builders/application/options.d.ts +11 -0
  5. package/src/builders/application/options.js +23 -1
  6. package/src/builders/application/setup-bundling.js +7 -7
  7. package/src/builders/dev-server/internal.d.ts +0 -1
  8. package/src/builders/dev-server/internal.js +1 -3
  9. package/src/builders/dev-server/vite-server.d.ts +2 -1
  10. package/src/builders/dev-server/vite-server.js +13 -2
  11. package/src/tools/esbuild/angular/file-reference-tracker.d.ts +1 -1
  12. package/src/tools/esbuild/application-code-bundle.d.ts +1 -6
  13. package/src/tools/esbuild/application-code-bundle.js +105 -70
  14. package/src/tools/esbuild/bundler-context.js +14 -10
  15. package/src/tools/esbuild/cache.d.ts +1 -1
  16. package/src/tools/esbuild/utils.d.ts +9 -0
  17. package/src/tools/esbuild/utils.js +14 -0
  18. package/src/tools/sass/sass-service.js +9 -4
  19. package/src/tools/vite/angular-memory-plugin.js +2 -2
  20. package/src/tools/vite/middlewares/ssr-middleware.d.ts +1 -4
  21. package/src/tools/vite/middlewares/ssr-middleware.js +25 -38
  22. package/src/typings.d.ts +19 -0
  23. package/src/utils/index-file/inline-critical-css.js +43 -33
  24. package/src/utils/normalize-cache.js +1 -1
  25. package/src/utils/server-rendering/fetch-patch.js +4 -5
  26. package/src/utils/server-rendering/load-esm-from-memory.d.ts +12 -2
  27. package/src/utils/server-rendering/manifest.d.ts +44 -0
  28. package/src/utils/server-rendering/manifest.js +88 -0
  29. package/src/utils/server-rendering/prerender.d.ts +22 -2
  30. package/src/utils/server-rendering/prerender.js +51 -40
  31. package/src/utils/server-rendering/render-worker.d.ts +7 -8
  32. package/src/utils/server-rendering/render-worker.js +10 -13
  33. package/src/utils/server-rendering/routes-extractor-worker.d.ts +2 -6
  34. package/src/utils/server-rendering/routes-extractor-worker.js +3 -34
  35. package/src/utils/server-rendering/main-bundle-exports.d.ts +0 -27
  36. package/src/utils/server-rendering/main-bundle-exports.js +0 -9
  37. package/src/utils/server-rendering/render-page.d.ts +0 -26
  38. package/src/utils/server-rendering/render-page.js +0 -114
@@ -12,12 +12,13 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
12
12
  Object.defineProperty(exports, "__esModule", { value: true });
13
13
  exports.createBrowserCodeBundleOptions = createBrowserCodeBundleOptions;
14
14
  exports.createBrowserPolyfillBundleOptions = createBrowserPolyfillBundleOptions;
15
- exports.createServerCodeBundleOptions = createServerCodeBundleOptions;
16
15
  exports.createServerPolyfillBundleOptions = createServerPolyfillBundleOptions;
16
+ exports.createServerMainCodeBundleOptions = createServerMainCodeBundleOptions;
17
17
  const node_assert_1 = __importDefault(require("node:assert"));
18
18
  const node_crypto_1 = require("node:crypto");
19
19
  const node_path_1 = require("node:path");
20
20
  const environment_options_1 = require("../../utils/environment-options");
21
+ const manifest_1 = require("../../utils/server-rendering/manifest");
21
22
  const compiler_plugin_1 = require("./angular/compiler-plugin");
22
23
  const compiler_plugin_options_1 = require("./compiler-plugin-options");
23
24
  const external_packages_plugin_1 = require("./external-packages-plugin");
@@ -110,29 +111,28 @@ function createBrowserPolyfillBundleOptions(options, target, sourceFileCache) {
110
111
  // cannot be used with fully incremental bundling yet.
111
112
  return hasTypeScriptEntries ? buildOptions : () => buildOptions;
112
113
  }
113
- /**
114
- * Create an esbuild 'build' options object for the server bundle.
115
- * @param options The builder's user-provider normalized options.
116
- * @returns An esbuild BuildOptions object.
117
- */
118
- function createServerCodeBundleOptions(options, target, sourceFileCache) {
119
- const { serverEntryPoint, workspaceRoot, ssrOptions, watch, externalPackages, prerenderOptions, polyfills, } = options;
120
- (0, node_assert_1.default)(serverEntryPoint, 'createServerCodeBundleOptions should not be called without a defined serverEntryPoint.');
121
- const { pluginOptions, styleOptions } = (0, compiler_plugin_options_1.createCompilerPluginOptions)(options, target, sourceFileCache);
122
- const mainServerNamespace = 'angular:server-render-utils';
123
- const entryPoints = {
124
- 'render-utils.server': mainServerNamespace,
125
- 'main.server': serverEntryPoint,
126
- };
127
- const ssrEntryPoint = ssrOptions?.entry;
128
- if (ssrEntryPoint) {
129
- entryPoints['server'] = ssrEntryPoint;
114
+ function createServerPolyfillBundleOptions(options, target, sourceFileCache) {
115
+ const serverPolyfills = [];
116
+ const polyfillsFromConfig = new Set(options.polyfills);
117
+ if (!(0, utils_1.isZonelessApp)(options.polyfills)) {
118
+ serverPolyfills.push('zone.js/node');
119
+ }
120
+ if (polyfillsFromConfig.has('@angular/localize') ||
121
+ polyfillsFromConfig.has('@angular/localize/init')) {
122
+ serverPolyfills.push('@angular/localize/init');
123
+ }
124
+ serverPolyfills.push('@angular/platform-server/init');
125
+ const namespace = 'angular:polyfills-server';
126
+ const polyfillBundleOptions = getEsBuildCommonPolyfillsOptions({
127
+ ...options,
128
+ polyfills: serverPolyfills,
129
+ }, namespace, false, sourceFileCache);
130
+ if (!polyfillBundleOptions) {
131
+ return;
130
132
  }
131
- const zoneless = (0, utils_1.isZonelessApp)(polyfills);
132
133
  const buildOptions = {
133
- ...getEsBuildCommonOptions(options),
134
+ ...polyfillBundleOptions,
134
135
  platform: 'node',
135
- splitting: true,
136
136
  outExtension: { '.js': '.mjs' },
137
137
  // Note: `es2015` is needed for RxJS v6. If not specified, `module` would
138
138
  // match and the ES5 distribution would be bundled and ends up breaking at
@@ -140,14 +140,45 @@ function createServerCodeBundleOptions(options, target, sourceFileCache) {
140
140
  // More details: https://github.com/angular/angular-cli/issues/25405.
141
141
  mainFields: ['es2020', 'es2015', 'module', 'main'],
142
142
  entryNames: '[name]',
143
- target,
144
143
  banner: {
145
- js: `import './polyfills.server.mjs';`,
144
+ js: [
145
+ // Note: Needed as esbuild does not provide require shims / proxy from ESModules.
146
+ // See: https://github.com/evanw/esbuild/issues/1921.
147
+ `import { createRequire } from 'node:module';`,
148
+ `globalThis['require'] ??= createRequire(import.meta.url);`,
149
+ ].join('\n'),
150
+ },
151
+ target,
152
+ entryPoints: {
153
+ 'polyfills.server': namespace,
146
154
  },
155
+ };
156
+ return () => buildOptions;
157
+ }
158
+ function createServerMainCodeBundleOptions(options, target, sourceFileCache) {
159
+ const { serverEntryPoint: mainServerEntryPoint, workspaceRoot, externalPackages, ssrOptions, polyfills, } = options;
160
+ (0, node_assert_1.default)(mainServerEntryPoint, 'createServerCodeBundleOptions should not be called without a defined serverEntryPoint.');
161
+ const { pluginOptions, styleOptions } = (0, compiler_plugin_options_1.createCompilerPluginOptions)(options, target, sourceFileCache);
162
+ const mainServerNamespace = 'angular:main-server';
163
+ const mainServerInjectPolyfillsNamespace = 'angular:main-server-inject-polyfills';
164
+ const mainServerInjectManifestNamespace = 'angular:main-server-inject-manifest';
165
+ const zoneless = (0, utils_1.isZonelessApp)(polyfills);
166
+ const entryPoints = {
167
+ 'main.server': mainServerNamespace,
168
+ };
169
+ const ssrEntryPoint = ssrOptions?.entry;
170
+ if (ssrEntryPoint) {
171
+ // Old behavior: 'server.ts' was bundled together with the SSR (Server-Side Rendering) code.
172
+ // This approach combined server-side logic and rendering into a single bundle.
173
+ entryPoints['server'] = ssrEntryPoint;
174
+ }
175
+ const buildOptions = {
176
+ ...getEsBuildServerCommonOptions(options),
177
+ target,
178
+ inject: [mainServerInjectPolyfillsNamespace, mainServerInjectManifestNamespace],
147
179
  entryPoints,
148
180
  supported: (0, utils_1.getFeatureSupport)(target, zoneless),
149
181
  plugins: [
150
- (0, loader_import_attribute_plugin_1.createLoaderImportAttributePlugin)(),
151
182
  (0, wasm_plugin_1.createWasmPlugin)({ allowAsync: zoneless, cache: sourceFileCache?.loadResultCache }),
152
183
  (0, sourcemap_ignorelist_plugin_1.createSourcemapIgnorelistPlugin)(),
153
184
  (0, compiler_plugin_1.createCompilerPlugin)(
@@ -164,20 +195,49 @@ function createServerCodeBundleOptions(options, target, sourceFileCache) {
164
195
  else {
165
196
  buildOptions.plugins.push((0, rxjs_esm_resolution_plugin_1.createRxjsEsmResolutionPlugin)());
166
197
  }
198
+ // Mark manifest and polyfills file as external as these are generated by a different bundle step.
199
+ (buildOptions.external ??= []).push(...utils_1.SERVER_GENERATED_EXTERNALS);
167
200
  buildOptions.plugins.push((0, virtual_module_plugin_1.createVirtualModulePlugin)({
201
+ namespace: mainServerInjectPolyfillsNamespace,
202
+ cache: sourceFileCache?.loadResultCache,
203
+ loadContent: () => ({
204
+ contents: `import './polyfills.server.mjs';`,
205
+ loader: 'js',
206
+ resolveDir: workspaceRoot,
207
+ }),
208
+ }), (0, virtual_module_plugin_1.createVirtualModulePlugin)({
209
+ namespace: mainServerInjectManifestNamespace,
210
+ cache: sourceFileCache?.loadResultCache,
211
+ loadContent: async () => {
212
+ const contents = [
213
+ // Configure `@angular/ssr` manifest.
214
+ `import manifest from './${manifest_1.SERVER_APP_MANIFEST_FILENAME}';`,
215
+ `import { ɵsetAngularAppManifest } from '@angular/ssr';`,
216
+ `ɵsetAngularAppManifest(manifest);`,
217
+ ];
218
+ return {
219
+ contents: contents.join('\n'),
220
+ loader: 'js',
221
+ resolveDir: workspaceRoot,
222
+ };
223
+ },
224
+ }), (0, virtual_module_plugin_1.createVirtualModulePlugin)({
168
225
  namespace: mainServerNamespace,
169
226
  cache: sourceFileCache?.loadResultCache,
170
227
  loadContent: async () => {
228
+ const mainServerEntryPointJsImport = entryFileToWorkspaceRelative(workspaceRoot, mainServerEntryPoint);
171
229
  const contents = [
172
- `export { ɵConsole } from '@angular/core';`,
173
- `export { renderApplication, renderModule, ɵSERVER_CONTEXT } from '@angular/platform-server';`,
230
+ // Re-export all symbols including default export from 'main.server.ts'
231
+ `export { default } from '${mainServerEntryPointJsImport}';`,
232
+ `export * from '${mainServerEntryPointJsImport}';`,
233
+ // Add @angular/ssr exports
234
+ `export {
235
+ ɵServerRenderContext,
236
+ ɵdestroyAngularServerApp,
237
+ ɵextractRoutesAndCreateRouteTree,
238
+ ɵgetOrCreateAngularServerApp,
239
+ } from '@angular/ssr';`,
174
240
  ];
175
- if (watch) {
176
- contents.push(`export { ɵresetCompiledComponents } from '@angular/core';`);
177
- }
178
- if (prerenderOptions?.discoverRoutes) {
179
- contents.push(`export { ɵgetRoutesFromAngularRouterConfig } from '@angular/ssr';`);
180
- }
181
241
  return {
182
242
  contents: contents.join('\n'),
183
243
  loader: 'js',
@@ -190,27 +250,9 @@ function createServerCodeBundleOptions(options, target, sourceFileCache) {
190
250
  }
191
251
  return buildOptions;
192
252
  }
193
- function createServerPolyfillBundleOptions(options, target, sourceFileCache) {
194
- const serverPolyfills = [];
195
- const polyfillsFromConfig = new Set(options.polyfills);
196
- if (!(0, utils_1.isZonelessApp)(options.polyfills)) {
197
- serverPolyfills.push('zone.js/node');
198
- }
199
- if (polyfillsFromConfig.has('@angular/localize') ||
200
- polyfillsFromConfig.has('@angular/localize/init')) {
201
- serverPolyfills.push('@angular/localize/init');
202
- }
203
- serverPolyfills.push('@angular/platform-server/init');
204
- const namespace = 'angular:polyfills-server';
205
- const polyfillBundleOptions = getEsBuildCommonPolyfillsOptions({
206
- ...options,
207
- polyfills: serverPolyfills,
208
- }, namespace, false, sourceFileCache);
209
- if (!polyfillBundleOptions) {
210
- return;
211
- }
212
- const buildOptions = {
213
- ...polyfillBundleOptions,
253
+ function getEsBuildServerCommonOptions(options) {
254
+ return {
255
+ ...getEsBuildCommonOptions(options),
214
256
  platform: 'node',
215
257
  outExtension: { '.js': '.mjs' },
216
258
  // Note: `es2015` is needed for RxJS v6. If not specified, `module` would
@@ -219,31 +261,18 @@ function createServerPolyfillBundleOptions(options, target, sourceFileCache) {
219
261
  // More details: https://github.com/angular/angular-cli/issues/25405.
220
262
  mainFields: ['es2020', 'es2015', 'module', 'main'],
221
263
  entryNames: '[name]',
222
- banner: {
223
- js: [
224
- // Note: Needed as esbuild does not provide require shims / proxy from ESModules.
225
- // See: https://github.com/evanw/esbuild/issues/1921.
226
- `import { createRequire } from 'node:module';`,
227
- `globalThis['require'] ??= createRequire(import.meta.url);`,
228
- ].join('\n'),
229
- },
230
- target,
231
- entryPoints: {
232
- 'polyfills.server': namespace,
233
- },
234
264
  };
235
- return () => buildOptions;
236
265
  }
237
266
  function getEsBuildCommonOptions(options) {
238
- const { workspaceRoot, outExtension, optimizationOptions, sourcemapOptions, tsconfig, externalDependencies, outputNames, preserveSymlinks, jit, loaderExtensions, jsonLogs, } = options;
267
+ const { workspaceRoot, outExtension, optimizationOptions, sourcemapOptions, tsconfig, externalDependencies, outputNames, preserveSymlinks, jit, loaderExtensions, jsonLogs, i18nOptions, } = options;
239
268
  // Ensure unique hashes for i18n translation changes when using post-process inlining.
240
269
  // This hash value is added as a footer to each file and ensures that the output file names (with hashes)
241
270
  // change when translation files have changed. If this is not done the post processed files may have
242
271
  // different content but would retain identical production file names which would lead to browser caching problems.
243
272
  let footer;
244
- if (options.i18nOptions.shouldInline) {
273
+ if (i18nOptions.shouldInline) {
245
274
  // Update file hashes to include translation file content
246
- const i18nHash = Object.values(options.i18nOptions.locales).reduce((data, locale) => data + locale.files.map((file) => file.integrity || '').join('|'), '');
275
+ const i18nHash = Object.values(i18nOptions.locales).reduce((data, locale) => data + locale.files.map((file) => file.integrity || '').join('|'), '');
247
276
  footer = { js: `/**i18n:${(0, node_crypto_1.createHash)('sha256').update(i18nHash).digest('hex')}*/` };
248
277
  }
249
278
  return {
@@ -267,7 +296,7 @@ function getEsBuildCommonOptions(options) {
267
296
  splitting: true,
268
297
  chunkNames: options.namedChunks ? '[name]-[hash]' : 'chunk-[hash]',
269
298
  tsconfig,
270
- external: externalDependencies,
299
+ external: externalDependencies ? [...externalDependencies] : undefined,
271
300
  write: false,
272
301
  preserveSymlinks,
273
302
  define: {
@@ -360,3 +389,9 @@ function getEsBuildCommonPolyfillsOptions(options, namespace, tryToResolvePolyfi
360
389
  }));
361
390
  return buildOptions;
362
391
  }
392
+ function entryFileToWorkspaceRelative(workspaceRoot, entryFile) {
393
+ return ('./' +
394
+ (0, node_path_1.relative)(workspaceRoot, entryFile)
395
+ .replace(/.[mc]?ts$/, '')
396
+ .replace(/\\/g, '/'));
397
+ }
@@ -138,6 +138,7 @@ class BundlerContext {
138
138
  }
139
139
  return result;
140
140
  }
141
+ // eslint-disable-next-line max-lines-per-function
141
142
  async #performBundle() {
142
143
  // Create esbuild options if not present
143
144
  if (this.#esbuildOptions === undefined) {
@@ -165,12 +166,6 @@ class BundlerContext {
165
166
  // For non-incremental builds, perform a single build
166
167
  result = await (0, esbuild_1.build)(this.#esbuildOptions);
167
168
  }
168
- if (this.#platformIsServer) {
169
- for (const entry of Object.values(result.metafile.outputs)) {
170
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
171
- entry['ng-platform-server'] = true;
172
- }
173
- }
174
169
  }
175
170
  catch (failure) {
176
171
  // Build failures will throw an exception which contains errors/warnings
@@ -280,6 +275,7 @@ class BundlerContext {
280
275
  for (const { imports } of Object.values(result.metafile.outputs)) {
281
276
  for (const importData of imports) {
282
277
  if (!importData.external ||
278
+ utils_1.SERVER_GENERATED_EXTERNALS.has(importData.path) ||
283
279
  (importData.kind !== 'import-statement' &&
284
280
  importData.kind !== 'dynamic-import' &&
285
281
  importData.kind !== 'require-call')) {
@@ -295,13 +291,21 @@ class BundlerContext {
295
291
  if (!/\.([cm]?js|css|wasm)(\.map)?$/i.test(file.path)) {
296
292
  fileType = BuildOutputFileType.Media;
297
293
  }
294
+ else if (this.#platformIsServer) {
295
+ fileType = BuildOutputFileType.Server;
296
+ }
298
297
  else {
299
- fileType = this.#platformIsServer
300
- ? BuildOutputFileType.Server
301
- : BuildOutputFileType.Browser;
298
+ fileType = BuildOutputFileType.Browser;
302
299
  }
303
300
  return (0, utils_1.convertOutputFile)(file, fileType);
304
301
  });
302
+ let externalConfiguration = this.#esbuildOptions.external;
303
+ if (this.#platformIsServer && externalConfiguration) {
304
+ externalConfiguration = externalConfiguration.filter((dep) => !utils_1.SERVER_GENERATED_EXTERNALS.has(dep));
305
+ if (!externalConfiguration.length) {
306
+ externalConfiguration = undefined;
307
+ }
308
+ }
305
309
  // Return the successful build results
306
310
  return {
307
311
  ...result,
@@ -310,7 +314,7 @@ class BundlerContext {
310
314
  externalImports: {
311
315
  [this.#platformIsServer ? 'server' : 'browser']: externalImports,
312
316
  },
313
- externalConfiguration: this.#esbuildOptions.external,
317
+ externalConfiguration,
314
318
  errors: undefined,
315
319
  };
316
320
  }
@@ -84,5 +84,5 @@ export declare class MemoryCache<V> extends Cache<V, Map<string, V>> {
84
84
  * Provides all the values currently present in the cache instance.
85
85
  * @returns An iterable of all values in the cache.
86
86
  */
87
- values(): IterableIterator<V>;
87
+ values(): MapIterator<V>;
88
88
  }
@@ -47,3 +47,12 @@ export declare function logMessages(logger: BuilderContext['logger'], executionR
47
47
  */
48
48
  export declare function isZonelessApp(polyfills: string[] | undefined): boolean;
49
49
  export declare function getEntryPointName(entryPoint: string): string;
50
+ /**
51
+ * A set of server-generated dependencies that are treated as external.
52
+ *
53
+ * These dependencies are marked as external because they are produced by a
54
+ * separate bundling process and are not included in the primary bundle. This
55
+ * ensures that these generated files are resolved from an external source rather
56
+ * than being part of the main bundle.
57
+ */
58
+ export declare const SERVER_GENERATED_EXTERNALS: Set<string>;
@@ -7,6 +7,7 @@
7
7
  * found in the LICENSE file at https://angular.dev/license
8
8
  */
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.SERVER_GENERATED_EXTERNALS = void 0;
10
11
  exports.logBuildStats = logBuildStats;
11
12
  exports.getChunkNameFromMetafile = getChunkNameFromMetafile;
12
13
  exports.calculateEstimatedTransferSizes = calculateEstimatedTransferSizes;
@@ -29,6 +30,7 @@ const node_path_1 = require("node:path");
29
30
  const node_url_1 = require("node:url");
30
31
  const node_zlib_1 = require("node:zlib");
31
32
  const semver_1 = require("semver");
33
+ const manifest_1 = require("../../utils/server-rendering/manifest");
32
34
  const stats_table_1 = require("../../utils/stats-table");
33
35
  const bundler_context_1 = require("./bundler-context");
34
36
  function logBuildStats(metafile, outputFiles, initial, budgetFailures, colors, changedFiles, estimatedTransferSizes, ssrOutputEnabled, verbose) {
@@ -385,3 +387,15 @@ function getEntryPointName(entryPoint) {
385
387
  .replace(/\.[cm]?[jt]s$/, '')
386
388
  .replace(/[\\/.]/g, '-');
387
389
  }
390
+ /**
391
+ * A set of server-generated dependencies that are treated as external.
392
+ *
393
+ * These dependencies are marked as external because they are produced by a
394
+ * separate bundling process and are not included in the primary bundle. This
395
+ * ensures that these generated files are resolved from an external source rather
396
+ * than being part of the main bundle.
397
+ */
398
+ exports.SERVER_GENERATED_EXTERNALS = new Set([
399
+ './polyfills.server.mjs',
400
+ './' + manifest_1.SERVER_APP_MANIFEST_FILENAME,
401
+ ]);
@@ -34,17 +34,22 @@ var __disposeResources = (this && this.__disposeResources) || (function (Suppres
34
34
  env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
35
35
  env.hasError = true;
36
36
  }
37
+ var r, s = 0;
37
38
  function next() {
38
- while (env.stack.length) {
39
- var rec = env.stack.pop();
39
+ while (r = env.stack.pop()) {
40
40
  try {
41
- var result = rec.dispose && rec.dispose.call(rec.value);
42
- if (rec.async) return Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
41
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
42
+ if (r.dispose) {
43
+ var result = r.dispose.call(r.value);
44
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
45
+ }
46
+ else s |= 1;
43
47
  }
44
48
  catch (e) {
45
49
  fail(e);
46
50
  }
47
51
  }
52
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
48
53
  if (env.hasError) throw env.error;
49
54
  }
50
55
  return next();
@@ -82,10 +82,10 @@ function createAngularMemoryPlugin(options) {
82
82
  // Returning a function, installs middleware after the main transform middleware but
83
83
  // before the built-in HTML middleware
84
84
  return () => {
85
- server.middlewares.use(middlewares_1.angularHtmlFallbackMiddleware);
86
85
  if (ssr) {
87
- server.middlewares.use((0, middlewares_1.createAngularSSRMiddleware)(server, outputFiles, indexHtmlTransformer));
86
+ server.middlewares.use((0, middlewares_1.createAngularSSRMiddleware)(server, indexHtmlTransformer));
88
87
  }
88
+ server.middlewares.use(middlewares_1.angularHtmlFallbackMiddleware);
89
89
  server.middlewares.use((0, middlewares_1.createAngularIndexHtmlMiddleware)(server, outputFiles, indexHtmlTransformer));
90
90
  };
91
91
  },
@@ -6,7 +6,4 @@
6
6
  * found in the LICENSE file at https://angular.dev/license
7
7
  */
8
8
  import type { Connect, ViteDevServer } from 'vite';
9
- export declare function createAngularSSRMiddleware(server: ViteDevServer, outputFiles: Map<string, {
10
- contents: Uint8Array;
11
- servable: boolean;
12
- }>, indexHtmlTransformer?: (content: string) => Promise<string>): Connect.NextHandleFunction;
9
+ export declare function createAngularSSRMiddleware(server: ViteDevServer, indexHtmlTransformer?: (content: string) => Promise<string>): Connect.NextHandleFunction;
@@ -8,49 +8,36 @@
8
8
  */
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.createAngularSSRMiddleware = createAngularSSRMiddleware;
11
- const render_page_1 = require("../../../utils/server-rendering/render-page");
12
11
  const utils_1 = require("../utils");
13
- function createAngularSSRMiddleware(server, outputFiles, indexHtmlTransformer) {
12
+ function createAngularSSRMiddleware(server, indexHtmlTransformer) {
13
+ let cachedAngularServerApp;
14
14
  return function (req, res, next) {
15
- const url = req.originalUrl;
16
- if (!req.url ||
17
- // Skip if path is not defined.
18
- !url ||
19
- // Skip if path is like a file.
20
- // NOTE: We use a mime type lookup to mitigate against matching requests like: /browse/pl.0ef59752c0cd457dbf1391f08cbd936f
21
- (0, utils_1.lookupMimeTypeFromRequest)(url)) {
22
- next();
23
- return;
15
+ if (req.url === undefined) {
16
+ return next();
24
17
  }
25
- const rawHtml = outputFiles.get('/index.server.html')?.contents;
26
- if (!rawHtml) {
27
- next();
28
- return;
29
- }
30
- server
31
- .transformIndexHtml(req.url, Buffer.from(rawHtml).toString('utf-8'))
32
- .then(async (processedHtml) => {
33
- const resolvedUrls = server.resolvedUrls;
34
- const baseUrl = resolvedUrls?.local[0] ?? resolvedUrls?.network[0];
35
- if (indexHtmlTransformer) {
36
- processedHtml = await indexHtmlTransformer(processedHtml);
18
+ const resolvedUrls = server.resolvedUrls;
19
+ const baseUrl = resolvedUrls?.local[0] ?? resolvedUrls?.network[0];
20
+ const url = new URL(req.url, baseUrl);
21
+ (async () => {
22
+ const { ɵgetOrCreateAngularServerApp } = (await server.ssrLoadModule('/main.server.mjs'));
23
+ const angularServerApp = ɵgetOrCreateAngularServerApp();
24
+ // Only Add the transform hook only if it's a different instance.
25
+ if (cachedAngularServerApp !== angularServerApp) {
26
+ angularServerApp.hooks.on('html:transform:pre', async ({ html }) => {
27
+ const processedHtml = await server.transformIndexHtml(url.pathname, html);
28
+ return indexHtmlTransformer?.(processedHtml) ?? processedHtml;
29
+ });
30
+ cachedAngularServerApp = angularServerApp;
31
+ }
32
+ const response = await angularServerApp.render(new Request(url, { signal: AbortSignal.timeout(30_000) }), undefined);
33
+ return response?.text();
34
+ })()
35
+ .then((content) => {
36
+ if (typeof content !== 'string') {
37
+ return next();
37
38
  }
38
- const { content: ssrContent } = await (0, render_page_1.renderPage)({
39
- document: processedHtml,
40
- route: new URL(req.originalUrl ?? '/', baseUrl).toString(),
41
- serverContext: 'ssr',
42
- loadBundle: (uri) =>
43
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
44
- server.ssrLoadModule(uri.slice(1)),
45
- // Files here are only needed for critical CSS inlining.
46
- outputFiles: {},
47
- // TODO: add support for critical css inlining.
48
- inlineCriticalCss: false,
49
- });
50
- res.setHeader('Content-Type', 'text/html');
51
- res.setHeader('Cache-Control', 'no-cache');
52
39
  (0, utils_1.appendServerConfiguredHeaders)(server, res);
53
- res.end(ssrContent);
40
+ res.end(content);
54
41
  })
55
42
  .catch((error) => next(error));
56
43
  };
@@ -0,0 +1,19 @@
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
+ // The `bundled_critters` causes issues with module mappings in Bazel,
10
+ // leading to unexpected behavior with esbuild. Specifically, the problem occurs
11
+ // when esbuild resolves to a different module or version than expected, due to
12
+ // how Bazel handles module mappings.
13
+ //
14
+ // This change aims to resolve esbuild types correctly and maintain consistency
15
+ // in the Bazel build process.
16
+
17
+ declare module 'esbuild' {
18
+ export * from 'esbuild-wasm';
19
+ }
@@ -22,38 +22,53 @@ const MEDIA_SET_HANDLER_PATTERN = /^this\.media=["'](.*)["'];?$/;
22
22
  */
23
23
  const CSP_MEDIA_ATTR = 'ngCspMedia';
24
24
  /**
25
- * Script text used to change the media value of the link tags.
25
+ * Script that dynamically updates the `media` attribute of `<link>` tags based on a custom attribute (`CSP_MEDIA_ATTR`).
26
26
  *
27
27
  * NOTE:
28
28
  * We do not use `document.querySelectorAll('link').forEach((s) => s.addEventListener('load', ...)`
29
- * because this does not always fire on Chome.
29
+ * because load events are not always triggered reliably on Chrome.
30
30
  * See: https://github.com/angular/angular-cli/issues/26932 and https://crbug.com/1521256
31
+ *
32
+ * The script:
33
+ * - Ensures the event target is a `<link>` tag with the `CSP_MEDIA_ATTR` attribute.
34
+ * - Updates the `media` attribute with the value of `CSP_MEDIA_ATTR` and then removes the attribute.
35
+ * - Removes the event listener when all relevant `<link>` tags have been processed.
36
+ * - Uses event capturing (the `true` parameter) since load events do not bubble up the DOM.
31
37
  */
32
- const LINK_LOAD_SCRIPT_CONTENT = [
33
- '(() => {',
34
- ` const CSP_MEDIA_ATTR = '${CSP_MEDIA_ATTR}';`,
35
- ' const documentElement = document.documentElement;',
36
- ' const listener = (e) => {',
37
- ' const target = e.target;',
38
- ` if (!target || target.tagName !== 'LINK' || !target.hasAttribute(CSP_MEDIA_ATTR)) {`,
39
- ' return;',
40
- ' }',
41
- ' target.media = target.getAttribute(CSP_MEDIA_ATTR);',
42
- ' target.removeAttribute(CSP_MEDIA_ATTR);',
43
- // Remove onload listener when there are no longer styles that need to be loaded.
44
- ' if (!document.head.querySelector(`link[${CSP_MEDIA_ATTR}]`)) {',
45
- ` documentElement.removeEventListener('load', listener);`,
46
- ' }',
47
- ' };',
48
- // We use an event with capturing (the true parameter) because load events don't bubble.
49
- ` documentElement.addEventListener('load', listener, true);`,
50
- '})();',
51
- ].join('\n');
52
- class CrittersExtended extends critters_1.default {
38
+ const LINK_LOAD_SCRIPT_CONTENT = `
39
+ (() => {
40
+ const CSP_MEDIA_ATTR = '${CSP_MEDIA_ATTR}';
41
+ const documentElement = document.documentElement;
42
+
43
+ // Listener for load events on link tags.
44
+ const listener = (e) => {
45
+ const target = e.target;
46
+ if (
47
+ !target ||
48
+ target.tagName !== 'LINK' ||
49
+ !target.hasAttribute(CSP_MEDIA_ATTR)
50
+ ) {
51
+ return;
52
+ }
53
+
54
+ target.media = target.getAttribute(CSP_MEDIA_ATTR);
55
+ target.removeAttribute(CSP_MEDIA_ATTR);
56
+
57
+ if (!document.head.querySelector(\`link[\${CSP_MEDIA_ATTR}]\`)) {
58
+ documentElement.removeEventListener('load', listener);
59
+ }
60
+ };
61
+
62
+ documentElement.addEventListener('load', listener, true);
63
+ })();
64
+ `.trim();
65
+ class CrittersBase extends critters_1.default {
66
+ }
67
+ /* eslint-enable @typescript-eslint/no-unsafe-declaration-merging */
68
+ class CrittersExtended extends CrittersBase {
53
69
  optionsExtended;
54
70
  warnings = [];
55
71
  errors = [];
56
- initialEmbedLinkedStylesheet;
57
72
  addedCspScriptsDocuments = new WeakSet();
58
73
  documentNonces = new WeakMap();
59
74
  constructor(optionsExtended) {
@@ -71,17 +86,12 @@ class CrittersExtended extends critters_1.default {
71
86
  reduceInlineStyles: false,
72
87
  mergeStylesheets: false,
73
88
  // Note: if `preload` changes to anything other than `media`, the logic in
74
- // `embedLinkedStylesheetOverride` will have to be updated.
89
+ // `embedLinkedStylesheet` will have to be updated.
75
90
  preload: 'media',
76
91
  noscriptFallback: true,
77
92
  inlineFonts: true,
78
93
  });
79
94
  this.optionsExtended = optionsExtended;
80
- // We can't use inheritance to override `embedLinkedStylesheet`, because it's not declared in
81
- // the `Critters` .d.ts which means that we can't call the `super` implementation. TS doesn't
82
- // allow for `super` to be cast to a different type.
83
- this.initialEmbedLinkedStylesheet = this.embedLinkedStylesheet;
84
- this.embedLinkedStylesheet = this.embedLinkedStylesheetOverride;
85
95
  }
86
96
  readFile(path) {
87
97
  const readAsset = this.optionsExtended.readAsset;
@@ -91,7 +101,7 @@ class CrittersExtended extends critters_1.default {
91
101
  * Override of the Critters `embedLinkedStylesheet` method
92
102
  * that makes it work with Angular's CSP APIs.
93
103
  */
94
- embedLinkedStylesheetOverride = async (link, document) => {
104
+ async embedLinkedStylesheet(link, document) {
95
105
  if (link.getAttribute('media') === 'print' && link.next?.name === 'noscript') {
96
106
  // Workaround for https://github.com/GoogleChromeLabs/critters/issues/64
97
107
  // NB: this is only needed for the webpack based builders.
@@ -102,7 +112,7 @@ class CrittersExtended extends critters_1.default {
102
112
  link?.next?.remove();
103
113
  }
104
114
  }
105
- const returnValue = await this.initialEmbedLinkedStylesheet(link, document);
115
+ const returnValue = await super.embedLinkedStylesheet(link, document);
106
116
  const cspNonce = this.findCspNonce(document);
107
117
  if (cspNonce) {
108
118
  const crittersMedia = link.getAttribute('onload')?.match(MEDIA_SET_HANDLER_PATTERN);
@@ -126,7 +136,7 @@ class CrittersExtended extends critters_1.default {
126
136
  });
127
137
  }
128
138
  return returnValue;
129
- };
139
+ }
130
140
  /**
131
141
  * Finds the CSP nonce for a specific document.
132
142
  */