@angular-devkit/build-angular 17.0.0-rc.2 → 17.0.0-rc.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 (33) hide show
  1. package/package.json +9 -8
  2. package/src/builders/app-shell/render-worker.d.ts +1 -1
  3. package/src/builders/app-shell/render-worker.js +16 -9
  4. package/src/builders/application/build-action.js +5 -2
  5. package/src/builders/application/execute-build.js +26 -16
  6. package/src/builders/dev-server/vite-server.js +40 -14
  7. package/src/builders/extract-i18n/application-extraction.js +1 -0
  8. package/src/builders/prerender/routes-extractor-worker.js +1 -1
  9. package/src/tools/babel/plugins/elide-angular-metadata.js +14 -2
  10. package/src/tools/esbuild/angular/compiler-plugin.js +2 -1
  11. package/src/tools/esbuild/application-code-bundle.d.ts +3 -2
  12. package/src/tools/esbuild/application-code-bundle.js +6 -3
  13. package/src/tools/esbuild/bundler-execution-result.d.ts +2 -0
  14. package/src/tools/esbuild/bundler-execution-result.js +14 -0
  15. package/src/tools/esbuild/commonjs-checker.js +3 -3
  16. package/src/tools/esbuild/javascript-transformer.d.ts +1 -0
  17. package/src/tools/esbuild/javascript-transformer.js +25 -13
  18. package/src/tools/esbuild/utils.d.ts +1 -1
  19. package/src/tools/esbuild/utils.js +18 -4
  20. package/src/utils/environment-options.d.ts +1 -0
  21. package/src/utils/environment-options.js +3 -1
  22. package/src/utils/routes-extractor/extractor.d.ts +1 -1
  23. package/src/utils/routes-extractor/extractor.js +2 -2
  24. package/src/utils/server-rendering/esm-in-memory-loader/loader-hooks.js +3 -2
  25. package/src/utils/server-rendering/fetch-patch.d.ts +8 -0
  26. package/src/utils/server-rendering/fetch-patch.js +66 -0
  27. package/src/utils/server-rendering/prerender.js +25 -30
  28. package/src/utils/server-rendering/render-worker.d.ts +4 -2
  29. package/src/utils/server-rendering/render-worker.js +8 -4
  30. package/src/utils/server-rendering/routes-extractor-worker.d.ts +5 -3
  31. package/src/utils/server-rendering/routes-extractor-worker.js +10 -4
  32. package/src/utils/server-rendering/prerender-server.d.ts +0 -21
  33. package/src/utils/server-rendering/prerender-server.js +0 -102
