@angular-devkit/build-angular 17.1.0 → 17.1.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,15 +1,15 @@
1
1
  {
2
2
  "name": "@angular-devkit/build-angular",
3
- "version": "17.1.0",
3
+ "version": "17.1.2",
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.1701.0",
11
- "@angular-devkit/build-webpack": "0.1701.0",
12
- "@angular-devkit/core": "17.1.0",
10
+ "@angular-devkit/architect": "0.1701.2",
11
+ "@angular-devkit/build-webpack": "0.1701.2",
12
+ "@angular-devkit/core": "17.1.2",
13
13
  "@babel/core": "7.23.7",
14
14
  "@babel/generator": "7.23.6",
15
15
  "@babel/helper-annotate-as-pure": "7.22.5",
@@ -20,7 +20,7 @@
20
20
  "@babel/preset-env": "7.23.7",
21
21
  "@babel/runtime": "7.23.7",
22
22
  "@discoveryjs/json-ext": "0.5.7",
23
- "@ngtools/webpack": "17.1.0",
23
+ "@ngtools/webpack": "17.1.2",
24
24
  "@vitejs/plugin-basic-ssl": "1.0.2",
25
25
  "ansi-colors": "4.1.3",
26
26
  "autoprefixer": "10.4.16",
@@ -63,7 +63,7 @@
63
63
  "tree-kill": "1.2.2",
64
64
  "tslib": "2.6.2",
65
65
  "undici": "6.2.1",
66
- "vite": "5.0.11",
66
+ "vite": "5.0.12",
67
67
  "watchpack": "2.4.0",
68
68
  "webpack": "5.89.0",
69
69
  "webpack-dev-middleware": "6.1.1",
@@ -70,7 +70,7 @@ async function normalizeOptions(context, projectName, options, extensions) {
70
70
  server: 'server',
71
71
  media: 'media',
72
72
  ...(typeof outputPath === 'string' ? undefined : outputPath),
73
- base: normalizeDirectoryPath(node_path_1.default.join(workspaceRoot, typeof outputPath === 'string' ? outputPath : outputPath.base)),
73
+ base: normalizeDirectoryPath(node_path_1.default.resolve(workspaceRoot, typeof outputPath === 'string' ? outputPath : outputPath.base)),
74
74
  };
75
75
  const outputNames = {
76
76
  bundles: options.outputHashing === schema_1.OutputHashing.All || options.outputHashing === schema_1.OutputHashing.Bundles
@@ -34,8 +34,6 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
34
34
  };
35
35
  Object.defineProperty(exports, "__esModule", { value: true });
36
36
  exports.setupServer = exports.serveWithVite = void 0;
37
- const remapping_1 = __importDefault(require("@ampproject/remapping"));
38
- const mrmime_1 = require("mrmime");
39
37
  const node_assert_1 = __importDefault(require("node:assert"));
40
38
  const promises_1 = require("node:fs/promises");
41
39
  const node_path_1 = require("node:path");
@@ -43,10 +41,10 @@ const bundler_context_1 = require("../../tools/esbuild/bundler-context");
43
41
  const javascript_transformer_1 = require("../../tools/esbuild/javascript-transformer");
44
42
  const rxjs_esm_resolution_plugin_1 = require("../../tools/esbuild/rxjs-esm-resolution-plugin");
45
43
  const utils_1 = require("../../tools/esbuild/utils");
44
+ const angular_memory_plugin_1 = require("../../tools/vite/angular-memory-plugin");
46
45
  const i18n_locale_plugin_1 = require("../../tools/vite/i18n-locale-plugin");
47
46
  const utils_2 = require("../../utils");
48
47
  const load_esm_1 = require("../../utils/load-esm");
49
- const render_page_1 = require("../../utils/server-rendering/render-page");
50
48
  const supported_browsers_1 = require("../../utils/supported-browsers");
51
49
  const webpack_browser_config_1 = require("../../utils/webpack-browser-config");
52
50
  const application_1 = require("../application");
@@ -324,7 +322,6 @@ function analyzeResultFiles(normalizePath, htmlIndexPath, resultFiles, generated
324
322
  }
325
323
  }
326
324
  }
