@angular/build 19.2.0-next.1 → 19.2.0-next.2

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@angular/build",
3
- "version": "19.2.0-next.1",
3
+ "version": "19.2.0-next.2",
4
4
  "description": "Official build system for Angular",
5
5
  "keywords": [
6
6
  "Angular CLI",
@@ -23,16 +23,16 @@
23
23
  "builders": "builders.json",
24
24
  "dependencies": {
25
25
  "@ampproject/remapping": "2.3.0",
26
- "@angular-devkit/architect": "0.1902.0-next.1",
27
- "@babel/core": "7.26.7",
26
+ "@angular-devkit/architect": "0.1902.0-next.2",
27
+ "@babel/core": "7.26.8",
28
28
  "@babel/helper-annotate-as-pure": "7.25.9",
29
29
  "@babel/helper-split-export-declaration": "7.24.7",
30
30
  "@babel/plugin-syntax-import-attributes": "7.26.0",
31
- "@inquirer/confirm": "5.1.4",
31
+ "@inquirer/confirm": "5.1.5",
32
32
  "@vitejs/plugin-basic-ssl": "1.2.0",
33
33
  "beasties": "0.2.0",
34
34
  "browserslist": "^4.23.0",
35
- "esbuild": "0.24.2",
35
+ "esbuild": "0.25.0",
36
36
  "fast-glob": "3.3.3",
37
37
  "https-proxy-agent": "7.0.6",
38
38
  "istanbul-lib-instrument": "6.0.3",
@@ -42,14 +42,14 @@
42
42
  "parse5-html-rewriting-stream": "7.0.0",
43
43
  "picomatch": "4.0.2",
44
44
  "piscina": "4.8.0",
45
- "rollup": "4.32.1",
46
- "sass": "1.83.4",
47
- "semver": "7.6.3",
48
- "vite": "6.0.11",
45
+ "rollup": "4.34.6",
46
+ "sass": "1.84.0",
47
+ "semver": "7.7.1",
48
+ "vite": "6.1.0",
49
49
  "watchpack": "2.4.2"
50
50
  },
51
51
  "optionalDependencies": {
52
- "lmdb": "3.2.2"
52
+ "lmdb": "3.2.6"
53
53
  },
54
54
  "peerDependencies": {
55
55
  "@angular/compiler": "^19.0.0 || ^19.2.0-next.0",
@@ -57,7 +57,7 @@
57
57
  "@angular/localize": "^19.0.0 || ^19.2.0-next.0",
58
58
  "@angular/platform-server": "^19.0.0 || ^19.2.0-next.0",
59
59
  "@angular/service-worker": "^19.0.0 || ^19.2.0-next.0",
60
- "@angular/ssr": "^19.2.0-next.1",
60
+ "@angular/ssr": "^19.2.0-next.2",
61
61
  "less": "^4.2.0",
62
62
  "ng-packagr": "^19.0.0 || ^19.2.0-next.0",
63
63
  "postcss": "^8.4.0",
@@ -127,6 +127,9 @@ async function* runEsBuildBuildAction(action, options) {
127
127
  if (!watcher) {
128
128
  return;
129
129
  }
130
+ // Used to force a full result on next rebuild if there were initial errors.
131
+ // This ensures at least one full result is emitted.
132
+ let hasInitialErrors = result.errors.length > 0;
130
133
  // Wait for changes and rebuild as needed
131
134
  const currentWatchFiles = new Set(result.watchFiles);
132
135
  try {
@@ -164,9 +167,11 @@ async function* runEsBuildBuildAction(action, options) {
164
167
  if (staleWatchFiles?.size) {
165
168
  watcher.remove([...staleWatchFiles]);
166
169
  }
167
- for (const outputResult of emitOutputResults(result, outputOptions, changes, incrementalResults ? rebuildState : undefined)) {
170
+ for (const outputResult of emitOutputResults(result, outputOptions, changes, incrementalResults && !hasInitialErrors ? rebuildState : undefined)) {
168
171
  yield outputResult;
169
172
  }
173
+ // Clear initial build errors flag if no errors are now present
174
+ hasInitialErrors &&= result.errors.length > 0;
170
175
  }
171
176
  }
172
177
  finally {
@@ -148,7 +148,6 @@ async function executeBuild(options, context, rebuildState) {
148
148
  }
149
149
  const { metafile, initialFiles, outputFiles } = bundlingResult;
150
150
  executionResult.outputFiles.push(...outputFiles);
151
- const changedFiles = rebuildState && executionResult.findChangedFiles(rebuildState.previousOutputInfo);
152
151
  // Analyze files for bundle budget failures if present
153
152
  let budgetFailures;
154
153
  if (options.budgets) {
@@ -218,6 +217,7 @@ async function executeBuild(options, context, rebuildState) {
218
217
  executionResult.addOutputFile('stats.json', JSON.stringify(metafile, null, 2), bundler_context_1.BuildOutputFileType.Root);
219
218
  }
220
219
  if (!jsonLogs) {
220
+ const changedFiles = rebuildState && executionResult.findChangedFiles(rebuildState.previousOutputInfo);
221
221
  executionResult.addLog((0, utils_1.logBuildStats)(metafile, outputFiles, initialFiles, budgetFailures, colors, changedFiles, estimatedTransferSizes, !!ssrOptions, verbose));
222
222
  }
223
223
  return executionResult;
@@ -38,7 +38,7 @@ async function executePostBundleSteps(metafile, options, outputFiles, assetFiles
38
38
  const allErrors = [];
39
39
  const allWarnings = [];
40
40
  const prerenderedRoutes = {};
41
- const { baseHref = '/', serviceWorker, i18nOptions, indexHtmlOptions, optimizationOptions, sourcemapOptions, outputMode, serverEntryPoint, prerenderOptions, appShellOptions, publicPath, workspaceRoot, partialSSRBuild, } = options;
41
+ const { baseHref = '/', serviceWorker, ssrOptions, indexHtmlOptions, optimizationOptions, sourcemapOptions, outputMode, serverEntryPoint, prerenderOptions, appShellOptions, publicPath, workspaceRoot, partialSSRBuild, } = options;
42
42
  // Index HTML content without CSS inlining to be used for server rendering (AppShell, SSG and SSR).
43
43
  // NOTE: Critical CSS inlining is deliberately omitted here, as it will be handled during server rendering.
44
44
  // Additionally, when using prerendering or AppShell, the index HTML file may be regenerated.
@@ -57,7 +57,7 @@ async function executePostBundleSteps(metafile, options, outputFiles, assetFiles
57
57
  }
58
58
  // Create server manifest
59
59
  const initialFilesPaths = new Set(initialFiles.keys());
60
- if (serverEntryPoint) {
60
+ if (serverEntryPoint && (outputMode || prerenderOptions || appShellOptions || ssrOptions)) {
61
61
  const { manifestContent, serverAssetsChunks } = (0, manifest_1.generateAngularServerAppManifest)(additionalHtmlOutputFiles, outputFiles, optimizationOptions.styles.inlineCritical ?? false, undefined, locale, baseHref, initialFilesPaths, metafile, publicPath);
62
62
  additionalOutputFiles.push(...serverAssetsChunks, (0, utils_1.createOutputFile)(manifest_1.SERVER_APP_MANIFEST_FILENAME, manifestContent, bundler_context_1.BuildOutputFileType.ServerApplication));
63
63
  }
@@ -26,12 +26,13 @@ const options_1 = require("./options");
26
26
  * @param initialFiles A map containing initial file information for the executed build.
27
27
  */
28
28
  async function inlineI18n(metafile, options, executionResult, initialFiles) {
29
- const { i18nOptions, optimizationOptions, baseHref } = options;
29
+ const { i18nOptions, optimizationOptions, baseHref, cacheOptions } = options;
30
30
  // Create the multi-threaded inliner with common options and the files generated from the build.
31
31
  const inliner = new i18n_inliner_1.I18nInliner({
32
32
  missingTranslation: i18nOptions.missingTranslationBehavior ?? 'warning',
33
33
  outputFiles: executionResult.outputFiles,
34
34
  shouldOptimize: optimizationOptions.scripts,
35
+ persistentCachePath: cacheOptions.enabled ? cacheOptions.path : undefined,
35
36
  }, environment_options_1.maxWorkers);
36
37
  const inlineResult = {
37
38
  errors: [],
@@ -41,6 +42,8 @@ async function inlineI18n(metafile, options, executionResult, initialFiles) {
41
42
  // For each active locale, use the inliner to process the output files of the build.
42
43
  const updatedOutputFiles = [];
43
44
  const updatedAssetFiles = [];
45
+ // Root and SSR entry files are not modified.
46
+ const unModifiedOutputFiles = executionResult.outputFiles.filter(({ type }) => type === bundler_context_1.BuildOutputFileType.Root || type === bundler_context_1.BuildOutputFileType.ServerRoot);
44
47
  try {
45
48
  for (const locale of i18nOptions.inlineLocales) {
46
49
  // A locale specific set of files is returned from the inliner.
@@ -51,7 +54,7 @@ async function inlineI18n(metafile, options, executionResult, initialFiles) {
51
54
  const { errors, warnings, additionalAssets, additionalOutputFiles, prerenderedRoutes: generatedRoutes, } = await (0, execute_post_bundle_1.executePostBundleSteps)(metafile, {
52
55
  ...options,
53
56
  baseHref: (0, options_1.getLocaleBaseHref)(baseHref, i18nOptions, locale) ?? baseHref,
54
- }, localeOutputFiles, executionResult.assetFiles, initialFiles, locale);
57
+ }, [...unModifiedOutputFiles, ...localeOutputFiles], executionResult.assetFiles, initialFiles, locale);
55
58
  localeOutputFiles.push(...additionalOutputFiles);
56
59
  inlineResult.errors.push(...errors);
57
60
  inlineResult.warnings.push(...warnings);
@@ -81,7 +84,7 @@ async function inlineI18n(metafile, options, executionResult, initialFiles) {
81
84
  // Update the result with all localized files.
82
85
  executionResult.outputFiles = [
83
86
  // Root and SSR entry files are not modified.
84
- ...executionResult.outputFiles.filter(({ type }) => type === bundler_context_1.BuildOutputFileType.Root || type === bundler_context_1.BuildOutputFileType.ServerRoot),
87
+ ...unModifiedOutputFiles,
85
88
  // Updated files for each locale.
86
89
  ...updatedOutputFiles,
87
90
  ];
@@ -3,8 +3,8 @@
3
3
  */
4
4
  export type Schema = {
5
5
  /**
6
- * The hosts that can access the development server. This option sets the Vite option of the
7
- * same name. For further details:
6
+ * The hosts that the development server will respond to. This option sets the Vite option
7
+ * of the same name. For further details:
8
8
  * https://vite.dev/config/server-options.html#server-allowedhosts
9
9
  */
10
10
  allowedHosts?: AllowedHosts;
@@ -86,8 +86,8 @@ export type Schema = {
86
86
  watch?: boolean;
87
87
  };
88
88
  /**
89
- * The hosts that can access the development server. This option sets the Vite option of the
90
- * same name. For further details:
89
+ * The hosts that the development server will respond to. This option sets the Vite option
90
+ * of the same name. For further details:
91
91
  * https://vite.dev/config/server-options.html#server-allowedhosts
92
92
  */
93
93
  export type AllowedHosts = string[] | boolean;
@@ -37,12 +37,12 @@
37
37
  "description": "SSL certificate to use for serving HTTPS."
38
38
  },
39
39
  "allowedHosts": {
40
- "description": "The hosts that can access the development server. This option sets the Vite option of the same name. For further details: https://vite.dev/config/server-options.html#server-allowedhosts",
40
+ "description": "The hosts that the development server will respond to. This option sets the Vite option of the same name. For further details: https://vite.dev/config/server-options.html#server-allowedhosts",
41
41
  "default": [],
42
42
  "oneOf": [
43
43
  {
44
44
  "type": "array",
45
- "description": "List of hosts that are allowed to access the development server.",
45
+ "description": "A list of hosts that the development server will respond to.",
46
46
  "items": {
47
47
  "type": "string"
48
48
  }
@@ -161,6 +161,7 @@ async function* serveWithVite(serverOptions, builderName, builderAction, context
161
161
  },
162
162
  });
163
163
  }
164
+ yield { baseUrl: '', success: false };
164
165
  continue;
165
166
  }
166
167
  // Clear existing error overlay on successful result
@@ -321,6 +322,29 @@ async function* serveWithVite(serverOptions, builderName, builderAction, context
321
322
  break;
322
323
  }
323
324
  });
325
+ // Setup component HMR invalidation
326
+ // Invalidation occurs when the runtime cannot update a component
327
+ server.hot.on('angular:invalidate', (data) => {
328
+ if (typeof data?.id !== 'string') {
329
+ context.logger.warn('Development server client sent invalid internal invalidate event.');
330
+ }
331
+ // Clear invalid template update
332
+ templateUpdates.delete(data.id);
333
+ // Some cases are expected unsupported update scenarios but some may be errors.
334
+ // If an error occurred, log the error in addition to the invalidation.
335
+ if (data.error) {
336
+ context.logger.error(`Component update failed${data.message ? `: ${data.message}` : '.'}` +
337
+ '\nPlease consider reporting the error at https://github.com/angular/angular-cli/issues');
338
+ }
339
+ else {
340
+ context.logger.warn(`Component update unsupported${data.message ? `: ${data.message}` : '.'}`);
341
+ }
342
+ server?.ws.send({
343
+ type: 'full-reload',
344
+ path: '*',
345
+ });
346
+ context.logger.info('Page reload sent to client(s).');
347
+ });
324
348
  const urls = server.resolvedUrls;
325
349
  if (urls && (urls.local.length || urls.network.length)) {
326
350
  serverUrl = new URL(urls.local[0] ?? urls.network[0]);
@@ -506,6 +530,7 @@ function updateResultRecord(outputPath, file, normalizePath, htmlIndexPath, gene
506
530
  }
507
531
  }
508
532
  }
533
+ // eslint-disable-next-line max-lines-per-function
509
534
  async function setupServer(serverOptions, outputFiles, assets, preserveSymlinks, externalMetadata, ssrMode, prebundleTransformer, target, zoneless, componentStyles, templateUpdates, prebundleLoaderExtensions, define, extensionMiddleware, indexHtmlTransformer, thirdPartySourcemaps = false) {
510
535
  const proxy = await (0, utils_2.loadProxyConfiguration)(serverOptions.workspaceRoot, serverOptions.proxyConfig);
511
536
  // dynamically import Vite for ESM compatibility
@@ -522,6 +547,14 @@ async function setupServer(serverOptions, outputFiles, assets, preserveSymlinks,
522
547
  ssrFiles = ['./main.server.mjs', './server.mjs'];
523
548
  break;
524
549
  }
550
+ /**
551
+ * Required when using `externalDependencies` to prevent Vite load errors.
552
+ *
553
+ * @note Can be removed if Vite introduces native support for externals.
554
+ * @note Vite misresolves browser modules in SSR when accessing URLs with multiple segments
555
+ * (e.g., 'foo/bar'), as they are not correctly re-based from the base href.
556
+ */
557
+ const preTransformRequests = externalMetadata.explicitBrowser.length === 0 && ssrMode === plugins_1.ServerSsrMode.NoSsr;
525
558
  const cacheDir = (0, node_path_1.join)(serverOptions.cacheOptions.path, serverOptions.buildTarget.project, 'vite');
526
559
  const configuration = {
527
560
  configFile: false,
@@ -550,7 +583,11 @@ async function setupServer(serverOptions, outputFiles, assets, preserveSymlinks,
550
583
  mainFields: ['es2020', 'browser', 'module', 'main'],
551
584
  preserveSymlinks,
552
585
  },
586
+ dev: {
587
+ preTransformRequests,
588
+ },
553
589
  server: {
590
+ preTransformRequests,
554
591
  warmup: {
555
592
  ssrFiles,
556
593
  },
@@ -571,6 +608,9 @@ async function setupServer(serverOptions, outputFiles, assets, preserveSymlinks,
571
608
  ? (proxy ?? {})
572
609
  : proxy,
573
610
  cors: {
611
+ // This will add the header `Access-Control-Allow-Origin: http://example.com`,
612
+ // where `http://example.com` is the requesting origin.
613
+ origin: true,
574
614
  // Allow preflight requests to be proxied.
575
615
  preflightContinue: true,
576
616
  },
@@ -587,9 +627,6 @@ async function setupServer(serverOptions, outputFiles, assets, preserveSymlinks,
587
627
  ...[...assets.values()].map(({ source }) => source),
588
628
  ],
589
629
  },
590
- // This is needed when `externalDependencies` is used to prevent Vite load errors.
591
- // NOTE: If Vite adds direct support for externals, this can be removed.
592
- preTransformRequests: externalMetadata.explicitBrowser.length === 0,
593
630
  },
594
631
  ssr: {
595
632
  // Note: `true` and `/.*/` have different sematics. When true, the `external` option is ignored.
@@ -631,7 +668,7 @@ async function setupServer(serverOptions, outputFiles, assets, preserveSymlinks,
631
668
  outputFiles,
632
669
  templateUpdates,
633
670
  external: externalMetadata.explicitBrowser,
634
- skipViteClient: serverOptions.liveReload === false && serverOptions.hmr === false,
671
+ disableViteTransport: !serverOptions.liveReload,
635
672
  }),
636
673
  ],
637
674
  // Browser only optimizeDeps. (This does not run for SSR dependencies).
@@ -136,7 +136,8 @@ function analyzeFileUpdates(stale, updated, compiler) {
136
136
  return null;
137
137
  }
138
138
  // Compare component meta decorator object literals
139
- if (hasUnsupportedMetaUpdates(staleDecoratorExpression, stale, updatedDecoratorExpression, updated)) {
139
+ const analysis = analyzeMetaUpdates(staleDecoratorExpression, stale, updatedDecoratorExpression, updated);
140
+ if (analysis === MetaUpdateAnalysis.Unsupported) {
140
141
  return null;
141
142
  }
142
143
  // Compare text of the member nodes to determine if any changes have occurred
@@ -145,7 +146,9 @@ function analyzeFileUpdates(stale, updated, compiler) {
145
146
  return null;
146
147
  }
147
148
  // If all previous class checks passed, this class is supported for HMR updates
148
- candidates.push(updatedNode);
149
+ if (analysis === MetaUpdateAnalysis.Supported) {
150
+ candidates.push(updatedNode);
151
+ }
149
152
  continue;
150
153
  }
151
154
  }
@@ -161,7 +164,19 @@ function analyzeFileUpdates(stale, updated, compiler) {
161
164
  /**
162
165
  * The set of Angular component metadata fields that are supported by HMR updates.
163
166
  */
164
- const SUPPORTED_FIELDS = new Set(['template', 'templateUrl', 'styles', 'styleUrl', 'stylesUrl']);
167
+ const SUPPORTED_FIELD_NAMES = new Set([
168
+ 'template',
169
+ 'templateUrl',
170
+ 'styles',
171
+ 'styleUrl',
172
+ 'stylesUrl',
173
+ ]);
174
+ var MetaUpdateAnalysis;
175
+ (function (MetaUpdateAnalysis) {
176
+ MetaUpdateAnalysis[MetaUpdateAnalysis["Supported"] = 0] = "Supported";
177
+ MetaUpdateAnalysis[MetaUpdateAnalysis["Unsupported"] = 1] = "Unsupported";
178
+ MetaUpdateAnalysis[MetaUpdateAnalysis["None"] = 2] = "None";
179
+ })(MetaUpdateAnalysis || (MetaUpdateAnalysis = {}));
165
180
  /**
166
181
  * Analyzes the metadata fields of a decorator call expression for unsupported HMR updates.
167
182
  * Only updates to supported fields can be present for HMR to be viable.
@@ -169,22 +184,25 @@ const SUPPORTED_FIELDS = new Set(['template', 'templateUrl', 'styles', 'styleUrl
169
184
  * @param staleSource The source file instance containing the stale call instance.
170
185
  * @param updatedCall A call expression instance.
171
186
  * @param updatedSource The source file instance containing the updated call instance.
172
- * @returns true, if unsupported metadata updates are present; false, otherwise.
187
+ * @returns A MetaUpdateAnalysis enum value.
173
188
  */
174
- function hasUnsupportedMetaUpdates(staleCall, staleSource, updatedCall, updatedSource) {
189
+ function analyzeMetaUpdates(staleCall, staleSource, updatedCall, updatedSource) {
175
190
  const staleObject = staleCall.arguments[0];
176
191
  const updatedObject = updatedCall.arguments[0];
192
+ let hasSupportedUpdate = false;
177
193
  if (!typescript_1.default.isObjectLiteralExpression(staleObject) || !typescript_1.default.isObjectLiteralExpression(updatedObject)) {
178
- return true;
194
+ return MetaUpdateAnalysis.Unsupported;
179
195
  }
196
+ const supportedFields = new Map();
180
197
  const unsupportedFields = [];
181
198
  for (const property of staleObject.properties) {
182
199
  if (!typescript_1.default.isPropertyAssignment(property) || typescript_1.default.isComputedPropertyName(property.name)) {
183
200
  // Unsupported object literal property
184
- return true;
201
+ return MetaUpdateAnalysis.Unsupported;
185
202
  }
186
203
  const name = property.name.text;
187
- if (SUPPORTED_FIELDS.has(name)) {
204
+ if (SUPPORTED_FIELD_NAMES.has(name)) {
205
+ supportedFields.set(name, property.initializer);
188
206
  continue;
189
207
  }
190
208
  unsupportedFields.push(property.initializer);
@@ -193,18 +211,31 @@ function hasUnsupportedMetaUpdates(staleCall, staleSource, updatedCall, updatedS
193
211
  for (const property of updatedObject.properties) {
194
212
  if (!typescript_1.default.isPropertyAssignment(property) || typescript_1.default.isComputedPropertyName(property.name)) {
195
213
  // Unsupported object literal property
196
- return true;
214
+ return MetaUpdateAnalysis.Unsupported;
197
215
  }
198
216
  const name = property.name.text;
199
- if (SUPPORTED_FIELDS.has(name)) {
217
+ if (SUPPORTED_FIELD_NAMES.has(name)) {
218
+ const staleInitializer = supportedFields.get(name);
219
+ // If the supported field was added or has its content changed, there has been a supported update
220
+ if (!staleInitializer ||
221
+ !equalRangeText(property.initializer, updatedSource, staleInitializer, staleSource)) {
222
+ hasSupportedUpdate = true;
223
+ }
224
+ // Remove the field entry to allow tracking removed fields
225
+ supportedFields.delete(name);
200
226
  continue;
201
227
  }
202
228
  // Compare in order
203
229
  if (!equalRangeText(property.initializer, updatedSource, unsupportedFields[i++], staleSource)) {
204
- return true;
230
+ return MetaUpdateAnalysis.Unsupported;
205
231
  }
206
232
  }
207
- return i !== unsupportedFields.length;
233
+ if (i !== unsupportedFields.length) {
234
+ return MetaUpdateAnalysis.Unsupported;
235
+ }
236
+ // Any remaining supported field indicates a field removal. This is also considered a supported update.
237
+ hasSupportedUpdate ||= supportedFields.size > 0;
238
+ return hasSupportedUpdate ? MetaUpdateAnalysis.Supported : MetaUpdateAnalysis.None;
208
239
  }
209
240
  /**
210
241
  * Compares the text from a provided range in a source file to the text of a range in a second source file.
@@ -14,6 +14,7 @@ exports.default = default_1;
14
14
  const core_1 = require("@babel/core");
15
15
  const istanbul_lib_instrument_1 = require("istanbul-lib-instrument");
16
16
  const node_assert_1 = __importDefault(require("node:assert"));
17
+ const node_url_1 = require("node:url");
17
18
  /**
18
19
  * A babel plugin factory function for adding istanbul instrumentation.
19
20
  *
@@ -25,9 +26,15 @@ function default_1() {
25
26
  visitor: {
26
27
  Program: {
27
28
  enter(path, state) {
29
+ const inputSourceMap = // eslint-disable-next-line @typescript-eslint/no-explicit-any
30
+ state.file.inputMap?.toObject();
31
+ // istanbul does not support URL as sources.
32
+ if (inputSourceMap?.sources) {
33
+ inputSourceMap.sources = inputSourceMap.sources.map((s) => s.startsWith('file://') ? (0, node_url_1.fileURLToPath)(s) : s);
34
+ }
28
35
  const visitor = (0, istanbul_lib_instrument_1.programVisitor)(core_1.types, state.filename, {
29
36
  // Babel returns a Converter object from the `convert-source-map` package
30
- inputSourceMap: state.file.inputMap?.toObject(),
37
+ inputSourceMap,
31
38
  });
32
39
  visitors.set(path, visitor);
33
40
  visitor.enter(path);
@@ -47,6 +47,7 @@ exports.createCompilerPlugin = createCompilerPlugin;
47
47
  const node_assert_1 = __importDefault(require("node:assert"));
48
48
  const node_crypto_1 = require("node:crypto");
49
49
  const path = __importStar(require("node:path"));
50
+ const node_url_1 = require("node:url");
50
51
  const environment_options_1 = require("../../../utils/environment-options");
51
52
  const compilation_1 = require("../../angular/compilation");
52
53
  const javascript_transformer_1 = require("../javascript-transformer");
@@ -233,7 +234,7 @@ function createCompilerPlugin(pluginOptions, compilationOrFactory, stylesheetBun
233
234
  let referencedFiles;
234
235
  let externalStylesheets;
235
236
  try {
236
- const initializationResult = await compilation.initialize(pluginOptions.tsconfig, hostOptions, createCompilerOptionsTransformer(setupWarnings, pluginOptions, preserveSymlinks, build.initialOptions.conditions));
237
+ const initializationResult = await compilation.initialize(pluginOptions.tsconfig, hostOptions, createCompilerOptionsTransformer(setupWarnings, pluginOptions, preserveSymlinks, build.initialOptions.conditions, build.initialOptions.absWorkingDir));
237
238
  shouldTsIgnoreJs = !initializationResult.compilerOptions.allowJs;
238
239
  // Isolated modules option ensures safe non-TypeScript transpilation.
239
240
  // Typescript printing support for sourcemaps is not yet integrated.
@@ -474,7 +475,7 @@ async function bundleExternalStylesheet(stylesheetBundler, stylesheetFile, exter
474
475
  });
475
476
  }
476
477
  }
477
- function createCompilerOptionsTransformer(setupWarnings, pluginOptions, preserveSymlinks, customConditions) {
478
+ function createCompilerOptionsTransformer(setupWarnings, pluginOptions, preserveSymlinks, customConditions, absWorkingDir) {
478
479
  return (compilerOptions) => {
479
480
  // target of 9 is ES2022 (using the number avoids an expensive import of typescript just for an enum)
480
481
  if (compilerOptions.target === undefined || compilerOptions.target < 9 /** ES2022 */) {
@@ -542,6 +543,10 @@ function createCompilerOptionsTransformer(setupWarnings, pluginOptions, preserve
542
543
  return {
543
544
  ...compilerOptions,
544
545
  noEmitOnError: false,
546
+ // Using the path as a URL is necessary here; otherwise, esbuild will not generate source maps correctly.
547
+ // https://github.com/evanw/esbuild/issues/4070
548
+ // https://github.com/evanw/esbuild/issues/4075
549
+ outDir: absWorkingDir ? (0, node_url_1.pathToFileURL)(absWorkingDir + '/').href : undefined,
545
550
  inlineSources: !!pluginOptions.sourcemap,
546
551
  inlineSourceMap: !!pluginOptions.sourcemap,
547
552
  sourceMap: undefined,
@@ -13,6 +13,7 @@ export interface I18nInlinerOptions {
13
13
  missingTranslation: 'error' | 'warning' | 'ignore';
14
14
  outputFiles: BuildOutputFile[];
15
15
  shouldOptimize?: boolean;
16
+ persistentCachePath?: string;
16
17
  }
17
18
  /**
18
19
  * A class that performs i18n translation inlining of JavaScript code.
@@ -22,6 +23,7 @@ export interface I18nInlinerOptions {
22
23
  */
23
24
  export declare class I18nInliner {
24
25
  #private;
26
+ private readonly options;
25
27
  constructor(options: I18nInlinerOptions, maxThreads?: number);
26
28
  /**
27
29
  * Performs inlining of translations for the provided locale and translations. The files that
@@ -41,4 +43,11 @@ export declare class I18nInliner {
41
43
  * @returns A void promise that resolves when closing is complete.
42
44
  */
43
45
  close(): Promise<void>;
46
+ /**
47
+ * Initializes the cache for storing translated bundles.
48
+ * If the cache is already initialized, it does nothing.
49
+ *
50
+ * @returns A promise that resolves once the cache initialization process is complete.
51
+ */
52
+ private initCache;
44
53
  }
@@ -6,12 +6,47 @@
6
6
  * Use of this source code is governed by an MIT-style license that can be
7
7
  * found in the LICENSE file at https://angular.dev/license
8
8
  */
9
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ var desc = Object.getOwnPropertyDescriptor(m, k);
12
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
13
+ desc = { enumerable: true, get: function() { return m[k]; } };
14
+ }
15
+ Object.defineProperty(o, k2, desc);
16
+ }) : (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ o[k2] = m[k];
19
+ }));
20
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
21
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
22
+ }) : function(o, v) {
23
+ o["default"] = v;
24
+ });
25
+ var __importStar = (this && this.__importStar) || (function () {
26
+ var ownKeys = function(o) {
27
+ ownKeys = Object.getOwnPropertyNames || function (o) {
28
+ var ar = [];
29
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
30
+ return ar;
31
+ };
32
+ return ownKeys(o);
33
+ };
34
+ return function (mod) {
35
+ if (mod && mod.__esModule) return mod;
36
+ var result = {};
37
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
38
+ __setModuleDefault(result, mod);
39
+ return result;
40
+ };
41
+ })();
9
42
  var __importDefault = (this && this.__importDefault) || function (mod) {
10
43
  return (mod && mod.__esModule) ? mod : { "default": mod };
11
44
  };
12
45
  Object.defineProperty(exports, "__esModule", { value: true });
13
46
  exports.I18nInliner = void 0;
14
47
  const node_assert_1 = __importDefault(require("node:assert"));
48
+ const node_crypto_1 = require("node:crypto");
49
+ const node_path_1 = require("node:path");
15
50
  const worker_pool_1 = require("../../utils/worker-pool");
16
51
  const bundler_context_1 = require("./bundler-context");
17
52
  const utils_1 = require("./utils");
@@ -27,37 +62,37 @@ const LOCALIZE_KEYWORD = '$localize';
27
62
  * localize function (`$localize`).
28
63
  */
29
64
  class I18nInliner {
65
+ options;
66
+ #cacheInitFailed = false;
30
67
  #workerPool;
68
+ #cache;
31
69
  #localizeFiles;
32
70
  #unmodifiedFiles;
33
- #fileToType = new Map();
34
71
  constructor(options, maxThreads) {
72
+ this.options = options;
35
73
  this.#unmodifiedFiles = [];
74
+ const { outputFiles, shouldOptimize, missingTranslation } = options;
36
75
  const files = new Map();
37
76
  const pendingMaps = [];
38
- for (const file of options.outputFiles) {
77
+ for (const file of outputFiles) {
39
78
  if (file.type === bundler_context_1.BuildOutputFileType.Root || file.type === bundler_context_1.BuildOutputFileType.ServerRoot) {
40
79
  // Skip also the server entry-point.
41
80
  // Skip stats and similar files.
42
81
  continue;
43
82
  }
44
- this.#fileToType.set(file.path, file.type);
45
- if (file.path.endsWith('.js') || file.path.endsWith('.mjs')) {
83
+ const fileExtension = (0, node_path_1.extname)(file.path);
84
+ if (fileExtension === '.js' || fileExtension === '.mjs') {
46
85
  // Check if localizations are present
47
86
  const contentBuffer = Buffer.isBuffer(file.contents)
48
87
  ? file.contents
49
88
  : Buffer.from(file.contents.buffer, file.contents.byteOffset, file.contents.byteLength);
50
89
  const hasLocalize = contentBuffer.includes(LOCALIZE_KEYWORD);
51
90
  if (hasLocalize) {
52
- // A Blob is an immutable data structure that allows sharing the data between workers
53
- // without copying until the data is actually used within a Worker. This is useful here
54
- // since each file may not actually be processed in each Worker and the Blob avoids
55
- // unneeded repeat copying of potentially large JavaScript files.
56
- files.set(file.path, new Blob([file.contents]));
91
+ files.set(file.path, file);
57
92
  continue;
58
93
  }
59
94
  }
60
- else if (file.path.endsWith('.js.map')) {
95
+ else if (fileExtension === '.map') {
61
96
  // The related JS file may not have been checked yet. To ensure that map files are not
62
97
  // missed, store any pending map files and check them after all output files.
63
98
  pendingMaps.push(file);
@@ -68,7 +103,7 @@ class I18nInliner {
68
103
  // Check if any pending map files should be processed by checking if the parent JS file is present
69
104
  for (const file of pendingMaps) {
70
105
  if (files.has(file.path.slice(0, -4))) {
71
- files.set(file.path, new Blob([file.contents]));
106
+ files.set(file.path, file);
72
107
  }
73
108
  else {
74
109
  this.#unmodifiedFiles.push(file);
@@ -80,9 +115,13 @@ class I18nInliner {
80
115
  maxThreads,
81
116
  // Extract options to ensure only the named options are serialized and sent to the worker
82
117
  workerData: {
83
- missingTranslation: options.missingTranslation,
84
- shouldOptimize: options.shouldOptimize,
85
- files,
118
+ missingTranslation,
119
+ shouldOptimize,
120
+ // A Blob is an immutable data structure that allows sharing the data between workers
121
+ // without copying until the data is actually used within a Worker. This is useful here
122
+ // since each file may not actually be processed in each Worker and the Blob avoids
123
+ // unneeded repeat copying of potentially large JavaScript files.
124
+ files: new Map(Array.from(files, ([name, file]) => [name, new Blob([file.contents])])),
86
125
  },
87
126
  });
88
127
  }
@@ -95,18 +134,41 @@ class I18nInliner {
95
134
  * @returns A promise that resolves to an array of OutputFiles representing a translated result.
96
135
  */
97
136
  async inlineForLocale(locale, translation) {
137
+ await this.initCache();
138
+ const { shouldOptimize, missingTranslation } = this.options;
98
139
  // Request inlining for each file that contains localize calls
99
140
  const requests = [];
100
- for (const filename of this.#localizeFiles.keys()) {
141
+ let fileCacheKeyBase;
142
+ for (const [filename, file] of this.#localizeFiles) {
143
+ let cacheKey;
101
144
  if (filename.endsWith('.map')) {
102
145
  continue;
103
146
  }
104
- const fileRequest = this.#workerPool.run({
105
- filename,
106
- locale,
107
- translation,
147
+ let cacheResultPromise = Promise.resolve(null);
148
+ if (this.#cache) {
149
+ fileCacheKeyBase ??= Buffer.from(JSON.stringify({ locale, translation, missingTranslation, shouldOptimize }), 'utf-8');
150
+ // NOTE: If additional options are added, this may need to be updated.
151
+ // TODO: Consider xxhash or similar instead of SHA256
152
+ cacheKey = (0, node_crypto_1.createHash)('sha256')
153
+ .update(file.hash)
154
+ .update(filename)
155
+ .update(fileCacheKeyBase)
156
+ .digest('hex');
157
+ // Failure to get the value should not fail the transform
158
+ cacheResultPromise = this.#cache.get(cacheKey).catch(() => null);
159
+ }
160
+ const fileResult = cacheResultPromise.then(async (cachedResult) => {
161
+ if (cachedResult) {
162
+ return cachedResult;
163
+ }
164
+ const result = await this.#workerPool.run({ filename, locale, translation });
165
+ if (this.#cache && cacheKey) {
166
+ // Failure to set the value should not fail the transform
167
+ await this.#cache.set(cacheKey, result).catch(() => { });
168
+ }
169
+ return result;
108
170
  });
109
- requests.push(fileRequest);
171
+ requests.push(fileResult);
110
172
  }
111
173
  // Wait for all file requests to complete
112
174
  const rawResults = await Promise.all(requests);
@@ -115,7 +177,7 @@ class I18nInliner {
115
177
  const warnings = [];
116
178
  const outputFiles = [
117
179
  ...rawResults.flatMap(({ file, code, map, messages }) => {
118
- const type = this.#fileToType.get(file);
180
+ const type = this.#localizeFiles.get(file)?.type;
119
181
  (0, node_assert_1.default)(type !== undefined, 'localized file should always have a type' + file);
120
182
  const resultFiles = [(0, utils_1.createOutputFile)(file, code, type)];
121
183
  if (map) {
@@ -146,5 +208,32 @@ class I18nInliner {
146
208
  close() {
147
209
  return this.#workerPool.destroy();
148
210
  }
211
+ /**
212
+ * Initializes the cache for storing translated bundles.
213
+ * If the cache is already initialized, it does nothing.
214
+ *
215
+ * @returns A promise that resolves once the cache initialization process is complete.
216
+ */
217
+ async initCache() {
218
+ if (this.#cache || this.#cacheInitFailed) {
219
+ return;
220
+ }
221
+ const { persistentCachePath } = this.options;
222
+ // Webcontainers currently do not support this persistent cache store.
223
+ if (!persistentCachePath || process.versions.webcontainer) {
224
+ return;
225
+ }
226
+ // Initialize a persistent cache for i18n transformations.
227
+ try {
228
+ const { LmbdCacheStore } = await Promise.resolve().then(() => __importStar(require('./lmdb-cache-store')));
229
+ this.#cache = new LmbdCacheStore((0, node_path_1.join)(persistentCachePath, 'angular-i18n.db'));
230
+ }
231
+ catch {
232
+ this.#cacheInitFailed = true;
233
+ // eslint-disable-next-line no-console
234
+ console.warn('Unable to initialize JavaScript cache storage.\n' +
235
+ 'This will not affect the build output content but may result in slower builds.');
236
+ }
237
+ }
149
238
  }
150
239
  exports.I18nInliner = I18nInliner;
@@ -42,6 +42,8 @@ var __importStar = (this && this.__importStar) || (function () {
42
42
  Object.defineProperty(exports, "__esModule", { value: true });
43
43
  exports.LessStylesheetLanguage = void 0;
44
44
  const promises_1 = require("node:fs/promises");
45
+ const node_path_1 = require("node:path");
46
+ const node_url_1 = require("node:url");
45
47
  /**
46
48
  * The lazy-loaded instance of the less stylesheet preprocessor.
47
49
  * It is only imported and initialized if a less stylesheet is used.
@@ -115,7 +117,7 @@ async function compileString(data, filename, options, resolver, unsafeInlineJava
115
117
  },
116
118
  };
117
119
  try {
118
- const result = await less.render(data, {
120
+ const { imports, map, css } = await less.render(data, {
119
121
  filename,
120
122
  paths: options.includePaths,
121
123
  plugins: [resolverPlugin],
@@ -123,15 +125,15 @@ async function compileString(data, filename, options, resolver, unsafeInlineJava
123
125
  javascriptEnabled: unsafeInlineJavaScript,
124
126
  sourceMap: options.sourcemap
125
127
  ? {
126
- sourceMapFileInline: true,
128
+ sourceMapFileInline: false,
127
129
  outputSourceFiles: true,
128
130
  }
129
131
  : undefined,
130
132
  });
131
133
  return {
132
- contents: result.css,
134
+ contents: options.sourcemap ? `${css}\n${sourceMapToUrlComment(map)}` : css,
133
135
  loader: 'css',
134
- watchFiles: [filename, ...result.imports],
136
+ watchFiles: [filename, ...imports],
135
137
  };
136
138
  }
137
139
  catch (error) {
@@ -182,3 +184,13 @@ function convertExceptionLocation(exception) {
182
184
  lineText: exception.extract && exception.extract[Math.trunc(exception.extract.length / 2)],
183
185
  };
184
186
  }
187
+ function sourceMapToUrlComment(sourceMap) {
188
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
189
+ const map = JSON.parse(sourceMap);
190
+ // Using file URLs instead of paths ensures that esbuild correctly resolves the source map.
191
+ // https://github.com/evanw/esbuild/issues/4070
192
+ // https://github.com/evanw/esbuild/issues/4075
193
+ map.sources = map.sources.map((source) => source && (0, node_path_1.isAbsolute)(source) ? (0, node_url_1.pathToFileURL)(source).href : source);
194
+ const urlSourceMap = Buffer.from(JSON.stringify(map), 'utf-8').toString('base64');
195
+ return `/*# sourceMappingURL=data:application/json;charset=utf-8;base64,${urlSourceMap} */`;
196
+ }
@@ -175,7 +175,7 @@ async function compileString(data, filePath, syntax, options, resolveUrl) {
175
175
  });
176
176
  return {
177
177
  loader: 'css',
178
- contents: sourceMap ? `${css}\n${sourceMapToUrlComment(sourceMap, (0, node_path_1.dirname)(filePath))}` : css,
178
+ contents: sourceMap ? `${css}\n${sourceMapToUrlComment(sourceMap)}` : css,
179
179
  watchFiles: loadedUrls.map((url) => (0, node_url_1.fileURLToPath)(url)),
180
180
  warnings,
181
181
  };
@@ -201,10 +201,7 @@ async function compileString(data, filePath, syntax, options, resolveUrl) {
201
201
  throw error;
202
202
  }
203
203
  }
204
- function sourceMapToUrlComment(sourceMap, root) {
205
- // Remove `file` protocol from all sourcemap sources and adjust to be relative to the input file.
206
- // This allows esbuild to correctly process the paths.
207
- sourceMap.sources = sourceMap.sources.map((source) => (0, node_path_1.relative)(root, (0, node_url_1.fileURLToPath)(source)));
204
+ function sourceMapToUrlComment(sourceMap) {
208
205
  const urlSourceMap = Buffer.from(JSON.stringify(sourceMap), 'utf-8').toString('base64');
209
206
  return `/*# sourceMappingURL=data:application/json;charset=utf-8;base64,${urlSourceMap} */`;
210
207
  }
@@ -35,6 +35,9 @@ const manifest_1 = require("../../utils/server-rendering/manifest");
35
35
  const stats_table_1 = require("../../utils/stats-table");
36
36
  const bundler_context_1 = require("./bundler-context");
37
37
  function logBuildStats(metafile, outputFiles, initial, budgetFailures, colors, changedFiles, estimatedTransferSizes, ssrOutputEnabled, verbose) {
38
+ // Remove the i18n subpath in case the build is using i18n.
39
+ // en-US/main.js -> main.js
40
+ const normalizedChangedFiles = new Set([...(changedFiles ?? [])].map((f) => (0, node_path_1.basename)(f)));
38
41
  const browserStats = [];
39
42
  const serverStats = [];
40
43
  let unchangedCount = 0;
@@ -45,7 +48,7 @@ function logBuildStats(metafile, outputFiles, initial, budgetFailures, colors, c
45
48
  continue;
46
49
  }
47
50
  // Show only changed files if a changed list is provided
48
- if (changedFiles && !changedFiles.has(file)) {
51
+ if (normalizedChangedFiles.size && !normalizedChangedFiles.has(file)) {
49
52
  ++unchangedCount;
50
53
  continue;
51
54
  }
@@ -12,7 +12,7 @@ interface AngularMemoryPluginOptions {
12
12
  outputFiles: AngularMemoryOutputFiles;
13
13
  templateUpdates?: ReadonlyMap<string, string>;
14
14
  external?: string[];
15
- skipViteClient?: boolean;
15
+ disableViteTransport?: boolean;
16
16
  }
17
17
  export declare function createAngularMemoryPlugin(options: AngularMemoryPluginOptions): Promise<Plugin>;
18
18
  export {};
@@ -47,24 +47,13 @@ async function createAngularMemoryPlugin(options) {
47
47
  // `/@id/${source}` but is currently closer to a raw external than a resolved file path.
48
48
  return source;
49
49
  }
50
- if (importer) {
50
+ if (importer && source[0] === '.') {
51
51
  const normalizedImporter = normalizePath(importer);
52
- if (source[0] === '.' && normalizedImporter.startsWith(virtualProjectRoot)) {
52
+ if (normalizedImporter.startsWith(virtualProjectRoot)) {
53
53
  // Remove query if present
54
54
  const [importerFile] = normalizedImporter.split('?', 1);
55
55
  source = '/' + (0, node_path_1.join)((0, node_path_1.dirname)((0, node_path_1.relative)(virtualProjectRoot, importerFile)), source);
56
56
  }
57
- else if (!ssr &&
58
- source[0] === '/' &&
59
- importer.endsWith('index.html') &&
60
- normalizedImporter.startsWith(virtualProjectRoot)) {
61
- // This is only needed when using SSR and `angularSsrMiddleware` (old style) to correctly resolve
62
- // .js files when using lazy-loading.
63
- // Remove query if present
64
- const [importerFile] = normalizedImporter.split('?', 1);
65
- source =
66
- '/' + (0, node_path_1.join)((0, node_path_1.dirname)((0, node_path_1.relative)(virtualProjectRoot, importerFile)), (0, node_path_1.basename)(source));
67
- }
68
57
  }
69
58
  const [file] = source.split('?', 1);
70
59
  if (outputFiles.has(normalizePath(file))) {
@@ -84,7 +73,7 @@ async function createAngularMemoryPlugin(options) {
84
73
  const codeContents = outputFiles.get(relativeFile)?.contents;
85
74
  if (codeContents === undefined) {
86
75
  if (relativeFile.endsWith('/node_modules/vite/dist/client/client.mjs')) {
87
- return options.skipViteClient ? '' : loadViteClientCode(file);
76
+ return loadViteClientCode(file, options.disableViteTransport);
88
77
  }
89
78
  return undefined;
90
79
  }
@@ -107,9 +96,9 @@ async function createAngularMemoryPlugin(options) {
107
96
  * @param file The absolute path to the Vite client code.
108
97
  * @returns
109
98
  */
110
- async function loadViteClientCode(file) {
99
+ async function loadViteClientCode(file, disableViteTransport = false) {
111
100
  const originalContents = await (0, promises_1.readFile)(file, 'utf-8');
112
- const updatedContents = originalContents.replace(`"You can also disable this overlay by setting ",
101
+ let updatedContents = originalContents.replace(`"You can also disable this overlay by setting ",
113
102
  h("code", { part: "config-option-name" }, "server.hmr.overlay"),
114
103
  " to ",
115
104
  h("code", { part: "config-option-value" }, "false"),
@@ -117,5 +106,11 @@ async function loadViteClientCode(file) {
117
106
  h("code", { part: "config-file-name" }, hmrConfigName),
118
107
  "."`, '');
119
108
  (0, node_assert_1.default)(originalContents !== updatedContents, 'Failed to update Vite client error overlay text.');
109
+ if (disableViteTransport) {
110
+ const previousUpdatedContents = updatedContents;
111
+ updatedContents = updatedContents.replace('transport.connect(handleMessage)', '');
112
+ (0, node_assert_1.default)(previousUpdatedContents !== updatedContents, 'Failed to update Vite client WebSocket disable.');
113
+ updatedContents = updatedContents.replace('console.debug("[vite] connecting...")', '');
114
+ }
120
115
  return updatedContents;
121
116
  }
@@ -10,7 +10,7 @@ Object.defineProperty(exports, "__esModule", { value: true });
10
10
  exports.normalizeCacheOptions = normalizeCacheOptions;
11
11
  const node_path_1 = require("node:path");
12
12
  /** Version placeholder is replaced during the build process with actual package version */
13
- const VERSION = '19.2.0-next.1';
13
+ const VERSION = '19.2.0-next.2';
14
14
  function hasCacheMetadata(value) {
15
15
  return (!!value &&
16
16
  typeof value === 'object' &&
@@ -11,6 +11,7 @@ exports.SERVER_APP_ENGINE_MANIFEST_FILENAME = exports.SERVER_APP_MANIFEST_FILENA
11
11
  exports.generateAngularServerAppEngineManifest = generateAngularServerAppEngineManifest;
12
12
  exports.generateAngularServerAppManifest = generateAngularServerAppManifest;
13
13
  const node_path_1 = require("node:path");
14
+ const node_vm_1 = require("node:vm");
14
15
  const bundler_context_1 = require("../../tools/esbuild/bundler-context");
15
16
  const utils_1 = require("../../tools/esbuild/utils");
16
17
  const environment_options_1 = require("../environment-options");
@@ -115,9 +116,13 @@ function generateAngularServerAppManifest(additionalHtmlOutputFiles, outputFiles
115
116
  const extension = (0, node_path_1.extname)(file.path);
116
117
  if (extension === '.html' || (inlineCriticalCss && extension === '.css')) {
117
118
  const jsChunkFilePath = `assets-chunks/${file.path.replace(/[./]/g, '_')}.mjs`;
118
- serverAssetsChunks.push((0, utils_1.createOutputFile)(jsChunkFilePath, `export default \`${escapeUnsafeChars(file.text)}\`;`, bundler_context_1.BuildOutputFileType.ServerApplication));
119
+ const escapedContent = escapeUnsafeChars(file.text);
120
+ serverAssetsChunks.push((0, utils_1.createOutputFile)(jsChunkFilePath, `export default \`${escapedContent}\`;`, bundler_context_1.BuildOutputFileType.ServerApplication));
121
+ // This is needed because JavaScript engines script parser convert `\r\n` to `\n` in template literals,
122
+ // which can result in an incorrect byte length.
123
+ const size = (0, node_vm_1.runInThisContext)(`new TextEncoder().encode(\`${escapedContent}\`).byteLength`);
119
124
  serverAssets[file.path] =
120
- `{size: ${file.size}, hash: '${file.hash}', text: () => import('./${jsChunkFilePath}').then(m => m.default)}`;
125
+ `{size: ${size}, hash: '${file.hash}', text: () => import('./${jsChunkFilePath}').then(m => m.default)}`;
121
126
  }
122
127
  }
123
128
  // When routes have been extracted, mappings are no longer needed, as preloads will be included in the metadata.
@@ -128,11 +128,11 @@ async function renderPages(baseHref, sourcemap, serializableRouteTreeNode, maxTh
128
128
  try {
129
129
  const renderingPromises = [];
130
130
  const appShellRouteWithLeadingSlash = appShellRoute && addLeadingSlash(appShellRoute);
131
- const baseHrefWithLeadingSlash = addLeadingSlash(baseHref);
131
+ const baseHrefPathnameWithLeadingSlash = new URL(baseHref, 'http://localhost').pathname;
132
132
  for (const { route, redirectTo } of serializableRouteTreeNode) {
133
133
  // Remove the base href from the file output path.
134
- const routeWithoutBaseHref = addTrailingSlash(route).startsWith(baseHrefWithLeadingSlash)
135
- ? addLeadingSlash(route.slice(baseHrefWithLeadingSlash.length))
134
+ const routeWithoutBaseHref = addTrailingSlash(route).startsWith(baseHrefPathnameWithLeadingSlash)
135
+ ? addLeadingSlash(route.slice(baseHrefPathnameWithLeadingSlash.length))
136
136
  : route;
137
137
  const outPath = node_path_1.posix.join(removeLeadingSlash(routeWithoutBaseHref), 'index.html');
138
138
  if (typeof redirectTo === 'string') {
@@ -29,8 +29,8 @@ function assertCompatibleAngularVersion(projectRoot) {
29
29
  process.exit(2);
30
30
  }
31
31
  const supportedAngularSemver = '^19.0.0 || ^19.2.0-next.0';
32
- if (supportedAngularSemver.startsWith('0.0.0')) {
33
- // Internal CLI testing version.
32
+ if (angularPkgJson['version'] === '0.0.0' || supportedAngularSemver.startsWith('0.0.0')) {
33
+ // Internal CLI and FW testing version.
34
34
  return;
35
35
  }
36
36
  const angularVersion = new semver_1.SemVer(angularPkgJson['version']);