@angular/build 20.1.0-rc.0 → 20.1.0

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": "20.1.0-rc.0",
3
+ "version": "20.1.0",
4
4
  "description": "Official build system for Angular",
5
5
  "keywords": [
6
6
  "Angular CLI",
@@ -23,7 +23,7 @@
23
23
  "builders": "builders.json",
24
24
  "dependencies": {
25
25
  "@ampproject/remapping": "2.3.0",
26
- "@angular-devkit/architect": "0.2001.0-rc.0",
26
+ "@angular-devkit/architect": "0.2001.0",
27
27
  "@babel/core": "7.27.7",
28
28
  "@babel/helper-annotate-as-pure": "7.27.3",
29
29
  "@babel/helper-split-export-declaration": "7.24.7",
@@ -53,17 +53,17 @@
53
53
  "lmdb": "3.4.1"
54
54
  },
55
55
  "peerDependencies": {
56
- "@angular/core": "^20.0.0 || ^20.1.0-next.0",
57
- "@angular/compiler": "^20.0.0 || ^20.1.0-next.0",
58
- "@angular/compiler-cli": "^20.0.0 || ^20.1.0-next.0",
59
- "@angular/localize": "^20.0.0 || ^20.1.0-next.0",
60
- "@angular/platform-browser": "^20.0.0 || ^20.1.0-next.0",
61
- "@angular/platform-server": "^20.0.0 || ^20.1.0-next.0",
62
- "@angular/service-worker": "^20.0.0 || ^20.1.0-next.0",
63
- "@angular/ssr": "^20.1.0-rc.0",
56
+ "@angular/core": "^20.0.0",
57
+ "@angular/compiler": "^20.0.0",
58
+ "@angular/compiler-cli": "^20.0.0",
59
+ "@angular/localize": "^20.0.0",
60
+ "@angular/platform-browser": "^20.0.0",
61
+ "@angular/platform-server": "^20.0.0",
62
+ "@angular/service-worker": "^20.0.0",
63
+ "@angular/ssr": "^20.1.0",
64
64
  "karma": "^6.4.0",
65
65
  "less": "^4.2.0",
66
- "ng-packagr": "^20.0.0 || ^20.1.0-next.0",
66
+ "ng-packagr": "^20.0.0",
67
67
  "postcss": "^8.4.0",
68
68
  "tailwindcss": "^2.0.0 || ^3.0.0 || ^4.0.0",
69
69
  "tslib": "^2.3.0",
@@ -6,10 +6,8 @@
6
6
  * found in the LICENSE file at https://angular.dev/license
7
7
  */
8
8
  import type { BuilderContext, BuilderOutput } from '@angular-devkit/architect';
9
- import type { ConfigOptions } from 'karma';
10
9
  import { ResultFile } from '../application/results';
11
10
  import { Schema as KarmaBuilderOptions } from './schema';
12
- export declare function execute(options: KarmaBuilderOptions, context: BuilderContext, karmaOptions: ConfigOptions, transforms?: {
13
- karmaOptions?: (options: ConfigOptions) => ConfigOptions;
14
- }): AsyncIterable<BuilderOutput>;
11
+ import type { KarmaBuilderTransformsOptions } from './index';
12
+ export declare function execute(options: KarmaBuilderOptions, context: BuilderContext, transforms?: KarmaBuilderTransformsOptions): AsyncIterable<BuilderOutput>;
15
13
  export declare function writeTestFiles(files: Record<string, ResultFile>, testDir: string): Promise<void>;
@@ -39,13 +39,16 @@ var __importStar = (this && this.__importStar) || (function () {
39
39
  return result;
40
40
  };
41
41
  })();
42
+ var __importDefault = (this && this.__importDefault) || function (mod) {
43
+ return (mod && mod.__esModule) ? mod : { "default": mod };
44
+ };
42
45
  Object.defineProperty(exports, "__esModule", { value: true });
43
46
  exports.execute = execute;
44
47
  exports.writeTestFiles = writeTestFiles;
45
48
  const node_crypto_1 = require("node:crypto");
46
49
  const fs = __importStar(require("node:fs/promises"));
47
50
  const node_module_1 = require("node:module");
48
- const path = __importStar(require("node:path"));
51
+ const node_path_1 = __importDefault(require("node:path"));
49
52
  const tinyglobby_1 = require("tinyglobby");
50
53
  const bundler_context_1 = require("../../tools/esbuild/bundler-context");
51
54
  const utils_1 = require("../../tools/esbuild/utils");
@@ -55,6 +58,7 @@ const index_1 = require("../application/index");
55
58
  const results_1 = require("../application/results");
56
59
  const schema_1 = require("../application/schema");
57
60
  const find_tests_1 = require("./find-tests");
61
+ const options_1 = require("./options");
58
62
  const localResolve = (0, node_module_1.createRequire)(__filename).resolve;
59
63
  const isWindows = process.platform === 'win32';
60
64
  class ApplicationBuildError extends Error {
@@ -79,20 +83,23 @@ class AngularAssetsMiddleware {
79
83
  // The latest build files will use the platform path separator.
80
84
  let pathname = url.pathname.slice(1);
81
85
  if (isWindows) {
82
- pathname = pathname.replaceAll(path.posix.sep, path.win32.sep);
86
+ pathname = pathname.replaceAll(node_path_1.default.posix.sep, node_path_1.default.win32.sep);
83
87
  }
84
88
  const file = this.latestBuildFiles.files[pathname];
85
89
  if (!file) {
86
90
  next();
87
91
  return;
88
92
  }
93
+ // Implementation of serverFile can be found here:
94
+ // https://github.com/karma-runner/karma/blob/84f85e7016efc2266fa6b3465f494a3fa151c85c/lib/middleware/common.js#L10
89
95
  switch (file.origin) {
90
96
  case 'disk':
91
97
  this.serveFile(file.inputPath, undefined, res, undefined, undefined, /* doNotCache */ true);
92
98
  break;
93
99
  case 'memory':
94
100
  // Include pathname to help with Content-Type headers.
95
- this.serveFile(`/unused/${url.pathname}`, undefined, res, undefined, file.contents, true);
101
+ this.serveFile(`/unused/${url.pathname}`, undefined, res, undefined, file.contents,
102
+ /* doNotCache */ false);
96
103
  break;
97
104
  }
98
105
  }
@@ -228,13 +235,15 @@ function injectKarmaReporter(buildOptions, buildIterator, karmaConfig, controlle
228
235
  ],
229
236
  });