327
- // eslint-disable-next-line max-lines-per-function
328
325
  async function setupServer(serverOptions, outputFiles, assets, preserveSymlinks, externalMetadata, ssr, prebundleTransformer, target, prebundleLoaderExtensions, extensionMiddleware, indexHtmlTransformer, thirdPartySourcemaps = false) {
329
326
  const proxy = await (0, utils_2.loadProxyConfiguration)(serverOptions.workspaceRoot, serverOptions.proxyConfig, true);
330
327
  // dynamically import Vite for ESM compatibility
@@ -344,7 +341,7 @@ async function setupServer(serverOptions, outputFiles, assets, preserveSymlinks,
344
341
  publicDir: false,
345
342
  esbuild: false,
346
343
  mode: 'development',
347
- appType: 'spa',
344
+ appType: 'mpa',
348
345
  css: {
349
346
  devSourcemap: true,
350
347
  },
@@ -409,189 +406,18 @@ async function setupServer(serverOptions, outputFiles, assets, preserveSymlinks,
409
406
  },
410
407
  plugins: [
411
408
  (0, i18n_locale_plugin_1.createAngularLocaleDataPlugin)(),
412
- {
413
- name: 'vite:angular-memory',
414
- // Ensures plugin hooks run before built-in Vite hooks
415
- enforce: 'pre',
416
- async resolveId(source, importer) {
417
- // Prevent vite from resolving an explicit external dependency (`externalDependencies` option)
418
- if (externalMetadata.explicit.includes(source)) {
419
- // This is still not ideal since Vite will still transform the import specifier to
420
- // `/@id/${source}` but is currently closer to a raw external than a resolved file path.
421
- return source;
422
- }
423
- if (importer && source[0] === '.' && importer.startsWith(virtualProjectRoot)) {
424
- // Remove query if present
425
- const [importerFile] = importer.split('?', 1);
426
- source =
427
- '/' +
428
- normalizePath((0, node_path_1.join)((0, node_path_1.dirname)((0, node_path_1.relative)(virtualProjectRoot, importerFile)), source));
429
- }
430
- const [file] = source.split('?', 1);
431
- if (outputFiles.has(file)) {
432
- return (0, node_path_1.join)(virtualProjectRoot, source);
433
- }
434
- },
435
- load(id) {
436
- const [file] = id.split('?', 1);
437
- const relativeFile = '/' + normalizePath((0, node_path_1.relative)(virtualProjectRoot, file));
438
- const codeContents = outputFiles.get(relativeFile)?.contents;
439
- if (codeContents === undefined) {
440
- if (relativeFile.endsWith('/node_modules/vite/dist/client/client.mjs')) {
441
- return loadViteClientCode(file);
442
- }
443
- return;
444
- }
445
- const code = Buffer.from(codeContents).toString('utf-8');
446
- const mapContents = outputFiles.get(relativeFile + '.map')?.contents;
447
- return {
448
- // Remove source map URL comments from the code if a sourcemap is present.
449
- // Vite will inline and add an additional sourcemap URL for the sourcemap.
450
- code: mapContents ? code.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, '') : code,
451
- map: mapContents && Buffer.from(mapContents).toString('utf-8'),
452
- };
453
- },
454
- configureServer(server) {
455
- const originalssrTransform = server.ssrTransform;
456
- server.ssrTransform = async (code, map, url, originalCode) => {
457
- const result = await originalssrTransform(code, null, url, originalCode);
458
- if (!result || !result.map || !map) {
459
- return result;
460
- }
461
- const remappedMap = (0, remapping_1.default)([result.map, map], () => null);
462
- // Set the sourcemap root to the workspace root. This is needed since we set a virtual path as root.
463
- remappedMap.sourceRoot = normalizePath(serverOptions.workspaceRoot) + '/';
464
- return {
465
- ...result,
466
- map: remappedMap,
467
- };
468
- };
469
- // Assets and resources get handled first
470
- server.middlewares.use(function angularAssetsMiddleware(req, res, next) {
471
- if (req.url === undefined || res.writableEnded) {
472
- return;
473
- }
474
- // Parse the incoming request.
475
- // The base of the URL is unused but required to parse the URL.
476
- const pathname = pathnameWithoutBasePath(req.url, server.config.base);
477
- const extension = (0, node_path_1.extname)(pathname);
478
- // Rewrite all build assets to a vite raw fs URL
479
- const assetSourcePath = assets.get(pathname);
480
- if (assetSourcePath !== undefined) {
481
- // Workaround to disable Vite transformer middleware.
482
- // See: https://github.com/vitejs/vite/blob/746a1daab0395f98f0afbdee8f364cb6cf2f3b3f/packages/vite/src/node/server/middlewares/transform.ts#L201 and
483
- // https://github.com/vitejs/vite/blob/746a1daab0395f98f0afbdee8f364cb6cf2f3b3f/packages/vite/src/node/server/transformRequest.ts#L204-L206
484
- req.headers.accept = 'text/html';
485
- // The encoding needs to match what happens in the vite static middleware.
486
- // ref: https://github.com/vitejs/vite/blob/d4f13bd81468961c8c926438e815ab6b1c82735e/packages/vite/src/node/server/middlewares/static.ts#L163
487
- req.url = `${server.config.base}@fs/${encodeURI(assetSourcePath)}`;
488
- next();
489
- return;
490
- }
491
- // Resource files are handled directly.
492
- // Global stylesheets (CSS files) are currently considered resources to workaround
493
- // dev server sourcemap issues with stylesheets.
494
- if (extension !== '.js' && extension !== '.html') {
495
- const outputFile = outputFiles.get(pathname);
496
- if (outputFile?.servable) {
497
- const mimeType = (0, mrmime_1.lookup)(extension);
498
- if (mimeType) {
499
- res.setHeader('Content-Type', mimeType);
500
- }
501
- res.setHeader('Cache-Control', 'no-cache');
502
- if (serverOptions.headers) {
503
- Object.entries(serverOptions.headers).forEach(([name, value]) => res.setHeader(name, value));
504
- }
505
- res.end(outputFile.contents);
506
- return;
507
- }
508
- }
509
- next();
510
- });
511
- if (extensionMiddleware?.length) {
512
- extensionMiddleware.forEach((middleware) => server.middlewares.use(middleware));
513
- }
514
- // Returning a function, installs middleware after the main transform middleware but
515
- // before the built-in HTML middleware
516
- return () => {
517
- function angularSSRMiddleware(req, res, next) {
518
- const url = req.originalUrl;
519
- if (
520
- // Skip if path is not defined.
521
- !url ||
522
- // Skip if path is like a file.
523
- // NOTE: We use a regexp to mitigate against matching requests like: /browse/pl.0ef59752c0cd457dbf1391f08cbd936f
524
- /^\.[a-z]{2,4}$/i.test((0, node_path_1.extname)(url.split('?')[0]))) {
525
- next();
526
- return;
527
- }
528
- const rawHtml = outputFiles.get('/index.server.html')?.contents;
529
- if (!rawHtml) {
530
- next();
531
- return;
532
- }
533
- transformIndexHtmlAndAddHeaders(url, rawHtml, res, next, async (html) => {
534
- const { content } = await (0, render_page_1.renderPage)({
535
- document: html,
536
- route: new URL(req.originalUrl ?? '/', server.resolvedUrls?.local[0]).toString(),
537
- serverContext: 'ssr',
538
- loadBundle: (uri) =>
539
- // eslint-disable-next-line @typescript-eslint/no-explicit-any
540
- server.ssrLoadModule(uri.slice(1)),
541
- // Files here are only needed for critical CSS inlining.
542
- outputFiles: {},
543
- // TODO: add support for critical css inlining.
544
- inlineCriticalCss: false,
545
- });
546
- return indexHtmlTransformer && content
547
- ? await indexHtmlTransformer(content)
548
- : content;
549
- });
550
- }
551
- if (ssr) {
552
- server.middlewares.use(angularSSRMiddleware);
553
- }
554
- server.middlewares.use(function angularIndexMiddleware(req, res, next) {
555
- if (!req.url) {
556
- next();
557
- return;
558
- }
559
- // Parse the incoming request.
560
- // The base of the URL is unused but required to parse the URL.
561
- const pathname = pathnameWithoutBasePath(req.url, server.config.base);
562
- if (pathname === '/' || pathname === `/index.html`) {
563
- const rawHtml = outputFiles.get('/index.html')?.contents;
564
- if (rawHtml) {
565
- transformIndexHtmlAndAddHeaders(req.url, rawHtml, res, next, indexHtmlTransformer);
566
- return;
567
- }
568
- }
569
- next();
570
- });
571
- };
572
- function transformIndexHtmlAndAddHeaders(url, rawHtml, res, next, additionalTransformer) {
573
- server
574
- .transformIndexHtml(url, Buffer.from(rawHtml).toString('utf-8'))
575
- .then(async (processedHtml) => {
576
- if (additionalTransformer) {
577
- const content = await additionalTransformer(processedHtml);
578
- if (!content) {
579
- next();
580
- return;
581
- }
582
- processedHtml = content;
583
- }
584
- res.setHeader('Content-Type', 'text/html');
585
- res.setHeader('Cache-Control', 'no-cache');
586
- if (serverOptions.headers) {
587
- Object.entries(serverOptions.headers).forEach(([name, value]) => res.setHeader(name, value));
588
- }
589
- res.end(processedHtml);
590
- })
591
- .catch((error) => next(error));
592
- }
593
- },
594
- },
409
+ (0, angular_memory_plugin_1.createAngularMemoryPlugin)({
410
+ workspaceRoot: serverOptions.workspaceRoot,
411
+ virtualProjectRoot,
412
+ outputFiles,
413
+ assets,
414
+ ssr,
415
+ external: externalMetadata.explicit,
416
+ indexHtmlTransformer,
417
+ extensionMiddleware,
418
+ extraHeaders: serverOptions.headers,
419
+ normalizePath,
420
+ }),
595
421
  ],
596
422
  // Browser only optimizeDeps. (This does not run for SSR dependencies).
597
423
  optimizeDeps: getDepOptimizationConfig({
@@ -627,31 +453,6 @@ async function setupServer(serverOptions, outputFiles, assets, preserveSymlinks,
627
453
  return configuration;
628
454
  }
629
455
  exports.setupServer = setupServer;
630
- /**
631
- * Reads the resolved Vite client code from disk and updates the content to remove
632
- * an unactionable suggestion to update the Vite configuration file to disable the
633
- * error overlay. The Vite configuration file is not present when used in the Angular
634
- * CLI.
635
- * @param file The absolute path to the Vite client code.
636
- * @returns
637
- */
638
- async function loadViteClientCode(file) {
639
- const originalContents = await (0, promises_1.readFile)(file, 'utf-8');
640
- let contents = originalContents.replace('You can also disable this overlay by setting', '');
641
- contents = contents.replace(
642
- // eslint-disable-next-line max-len
643
- '<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>', '');
644
- (0, node_assert_1.default)(originalContents !== contents, 'Failed to update Vite client error overlay text.');
645
- return contents;
646
- }
647
- function pathnameWithoutBasePath(url, basePath) {
648
- const parsedUrl = new URL(url, 'http://localhost');
649
- const pathname = decodeURIComponent(parsedUrl.pathname);
650
- // slice(basePath.length - 1) to retain the trailing slash
651
- return basePath !== '/' && pathname.startsWith(basePath)
652
- ? pathname.slice(basePath.length - 1)
653
- : pathname;
654
- }
655
456
  function getDepOptimizationConfig({ disabled, exclude, include, target, prebundleTransformer, ssr, loader, thirdPartySourcemaps, }) {
656
457
  const plugins = [
657
458
  {
@@ -20,9 +20,11 @@ async function extractMessages(options, builderName, context, extractorConstruct
20
20
  // Setup the build options for the application based on the buildTarget option
21
21
  const buildOptions = (await context.validateOptions(await context.getTargetOptions(options.buildTarget), builderName));
22
22
  buildOptions.optimization = false;
23
- buildOptions.sourceMap = { scripts: true, vendor: true };
23
+ buildOptions.sourceMap = { scripts: true, vendor: true, styles: false };
24
24
  buildOptions.localize = false;
25
25
  buildOptions.budgets = undefined;
26
+ buildOptions.index = false;
27
+ buildOptions.serviceWorker = false;
26
28
  let build;
27
29
  if (builderName === '@angular-devkit/build-angular:application') {
28
30
  build = application_1.buildApplicationInternal;
@@ -43,7 +43,7 @@ async function extract() {
43
43
  const browserIndexInputPath = path.join(outputPath, indexFile);
44
44
  const document = await fs.promises.readFile(browserIndexInputPath, 'utf8');
45
45
  const bootstrapAppFnOrModule = bootstrapAppFn || AppServerModule;
46
- (0, node_assert_1.default)(bootstrapAppFnOrModule, `Neither an AppServerModule nor a bootstrapping function was exported from: ${serverBundlePath}.`);
46
+ (0, node_assert_1.default)(bootstrapAppFnOrModule, `The file "${serverBundlePath}" does not have a default export for an AppServerModule or a bootstrapping function.`);
47
47
  const routes = [];
48
48
  for await (const { route, success } of extractRoutes(bootstrapAppFnOrModule, document)) {
49
49
  if (success) {
@@ -110,18 +110,20 @@ class ComponentStylesheetBundler {
110
110
  if (!result.errors) {
111
111
  for (const outputFile of result.outputFiles) {
112
112
  const filename = node_path_1.default.basename(outputFile.path);
113
- // Needed for Bazel as otherwise the files will not be written in the correct place.
114
- outputFile.path = node_path_1.default.join(this.options.workspaceRoot, outputFile.path);
115
- if (outputFile.type === bundler_context_1.BuildOutputFileType.Media) {
116
- // The output files could also contain resources (images/fonts/etc.) that were referenced
117
- outputFiles.push(outputFile);
113
+ if (outputFile.type === bundler_context_1.BuildOutputFileType.Media || filename.endsWith('.css.map')) {
114
+ // The output files could also contain resources (images/fonts/etc.) that were referenced and the map files.
115
+ // Clone the output file to avoid amending the original path which would causes problems during rebuild.
116
+ const clonedOutputFile = outputFile.clone();
117
+ // Needed for Bazel as otherwise the files will not be written in the correct place,
118
+ // this is because esbuild will resolve the output file from the outdir which is currently set to `workspaceRoot` twice,
119
+ // once in the stylesheet and the other in the application code bundler.
120
+ // Ex: `../../../../../app.component.css.map`.
121
+ clonedOutputFile.path = node_path_1.default.join(this.options.workspaceRoot, outputFile.path);
122
+ outputFiles.push(clonedOutputFile);
118
123
  }
119
124
  else if (filename.endsWith('.css')) {
120
125
  contents = outputFile.text;
121
126
  }
122
- else if (filename.endsWith('.css.map')) {
123
- outputFiles.push(outputFile);
124
- }
125
127
  else {
126
128
  throw new Error(`Unexpected non CSS/Media file "${filename}" outputted during component stylesheet processing.`);
127
129
  }
@@ -18,6 +18,7 @@ const node_path_1 = require("node:path");
18
18
  const environment_options_1 = require("../../utils/environment-options");
19
19
  const compiler_plugin_1 = require("./angular/compiler-plugin");
20
20
  const compiler_plugin_options_1 = require("./compiler-plugin-options");
21
+ const external_packages_plugin_1 = require("./external-packages-plugin");
21
22
  const i18n_locale_plugin_1 = require("./i18n-locale-plugin");
22
23
  const rxjs_esm_resolution_plugin_1 = require("./rxjs-esm-resolution-plugin");
23
24
  const sourcemap_ignorelist_plugin_1 = require("./sourcemap-ignorelist-plugin");
@@ -47,12 +48,20 @@ function createBrowserCodeBundleOptions(options, target, sourceFileCache) {
47
48
  styleOptions),
48
49
  ],
49
50
  };
50
- if (options.externalPackages) {
51
- buildOptions.packages = 'external';
52
- }
53
51
  if (options.plugins) {
54
52
  buildOptions.plugins?.push(...options.plugins);
55
53
  }
54
+ if (options.externalPackages) {
55
+ // Package files affected by a customized loader should not be implicitly marked as external
56
+ if (options.loaderExtensions || options.plugins) {
57
+ // Plugin must be added after custom plugins to ensure any added loader options are considered
58
+ buildOptions.plugins?.push((0, external_packages_plugin_1.createExternalPackagesPlugin)());
59
+ }
60
+ else {
61
+ // Safe to use the packages external option directly
62
+ buildOptions.packages = 'external';
63
+ }
64
+ }
56
65
  return buildOptions;
57
66
  }
58
67
  exports.createBrowserCodeBundleOptions = createBrowserCodeBundleOptions;
@@ -177,10 +177,12 @@ class BundlerContext {
177
177
  if (this.incremental) {
178
178
  // When incremental always add any files from the load result cache
179
179
  if (this.#loadCache) {
180
- this.#loadCache.watchFiles
181
- .filter((file) => !isInternalAngularFile(file))
182
- // watch files are fully resolved paths
183
- .forEach((file) => this.watchFiles.add(file));
180
+ for (const file of this.#loadCache.watchFiles) {
181
+ if (!isInternalAngularFile(file)) {
182
+ // watch files are fully resolved paths
183
+ this.watchFiles.add(file);
184
+ }
185
+ }
184
186
  }
185
187
  }
186
188
  }
@@ -189,10 +191,12 @@ class BundlerContext {
189
191
  // currently enabled with watch mode where watch files are needed.
190
192
  if (this.incremental) {
191
193
  // Add input files except virtual angular files which do not exist on disk
192
- Object.keys(result.metafile.inputs)
193
- .filter((input) => !isInternalAngularFile(input))
194
- // input file paths are always relative to the workspace root
195
- .forEach((input) => this.watchFiles.add((0, node_path_1.join)(this.workspaceRoot, input)));
194
+ for (const input of Object.keys(result.metafile.inputs)) {
195
+ if (!isInternalAngularFile(input)) {
196
+ // input file paths are always relative to the workspace root
197
+ this.watchFiles.add((0, node_path_1.join)(this.workspaceRoot, input));
198
+ }
199
+ }
196
200
  }
197
201
  // Return if the build encountered any errors
198
202
  if (result.errors.length) {
@@ -256,7 +260,9 @@ class BundlerContext {
256
260
  for (const { imports } of Object.values(result.metafile.outputs)) {
257
261
  for (const importData of imports) {
258
262
  if (!importData.external ||
259
- (importData.kind !== 'import-statement' && importData.kind !== 'dynamic-import')) {
263
+ (importData.kind !== 'import-statement' &&
264
+ importData.kind !== 'dynamic-import' &&
265
+ importData.kind !== 'require-call')) {
260
266
  continue;
261
267
  }
262
268
  externalImports.add(importData.path);
@@ -0,0 +1,16 @@
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 type { Plugin } from 'esbuild';
9
+ /**
10
+ * Creates a plugin that marks any resolved path as external if it is within a node modules directory.
11
+ * This is used instead of the esbuild `packages` option to avoid marking files that should be loaded
12
+ * via customized loaders. This is necessary to prevent Vite development server pre-bundling errors.
13
+ *
14
+ * @returns An esbuild plugin.
15
+ */
16
+ export declare function createExternalPackagesPlugin(): Plugin;
@@ -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.createExternalPackagesPlugin = void 0;
11
+ const node_path_1 = require("node:path");
12
+ const EXTERNAL_PACKAGE_RESOLUTION = Symbol('EXTERNAL_PACKAGE_RESOLUTION');
13
+ /**
14
+ * Creates a plugin that marks any resolved path as external if it is within a node modules directory.
15
+ * This is used instead of the esbuild `packages` option to avoid marking files that should be loaded
16
+ * via customized loaders. This is necessary to prevent Vite development server pre-bundling errors.
17
+ *
18
+ * @returns An esbuild plugin.
19
+ */
20
+ function createExternalPackagesPlugin() {
21
+ return {
22
+ name: 'angular-external-packages',
23
+ setup(build) {
24
+ // Safe to use native packages external option if no loader options present
25
+ if (build.initialOptions.loader === undefined ||
26
+ Object.keys(build.initialOptions.loader).length === 0) {
27
+ build.initialOptions.packages = 'external';
28
+ return;
29
+ }
30
+ const loaderFileExtensions = new Set(Object.keys(build.initialOptions.loader));
31
+ // Only attempt resolve of non-relative and non-absolute paths
32
+ build.onResolve({ filter: /^[^./]/ }, async (args) => {
33
+ if (args.pluginData?.[EXTERNAL_PACKAGE_RESOLUTION]) {
34
+ return null;
35
+ }
36
+ const { importer, kind, resolveDir, namespace, pluginData = {} } = args;
37
+ pluginData[EXTERNAL_PACKAGE_RESOLUTION] = true;
38
+ const result = await build.resolve(args.path, {
39
+ importer,
40
+ kind,
41
+ namespace,
42
+ pluginData,
43
+ resolveDir,
44
+ });
45
+ // Return result if unable to resolve or explicitly marked external (externalDependencies option)
46
+ if (!result.path || result.external) {
47
+ return result;
48
+ }
49
+ // Allow customized loaders to run against configured paths regardless of location
50
+ if (loaderFileExtensions.has((0, node_path_1.extname)(result.path))) {
51
+ return result;
52
+ }
53
+ // Mark paths from a node modules directory as external
54
+ if (/[\\/]node_modules[\\/]/.test(result.path)) {
55
+ return {
56
+ path: args.path,
57
+ external: true,
58
+ };
59
+ }
60
+ // Otherwise return original result
61
+ return result;
62
+ });
63
+ },
64
+ };
65
+ }
66
+ exports.createExternalPackagesPlugin = createExternalPackagesPlugin;
@@ -11,6 +11,7 @@ exports.createCssResourcePlugin = void 0;
11
11
  const promises_1 = require("node:fs/promises");
12
12
  const node_path_1 = require("node:path");
13
13
  const load_result_cache_1 = require("../load-result-cache");
14
+ const CSS_RESOURCE_NAMESPACE = 'angular:css-resource';
14
15
  /**
15
16
  * Symbol marker used to indicate CSS resource resolution is being attempted.
16
17
  * This is used to prevent an infinite loop within the plugin's resolve hook.
@@ -89,10 +90,10 @@ function createCssResourcePlugin(cache) {
89
90
  // Use a relative path to prevent fully resolved paths in the metafile (JSON stats file).
90
91
  // This is only necessary for custom namespaces. esbuild will handle the file namespace.
91
92
  path: (0, node_path_1.relative)(build.initialOptions.absWorkingDir ?? '', result.path),
92
- namespace: 'css-resource',
93
+ namespace: CSS_RESOURCE_NAMESPACE,
93
94
  };
94
95
  });
95
- build.onLoad({ filter: /./, namespace: 'css-resource' }, (0, load_result_cache_1.createCachedLoad)(cache, async (args) => {
96
+ build.onLoad({ filter: /./, namespace: CSS_RESOURCE_NAMESPACE }, (0, load_result_cache_1.createCachedLoad)(cache, async (args) => {
96
97
  const resourcePath = (0, node_path_1.join)(build.initialOptions.absWorkingDir ?? '', args.path);
97
98
  return {
98
99
  contents: await (0, promises_1.readFile)(resourcePath),
@@ -0,0 +1,24 @@
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 type { Connect, Plugin } from 'vite';
9
+ export interface AngularMemoryPluginOptions {
10
+ workspaceRoot: string;
11
+ virtualProjectRoot: string;
12
+ outputFiles: Map<string, {
13
+ contents: Uint8Array;
14
+ servable: boolean;
15
+ }>;
16
+ assets: Map<string, string>;
17
+ ssr: boolean;
18
+ external?: string[];
19
+ extensionMiddleware?: Connect.NextHandleFunction[];
20
+ extraHeaders?: Record<string, string>;
21
+ indexHtmlTransformer?: (content: string) => Promise<string>;
22
+ normalizePath: (path: string) => string;
23
+ }
24
+ export declare function createAngularMemoryPlugin(options: AngularMemoryPluginOptions): Plugin;
@@ -0,0 +1,251 @@
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
+ var __importDefault = (this && this.__importDefault) || function (mod) {
10
+ return (mod && mod.__esModule) ? mod : { "default": mod };
11
+ };
12
+ Object.defineProperty(exports, "__esModule", { value: true });
13
+ exports.createAngularMemoryPlugin = void 0;
14
+ const remapping_1 = __importDefault(require("@ampproject/remapping"));
15
+ const mrmime_1 = require("mrmime");
16
+ const node_assert_1 = __importDefault(require("node:assert"));
17
+ const promises_1 = require("node:fs/promises");
18
+ const node_path_1 = require("node:path");
19
+ const render_page_1 = require("../../utils/server-rendering/render-page");
20
+ // eslint-disable-next-line max-lines-per-function
21
+ function createAngularMemoryPlugin(options) {
22
+ const { workspaceRoot, virtualProjectRoot, outputFiles, assets, external, ssr, extensionMiddleware, extraHeaders, indexHtmlTransformer, normalizePath, } = options;
23
+ return {
24
+ name: 'vite:angular-memory',
25
+ // Ensures plugin hooks run before built-in Vite hooks
26
+ enforce: 'pre',
27
+ async resolveId(source, importer) {
28
+ // Prevent vite from resolving an explicit external dependency (`externalDependencies` option)
29
+ if (external?.includes(source)) {
30
+ // This is still not ideal since Vite will still transform the import specifier to
31
+ // `/@id/${source}` but is currently closer to a raw external than a resolved file path.
32
+ return source;
33
+ }
34
+ if (importer && source[0] === '.' && importer.startsWith(virtualProjectRoot)) {
35
+ // Remove query if present
36
+ const [importerFile] = importer.split('?', 1);
37
+ source =
38
+ '/' + normalizePath((0, node_path_1.join)((0, node_path_1.dirname)((0, node_path_1.relative)(virtualProjectRoot, importerFile)), source));
39
+ }
40
+ const [file] = source.split('?', 1);
41
+ if (outputFiles.has(file)) {
42
+ return (0, node_path_1.join)(virtualProjectRoot, source);
43
+ }
44
+ },
45
+ load(id) {
46
+ const [file] = id.split('?', 1);
47
+ const relativeFile = '/' + normalizePath((0, node_path_1.relative)(virtualProjectRoot, file));
48
+ const codeContents = outputFiles.get(relativeFile)?.contents;
49
+ if (codeContents === undefined) {
50
+ if (relativeFile.endsWith('/node_modules/vite/dist/client/client.mjs')) {
51
+ return loadViteClientCode(file);
52
+ }
53
+ return;
54
+ }
55
+ const code = Buffer.from(codeContents).toString('utf-8');
56
+ const mapContents = outputFiles.get(relativeFile + '.map')?.contents;
57
+ return {
58
+ // Remove source map URL comments from the code if a sourcemap is present.
59
+ // Vite will inline and add an additional sourcemap URL for the sourcemap.
60
+ code: mapContents ? code.replace(/^\/\/# sourceMappingURL=[^\r\n]*/gm, '') : code,
61
+ map: mapContents && Buffer.from(mapContents).toString('utf-8'),
62
+ };
63
+ },
64
+ // eslint-disable-next-line max-lines-per-function
65
+ configureServer(server) {
66
+ const originalssrTransform = server.ssrTransform;
67
+ server.ssrTransform = async (code, map, url, originalCode) => {
68
+ const result = await originalssrTransform(code, null, url, originalCode);
69
+ if (!result || !result.map || !map) {
70
+ return result;
71
+ }
72
+ const remappedMap = (0, remapping_1.default)([result.map, map], () => null);
73
+ // Set the sourcemap root to the workspace root. This is needed since we set a virtual path as root.
74
+ remappedMap.sourceRoot = normalizePath(workspaceRoot) + '/';
75
+ return {
76
+ ...result,
77
+ map: remappedMap,
78
+ };
79
+ };
80
+ // Assets and resources get handled first
81
+ server.middlewares.use(function angularAssetsMiddleware(req, res, next) {
82
+ if (req.url === undefined || res.writableEnded) {
83
+ return;
84
+ }
85
+ // Parse the incoming request.
86
+ // The base of the URL is unused but required to parse the URL.
87
+ const pathname = pathnameWithoutBasePath(req.url, server.config.base);
88
+ const extension = (0, node_path_1.extname)(pathname);
89
+ // Rewrite all build assets to a vite raw fs URL
90
+ const assetSourcePath = assets.get(pathname);
91
+ if (assetSourcePath !== undefined) {
92
+ // Workaround to disable Vite transformer middleware.
93
+ // See: https://github.com/vitejs/vite/blob/746a1daab0395f98f0afbdee8f364cb6cf2f3b3f/packages/vite/src/node/server/middlewares/transform.ts#L201 and
94
+ // https://github.com/vitejs/vite/blob/746a1daab0395f98f0afbdee8f364cb6cf2f3b3f/packages/vite/src/node/server/transformRequest.ts#L204-L206
95
+ req.headers.accept = 'text/html';
96
+ // The encoding needs to match what happens in the vite static middleware.
97
+ // ref: https://github.com/vitejs/vite/blob/d4f13bd81468961c8c926438e815ab6b1c82735e/packages/vite/src/node/server/middlewares/static.ts#L163
98
+ req.url = `${server.config.base}@fs/${encodeURI(assetSourcePath)}`;
99
+ next();
100
+ return;
101
+ }
102
+ // Resource files are handled directly.
103
+ // Global stylesheets (CSS files) are currently considered resources to workaround
104
+ // dev server sourcemap issues with stylesheets.
105
+ if (extension !== '.js' && extension !== '.html') {
106
+ const outputFile = outputFiles.get(pathname);
107
+ if (outputFile?.servable) {
108
+ const mimeType = (0, mrmime_1.lookup)(extension);
109
+ if (mimeType) {
110
+ res.setHeader('Content-Type', mimeType);
111
+ }
112
+ res.setHeader('Cache-Control', 'no-cache');
113
+ if (extraHeaders) {
114
+ Object.entries(extraHeaders).forEach(([name, value]) => res.setHeader(name, value));
115
+ }
116
+ res.end(outputFile.contents);
117
+ return;
118
+ }
119
+ }
120
+ next();
121
+ });
122
+ if (extensionMiddleware?.length) {
123
+ extensionMiddleware.forEach((middleware) => server.middlewares.use(middleware));
124
+ }
125
+ // Returning a function, installs middleware after the main transform middleware but
126
+ // before the built-in HTML middleware
127
+ return () => {
128
+ server.middlewares.use(angularHtmlFallbackMiddleware);
129
+ function angularSSRMiddleware(req, res, next) {
130
+ const url = req.originalUrl;
131
+ if (!req.url ||
132
+ // Skip if path is not defined.
133
+ !url ||
134
+ // Skip if path is like a file.
135
+ // NOTE: We use a mime type lookup to mitigate against matching requests like: /browse/pl.0ef59752c0cd457dbf1391f08cbd936f
136
+ lookupMimeTypeFromRequest(url)) {
137
+ next();
138
+ return;
139
+ }
140
+ const rawHtml = outputFiles.get('/index.server.html')?.contents;
141
+ if (!rawHtml) {
142
+ next();
143
+ return;
144
+ }
145
+ transformIndexHtmlAndAddHeaders(req.url, rawHtml, res, next, async (html) => {
146
+ const { content } = await (0, render_page_1.renderPage)({
147
+ document: html,
148
+ route: new URL(req.originalUrl ?? '/', server.resolvedUrls?.local[0]).toString(),
149
+ serverContext: 'ssr',
150
+ loadBundle: (uri) =>
151
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
152
+ server.ssrLoadModule(uri.slice(1)),
153
+ // Files here are only needed for critical CSS inlining.
154
+ outputFiles: {},
155
+ // TODO: add support for critical css inlining.
156
+ inlineCriticalCss: false,
157
+ });
158
+ return indexHtmlTransformer && content ? await indexHtmlTransformer(content) : content;
159
+ });
160
+ }
161
+ if (ssr) {
162
+ server.middlewares.use(angularSSRMiddleware);
163
+ }
164
+ server.middlewares.use(function angularIndexMiddleware(req, res, next) {
165
+ if (!req.url) {
166
+ next();
167
+ return;
168
+ }
169
+ // Parse the incoming request.
170
+ // The base of the URL is unused but required to parse the URL.
171
+ const pathname = pathnameWithoutBasePath(req.url, server.config.base);
172
+ if (pathname === '/' || pathname === `/index.html`) {
173
+ const rawHtml = outputFiles.get('/index.html')?.contents;
174
+ if (rawHtml) {
175
+ transformIndexHtmlAndAddHeaders(req.url, rawHtml, res, next, indexHtmlTransformer);
176
+ return;
177
+ }
178
+ }
179
+ next();
180
+ });
181
+ };
182
+ function transformIndexHtmlAndAddHeaders(url, rawHtml, res, next, additionalTransformer) {
183
+ server
184
+ .transformIndexHtml(url, Buffer.from(rawHtml).toString('utf-8'))
185
+ .then(async (processedHtml) => {
186
+ if (additionalTransformer) {
187
+ const content = await additionalTransformer(processedHtml);
188
+ if (!content) {
189
+ next();
190
+ return;
191
+ }
192
+ processedHtml = content;
193
+ }
194
+ res.setHeader('Content-Type', 'text/html');
195
+ res.setHeader('Cache-Control', 'no-cache');
196
+ if (extraHeaders) {
197
+ Object.entries(extraHeaders).forEach(([name, value]) => res.setHeader(name, value));
198
+ }
199
+ res.end(processedHtml);
200
+ })
201
+ .catch((error) => next(error));
202
+ }
203
+ },
204
+ };
205
+ }
206
+ exports.createAngularMemoryPlugin = createAngularMemoryPlugin;
207
+ /**
208
+ * Reads the resolved Vite client code from disk and updates the content to remove
209
+ * an unactionable suggestion to update the Vite configuration file to disable the
210
+ * error overlay. The Vite configuration file is not present when used in the Angular
211
+ * CLI.
212
+ * @param file The absolute path to the Vite client code.
213
+ * @returns
214
+ */
215
+ async function loadViteClientCode(file) {
216
+ const originalContents = await (0, promises_1.readFile)(file, 'utf-8');
217
+ let contents = originalContents.replace('You can also disable this overlay by setting', '');
218
+ contents = contents.replace(
219
+ // eslint-disable-next-line max-len
220
+ '<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>', '');
221
+ (0, node_assert_1.default)(originalContents !== contents, 'Failed to update Vite client error overlay text.');
222
+ return contents;
223
+ }
224
+ function pathnameWithoutBasePath(url, basePath) {
225
+ const parsedUrl = new URL(url, 'http://localhost');
226
+ const pathname = decodeURIComponent(parsedUrl.pathname);
227
+ // slice(basePath.length - 1) to retain the trailing slash
228
+ return basePath !== '/' && pathname.startsWith(basePath)
229
+ ? pathname.slice(basePath.length - 1)
230
+ : pathname;
231
+ }
232
+ function angularHtmlFallbackMiddleware(req, res, next) {
233
+ // Similar to how it is handled in vite
234
+ // https://github.com/vitejs/vite/blob/main/packages/vite/src/node/server/middlewares/htmlFallback.ts#L15C19-L15C45
235
+ if ((req.method === 'GET' || req.method === 'HEAD') &&
236
+ (!req.url || !lookupMimeTypeFromRequest(req.url)) &&
237
+ (!req.headers.accept ||
238
+ req.headers.accept.includes('text/html') ||
239
+ req.headers.accept.includes('text/*') ||
240
+ req.headers.accept.includes('*/*'))) {
241
+ req.url = '/index.html';
242
+ }
243
+ next();
244
+ }
245
+ function lookupMimeTypeFromRequest(url) {
246
+ const extension = (0, node_path_1.extname)(url.split('?')[0]);
247
+ if (extension === '.ico') {
248
+ return 'image/x-icon';
249
+ }
250
+ return extension && (0, mrmime_1.lookup)(extension);
251
+ }
@@ -23,23 +23,31 @@ const MEDIA_SET_HANDLER_PATTERN = /^this\.media=["'](.*)["'];?$/;
23
23
  const CSP_MEDIA_ATTR = 'ngCspMedia';
24
24
  /**
25
25
  * Script text used to change the media value of the link tags.
26
+ *
27
+ * NOTE:
28
+ * We do not use `document.querySelectorAll('link').forEach((s) => s.addEventListener('load', ...)`
29
+ * because this does not always fire on Chome.
30
+ * See: https://github.com/angular/angular-cli/issues/26932 and https://crbug.com/1521256
26
31
  */
27
32
  const LINK_LOAD_SCRIPT_CONTENT = [
28
- `(() => {`,
29
- // Save the `children` in a variable since they're a live DOM node collection.
30
- // We iterate over the direct descendants, instead of going through a `querySelectorAll`,
31
- // because we know that the tags will be directly inside the `head`.
32
- ` const children = document.head.children;`,
33
- // Declare `onLoad` outside the loop to avoid leaking memory.
34
- // Can't be an arrow function, because we need `this` to refer to the DOM node.
35
- ` function onLoad() {this.media = this.getAttribute('${CSP_MEDIA_ATTR}');}`,
36
- // Has to use a plain for loop, because some browsers don't support
37
- // `forEach` on `children` which is a `HTMLCollection`.
38
- ` for (let i = 0; i < children.length; i++) {`,
39
- ` const child = children[i];`,
40
- ` child.hasAttribute('${CSP_MEDIA_ATTR}') && child.addEventListener('load', onLoad);`,
41
- ` }`,
42
- `})();`,
33
+ '(() => {',
34
+ ` const CSP_MEDIA_ATTR = '${CSP_MEDIA_ATTR}';`,
35
+ ' const documentElement = document.documentElement;',
36
+ ' const listener = (e) => {',
37
+ ' const target = e.target;',
38
+ ` if (!target || target.tagName !== 'LINK' || !target.hasAttribute(CSP_MEDIA_ATTR)) {`,
39
+ ' return;',
40
+ ' }',
41
+ ' target.media = target.getAttribute(CSP_MEDIA_ATTR);',
42
+ ' target.removeAttribute(CSP_MEDIA_ATTR);',
43
+ // Remove onload listener when there are no longer styles that need to be loaded.
44
+ ' if (!document.head.querySelector(`link[${CSP_MEDIA_ATTR}]`)) {',
45
+ ` documentElement.removeEventListener('load', listener);`,
46
+ ' }',
47
+ ' };',
48
+ // We use an event with capturing (the true parameter) because load events don't bubble.
49
+ ` documentElement.addEventListener('load', listener, true);`,
50
+ '})();',
43
51
  ].join('\n');
44
52
  class CrittersExtended extends critters_1.default {
45
53
  optionsExtended;
@@ -105,7 +113,7 @@ class CrittersExtended extends critters_1.default {
105
113
  // `addEventListener` to apply the media query instead.
106
114
  link.removeAttribute('onload');
107
115
  link.setAttribute(CSP_MEDIA_ATTR, crittersMedia[1]);
108
- this.conditionallyInsertCspLoadingScript(document, cspNonce);
116
+ this.conditionallyInsertCspLoadingScript(document, cspNonce, link);
109
117
  }
110
118
  // Ideally we would hook in at the time Critters inserts the `style` tags, but there isn't
111
119
  // a way of doing that at the moment so we fall back to doing it any time a `link` tag is
@@ -137,16 +145,16 @@ class CrittersExtended extends critters_1.default {
137
145
  * Inserts the `script` tag that swaps the critical CSS at runtime,
138
146
  * if one hasn't been inserted into the document already.
139
147
  */
140
- conditionallyInsertCspLoadingScript(document, nonce) {
148
+ conditionallyInsertCspLoadingScript(document, nonce, link) {
141
149
  if (this.addedCspScriptsDocuments.has(document)) {
142
150
  return;
143
151
  }
144
152
  const script = document.createElement('script');
145
153
  script.setAttribute('nonce', nonce);
146
154
  script.textContent = LINK_LOAD_SCRIPT_CONTENT;
147
- // Append the script to the head since it needs to
148
- // run as early as possible, after the `link` tags.
149
- document.head.appendChild(script);
155
+ // Prepend the script to the head since it needs to
156
+ // run as early as possible, before the `link` tags.
157
+ document.head.insertBefore(script, link);
150
158
  this.addedCspScriptsDocuments.add(document);
151
159
  }
152
160
  }
@@ -135,9 +135,9 @@ function normalizeProxyConfiguration(proxy) {
135
135
  }
136
136
  // TODO: Consider upstreaming glob support
137
137
  for (const key of Object.keys(normalizedProxy)) {
138
- if ((0, fast_glob_1.isDynamicPattern)(key)) {
139
- const { output } = (0, picomatch_1.parse)(key);
140
- normalizedProxy[`^${output}$`] = normalizedProxy[key];
138
+ if (key[0] !== '^' && (0, fast_glob_1.isDynamicPattern)(key)) {
139
+ const pattern = (0, picomatch_1.makeRe)(key).source;
140
+ normalizedProxy[pattern] = normalizedProxy[key];
141
141
  delete normalizedProxy[key];
142
142
  }
143
143
  }
@@ -29,8 +29,12 @@ var __importStar = (this && this.__importStar) || function (mod) {
29
29
  __setModuleDefault(result, mod);
30
30
  return result;
31
31
  };
32
+ var __importDefault = (this && this.__importDefault) || function (mod) {
33
+ return (mod && mod.__esModule) ? mod : { "default": mod };
34
+ };
32
35
  Object.defineProperty(exports, "__esModule", { value: true });
33
36
  exports.renderPage = void 0;
37
+ const node_assert_1 = __importDefault(require("node:assert"));
34
38
  const node_path_1 = require("node:path");
35
39
  const load_esm_1 = require("../load-esm");
36
40
  /**
@@ -65,6 +69,7 @@ async function renderPage({ route, serverContext, document, inlineCriticalCss, o
65
69
  },
66
70
  ];
67
71
  let html;
72
+ (0, node_assert_1.default)(bootstrapAppFnOrModule, 'The file "./main.server.mjs" does not have a default export for an AppServerModule or a bootstrapping function.');
68
73
  if (isBootstrapFn(bootstrapAppFnOrModule)) {
69
74
  html = await renderApplication(bootstrapAppFnOrModule, {
70
75
  document,