package/package.json CHANGED
@@ -1,15 +1,15 @@
1
1
  {
2
2
  "name": "@angular-devkit/build-angular",
3
- "version": "17.0.0-rc.2",
3
+ "version": "17.0.0-rc.3",
4
4
  "description": "Angular Webpack Build Facade",
5
5
  "main": "src/index.js",
6
6
  "typings": "src/index.d.ts",
7
7
  "builders": "builders.json",
8
8
  "dependencies": {
9
9
  "@ampproject/remapping": "2.2.1",
10
- "@angular-devkit/architect": "0.1700.0-rc.2",
11
- "@angular-devkit/build-webpack": "0.1700.0-rc.2",
12
- "@angular-devkit/core": "17.0.0-rc.2",
10
+ "@angular-devkit/architect": "0.1700.0-rc.3",
11
+ "@angular-devkit/build-webpack": "0.1700.0-rc.3",
12
+ "@angular-devkit/core": "17.0.0-rc.3",
13
13
  "@babel/core": "7.23.2",
14
14
  "@babel/generator": "7.23.0",
15
15
  "@babel/helper-annotate-as-pure": "7.22.5",
@@ -20,7 +20,7 @@
20
20
  "@babel/preset-env": "7.23.2",
21
21
  "@babel/runtime": "7.23.2",
22
22
  "@discoveryjs/json-ext": "0.5.7",
23
- "@ngtools/webpack": "17.0.0-rc.2",
23
+ "@ngtools/webpack": "17.0.0-rc.3",
24
24
  "@vitejs/plugin-basic-ssl": "1.0.1",
25
25
  "ansi-colors": "4.1.3",
26
26
  "autoprefixer": "10.4.16",
@@ -49,7 +49,7 @@
49
49
  "open": "8.4.2",
50
50
  "ora": "5.4.1",
51
51
  "parse5-html-rewriting-stream": "7.0.0",
52
- "picomatch": "2.3.1",
52
+ "picomatch": "3.0.1",
53
53
  "piscina": "4.1.0",
54
54
  "postcss": "8.4.31",
55
55
  "postcss-loader": "7.3.3",
@@ -60,10 +60,11 @@
60
60
  "semver": "7.5.4",
61
61
  "source-map-loader": "4.0.1",
62
62
  "source-map-support": "0.5.21",
63
- "terser": "5.22.0",
63
+ "terser": "5.24.0",
64
64
  "text-table": "0.2.0",
65
65
  "tree-kill": "1.2.2",
66
66
  "tslib": "2.6.2",
67
+ "undici": "5.27.0",
67
68
  "vite": "4.5.0",
68
69
  "webpack": "5.89.0",
69
70
  "webpack-dev-middleware": "6.1.1",
@@ -128,7 +129,7 @@
128
129
  "url": "https://github.com/angular/angular-cli.git"
129
130
  },
130
131
  "engines": {
131
- "node": ">=18.13.0",
132
+ "node": "^18.13.0 || >=20.9.0",
132
133
  "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
133
134
  "yarn": ">= 1.13.0"
134
135
  },
@@ -20,7 +20,7 @@ interface RenderRequest {
20
20
  /**
21
21
  * An optional URL path that represents the Angular route that should be rendered.
22
22
  */
23
- url: string | undefined;
23
+ url: string;
24
24
  }
25
25
  /**
26
26
  * Renders an application based on a provided server bundle path, initial document, and optional URL route.
@@ -54,23 +54,30 @@ async function render({ serverBundlePath, document, url }) {
54
54
  useValue: 'app-shell',
55
55
  },
56
56
  ];
57
+ let renderAppPromise;
57
58
  // Render platform server module
58
59
  if (isBootstrapFn(bootstrapAppFn)) {
59
60
  (0, node_assert_1.default)(renderApplication, `renderApplication was not exported from: ${serverBundlePath}.`);
60
- return renderApplication(bootstrapAppFn, {
61
+ renderAppPromise = renderApplication(bootstrapAppFn, {
61
62
  document,
62
63
  url,
63
64
  platformProviders,
64
65
  });
65
66
  }
66
- (0, node_assert_1.default)(renderModule, `renderModule was not exported from: ${serverBundlePath}.`);
67
- const moduleClass = bootstrapAppFn || AppServerModule;
68
- (0, node_assert_1.default)(moduleClass, `Neither an AppServerModule nor a bootstrapping function was exported from: ${serverBundlePath}.`);
69
- return renderModule(moduleClass, {
70
- document,
71
- url,
72
- extraProviders: platformProviders,
73
- });
67
+ else {
68
+ (0, node_assert_1.default)(renderModule, `renderModule was not exported from: ${serverBundlePath}.`);
69
+ const moduleClass = bootstrapAppFn || AppServerModule;
70
+ (0, node_assert_1.default)(moduleClass, `Neither an AppServerModule nor a bootstrapping function was exported from: ${serverBundlePath}.`);
71
+ renderAppPromise = renderModule(moduleClass, {
72
+ document,
73
+ url,
74
+ extraProviders: platformProviders,
75
+ });
76
+ }
77
+ // The below should really handled by the framework!!!.
78
+ let timer;
79
+ const renderingTimeout = new Promise((_, reject) => (timer = setTimeout(() => reject(new Error(`Page ${new URL(url, 'resolve://').pathname} did not render in 30 seconds.`)), 30000)));
80
+ return Promise.race([renderAppPromise, renderingTimeout]).finally(() => clearTimeout(timer));
74
81
  }
75
82
  function isBootstrapFn(value) {
76
83
  // We can differentiate between a module and a bootstrap function by reading compiler-generated `ɵmod` static property:
@@ -38,6 +38,7 @@ const promises_1 = __importDefault(require("node:fs/promises"));
38
38
  const node_path_1 = __importDefault(require("node:path"));
39
39
  const sass_language_1 = require("../../tools/esbuild/stylesheets/sass-language");
40
40
  const utils_1 = require("../../tools/esbuild/utils");
41
+ const environment_options_1 = require("../../utils/environment-options");
41
42
  const error_1 = require("../../utils/error");
42
43
  async function* runEsBuildBuildAction(action, options) {
43
44
  const { writeToFileSystemFilter, writeToFileSystem = true, watch, poll, logger, deleteOutputPath, cacheOptions, outputPath, verbose, projectRoot, workspaceRoot, progress, } = options;
@@ -95,8 +96,10 @@ async function* runEsBuildBuildAction(action, options) {
95
96
  });
96
97
  // Setup abort support
97
98
  options.signal?.addEventListener('abort', () => void watcher?.close());
98
- // Temporarily watch the entire project
99
- watcher.add(projectRoot);
99
+ // Watch the entire project root if 'NG_BUILD_WATCH_ROOT' environment variable is set
100
+ if (environment_options_1.shouldWatchRoot) {
101
+ watcher.add(projectRoot);
102
+ }
100
103
  // Watch workspace for package manager changes
101
104
  const packageWatchFiles = [
102
105
  // manifest can affect module resolution
@@ -19,6 +19,7 @@ const global_styles_1 = require("../../tools/esbuild/global-styles");
19
19
  const license_extractor_1 = require("../../tools/esbuild/license-extractor");
20
20
  const utils_1 = require("../../tools/esbuild/utils");
21
21
  const bundle_calculator_1 = require("../../utils/bundle-calculator");
22
+ const color_1 = require("../../utils/color");
22
23
  const copy_assets_1 = require("../../utils/copy-assets");
23
24
  const supported_browsers_1 = require("../../utils/supported-browsers");
24
25
  const execute_post_bundle_1 = require("./execute-post-bundle");
@@ -112,13 +113,15 @@ async function executeBuild(options, context, rebuildState) {
112
113
  if (options.budgets) {
113
114
  const compatStats = (0, budget_stats_1.generateBudgetStats)(metafile, initialFiles);
114
115
  budgetFailures = [...(0, bundle_calculator_1.checkBudgets)(options.budgets, compatStats, true)];
115
- for (const { severity, message } of budgetFailures) {
116
- if (severity === 'error') {
117
- context.logger.error(message);
118
- }
119
- else {
120
- context.logger.warn(message);
121
- }
116
+ if (budgetFailures.length > 0) {
117
+ await (0, utils_1.logMessages)(context, {
118
+ errors: budgetFailures
119
+ .filter((failure) => failure.severity === 'error')
120
+ .map((failure) => ({ text: failure.message, location: null })),
121
+ warnings: budgetFailures
122
+ .filter((failure) => failure.severity !== 'error')
123
+ .map((failure) => ({ text: failure.message, location: null })),
124
+ });
122
125
  }
123
126
  }
124
127
  // Calculate estimated transfer size if scripts are optimized
@@ -146,11 +149,20 @@ async function executeBuild(options, context, rebuildState) {
146
149
  executionResult.outputFiles.push(...result.additionalOutputFiles);
147
150
  executionResult.assetFiles.push(...result.additionalAssets);
148
151
  }
152
+ await printWarningsAndErrorsToConsole(context, warnings, errors);
149
153
  if (prerenderOptions) {
150
154
  executionResult.addOutputFile('prerendered-routes.json', JSON.stringify({ routes: prerenderedRoutes.sort((a, b) => a.localeCompare(b)) }, null, 2), bundler_context_1.BuildOutputFileType.Root);
155
+ let prerenderMsg = `Prerendered ${prerenderedRoutes.length} static route`;
156
+ if (prerenderedRoutes.length > 1) {
157
+ prerenderMsg += 's.';
158
+ }
159
+ else {
160
+ prerenderMsg += '.';
161
+ }
162
+ context.logger.info(color_1.colors.magenta(prerenderMsg) + '\n');
151
163
  }
152
- printWarningsAndErrorsToConsole(context, warnings, errors);
153
- (0, utils_1.logBuildStats)(context, metafile, initialFiles, budgetFailures, estimatedTransferSizes);
164
+ const changedFiles = rebuildState && executionResult.findChangedFiles(rebuildState.previousOutputHashes);
165
+ (0, utils_1.logBuildStats)(context, metafile, initialFiles, budgetFailures, changedFiles, estimatedTransferSizes);
154
166
  // Write metafile if stats option is enabled
155
167
  if (options.stats) {
156
168
  executionResult.addOutputFile('stats.json', JSON.stringify(metafile, null, 2), bundler_context_1.BuildOutputFileType.Root);
@@ -158,11 +170,9 @@ async function executeBuild(options, context, rebuildState) {
158
170
  return executionResult;
159
171
  }
160
172
  exports.executeBuild = executeBuild;
161
- function printWarningsAndErrorsToConsole(context, warnings, errors) {
162
- for (const error of errors) {
163
- context.logger.error(error);
164
- }
165
- for (const warning of warnings) {
166
- context.logger.warn(warning);
167
- }
173
+ async function printWarningsAndErrorsToConsole(context, warnings, errors) {
174
+ await (0, utils_1.logMessages)(context, {
175
+ errors: errors.map((text) => ({ text, location: null })),
176
+ warnings: warnings.map((text) => ({ text, location: null })),
177
+ });
168
178
  }
@@ -37,6 +37,7 @@ exports.setupServer = exports.serveWithVite = void 0;
37
37
  const remapping_1 = __importDefault(require("@ampproject/remapping"));
38
38
  const mrmime_1 = require("mrmime");
39
39
  const node_assert_1 = __importDefault(require("node:assert"));
40
+ const node_crypto_1 = require("node:crypto");
40
41
  const promises_1 = require("node:fs/promises");
41
42
  const node_path_1 = __importDefault(require("node:path"));
42
43
  const bundler_context_1 = require("../../tools/esbuild/bundler-context");
@@ -157,7 +158,7 @@ async function* serveWithVite(serverOptions, builderName, context, plugins) {
157
158
  }
158
159
  }
159
160
  if (server) {
160
- handleUpdate(generatedFiles, server, serverOptions, context.logger);
161
+ handleUpdate(normalizePath, generatedFiles, server, serverOptions, context.logger);
161
162
  }
162
163
  else {
163
164
  const projectName = context.target?.project;
@@ -189,13 +190,13 @@ async function* serveWithVite(serverOptions, builderName, context, plugins) {
189
190
  await new Promise((resolve) => (deferred = resolve));
190
191
  }
191
192
  exports.serveWithVite = serveWithVite;
192
- function handleUpdate(generatedFiles, server, serverOptions, logger) {
193
+ function handleUpdate(normalizePath, generatedFiles, server, serverOptions, logger) {
193
194
  const updatedFiles = [];
194
195
  // Invalidate any updated files
195
196
  for (const [file, record] of generatedFiles) {
196
197
  if (record.updated) {
197
198
  updatedFiles.push(file);
198
- const updatedModules = server.moduleGraph.getModulesByFile(file);
199
+ const updatedModules = server.moduleGraph.getModulesByFile(normalizePath(node_path_1.default.join(server.config.root, file)));
199
200
  updatedModules?.forEach((m) => server?.moduleGraph.invalidateModule(m));
200
201
  }
201
202
  }
@@ -207,8 +208,7 @@ function handleUpdate(generatedFiles, server, serverOptions, logger) {
207
208
  const timestamp = Date.now();
208
209
  server.ws.send({
209
210
  type: 'update',
210
- updates: updatedFiles.map((f) => {
211
- const filePath = f.slice(1); // Remove leading slash.
211
+ updates: updatedFiles.map((filePath) => {
212
212
  return {
213
213
  type: 'css-update',
214
214
  timestamp,
@@ -240,7 +240,7 @@ function analyzeResultFiles(normalizePath, htmlIndexPath, resultFiles, generated
240
240
  filePath = '/index.html';
241
241
  }
242
242
  else {
243
- filePath = '/' + normalizePath(file.path);
243
+ filePath = normalizePath(file.path);
244
244
  }
245
245
  seen.add(filePath);
246
246
  // Skip analysis of sourcemaps
@@ -282,11 +282,13 @@ async function setupServer(serverOptions, outputFiles, assets, preserveSymlinks,
282
282
  const proxy = await (0, load_proxy_config_1.loadProxyConfiguration)(serverOptions.workspaceRoot, serverOptions.proxyConfig, true);
283
283
  // dynamically import Vite for ESM compatibility
284
284
  const { normalizePath } = await Promise.resolve().then(() => __importStar(require('vite')));
285
+ // Path will not exist on disk and only used to provide separate path for Vite requests
286
+ const virtualProjectRoot = normalizePath(node_path_1.default.join(serverOptions.workspaceRoot, `.angular/vite-root/${(0, node_crypto_1.randomUUID)()}/`));
285
287
  const configuration = {
286
288
  configFile: false,
287
289
  envFile: false,
288
290
  cacheDir: node_path_1.default.join(serverOptions.cacheOptions.path, 'vite'),
289
- root: serverOptions.workspaceRoot,
291
+ root: virtualProjectRoot,
290
292
  publicDir: false,
291
293
  esbuild: false,
292
294
  mode: 'development',
@@ -316,7 +318,7 @@ async function setupServer(serverOptions, outputFiles, assets, preserveSymlinks,
316
318
  },
317
319
  ssr: {
318
320
  // Exclude any provided dependencies (currently build defined externals)
319
- external: externalMetadata.implicit,
321
+ external: externalMetadata.explicit,
320
322
  },
321
323
  plugins: [
322
324
  (0, i18n_locale_plugin_1.createAngularLocaleDataPlugin)(),
@@ -331,24 +333,31 @@ async function setupServer(serverOptions, outputFiles, assets, preserveSymlinks,
331
333
  // `/@id/${source}` but is currently closer to a raw external than a resolved file path.
332
334
  return source;
333
335
  }
334
- if (importer && source.startsWith('.')) {
336
+ if (importer && source[0] === '.' && importer.startsWith(virtualProjectRoot)) {
335
337
  // Remove query if present
336
338
  const [importerFile] = importer.split('?', 1);
337
- source = normalizePath(node_path_1.default.join(node_path_1.default.dirname(importerFile), source));
339
+ source = normalizePath(node_path_1.default.join(node_path_1.default.dirname(node_path_1.default.relative(virtualProjectRoot, importerFile)), source));
340
+ }
341
+ if (source[0] === '/') {
342
+ source = source.slice(1);
338
343
  }
339
344
  const [file] = source.split('?', 1);
340
345
  if (outputFiles.has(file)) {
341
- return source;
346
+ return node_path_1.default.join(virtualProjectRoot, source);
342
347
  }
343
348
  },
344
349
  load(id) {
345
350
  const [file] = id.split('?', 1);
346
- const codeContents = outputFiles.get(file)?.contents;
351
+ const relativeFile = normalizePath(node_path_1.default.relative(virtualProjectRoot, file));
352
+ const codeContents = outputFiles.get(relativeFile)?.contents;
347
353
  if (codeContents === undefined) {
354
+ if (relativeFile.endsWith('/node_modules/vite/dist/client/client.mjs')) {
355
+ return loadViteClientCode(file);
356
+ }
348
357
  return;
349
358
  }
350
359
  const code = Buffer.from(codeContents).toString('utf-8');
351
- const mapContents = outputFiles.get(file + '.map')?.contents;
360
+ const mapContents = outputFiles.get(relativeFile + '.map')?.contents;
352
361
  return {
353
362
  // Remove source map URL comments from the code if a sourcemap is present.
354
363
  // Vite will inline and add an additional sourcemap URL for the sourcemap.
@@ -422,7 +431,7 @@ async function setupServer(serverOptions, outputFiles, assets, preserveSymlinks,
422
431
  next();
423
432
  return;
424
433
  }
425
- const rawHtml = outputFiles.get('/index.server.html')?.contents;
434
+ const rawHtml = outputFiles.get('index.server.html')?.contents;
426
435
  if (!rawHtml) {
427
436
  next();
428
437
  return;
@@ -538,6 +547,23 @@ async function setupServer(serverOptions, outputFiles, assets, preserveSymlinks,
538
547
  return configuration;
539
548
  }
540
549
  exports.setupServer = setupServer;
550
+ /**
551
+ * Reads the resolved Vite client code from disk and updates the content to remove
552
+ * an unactionable suggestion to update the Vite configuration file to disable the
553
+ * error overlay. The Vite configuration file is not present when used in the Angular
554
+ * CLI.
555
+ * @param file The absolute path to the Vite client code.
556
+ * @returns
557
+ */
558
+ async function loadViteClientCode(file) {
559
+ const originalContents = await (0, promises_1.readFile)(file, 'utf-8');
560
+ let contents = originalContents.replace('You can also disable this overlay by setting', '');
561
+ contents = contents.replace(
562
+ // eslint-disable-next-line max-len
563
+ '<code part="config-option-name">server.hmr.overlay</code> to <code part="config-option-value">false</code> in <code part="config-file-name">vite.config.js.</code>', '');
564
+ (0, node_assert_1.default)(originalContents !== contents, 'Failed to update Vite client error overlay text.');
565
+ return contents;
566
+ }
541
567
  function pathnameWithoutServePath(url, serverOptions) {
542
568
  const parsedUrl = new URL(url, 'http://localhost');
543
569
  let pathname = decodeURIComponent(parsedUrl.pathname);
@@ -22,6 +22,7 @@ async function extractMessages(options, builderName, context, extractorConstruct
22
22
  buildOptions.optimization = false;
23
23
  buildOptions.sourceMap = { scripts: true, vendor: true };
24
24
  buildOptions.localize = false;
25
+ buildOptions.budgets = undefined;
25
26
  let build;
26
27
  if (builderName === '@angular-devkit/build-angular:application') {
27
28
  build = application_1.buildApplicationInternal;
@@ -45,7 +45,7 @@ async function extract() {
45
45
  const bootstrapAppFnOrModule = bootstrapAppFn || AppServerModule;
46
46
  (0, node_assert_1.default)(bootstrapAppFnOrModule, `Neither an AppServerModule nor a bootstrapping function was exported from: ${serverBundlePath}.`);
47
47
  const routes = [];
48
- for await (const { route, success } of extractRoutes(bootstrapAppFnOrModule, document, '')) {
48
+ for await (const { route, success } of extractRoutes(bootstrapAppFnOrModule, document)) {
49
49
  if (success) {
50
50
  routes.push(route);
51
51
  }
@@ -17,6 +17,10 @@ const SET_CLASS_METADATA_NAME = 'ɵsetClassMetadata';
17
17
  * Name of the asynchronous Angular class metadata function created by the Angular compiler.
18
18
  */
19
19
  const SET_CLASS_METADATA_ASYNC_NAME = 'ɵsetClassMetadataAsync';
20
+ /**
21
+ * Name of the function that sets debug information on classes.
22
+ */
23
+ const SET_CLASS_DEBUG_INFO_NAME = 'ɵsetClassDebugInfo';
20
24
  /**
21
25
  * Provides one or more keywords that if found within the content of a source file indicate
22
26
  * that this plugin should be used with a source file.
@@ -24,7 +28,7 @@ const SET_CLASS_METADATA_ASYNC_NAME = 'ɵsetClassMetadataAsync';
24
28
  * @returns An a string iterable containing one or more keywords.
25
29
  */
26
30
  function getKeywords() {
27
- return [SET_CLASS_METADATA_NAME, SET_CLASS_METADATA_ASYNC_NAME];
31
+ return [SET_CLASS_METADATA_NAME, SET_CLASS_METADATA_ASYNC_NAME, SET_CLASS_DEBUG_INFO_NAME];
28
32
  }
29
33
  exports.getKeywords = getKeywords;
30
34
  /**
@@ -48,7 +52,8 @@ function default_1() {
48
52
  }
49
53
  if (calleeName !== undefined &&
50
54
  (isRemoveClassMetadataCall(calleeName, callArguments) ||
51
- isRemoveClassmetadataAsyncCall(calleeName, callArguments))) {
55
+ isRemoveClassmetadataAsyncCall(calleeName, callArguments) ||
56
+ isSetClassDebugInfoCall(calleeName, callArguments))) {
52
57
  // The metadata function is always emitted inside a function expression
53
58
  const parent = path.getFunctionParent();
54
59
  if (parent && (parent.isFunctionExpression() || parent.isArrowFunctionExpression())) {
@@ -84,6 +89,13 @@ function isRemoveClassmetadataAsyncCall(name, args) {
84
89
  isInlineFunction(args[1]) &&
85
90
  isInlineFunction(args[2]));
86
91
  }
92
+ /** Determines if a function call is a call to `setClassDebugInfo`. */
93
+ function isSetClassDebugInfoCall(name, args) {
94
+ return (name === SET_CLASS_DEBUG_INFO_NAME &&
95
+ args.length === 2 &&
96
+ core_1.types.isIdentifier(args[0]) &&
97
+ core_1.types.isObjectExpression(args[1]));
98
+ }
87
99
  /** Determines if a node is an inline function expression. */
88
100
  function isInlineFunction(node) {
89
101
  return core_1.types.isFunctionExpression(node) || core_1.types.isArrowFunctionExpression(node);
@@ -172,7 +172,8 @@ function createCompilerPlugin(pluginOptions, styleOptions) {
172
172
  // Return bundled worker file entry name to be used in the built output
173
173
  const workerCodeFile = workerResult.outputFiles.find((file) => file.path.endsWith('.js'));
174
174
  (0, node_assert_1.default)(workerCodeFile, 'Web Worker bundled code file should always be present.');
175
- return path.relative(build.initialOptions.outdir ?? '', workerCodeFile.path);
175
+ const workerCodePath = path.relative(build.initialOptions.outdir ?? '', workerCodeFile.path);
176
+ return workerCodePath.replaceAll('\\', '/');
176
177
  },
177
178
  };
178
179
  // Initialize the Angular compilation for the current build.
@@ -8,12 +8,13 @@
8
8
  import type { BuildOptions } from 'esbuild';
9
9
  import type { NormalizedApplicationBuildOptions } from '../../builders/application/options';
10
10
  import { SourceFileCache } from './angular/source-file-cache';
11
+ import { BundlerOptionsFactory } from './bundler-context';
11
12
  export declare function createBrowserCodeBundleOptions(options: NormalizedApplicationBuildOptions, target: string[], sourceFileCache?: SourceFileCache): BuildOptions;
12
- export declare function createBrowserPolyfillBundleOptions(options: NormalizedApplicationBuildOptions, target: string[], sourceFileCache?: SourceFileCache): BuildOptions | undefined;
13
+ export declare function createBrowserPolyfillBundleOptions(options: NormalizedApplicationBuildOptions, target: string[], sourceFileCache?: SourceFileCache): BuildOptions | BundlerOptionsFactory | undefined;
13
14
  /**
14
15
  * Create an esbuild 'build' options object for the server bundle.
15
16
  * @param options The builder's user-provider normalized options.
16
17
  * @returns An esbuild BuildOptions object.
17
18
  */
18
19
  export declare function createServerCodeBundleOptions(options: NormalizedApplicationBuildOptions, target: string[], sourceFileCache: SourceFileCache): BuildOptions;
19
- export declare function createServerPolyfillBundleOptions(options: NormalizedApplicationBuildOptions, target: string[], sourceFileCache?: SourceFileCache): BuildOptions | undefined;
20
+ export declare function createServerPolyfillBundleOptions(options: NormalizedApplicationBuildOptions, target: string[], sourceFileCache?: SourceFileCache): BundlerOptionsFactory | undefined;
@@ -63,6 +63,7 @@ function createBrowserPolyfillBundleOptions(options, target, sourceFileCache) {
63
63
  return;
64
64
  }
65
65
  const { outputNames, polyfills } = options;
66
+ const hasTypeScriptEntries = polyfills?.some((entry) => /\.[cm]?tsx?$/.test(entry));
66
67
  const buildOptions = {
67
68
  ...polyfillBundleOptions,
68
69
  platform: 'browser',
@@ -78,7 +79,6 @@ function createBrowserPolyfillBundleOptions(options, target, sourceFileCache) {
78
79
  },
79
80
  };
80
81
  // Only add the Angular TypeScript compiler if TypeScript files are provided in the polyfills
81
- const hasTypeScriptEntries = polyfills?.some((entry) => /\.[cm]?tsx?$/.test(entry));
82
82
  if (hasTypeScriptEntries) {
83
83
  buildOptions.plugins ??= [];
84
84
  const { pluginOptions, styleOptions } = (0, compiler_plugin_options_1.createCompilerPluginOptions)(options, target, sourceFileCache);
@@ -88,7 +88,10 @@ function createBrowserPolyfillBundleOptions(options, target, sourceFileCache) {
88
88
  // Component stylesheet options are unused for polyfills but required by the plugin
89
89
  styleOptions));
90
90
  }
91
- return buildOptions;
91
+ // Use an options factory to allow fully incremental bundling when no TypeScript files are present.
92
+ // The TypeScript compilation is not currently integrated into the bundler invalidation so
93
+ // cannot be used with fully incremental bundling yet.
94
+ return hasTypeScriptEntries ? buildOptions : () => buildOptions;
92
95
  }
93
96
  exports.createBrowserPolyfillBundleOptions = createBrowserPolyfillBundleOptions;
94
97
  /**
@@ -232,7 +235,7 @@ function createServerPolyfillBundleOptions(options, target, sourceFileCache) {
232
235
  }));
233
236
  }
234
237
  buildOptions.plugins.push((0, rxjs_esm_resolution_plugin_1.createRxjsEsmResolutionPlugin)());
235
- return buildOptions;
238
+ return () => buildOptions;
236
239
  }
237
240
  exports.createServerPolyfillBundleOptions = createServerPolyfillBundleOptions;
238
241
  function getEsBuildCommonOptions(options) {
@@ -17,6 +17,7 @@ export interface RebuildState {
17
17
  rebuildContexts: BundlerContext[];
18
18
  codeBundleCache?: SourceFileCache;
19
19
  fileChanges: ChangedFiles;
20
+ previousOutputHashes: Map<string, string>;
20
21
  }
21
22
  /**
22
23
  * Represents the result of a single builder execute call.
@@ -57,5 +58,6 @@ export declare class ExecutionResult {
57
58
  };
58
59
  get watchFiles(): string[];
59
60
  createRebuildState(fileChanges: ChangedFiles): RebuildState;
61
+ findChangedFiles(previousOutputHashes: Map<string, string>): Set<string>;
60
62
  dispose(): Promise<void>;
61
63
  }
@@ -60,6 +60,9 @@ class ExecutionResult {
60
60
  if (this.codeBundleCache?.referencedFiles) {
61
61
  files.push(...this.codeBundleCache.referencedFiles);
62
62
  }
63
+ if (this.codeBundleCache?.loadResultCache) {
64
+ files.push(...this.codeBundleCache.loadResultCache.watchFiles);
65
+ }
63
66
  return files;
64
67
  }
65
68
  createRebuildState(fileChanges) {
@@ -68,8 +71,19 @@ class ExecutionResult {
68
71
  rebuildContexts: this.rebuildContexts,
69
72
  codeBundleCache: this.codeBundleCache,
70
73
  fileChanges,
74
+ previousOutputHashes: new Map(this.outputFiles.map((file) => [file.path, file.hash])),
71
75
  };
72
76
  }
77
+ findChangedFiles(previousOutputHashes) {
78
+ const changed = new Set();
79
+ for (const file of this.outputFiles) {
80
+ const previousHash = previousOutputHashes.get(file.path);
81
+ if (previousHash === undefined || previousHash !== file.hash) {
82
+ changed.add(file.path);
83
+ }
84
+ }
85
+ return changed;
86
+ }
73
87
  async dispose() {
74
88
  await Promise.allSettled(this.rebuildContexts.map((context) => context.dispose()));
75
89
  }
@@ -84,11 +84,11 @@ function checkCommonJSModules(metafile, allowedCommonJsDependencies) {
84
84
  }
85
85
  }
86
86
  if (notAllowed) {
87
- // Issue a diagnostic message and skip all descendants since they are also most
88
- // likely not ESM but solved by addressing this import.
87
+ // Issue a diagnostic message for CommonJS module
89
88
  messages.push(createCommonJSModuleError(request, currentFile));
90
- continue;
91
89
  }
90
+ // Skip all descendants since they are also most likely not ESM but solved by addressing this import
91
+ continue;
92
92
  }
93
93
  // Add the path so that its imports can be checked
94
94
  files.push(imported.path);
@@ -23,6 +23,7 @@ export interface JavaScriptTransformerOptions {
23
23
  */
24
24
  export declare class JavaScriptTransformer {
25
25
  #private;
26
+ readonly maxThreads: number;
26
27
  constructor(options: JavaScriptTransformerOptions, maxThreads: number);
27
28
  /**
28
29
  * Performs JavaScript transformations on a file from the filesystem.
@@ -20,16 +20,11 @@ const piscina_1 = __importDefault(require("piscina"));
20
20
  * and advanced optimizations.
21
21
  */
22
22
  class JavaScriptTransformer {
23
+ maxThreads;
23
24
  #workerPool;
24
25
  #commonOptions;
25
26
  constructor(options, maxThreads) {
26
- this.#workerPool = new piscina_1.default({
27
- filename: require.resolve('./javascript-transformer-worker'),
28
- minThreads: 1,
29
- maxThreads,
30
- // Shutdown idle threads after 1 second of inactivity
31
- idleTimeout: 1000,
32
- });
27
+ this.maxThreads = maxThreads;
33
28
  // Extract options to ensure only the named options are serialized and sent to the worker
34
29
  const { sourcemap, thirdPartySourcemaps = false, advancedOptimizations = false, jit = false, } = options;
35
30
  this.#commonOptions = {
@@ -39,6 +34,16 @@ class JavaScriptTransformer {
39
34
  jit,
40
35
  };
41
36
  }
37
+ #ensureWorkerPool() {
38
+ this.#workerPool ??= new piscina_1.default({
39
+ filename: require.resolve('./javascript-transformer-worker'),
40
+ minThreads: 1,
41
+ maxThreads: this.maxThreads,
42
+ // Shutdown idle threads after 1 second of inactivity
43
+ idleTimeout: 1000,
44
+ });
45
+ return this.#workerPool;
46
+ }
42
47
  /**
43
48
  * Performs JavaScript transformations on a file from the filesystem.
44
49
  * If no transformations are required, the data for the original file will be returned.
@@ -49,7 +54,7 @@ class JavaScriptTransformer {
49
54
  transformFile(filename, skipLinker) {
50
55
  // Always send the request to a worker. Files are almost always from node modules which means
51
56
  // they may need linking. The data is also not yet available to perform most transformation checks.
52
- return this.#workerPool.run({
57
+ return this.#ensureWorkerPool().run({
53
58
  filename,
54
59
  skipLinker,
55
60
  ...this.#commonOptions,
@@ -71,7 +76,7 @@ class JavaScriptTransformer {
71
76
  (!!this.#commonOptions.thirdPartySourcemaps || !/[\\/]node_modules[\\/]/.test(filename));
72
77
  return Buffer.from(keepSourcemap ? data : data.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, ''), 'utf-8');
73
78
  }
74
- return this.#workerPool.run({
79
+ return this.#ensureWorkerPool().run({
75
80
  filename,
76
81
  data,
77
82
  skipLinker,
@@ -82,10 +87,17 @@ class JavaScriptTransformer {
82
87
  * Stops all active transformation tasks and shuts down all workers.
83
88
  * @returns A void promise that resolves when closing is complete.
84
89
  */
85
- close() {
86
- // Workaround piscina bug where a worker thread will be recreated after destroy to meet the minimum.
87
- this.#workerPool.options.minThreads = 0;
88
- return this.#workerPool.destroy();
90
+ async close() {
91
+ if (this.#workerPool) {
92
+ // Workaround piscina bug where a worker thread will be recreated after destroy to meet the minimum.
93
+ this.#workerPool.options.minThreads = 0;
94
+ try {
95
+ await this.#workerPool.destroy();
96
+ }
97
+ finally {
98
+ this.#workerPool = undefined;
99
+ }
100
+ }
89
101
  }
90
102
  }
91
103
  exports.JavaScriptTransformer = JavaScriptTransformer;
@@ -10,7 +10,7 @@ import { BuildOptions, Metafile, OutputFile, PartialMessage } from 'esbuild';
10
10
  import { BudgetCalculatorResult } from '../../utils/bundle-calculator';
11
11
  import { BuildOutputFile, BuildOutputFileType, InitialFileRecord } from './bundler-context';
12
12
  import { BuildOutputAsset } from './bundler-execution-result';
13
- export declare function logBuildStats(context: BuilderContext, metafile: Metafile, initial: Map<string, InitialFileRecord>, budgetFailures: BudgetCalculatorResult[] | undefined, estimatedTransferSizes?: Map<string, number>): void;
13
+ export declare function logBuildStats(context: BuilderContext, metafile: Metafile, initial: Map<string, InitialFileRecord>, budgetFailures: BudgetCalculatorResult[] | undefined, changedFiles?: Set<string>, estimatedTransferSizes?: Map<string, number>): void;
14
14
  export declare function calculateEstimatedTransferSizes(outputFiles: OutputFile[]): Promise<Map<string, number>>;
15
15
  export declare function withSpinner<T>(text: string, action: () => T | Promise<T>): Promise<T>;
16
16
  export declare function withNoProgress<T>(text: string, action: () => T | Promise<T>): Promise<T>;
@@ -46,8 +46,9 @@ const spinner_1 = require("../../utils/spinner");
46
46
  const stats_1 = require("../webpack/utils/stats");
47
47
  const bundler_context_1 = require("./bundler-context");
48
48
  const compressAsync = (0, node_util_1.promisify)(node_zlib_1.brotliCompress);
49
- function logBuildStats(context, metafile, initial, budgetFailures, estimatedTransferSizes) {
49
+ function logBuildStats(context, metafile, initial, budgetFailures, changedFiles, estimatedTransferSizes) {
50
50
  const stats = [];
51
+ let unchangedCount = 0;
51
52
  for (const [file, output] of Object.entries(metafile.outputs)) {
52
53
  // Only display JavaScript and CSS files
53
54
  if (!file.endsWith('.js') && !file.endsWith('.css')) {
@@ -58,6 +59,11 @@ function logBuildStats(context, metafile, initial, budgetFailures, estimatedTran
58
59
  if (output['ng-component']) {
59
60
  continue;
60
61
  }
62
+ // Show only changed files if a changed list is provided
63
+ if (changedFiles && !changedFiles.has(file)) {
64
+ ++unchangedCount;
65
+ continue;
66
+ }
61
67
  let name = initial.get(file)?.name;
62
68
  if (name === undefined && output.entryPoint) {
63
69
  name = node_path_1.default
@@ -70,8 +76,16 @@ function logBuildStats(context, metafile, initial, budgetFailures, estimatedTran
70
76
  stats: [file, name ?? '-', output.bytes, estimatedTransferSizes?.get(file) ?? '-'],
71
77
  });
72
78
  }
73
- const tableText = (0, stats_1.generateBuildStatsTable)(stats, true, true, !!estimatedTransferSizes, budgetFailures);
74
- context.logger.info('\n' + tableText + '\n');
79
+ if (stats.length > 0) {
80
+ const tableText = (0, stats_1.generateBuildStatsTable)(stats, true, unchangedCount === 0, !!estimatedTransferSizes, budgetFailures);
81
+ context.logger.info('\n' + tableText + '\n');
82
+ }
83
+ else if (changedFiles !== undefined) {
84
+ context.logger.info('\nNo output file changes.\n');
85
+ }
86
+ if (unchangedCount > 0) {
87
+ context.logger.info(`Unchanged output files: ${unchangedCount}`);
88
+ }
75
89
  }
76
90
  exports.logBuildStats = logBuildStats;
77
91
  async function calculateEstimatedTransferSizes(outputFiles) {
@@ -327,7 +341,7 @@ function transformSupportedBrowsersToTargets(supportedBrowsers) {
327
341
  return transformed;
328
342
  }
329
343
  exports.transformSupportedBrowsersToTargets = transformSupportedBrowsersToTargets;
330
- const SUPPORTED_NODE_VERSIONS = '>=18.13.0';
344
+ const SUPPORTED_NODE_VERSIONS = '^18.13.0 || >=20.9.0';
331
345
  /**
332
346
  * Transform supported Node.js versions to esbuild target.
333
347
  * @see https://esbuild.github.io/api/#target
@@ -12,3 +12,4 @@ export declare const maxWorkers: number;
12
12
  export declare const useParallelTs: boolean;
13
13
  export declare const useLegacySass: boolean;
14
14
  export declare const debugPerformance: boolean;
15
+ export declare const shouldWatchRoot: boolean;
@@ -7,7 +7,7 @@
7
7
  * found in the LICENSE file at https://angular.io/license
8
8
  */
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.debugPerformance = exports.useLegacySass = exports.useParallelTs = exports.maxWorkers = exports.allowMinify = exports.shouldBeautify = exports.allowMangle = void 0;
10
+ exports.shouldWatchRoot = exports.debugPerformance = exports.useLegacySass = exports.useParallelTs = exports.maxWorkers = exports.allowMinify = exports.shouldBeautify = exports.allowMangle = void 0;
11
11
  const color_1 = require("./color");
12
12
  function isDisabled(variable) {
13
13
  return variable === '0' || variable.toLowerCase() === 'false';
@@ -81,3 +81,5 @@ exports.useLegacySass = (() => {
81
81
  })();
82
82
  const debugPerfVariable = process.env['NG_BUILD_DEBUG_PERF'];
83
83
  exports.debugPerformance = isPresent(debugPerfVariable) && isEnabled(debugPerfVariable);
84
+ const watchRootVariable = process.env['NG_BUILD_WATCH_ROOT'];
85
+ exports.shouldWatchRoot = isPresent(watchRootVariable) && isEnabled(watchRootVariable);
@@ -11,5 +11,5 @@ interface RouterResult {
11
11
  success: boolean;
12
12
  redirect: boolean;
13
13
  }
14
- export declare function extractRoutes(bootstrapAppFnOrModule: (() => Promise<ApplicationRef>) | Type<unknown>, document: string, url: string): AsyncIterableIterator<RouterResult>;
14
+ export declare function extractRoutes(bootstrapAppFnOrModule: (() => Promise<ApplicationRef>) | Type<unknown>, document: string): AsyncIterableIterator<RouterResult>;
15
15
  export {};
@@ -38,11 +38,11 @@ async function* getRoutesFromRouterConfig(routes, compiler, parentInjector, pare
38
38
  }
39
39
  }
40
40
  }
41
- export async function* extractRoutes(bootstrapAppFnOrModule, document, url) {
41
+ export async function* extractRoutes(bootstrapAppFnOrModule, document) {
42
42
  const platformRef = createPlatformFactory(platformCore, 'server', [
43
43
  {
44
44
  provide: INITIAL_CONFIG,
45
- useValue: { document, url },
45
+ useValue: { document, url: '' },
46
46
  },
47
47
  {
48
48
  provide: ɵConsole,
@@ -47,7 +47,9 @@ function resolve(specifier, context, nextResolve) {
47
47
  }
48
48
  exports.resolve = resolve;
49
49
  async function load(url, context, nextLoad) {
50
- if (isFileProtocol(url)) {
50
+ const { format } = context;
51
+ // CommonJs modules require no transformations and are not in memory.
52
+ if (format !== 'commonjs' && isFileProtocol(url)) {
51
53
  const filePath = (0, url_1.fileURLToPath)(url);
52
54
  // Remove '/' or drive letter for Windows that was added in the above 'resolve'.
53
55
  let source = outputFiles[(0, node_path_1.relative)('/', filePath)] ?? TRANSFORMED_FILES[filePath];
@@ -55,7 +57,6 @@ async function load(url, context, nextLoad) {
55
57
  source = TRANSFORMED_FILES[filePath] = Buffer.from(await javascriptTransformer.transformFile(filePath)).toString('utf-8');
56
58
  }
57
59
  if (source !== undefined) {
58
- const { format } = context;
59
60
  return {
60
61
  format,
61
62
  shortCircuit: true,
@@ -0,0 +1,8 @@
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.io/license
7
+ */
8
+ export declare function patchFetchToLoadInMemoryAssets(): void;
@@ -0,0 +1,66 @@
1
+ "use strict";
2
+ /**
3
+ * @license
4
+ * Copyright Google LLC All Rights Reserved.
5
+ *
6
+ * Use of this source code is governed by an MIT-style license that can be
7
+ * found in the LICENSE file at https://angular.io/license
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.patchFetchToLoadInMemoryAssets = void 0;
11
+ const mrmime_1 = require("mrmime");
12
+ const promises_1 = require("node:fs/promises");
13
+ const node_path_1 = require("node:path");
14
+ const node_worker_threads_1 = require("node:worker_threads");
15
+ const undici_1 = require("undici");
16
+ /**
17
+ * This is passed as workerData when setting up the worker via the `piscina` package.
18
+ */
19
+ const { assetFiles } = node_worker_threads_1.workerData;
20
+ const assetsCache = new Map();
21
+ const RESOLVE_PROTOCOL = 'resolve:';
22
+ function patchFetchToLoadInMemoryAssets() {
23
+ const global = globalThis;
24
+ const originalFetch = global.fetch;
25
+ const patchedFetch = async (input, init) => {
26
+ let url;
27
+ if (input instanceof URL) {
28
+ url = input;
29
+ }
30
+ else if (typeof input === 'string') {
31
+ url = new URL(input, RESOLVE_PROTOCOL + '//');
32
+ }
33
+ else if (typeof input === 'object' && 'url' in input) {
34
+ url = new URL(input.url, RESOLVE_PROTOCOL + '//');
35
+ }
36
+ else {
37
+ return originalFetch(input, init);
38
+ }
39
+ const { pathname, protocol } = url;
40
+ if (protocol !== RESOLVE_PROTOCOL || !assetFiles[pathname]) {
41
+ // Only handle relative requests or files that are in assets.
42
+ return originalFetch(input, init);
43
+ }
44
+ const cachedAsset = assetsCache.get(pathname);
45
+ if (cachedAsset) {
46
+ const { content, headers } = cachedAsset;
47
+ return new undici_1.Response(content, {
48
+ headers,
49
+ });
50
+ }
51
+ const extension = (0, node_path_1.extname)(pathname);
52
+ const mimeType = (0, mrmime_1.lookup)(extension);
53
+ const content = await (0, promises_1.readFile)(assetFiles[pathname]);
54
+ const headers = mimeType
55
+ ? {
56
+ 'Content-Type': mimeType,
57
+ }
58
+ : undefined;
59
+ assetsCache.set(pathname, { headers, content });
60
+ return new undici_1.Response(content, {
61
+ headers,
62
+ });
63
+ };
64
+ global.fetch = patchedFetch;
65
+ }
66
+ exports.patchFetchToLoadInMemoryAssets = patchFetchToLoadInMemoryAssets;
@@ -16,7 +16,6 @@ const node_path_1 = require("node:path");
16
16
  const piscina_1 = __importDefault(require("piscina"));
17
17
  const bundler_context_1 = require("../../tools/esbuild/bundler-context");
18
18
  const node_18_utils_1 = require("./esm-in-memory-loader/node-18-utils");
19
- const prerender_server_1 = require("./prerender-server");
20
19
  async function prerenderPages(workspaceRoot, appShellOptions = {}, prerenderOptions = {}, outputFiles, assets, document, sourcemap = false, inlineCriticalCss = false, maxThreads = 1, verbose = false) {
21
20
  const outputFilesForWorker = {};
22
21
  const serverBundlesSourceMaps = new Map();
@@ -45,37 +44,33 @@ async function prerenderPages(workspaceRoot, appShellOptions = {}, prerenderOpti
45
44
  }
46
45
  }
47
46
  serverBundlesSourceMaps.clear();
48
- // Start server to handle HTTP requests to assets.
49
- // TODO: consider starting this is a seperate process to avoid any blocks to the main thread.
50
- const { address: assetsServerAddress, close: closeAssetsServer } = await (0, prerender_server_1.startServer)(assets);
51
- try {
52
- // Get routes to prerender
53
- const { routes: allRoutes, warnings: routesWarnings } = await getAllRoutes(workspaceRoot, outputFilesForWorker, document, appShellOptions, prerenderOptions, sourcemap, verbose, assetsServerAddress);
54
- if (routesWarnings?.length) {
55
- warnings.push(...routesWarnings);
56
- }
57
- if (allRoutes.size < 1) {
58
- return {
59
- errors,
60
- warnings,
61
- output: {},
62
- prerenderedRoutes: allRoutes,
63
- };
64
- }
65
- // Render routes
66
- const { warnings: renderingWarnings, errors: renderingErrors, output, } = await renderPages(sourcemap, allRoutes, maxThreads, workspaceRoot, outputFilesForWorker, inlineCriticalCss, document, assetsServerAddress, appShellOptions);
67
- errors.push(...renderingErrors);
68
- warnings.push(...renderingWarnings);
47
+ const assetsReversed = {};
48
+ for (const { source, destination } of assets) {
49
+ assetsReversed[addLeadingSlash(destination.replace(/\\/g, node_path_1.posix.sep))] = source;
50
+ }
51
+ // Get routes to prerender
52
+ const { routes: allRoutes, warnings: routesWarnings } = await getAllRoutes(workspaceRoot, outputFilesForWorker, assetsReversed, document, appShellOptions, prerenderOptions, sourcemap, verbose);
53
+ if (routesWarnings?.length) {
54
+ warnings.push(...routesWarnings);
55
+ }
56
+ if (allRoutes.size < 1) {
69
57
  return {
70
58
  errors,
71
59
  warnings,
72
- output,
60
+ output: {},
73
61
  prerenderedRoutes: allRoutes,
74
62
  };
75
63
  }
76
- finally {
77
- void closeAssetsServer?.();
78
- }
64
+ // Render routes
65
+ const { warnings: renderingWarnings, errors: renderingErrors, output, } = await renderPages(sourcemap, allRoutes, maxThreads, workspaceRoot, outputFilesForWorker, assetsReversed, inlineCriticalCss, document, appShellOptions);
66
+ errors.push(...renderingErrors);
67
+ warnings.push(...renderingWarnings);
68
+ return {
69
+ errors,
70
+ warnings,
71
+ output,
72
+ prerenderedRoutes: allRoutes,
73
+ };
79
74
  }
80
75
  exports.prerenderPages = prerenderPages;
81
76
  class RoutesSet extends Set {
@@ -83,7 +78,7 @@ class RoutesSet extends Set {
83
78
  return super.add(addLeadingSlash(value));
84
79
  }
85
80
  }
86
- async function renderPages(sourcemap, allRoutes, maxThreads, workspaceRoot, outputFilesForWorker, inlineCriticalCss, document, baseUrl, appShellOptions) {
81
+ async function renderPages(sourcemap, allRoutes, maxThreads, workspaceRoot, outputFilesForWorker, assetFilesForWorker, inlineCriticalCss, document, appShellOptions) {
87
82
  const output = {};
88
83
  const warnings = [];
89
84
  const errors = [];
@@ -97,9 +92,9 @@ async function renderPages(sourcemap, allRoutes, maxThreads, workspaceRoot, outp
97
92
  workerData: {
98
93
  workspaceRoot,
99
94
  outputFiles: outputFilesForWorker,
95
+ assetFiles: assetFilesForWorker,
100
96
  inlineCriticalCss,
101
97
  document,
102
- baseUrl,
103
98
  },
104
99
  execArgv: workerExecArgv,
105
100
  });
@@ -139,7 +134,7 @@ async function renderPages(sourcemap, allRoutes, maxThreads, workspaceRoot, outp
139
134
  output,
140
135
  };
141
136
  }
142
- async function getAllRoutes(workspaceRoot, outputFilesForWorker, document, appShellOptions, prerenderOptions, sourcemap, verbose, assetsServerAddress) {
137
+ async function getAllRoutes(workspaceRoot, outputFilesForWorker, assetFilesForWorker, document, appShellOptions, prerenderOptions, sourcemap, verbose) {
143
138
  const { routesFile, discoverRoutes } = prerenderOptions;
144
139
  const routes = new RoutesSet();
145
140
  const { route: appShellRoute } = appShellOptions;
@@ -165,9 +160,9 @@ async function getAllRoutes(workspaceRoot, outputFilesForWorker, document, appSh
165
160
  workerData: {
166
161
  workspaceRoot,
167
162
  outputFiles: outputFilesForWorker,
163
+ assetFiles: assetFilesForWorker,
168
164
  document,
169
165
  verbose,
170
- url: assetsServerAddress,
171
166
  },
172
167
  execArgv: workerExecArgv,
173
168
  });
@@ -10,11 +10,13 @@ import { RenderResult, ServerContext } from './render-page';
10
10
  export interface RenderWorkerData extends ESMInMemoryFileLoaderWorkerData {
11
11
  document: string;
12
12
  inlineCriticalCss?: boolean;
13
- baseUrl: string;
13
+ assetFiles: Record</** Destination */ string, /** Source */ string>;
14
14
  }
15
15
  export interface RenderOptions {
16
16
  route: string;
17
17
  serverContext: ServerContext;
18
18
  }
19
19
  /** Renders an application based on a provided options. */
20
- export default function (options: RenderOptions): Promise<RenderResult>;
20
+ declare function render(options: RenderOptions): Promise<RenderResult>;
21
+ declare const _default: typeof render;
22
+ export default _default;
@@ -8,19 +8,23 @@
8
8
  */
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
10
  const node_worker_threads_1 = require("node:worker_threads");
11
+ const fetch_patch_1 = require("./fetch-patch");
11
12
  const render_page_1 = require("./render-page");
12
13
  /**
13
14
  * This is passed as workerData when setting up the worker via the `piscina` package.
14
15
  */
15
- const { outputFiles, document, inlineCriticalCss, baseUrl } = node_worker_threads_1.workerData;
16
+ const { outputFiles, document, inlineCriticalCss } = node_worker_threads_1.workerData;
16
17
  /** Renders an application based on a provided options. */
17
- function default_1(options) {
18
+ async function render(options) {
18
19
  return (0, render_page_1.renderPage)({
19
20
  ...options,
20
- route: baseUrl + options.route,
21
21
  outputFiles,
22
22
  document,
23
23
  inlineCriticalCss,
24
24
  });
25
25
  }
26
- exports.default = default_1;
26
+ function initialize() {
27
+ (0, fetch_patch_1.patchFetchToLoadInMemoryAssets)();
28
+ return render;
29
+ }
30
+ exports.default = initialize();
@@ -9,11 +9,13 @@ import type { ESMInMemoryFileLoaderWorkerData } from './esm-in-memory-loader/loa
9
9
  export interface RoutesExtractorWorkerData extends ESMInMemoryFileLoaderWorkerData {
10
10
  document: string;
11
11
  verbose: boolean;
12
- url: string;
13
- assetsServerAddress: string;
12
+ assetFiles: Record</** Destination */ string, /** Source */ string>;
14
13
  }
15
14
  export interface RoutersExtractorWorkerResult {
16
15
  routes: string[];
17
16
  warnings?: string[];
18
17
  }
19
- export default function (): Promise<RoutersExtractorWorkerResult>;
18
+ /** Renders an application based on a provided options. */
19
+ declare function extractRoutes(): Promise<RoutersExtractorWorkerResult>;
20
+ declare const _default: typeof extractRoutes;
21
+ export default _default;
@@ -9,17 +9,19 @@
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
10
  const node_worker_threads_1 = require("node:worker_threads");
11
11
  const load_esm_1 = require("../load-esm");
12
+ const fetch_patch_1 = require("./fetch-patch");
12
13
  /**
13
14
  * This is passed as workerData when setting up the worker via the `piscina` package.
14
15
  */
15
- const { document, verbose, url } = node_worker_threads_1.workerData;
16
- async function default_1() {
16
+ const { document, verbose } = node_worker_threads_1.workerData;
17
+ /** Renders an application based on a provided options. */
18
+ async function extractRoutes() {
17
19
  const { extractRoutes } = await (0, load_esm_1.loadEsmModule)('./render-utils.server.mjs');
18
20
  const { default: bootstrapAppFnOrModule } = await (0, load_esm_1.loadEsmModule)('./main.server.mjs');
19
21
  const skippedRedirects = [];
20
22
  const skippedOthers = [];
21
23
  const routes = [];
22
- for await (const { route, success, redirect } of extractRoutes(bootstrapAppFnOrModule, document, url)) {
24
+ for await (const { route, success, redirect } of extractRoutes(bootstrapAppFnOrModule, document)) {
23
25
  if (success) {
24
26
  routes.push(route);
25
27
  continue;
@@ -44,4 +46,8 @@ async function default_1() {
44
46
  }
45
47
  return { routes, warnings };
46
48
  }
47
- exports.default = default_1;
49
+ function initialize() {
50
+ (0, fetch_patch_1.patchFetchToLoadInMemoryAssets)();
51
+ return extractRoutes;
52
+ }
53
+ exports.default = initialize();
@@ -1,21 +0,0 @@
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.io/license
7
- */
8
- import { BuildOutputAsset } from '../../tools/esbuild/bundler-execution-result';
9
- /**
10
- * Start a server that can handle HTTP requests to assets.
11
- *
12
- * @example
13
- * ```ts
14
- * httpClient.get('/assets/content.json');
15
- * ```
16
- * @returns the server address.
17
- */
18
- export declare function startServer(assets: Readonly<BuildOutputAsset[]>): Promise<{
19
- address: string;
20
- close?: () => void;
21
- }>;
@@ -1,102 +0,0 @@
1
- "use strict";
2
- /**
3
- * @license
4
- * Copyright Google LLC All Rights Reserved.
5
- *
6
- * Use of this source code is governed by an MIT-style license that can be
7
- * found in the LICENSE file at https://angular.io/license
8
- */
9
- Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.startServer = void 0;
11
- const mrmime_1 = require("mrmime");
12
- const promises_1 = require("node:fs/promises");
13
- const node_http_1 = require("node:http");
14
- const node_path_1 = require("node:path");
15
- /**
16
- * Start a server that can handle HTTP requests to assets.
17
- *
18
- * @example
19
- * ```ts
20
- * httpClient.get('/assets/content.json');
21
- * ```
22
- * @returns the server address.
23
- */
24
- async function startServer(assets) {
25
- if (Object.keys(assets).length === 0) {
26
- return {
27
- address: '',
28
- };
29
- }
30
- const assetsReversed = {};
31
- for (const { source, destination } of assets) {
32
- assetsReversed[addLeadingSlash(destination.replace(/\\/g, node_path_1.posix.sep))] = source;
33
- }
34
- const assetsCache = new Map();
35
- const server = (0, node_http_1.createServer)(requestHandler(assetsReversed, assetsCache));
36
- await new Promise((resolve) => {
37
- server.listen(0, '127.0.0.1', resolve);
38
- });
39
- const serverAddress = server.address();
40
- let address;
41
- if (!serverAddress) {
42
- address = '';
43
- }
44
- else if (typeof serverAddress === 'string') {
45
- address = serverAddress;
46
- }
47
- else {
48
- const { port, address: host } = serverAddress;
49
- address = `http://${host}:${port}`;
50
- }
51
- return {
52
- address,
53
- close: () => {
54
- assetsCache.clear();
55
- server.unref();
56
- server.close();
57
- },
58
- };
59
- }
60
- exports.startServer = startServer;
61
- function requestHandler(assetsReversed, assetsCache) {
62
- return (req, res) => {
63
- if (!req.url) {
64
- res.destroy(new Error('Request url was empty.'));
65
- return;
66
- }
67
- const { pathname } = new URL(req.url, 'resolve://');
68
- const asset = assetsReversed[pathname];
69
- if (!asset) {
70
- res.statusCode = 404;
71
- res.statusMessage = 'Asset not found.';
72
- res.end();
73
- return;
74
- }
75
- const cachedAsset = assetsCache.get(pathname);
76
- if (cachedAsset) {
77
- const { content, mimeType } = cachedAsset;
78
- if (mimeType) {
79
- res.setHeader('Content-Type', mimeType);
80
- }
81
- res.end(content);
82
- return;
83
- }
84
- (0, promises_1.readFile)(asset)
85
- .then((content) => {
86
- const extension = (0, node_path_1.extname)(pathname);
87
- const mimeType = (0, mrmime_1.lookup)(extension);
88
- assetsCache.set(pathname, {
89
- mimeType,
90
- content,
91
- });
92
- if (mimeType) {
93
- res.setHeader('Content-Type', mimeType);
94
- }
95
- res.end(content);
96
- })
97
- .catch((e) => res.destroy(e));
98
- };
99
- }
100
- function addLeadingSlash(value) {
101
- return value.charAt(0) === '/' ? value : '/' + value;
102
- }