230
237
  }
231
- function execute(options, context, karmaOptions, transforms = {}) {
238
+ function execute(options, context, transforms) {
239
+ const normalizedOptions = (0, options_1.normalizeOptions)(context, options);
240
+ const karmaOptions = getBaseKarmaOptions(normalizedOptions, context);
232
241
  let karmaServer;
233
242
  return new ReadableStream({
234
243
  async start(controller) {
235
244
  let init;
236
245
  try {
237
- init = await initializeApplication(options, context, karmaOptions, transforms);
246
+ init = await initializeApplication(normalizedOptions, context, karmaOptions, transforms);
238
247
  }
239
248
  catch (err) {
240
249
  if (err instanceof ApplicationBuildError) {
@@ -272,13 +281,7 @@ async function getProjectSourceRoot(context) {
272
281
  const { projectSourceRoot } = (0, project_metadata_1.getProjectRootPaths)(context.workspaceRoot, projectMetadata);
273
282
  return projectSourceRoot;
274
283
  }
275
- function normalizePolyfills(polyfills) {
276
- if (typeof polyfills === 'string') {
277
- polyfills = [polyfills];
278
- }
279
- else if (!polyfills) {
280
- polyfills = [];
281
- }
284
+ function normalizePolyfills(polyfills = []) {
282
285
  const jasmineGlobalEntryPoint = localResolve('./polyfills/jasmine_global.js');
283
286
  const jasmineGlobalCleanupEntrypoint = localResolve('./polyfills/jasmine_global_cleanup.js');
284
287
  const sourcemapEntrypoint = localResolve('./polyfills/init_sourcemaps.js');
@@ -293,12 +296,12 @@ function normalizePolyfills(polyfills) {
293
296
  }
294
297
  async function collectEntrypoints(options, context, projectSourceRoot) {
295
298
  // Glob for files to test.
296
- const testFiles = await (0, find_tests_1.findTests)(options.include ?? [], options.exclude ?? [], context.workspaceRoot, projectSourceRoot);
299
+ const testFiles = await (0, find_tests_1.findTests)(options.include, options.exclude, context.workspaceRoot, projectSourceRoot);
297
300
  return (0, find_tests_1.getTestEntrypoints)(testFiles, { projectSourceRoot, workspaceRoot: context.workspaceRoot });
298
301
  }
299
302
  // eslint-disable-next-line max-lines-per-function
300
- async function initializeApplication(options, context, karmaOptions, transforms = {}) {
301
- const outputPath = path.join(context.workspaceRoot, 'dist/test-out', (0, node_crypto_1.randomUUID)());
303
+ async function initializeApplication(options, context, karmaOptions, transforms) {
304
+ const outputPath = node_path_1.default.join(context.workspaceRoot, 'dist/test-out', (0, node_crypto_1.randomUUID)());
302
305
  const projectSourceRoot = await getProjectSourceRoot(context);
303
306
  const [karma, entryPoints] = await Promise.all([
304
307
  Promise.resolve().then(() => __importStar(require('karma'))),
@@ -329,19 +332,13 @@ async function initializeApplication(options, context, karmaOptions, transforms
329
332
  index: false,
330
333
  outputHashing: schema_1.OutputHashing.None,
331
334
  optimization: false,
332
- sourceMap: options.codeCoverage
333
- ? {
334
- scripts: true,
335
- styles: true,
336
- vendor: true,
337
- }
338
- : options.sourceMap,
335
+ sourceMap: options.sourceMap,
339
336
  instrumentForCoverage,
340
337
  styles: options.styles,
341
338
  scripts: options.scripts,
342
339
  polyfills,
343
340
  webWorkerTsConfig: options.webWorkerTsConfig,
344
- watch: options.watch ?? !karmaOptions.singleRun,
341
+ watch: options.watch,
345
342
  stylePreprocessorOptions: options.stylePreprocessorOptions,
346
343
  inlineStyleLanguage: options.inlineStyleLanguage,
347
344
  fileReplacements: options.fileReplacements,
@@ -410,6 +407,7 @@ async function initializeApplication(options, context, karmaOptions, transforms
410
407
  scriptsFiles.push({
411
408
  pattern: `${outputPath}/${outputName}`,
412
409
  watched: false,
410
+ included: typeof scriptEntry === 'string' ? true : scriptEntry.inject !== false,
413
411
  type: 'js',
414
412
  });
415
413
  }
@@ -436,7 +434,7 @@ async function initializeApplication(options, context, karmaOptions, transforms
436
434
  // Serve CSS outputs on page load, these are the global styles.
437
435
  karmaOptions.files.push({ pattern: `*.css`, type: 'css', watched: false });
438
436
  }
439
- const parsedKarmaConfig = await karma.config.parseConfig(options.karmaConfig && path.resolve(context.workspaceRoot, options.karmaConfig), transforms.karmaOptions ? transforms.karmaOptions(karmaOptions) : karmaOptions, { promiseConfig: true, throwErrors: true });
437
+ const parsedKarmaConfig = await karma.config.parseConfig(options.karmaConfig, transforms?.karmaOptions ? await transforms.karmaOptions(karmaOptions) : karmaOptions, { promiseConfig: true, throwErrors: true });
440
438
  // Check for jsdom which does not support executing ESM scripts.
441
439
  // If present, remove jsdom and issue a warning.
442
440
  const updatedBrowsers = parsedKarmaConfig.browsers?.filter((browser) => browser !== 'jsdom');
@@ -480,8 +478,8 @@ async function initializeApplication(options, context, karmaOptions, transforms
480
478
  junitReporterOptions.outputDir = context.workspaceRoot;
481
479
  }
482
480
  else if (typeof junitReporterOptions.outputDir === 'string' &&
483
- !path.isAbsolute(junitReporterOptions.outputDir)) {
484
- junitReporterOptions.outputDir = path.join(context.workspaceRoot, junitReporterOptions.outputDir);
481
+ !node_path_1.default.isAbsolute(junitReporterOptions.outputDir)) {
482
+ junitReporterOptions.outputDir = node_path_1.default.join(context.workspaceRoot, junitReporterOptions.outputDir);
485
483
  }
486
484
  }
487
485
  else {
@@ -511,9 +509,9 @@ async function writeTestFiles(files, testDir) {
511
509
  if (file.type !== bundler_context_1.BuildOutputFileType.Browser && file.type !== bundler_context_1.BuildOutputFileType.Media) {
512
510
  return;
513
511
  }
514
- const fullFilePath = path.join(testDir, filePath);
512
+ const fullFilePath = node_path_1.default.join(testDir, filePath);
515
513
  // Ensure output subdirectories exist
516
- const fileBasePath = path.dirname(fullFilePath);
514
+ const fileBasePath = node_path_1.default.dirname(fullFilePath);
517
515
  if (fileBasePath && !directoryExists.has(fileBasePath)) {
518
516
  await fs.mkdir(fileBasePath, { recursive: true });
519
517
  directoryExists.add(fileBasePath);
@@ -554,7 +552,71 @@ function getInstrumentationExcludedPaths(root, excludedPaths) {
554
552
  const excluded = new Set();
555
553
  for (const excludeGlob of excludedPaths) {
556
554
  const excludePath = excludeGlob[0] === '/' ? excludeGlob.slice(1) : excludeGlob;
557
- (0, tinyglobby_1.globSync)(excludePath, { cwd: root }).forEach((p) => excluded.add(path.join(root, p)));
555
+ (0, tinyglobby_1.globSync)(excludePath, { cwd: root }).forEach((p) => excluded.add(node_path_1.default.join(root, p)));
558
556
  }
559
557
  return excluded;
560
558
  }
559
+ function getBaseKarmaOptions(options, context) {
560
+ // Determine project name from builder context target
561
+ const projectName = context.target?.project;
562
+ if (!projectName) {
563
+ throw new Error(`The 'karma' builder requires a target to be specified.`);
564
+ }
565
+ const karmaOptions = options.karmaConfig
566
+ ? {}
567
+ : getBuiltInKarmaConfig(context.workspaceRoot, projectName);
568
+ const singleRun = !options.watch;
569
+ karmaOptions.singleRun = singleRun;
570
+ // Workaround https://github.com/angular/angular-cli/issues/28271, by clearing context by default
571
+ // for single run executions. Not clearing context for multi-run (watched) builds allows the
572
+ // Jasmine Spec Runner to be visible in the browser after test execution.
573
+ karmaOptions.client ??= {};
574
+ karmaOptions.client.clearContext ??= singleRun;
575
+ // Convert browsers from a string to an array
576
+ if (options.browsers) {
577
+ karmaOptions.browsers = options.browsers;
578
+ }
579
+ if (options.reporters) {
580
+ karmaOptions.reporters = options.reporters;
581
+ }
582
+ return karmaOptions;
583
+ }
584
+ function getBuiltInKarmaConfig(workspaceRoot, projectName) {
585
+ let coverageFolderName = projectName.charAt(0) === '@' ? projectName.slice(1) : projectName;
586
+ coverageFolderName = coverageFolderName.toLowerCase();
587
+ const workspaceRootRequire = (0, node_module_1.createRequire)(workspaceRoot + '/');
588
+ // Any changes to the config here need to be synced to: packages/schematics/angular/config/files/karma.conf.js.template
589
+ return {
590
+ basePath: '',
591
+ frameworks: ['jasmine'],
592
+ plugins: [
593
+ 'karma-jasmine',
594
+ 'karma-chrome-launcher',
595
+ 'karma-jasmine-html-reporter',
596
+ 'karma-coverage',
597
+ ].map((p) => workspaceRootRequire(p)),
598
+ jasmineHtmlReporter: {
599
+ suppressAll: true, // removes the duplicated traces
600
+ },
601
+ coverageReporter: {
602
+ dir: node_path_1.default.join(workspaceRoot, 'coverage', coverageFolderName),
603
+ subdir: '.',
604
+ reporters: [{ type: 'html' }, { type: 'text-summary' }],
605
+ },
606
+ reporters: ['progress', 'kjhtml'],
607
+ browsers: ['Chrome'],
608
+ customLaunchers: {
609
+ // Chrome configured to run in a bazel sandbox.
610
+ // Disable the use of the gpu and `/dev/shm` because it causes Chrome to
611
+ // crash on some environments.
612
+ // See:
613
+ // https://github.com/puppeteer/puppeteer/blob/v1.0.0/docs/troubleshooting.md#tips
614
+ // https://stackoverflow.com/questions/50642308/webdriverexception-unknown-error-devtoolsactiveport-file-doesnt-exist-while-t
615
+ ChromeHeadlessNoSandbox: {
616
+ base: 'ChromeHeadless',
617
+ flags: ['--no-sandbox', '--headless', '--disable-gpu', '--disable-dev-shm-usage'],
618
+ },
619
+ },
620
+ restartOnFileChange: true,
621
+ };
622
+ }
@@ -8,16 +8,13 @@
8
8
  import { type Builder, type BuilderContext, type BuilderOutput } from '@angular-devkit/architect';
9
9
  import type { ConfigOptions } from 'karma';
10
10
  import type { Schema as KarmaBuilderOptions } from './schema';
11
- export type KarmaConfigOptions = ConfigOptions & {
12
- buildWebpack?: unknown;
13
- configFile?: string;
14
- };
11
+ export interface KarmaBuilderTransformsOptions {
12
+ karmaOptions?: (options: ConfigOptions) => ConfigOptions | Promise<ConfigOptions>;
13
+ }
15
14
  /**
16
15
  * @experimental Direct usage of this function is considered experimental.
17
16
  */
18
- export declare function execute(options: KarmaBuilderOptions, context: BuilderContext, transforms?: {
19
- karmaOptions?: (options: KarmaConfigOptions) => KarmaConfigOptions;
20
- }): AsyncIterable<BuilderOutput>;
17
+ export declare function execute(options: KarmaBuilderOptions, context: BuilderContext, transforms?: KarmaBuilderTransformsOptions): AsyncIterable<BuilderOutput>;
21
18
  export type { KarmaBuilderOptions };
22
19
  declare const builder: Builder<KarmaBuilderOptions>;
23
20
  export default builder;
@@ -39,101 +39,15 @@ var __importStar = (this && this.__importStar) || (function () {
39
39
  return result;
40
40
  };
41
41
  })();
42
- var __importDefault = (this && this.__importDefault) || function (mod) {
43
- return (mod && mod.__esModule) ? mod : { "default": mod };
44
- };
45
42
  Object.defineProperty(exports, "__esModule", { value: true });
46
43
  exports.execute = execute;
47
44
  const architect_1 = require("@angular-devkit/architect");
48
- const node_module_1 = require("node:module");
49
- const node_path_1 = __importDefault(require("node:path"));
50
45
  /**
51
46
  * @experimental Direct usage of this function is considered experimental.
52
47
  */
53
- async function* execute(options, context, transforms = {}) {
48
+ async function* execute(options, context, transforms) {
54
49
  const { execute } = await Promise.resolve().then(() => __importStar(require('./application_builder')));
55
- const karmaOptions = getBaseKarmaOptions(options, context);
56
- yield* execute(options, context, karmaOptions, transforms);
57
- }
58
- function getBaseKarmaOptions(options, context) {
59
- let singleRun;
60
- if (options.watch !== undefined) {
61
- singleRun = !options.watch;
62
- }
63
- // Determine project name from builder context target
64
- const projectName = context.target?.project;
65
- if (!projectName) {
66
- throw new Error(`The 'karma' builder requires a target to be specified.`);
67
- }
68
- const karmaOptions = options.karmaConfig
69
- ? {}
70
- : getBuiltInKarmaConfig(context.workspaceRoot, projectName);
71
- karmaOptions.singleRun = singleRun;
72
- // Workaround https://github.com/angular/angular-cli/issues/28271, by clearing context by default
73
- // for single run executions. Not clearing context for multi-run (watched) builds allows the
74
- // Jasmine Spec Runner to be visible in the browser after test execution.
75
- karmaOptions.client ??= {};
76
- karmaOptions.client.clearContext ??= singleRun ?? false; // `singleRun` defaults to `false` per Karma docs.
77
- // Convert browsers from a string to an array
78
- if (typeof options.browsers === 'string' && options.browsers) {
79
- karmaOptions.browsers = options.browsers.split(',').map((browser) => browser.trim());
80
- }
81
- else if (options.browsers === false) {
82
- karmaOptions.browsers = [];
83
- }
84
- if (options.reporters) {
85
- // Split along commas to make it more natural, and remove empty strings.
86
- const reporters = options.reporters
87
- .reduce((acc, curr) => acc.concat(curr.split(',')), [])
88
- .filter((x) => !!x);
89
- if (reporters.length > 0) {
90
- karmaOptions.reporters = reporters;
91
- }
92
- }
93
- return karmaOptions;
94
- }
95
- function getBuiltInKarmaConfig(workspaceRoot, projectName) {
96
- let coverageFolderName = projectName.charAt(0) === '@' ? projectName.slice(1) : projectName;
97
- coverageFolderName = coverageFolderName.toLowerCase();
98
- const workspaceRootRequire = (0, node_module_1.createRequire)(workspaceRoot + '/');
99
- // Any changes to the config here need to be synced to: packages/schematics/angular/config/files/karma.conf.js.template
100
- return {
101
- basePath: '',
102
- rootUrl: '/',
103
- frameworks: ['jasmine'],
104
- plugins: [
105
- 'karma-jasmine',
106
- 'karma-chrome-launcher',
107
- 'karma-jasmine-html-reporter',
108
- 'karma-coverage',
109
- ].map((p) => workspaceRootRequire(p)),
110
- proxies: {
111
- '/': '/base/',
112
- },
113
- jasmineHtmlReporter: {
114
- suppressAll: true, // removes the duplicated traces
115
- },
116
- coverageReporter: {
117
- dir: node_path_1.default.join(workspaceRoot, 'coverage', coverageFolderName),
118
- subdir: '.',
119
- reporters: [{ type: 'html' }, { type: 'text-summary' }],
120
- },
121
- reporters: ['progress', 'kjhtml'],
122
- browsers: ['Chrome'],
123
- customLaunchers: {
124
- // Chrome configured to run in a bazel sandbox.
125
- // Disable the use of the gpu and `/dev/shm` because it causes Chrome to
126
- // crash on some environments.
127
- // See:
128
- // https://github.com/puppeteer/puppeteer/blob/v1.0.0/docs/troubleshooting.md#tips
129
- // https://stackoverflow.com/questions/50642308/webdriverexception-unknown-error-devtoolsactiveport-file-doesnt-exist-while-t
130
- ChromeHeadlessNoSandbox: {
131
- base: 'ChromeHeadless',
132
- flags: ['--no-sandbox', '--headless', '--disable-gpu', '--disable-dev-shm-usage'],
133
- },
134
- },
135
- restartOnFileChange: true,
136
- };
50
+ yield* execute(options, context, transforms);
137
51
  }
138
52
  const builder = (0, architect_1.createBuilder)(execute);
139
53
  exports.default = builder;
@@ -0,0 +1,42 @@
1
+ /**
2
+ * @license
3
+ * Copyright Google LLC All Rights Reserved.
4
+ *
5
+ * Use of this source code is governed by an MIT-style license that can be
6
+ * found in the LICENSE file at https://angular.dev/license
7
+ */
8
+ import type { BuilderContext } from '@angular-devkit/architect';
9
+ import { Schema as KarmaBuilderOptions } from './schema';
10
+ export type NormalizedKarmaBuilderOptions = ReturnType<typeof normalizeOptions>;
11
+ export declare function normalizeOptions(context: BuilderContext, options: KarmaBuilderOptions): {
12
+ sourceMap: import("./schema").SourceMapUnion | undefined;
13
+ karmaConfig: string | undefined;
14
+ reporters: string[] | undefined;
15
+ browsers: string[] | undefined;
16
+ watch: boolean;
17
+ include: string[];
18
+ exclude: string[];
19
+ aot?: boolean;
20
+ assets?: import("./schema").AssetPattern[];
21
+ codeCoverage?: boolean;
22
+ codeCoverageExclude?: string[];
23
+ define?: {
24
+ [key: string]: string;
25
+ };
26
+ externalDependencies?: string[];
27
+ fileReplacements?: import("./schema").FileReplacement[];
28
+ inlineStyleLanguage?: import("./schema").InlineStyleLanguage;
29
+ loader?: {
30
+ [key: string]: any;
31
+ };
32
+ main?: string;
33
+ poll?: number;
34
+ polyfills?: string[];
35
+ preserveSymlinks?: boolean;
36
+ progress?: boolean;
37
+ scripts?: import("./schema").ScriptElement[];
38
+ stylePreprocessorOptions?: import("./schema").StylePreprocessorOptions;
39
+ styles?: import("./schema").StyleElement[];
40
+ tsConfig: string;
41
+ webWorkerTsConfig?: string;
42
+ };
@@ -0,0 +1,43 @@
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.dev/license
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.normalizeOptions = normalizeOptions;
11
+ const node_path_1 = require("node:path");
12
+ function normalizeOptions(context, options) {
13
+ const { sourceMap, karmaConfig, browsers, watch = true, include = [], exclude = [], reporters = [], ...rest } = options;
14
+ let normalizedBrowsers;
15
+ if (typeof options.browsers === 'string' && options.browsers) {
16
+ normalizedBrowsers = options.browsers.split(',').map((browser) => browser.trim());
17
+ }
18
+ else if (options.browsers === false) {
19
+ normalizedBrowsers = [];
20
+ }
21
+ // Split along commas to make it more natural, and remove empty strings.
22
+ const normalizedReporters = reporters
23
+ .reduce((acc, curr) => acc.concat(curr.split(',')), [])
24
+ .filter((x) => !!x);
25
+ // Sourcemaps are always needed when code coverage is enabled.
26
+ const normalizedSourceMap = options.codeCoverage
27
+ ? {
28
+ scripts: true,
29
+ styles: true,
30
+ vendor: true,
31
+ }
32
+ : sourceMap;
33
+ return {
34
+ ...rest,
35
+ sourceMap: normalizedSourceMap,
36
+ karmaConfig: karmaConfig ? (0, node_path_1.resolve)(context.workspaceRoot, karmaConfig) : undefined,
37
+ reporters: normalizedReporters.length ? normalizedReporters : undefined,
38
+ browsers: normalizedBrowsers,
39
+ watch,
40
+ include,
41
+ exclude,
42
+ };
43
+ }
@@ -120,7 +120,7 @@ export type Schema = {
120
120
  */
121
121
  tsConfig: string;
122
122
  /**
123
- * Run build when files change.
123
+ * Re-run tests when source files change.
124
124
  */
125
125
  watch?: boolean;
126
126
  /**
@@ -229,7 +229,8 @@
229
229
  },
230
230
  "watch": {
231
231
  "type": "boolean",
232
- "description": "Run build when files change."
232
+ "description": "Re-run tests when source files change.",
233
+ "default": true
233
234
  },
234
235
  "poll": {
235
236
  "type": "number",
@@ -7,9 +7,9 @@
7
7
  */
8
8
  import type { BuilderContext, BuilderOutput } from '@angular-devkit/architect';
9
9
  import type { ApplicationBuilderExtensions } from '../application/options';
10
- import type { Schema as UnitTestOptions } from './schema';
11
- export type { UnitTestOptions };
10
+ import type { Schema as UnitTestBuilderOptions } from './schema';
11
+ export type { UnitTestBuilderOptions };
12
12
  /**
13
13
  * @experimental Direct usage of this function is considered experimental.
14
14
  */
15
- export declare function execute(options: UnitTestOptions, context: BuilderContext, extensions?: ApplicationBuilderExtensions): AsyncIterable<BuilderOutput>;
15
+ export declare function execute(options: UnitTestBuilderOptions, context: BuilderContext, extensions?: ApplicationBuilderExtensions): AsyncIterable<BuilderOutput>;
@@ -94,7 +94,11 @@ async function* execute(options, context, extensions = {}) {
94
94
  optimization: false,
95
95
  tsConfig: normalizedOptions.tsConfig,
96
96
  entryPoints,
97
- externalDependencies: ['vitest', ...(buildTargetOptions.externalDependencies ?? [])],
97
+ externalDependencies: [
98
+ 'vitest',
99
+ '@vitest/browser/context',
100
+ ...(buildTargetOptions.externalDependencies ?? []),
101
+ ],
98
102
  };
99
103
  extensions ??= {};
100
104
  extensions.codePlugins ??= [];
@@ -144,9 +148,11 @@ async function* execute(options, context, extensions = {}) {
144
148
  return { success: false };
145
149
  }
146
150
  // Add setup file entries for TestBed initialization and project polyfills
147
- const setupFiles = ['init-testbed.js'];
151
+ const setupFiles = ['init-testbed.js', ...normalizedOptions.setupFiles];
148
152
  if (buildTargetOptions?.polyfills?.length) {
149
- setupFiles.push('polyfills.js');
153
+ // Placed first as polyfills may be required by the Testbed initialization
154
+ // or other project provided setup files (e.g., zone.js, ECMAScript polyfills).
155
+ setupFiles.unshift('polyfills.js');
150
156
  }
151
157
  const debugOptions = normalizedOptions.debug
152
158
  ? {
@@ -6,7 +6,7 @@
6
6
  * found in the LICENSE file at https://angular.dev/license
7
7
  */
8
8
  import { type Builder } from '@angular-devkit/architect';
9
- import { type UnitTestOptions, execute } from './builder';
10
- export { type UnitTestOptions, execute };
11
- declare const builder: Builder<UnitTestOptions>;
9
+ import { type UnitTestBuilderOptions, execute } from './builder';
10
+ export { type UnitTestBuilderOptions, execute };
11
+ declare const builder: Builder<UnitTestBuilderOptions>;
12
12
  export default builder;
@@ -6,5 +6,5 @@
6
6
  * found in the LICENSE file at https://angular.dev/license
7
7
  */
8
8
  import type { BuilderContext, BuilderOutput } from '@angular-devkit/architect';
9
- import { type NormalizedUnitTestOptions } from './options';
10
- export declare function useKarmaBuilder(context: BuilderContext, unitTestOptions: NormalizedUnitTestOptions): Promise<AsyncIterable<BuilderOutput>>;
9
+ import { type NormalizedUnitTestBuilderOptions } from './options';
10
+ export declare function useKarmaBuilder(context: BuilderContext, unitTestOptions: NormalizedUnitTestBuilderOptions): Promise<AsyncIterable<BuilderOutput>>;
@@ -46,6 +46,9 @@ async function useKarmaBuilder(context, unitTestOptions) {
46
46
  if (unitTestOptions.debug) {
47
47
  context.logger.warn('The "karma" test runner does not support the "debug" option. The option will be ignored.');
48
48
  }
49
+ if (unitTestOptions.setupFiles.length) {
50
+ context.logger.warn('The "karma" test runner does not support the "setupFiles" option. The option will be ignored.');
51
+ }
49
52
  const buildTargetOptions = (await context.validateOptions(await context.getTargetOptions(unitTestOptions.buildTarget), await context.getBuilderNameForTarget(unitTestOptions.buildTarget)));
50
53
  buildTargetOptions.polyfills = (0, options_1.injectTestingPolyfills)(buildTargetOptions.polyfills);
51
54
  const options = {
@@ -6,9 +6,9 @@
6
6
  * found in the LICENSE file at https://angular.dev/license
7
7
  */
8
8
  import { type BuilderContext } from '@angular-devkit/architect';
9
- import type { Schema as UnitTestOptions } from './schema';
10
- export type NormalizedUnitTestOptions = Awaited<ReturnType<typeof normalizeOptions>>;
11
- export declare function normalizeOptions(context: BuilderContext, projectName: string, options: UnitTestOptions): Promise<{
9
+ import type { Schema as UnitTestBuilderOptions } from './schema';
10
+ export type NormalizedUnitTestBuilderOptions = Awaited<ReturnType<typeof normalizeOptions>>;
11
+ export declare function normalizeOptions(context: BuilderContext, projectName: string, options: UnitTestBuilderOptions): Promise<{
12
12
  workspaceRoot: string;
13
13
  projectRoot: string;
14
14
  projectSourceRoot: string;
@@ -27,5 +27,6 @@ export declare function normalizeOptions(context: BuilderContext, projectName: s
27
27
  watch: boolean;
28
28
  debug: boolean;
29
29
  providersFile: string | undefined;
30
+ setupFiles: string[];
30
31
  }>;
31
32
  export declare function injectTestingPolyfills(polyfills?: string[]): string[];
@@ -54,6 +54,9 @@ async function normalizeOptions(context, projectName, options) {
54
54
  watch: options.watch ?? (0, tty_1.isTTY)(),
55
55
  debug: options.debug ?? false,
56
56
  providersFile: options.providersFile && node_path_1.default.join(workspaceRoot, options.providersFile),
57
+ setupFiles: options.setupFiles
58
+ ? options.setupFiles.map((setupFile) => node_path_1.default.join(workspaceRoot, setupFile))
59
+ : [],
57
60
  };
58
61
  }
59
62
  function injectTestingPolyfills(polyfills = []) {
@@ -55,12 +55,19 @@ export type Schema = {
55
55
  * The name of the test runner to use for test execution.
56
56
  */
57
57
  runner: Runner;
58
+ /**
59
+ * A list of global setup and configuration files that are included before the test files.
60
+ * The application's polyfills are always included before these files. The Angular Testbed
61
+ * is also initialized prior to the execution of these files.
62
+ */
63
+ setupFiles?: string[];
58
64
  /**
59
65
  * The name of the TypeScript configuration file.
60
66
  */
61
67
  tsConfig: string;
62
68
  /**
63
- * Run build when files change.
69
+ * Re-run tests when source files change. Defaults to `true` in TTY environments and `false`
70
+ * otherwise.
64
71
  */
65
72
  watch?: boolean;
66
73
  };
@@ -44,7 +44,7 @@
44
44
  },
45
45
  "watch": {
46
46
  "type": "boolean",
47
- "description": "Run build when files change."
47
+ "description": "Re-run tests when source files change. Defaults to `true` in TTY environments and `false` otherwise."
48
48
  },
49
49
  "debug": {
50
50
  "type": "boolean",
@@ -76,7 +76,14 @@
76
76
  "type": "array",
77
77
  "minItems": 1,
78
78
  "maxItems": 2,
79
- "items": [{ "$ref": "#/definitions/coverage-reporters" }, { "type": "object" }]
79
+ "items": [
80
+ {
81
+ "$ref": "#/definitions/coverage-reporters"
82
+ },
83
+ {
84
+ "type": "object"
85
+ }
86
+ ]
80
87
  }
81
88
  ]
82
89
  }
@@ -92,6 +99,13 @@
92
99
  "type": "string",
93
100
  "description": "TypeScript file that exports an array of Angular providers to use during test execution. The array must be a default export.",
94
101
  "minLength": 1
102
+ },
103
+ "setupFiles": {
104
+ "type": "array",
105
+ "items": {
106
+ "type": "string"
107
+ },
108
+ "description": "A list of global setup and configuration files that are included before the test files. The application's polyfills are always included before these files. The Angular Testbed is also initialized prior to the execution of these files."
95
109
  }
96
110
  },
97
111
  "additionalProperties": false,
package/src/index.d.ts CHANGED
@@ -12,3 +12,5 @@ export type { BuildOutputAsset } from './tools/esbuild/bundler-execution-result'
12
12
  export { executeDevServerBuilder, type DevServerBuilderOptions, type DevServerBuilderOutput, } from './builders/dev-server';
13
13
  export { execute as executeExtractI18nBuilder, type ExtractI18nBuilderOptions, } from './builders/extract-i18n';
14
14
  export { execute as executeNgPackagrBuilder, type NgPackagrBuilderOptions, } from './builders/ng-packagr';
15
+ export { execute as executeUnitTestBuilder, type UnitTestBuilderOptions, } from './builders/unit-test';
16
+ export { execute as executeKarmaBuilder, type KarmaBuilderOptions } from './builders/karma';
package/src/index.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * found in the LICENSE file at https://angular.dev/license
8
8
  */
9
9
  Object.defineProperty(exports, "__esModule", { value: true });
10
- exports.executeNgPackagrBuilder = exports.executeExtractI18nBuilder = exports.executeDevServerBuilder = exports.BuildOutputFileType = exports.buildApplication = void 0;
10
+ exports.executeKarmaBuilder = exports.executeUnitTestBuilder = exports.executeNgPackagrBuilder = exports.executeExtractI18nBuilder = exports.executeDevServerBuilder = exports.BuildOutputFileType = exports.buildApplication = void 0;
11
11
  var application_1 = require("./builders/application");
12
12
  Object.defineProperty(exports, "buildApplication", { enumerable: true, get: function () { return application_1.buildApplication; } });
13
13
  var bundler_context_1 = require("./tools/esbuild/bundler-context");
@@ -18,3 +18,7 @@ var extract_i18n_1 = require("./builders/extract-i18n");
18
18
  Object.defineProperty(exports, "executeExtractI18nBuilder", { enumerable: true, get: function () { return extract_i18n_1.execute; } });
19
19
  var ng_packagr_1 = require("./builders/ng-packagr");
20
20
  Object.defineProperty(exports, "executeNgPackagrBuilder", { enumerable: true, get: function () { return ng_packagr_1.execute; } });
21
+ var unit_test_1 = require("./builders/unit-test");
22
+ Object.defineProperty(exports, "executeUnitTestBuilder", { enumerable: true, get: function () { return unit_test_1.execute; } });
23
+ var karma_1 = require("./builders/karma");
24
+ Object.defineProperty(exports, "executeKarmaBuilder", { enumerable: true, get: function () { return karma_1.execute; } });
package/src/private.d.ts CHANGED
@@ -11,7 +11,6 @@ export { buildApplicationInternal } from './builders/application';
11
11
  export type { ApplicationBuilderInternalOptions } from './builders/application/options';
12
12
  export { type Result, type ResultFile, ResultKind } from './builders/application/results';
13
13
  export { serveWithVite } from './builders/dev-server/vite-server';
14
- export { execute as executeKarmaInternal } from './builders/karma/application_builder';
15
14
  export * from './tools/babel/plugins';
16
15
  export type { ExternalResultMetadata } from './tools/esbuild/bundler-execution-result';
17
16
  export { emitFilesToDisk } from './tools/esbuild/utils';
package/src/private.js CHANGED
@@ -21,7 +21,7 @@ var __exportStar = (this && this.__exportStar) || function(m, exports) {
21
21
  for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
22
22
  };
23
23
  Object.defineProperty(exports, "__esModule", { value: true });
24
- exports.loadPostcssConfiguration = exports.generateSearchDirectories = exports.findTailwindConfiguration = exports.getTestEntrypoints = exports.findTests = exports.assertCompatibleAngularVersion = exports.getSupportedBrowsers = exports.generateBuildStatsTable = exports.augmentAppWithServiceWorker = exports.purgeStaleBuildCache = exports.createTranslationLoader = exports.loadProxyConfiguration = exports.InlineCriticalCssProcessor = exports.IndexHtmlGenerator = exports.loadTranslations = exports.createI18nOptions = exports.deleteOutputDir = exports.checkPort = exports.JavaScriptTransformer = exports.createJitResourceTransformer = exports.SourceFileCache = exports.SassWorkerImplementation = exports.transformSupportedBrowsersToTargets = exports.emitFilesToDisk = exports.executeKarmaInternal = exports.serveWithVite = exports.ResultKind = exports.buildApplicationInternal = void 0;
24
+ exports.loadPostcssConfiguration = exports.generateSearchDirectories = exports.findTailwindConfiguration = exports.getTestEntrypoints = exports.findTests = exports.assertCompatibleAngularVersion = exports.getSupportedBrowsers = exports.generateBuildStatsTable = exports.augmentAppWithServiceWorker = exports.purgeStaleBuildCache = exports.createTranslationLoader = exports.loadProxyConfiguration = exports.InlineCriticalCssProcessor = exports.IndexHtmlGenerator = exports.loadTranslations = exports.createI18nOptions = exports.deleteOutputDir = exports.checkPort = exports.JavaScriptTransformer = exports.createJitResourceTransformer = exports.SourceFileCache = exports.SassWorkerImplementation = exports.transformSupportedBrowsersToTargets = exports.emitFilesToDisk = exports.serveWithVite = exports.ResultKind = exports.buildApplicationInternal = void 0;
25
25
  exports.createCompilerPlugin = createCompilerPlugin;
26
26
  /**
27
27
  * @fileoverview
@@ -39,8 +39,6 @@ var results_1 = require("./builders/application/results");
39
39
  Object.defineProperty(exports, "ResultKind", { enumerable: true, get: function () { return results_1.ResultKind; } });
40
40
  var vite_server_1 = require("./builders/dev-server/vite-server");
41
41
  Object.defineProperty(exports, "serveWithVite", { enumerable: true, get: function () { return vite_server_1.serveWithVite; } });
42
- var application_builder_1 = require("./builders/karma/application_builder");
43
- Object.defineProperty(exports, "executeKarmaInternal", { enumerable: true, get: function () { return application_builder_1.execute; } });
44
42
  // Tools
45
43
  __exportStar(require("./tools/babel/plugins"), exports);
46
44
  var utils_1 = require("./tools/esbuild/utils");
@@ -55,9 +55,12 @@ function convertTypeScriptDiagnosticInfo(typescript, info, textPrefix) {
55
55
  function convertTypeScriptDiagnostic(typescript, diagnostic) {
56
56
  let codePrefix = 'TS';
57
57
  let code = `${diagnostic.code}`;
58
- if (diagnostic.source === 'ngtsc') {
58
+ // Custom ngtsc diagnostics are prefixed with -99 which isn't a valid TypeScript diagnostic code.
59
+ // Strip it and mark the diagnostic as coming from Angular. Note that we can't rely on
60
+ // `diagnostic.source`, because it isn't always produced. This is identical to:
61
+ // https://github.com/angular/angular/blob/main/packages/compiler-cli/src/ngtsc/diagnostics/src/util.ts
62
+ if (code.startsWith('-99')) {
59
63
  codePrefix = 'NG';
60
- // Remove `-99` Angular prefix from diagnostic code
61
64
  code = code.slice(3);
62
65
  }
63
66
  const message = convertTypeScriptDiagnosticInfo(typescript, diagnostic, `${codePrefix}${code}: `);
@@ -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 = '20.1.0-rc.0';
13
+ const VERSION = '20.1.0';
14
14
  function hasCacheMetadata(value) {
15
15
  return (!!value &&
16
16
  typeof value === 'object' &&
@@ -28,7 +28,7 @@ function assertCompatibleAngularVersion(projectRoot) {
28
28
  'This likely indicates a corrupted local installation. Please try reinstalling your packages.');
29
29
  process.exit(2);
30
30
  }
31
- const supportedAngularSemver = '^20.0.0 || ^20.1.0-next.0';
31
+ const supportedAngularSemver = '^20.0.0';
32
32
  if (angularPkgJson['version'] === '0.0.0' || supportedAngularSemver.startsWith('0.0.0')) {
33
33
  // Internal CLI and FW testing version.
34
34
  return;