@angular/build 21.0.0-next.3 → 21.0.0-next.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (43) hide show
  1. package/package.json +8 -8
  2. package/src/builders/application/index.js +2 -1
  3. package/src/builders/karma/application_builder.d.ts +0 -2
  4. package/src/builders/karma/application_builder.js +39 -351
  5. package/src/builders/karma/assets-middleware.d.ts +26 -0
  6. package/src/builders/karma/assets-middleware.js +65 -0
  7. package/src/builders/karma/coverage.d.ts +9 -0
  8. package/src/builders/karma/coverage.js +31 -0
  9. package/src/builders/karma/karma-config.d.ts +11 -0
  10. package/src/builders/karma/karma-config.js +79 -0
  11. package/src/builders/karma/polyfills-plugin.d.ts +13 -0
  12. package/src/builders/karma/polyfills-plugin.js +74 -0
  13. package/src/builders/karma/progress-reporter.d.ts +17 -0
  14. package/src/builders/karma/progress-reporter.js +73 -0
  15. package/src/builders/karma/utils.d.ts +17 -0
  16. package/src/builders/karma/utils.js +66 -0
  17. package/src/builders/unit-test/builder.js +140 -44
  18. package/src/builders/unit-test/options.d.ts +4 -1
  19. package/src/builders/unit-test/options.js +11 -5
  20. package/src/builders/unit-test/runners/api.d.ts +19 -1
  21. package/src/builders/unit-test/runners/dependency-checker.d.ts +43 -0
  22. package/src/builders/unit-test/runners/dependency-checker.js +82 -0
  23. package/src/builders/unit-test/runners/karma/executor.js +26 -2
  24. package/src/builders/unit-test/runners/karma/index.js +17 -0
  25. package/src/builders/unit-test/runners/vitest/browser-provider.d.ts +3 -2
  26. package/src/builders/unit-test/runners/vitest/build-options.js +1 -0
  27. package/src/builders/unit-test/runners/vitest/executor.d.ts +4 -5
  28. package/src/builders/unit-test/runners/vitest/executor.js +20 -133
  29. package/src/builders/unit-test/runners/vitest/index.js +19 -2
  30. package/src/builders/unit-test/runners/vitest/plugins.d.ts +23 -0
  31. package/src/builders/unit-test/runners/vitest/plugins.js +131 -0
  32. package/src/builders/unit-test/schema.d.ts +54 -30
  33. package/src/builders/unit-test/schema.js +1 -1
  34. package/src/builders/unit-test/schema.json +65 -16
  35. package/src/tools/esbuild/application-code-bundle.js +4 -7
  36. package/src/tools/vite/middlewares/assets-middleware.d.ts +2 -0
  37. package/src/tools/vite/middlewares/assets-middleware.js +31 -0
  38. package/src/tools/vite/utils.js +0 -1
  39. package/src/utils/normalize-cache.js +1 -1
  40. package/src/utils/supported-browsers.js +7 -3
  41. package/src/utils/test-files.d.ts +17 -0
  42. package/src/utils/test-files.js +82 -0
  43. package/.browserslistrc +0 -7
@@ -0,0 +1,31 @@
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
+ 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.createInstrumentationFilter = createInstrumentationFilter;
14
+ exports.getInstrumentationExcludedPaths = getInstrumentationExcludedPaths;
15
+ const node_path_1 = __importDefault(require("node:path"));
16
+ const tinyglobby_1 = require("tinyglobby");
17
+ function createInstrumentationFilter(includedBasePath, excludedPaths) {
18
+ return (request) => {
19
+ return (!excludedPaths.has(request) &&
20
+ !/\.(e2e|spec)\.tsx?$|[\\/]node_modules[\\/]/.test(request) &&
21
+ request.startsWith(includedBasePath));
22
+ };
23
+ }
24
+ function getInstrumentationExcludedPaths(root, excludedPaths) {
25
+ const excluded = new Set();
26
+ for (const excludeGlob of excludedPaths) {
27
+ const excludePath = excludeGlob[0] === '/' ? excludeGlob.slice(1) : excludeGlob;
28
+ (0, tinyglobby_1.globSync)(excludePath, { cwd: root }).forEach((p) => excluded.add(node_path_1.default.join(root, p)));
29
+ }
30
+ return excluded;
31
+ }
@@ -0,0 +1,11 @@
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 type { ConfigOptions } from 'karma';
10
+ import type { NormalizedKarmaBuilderOptions } from './options';
11
+ export declare function getBaseKarmaOptions(options: NormalizedKarmaBuilderOptions, context: BuilderContext): ConfigOptions;
@@ -0,0 +1,79 @@
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
+ 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.getBaseKarmaOptions = getBaseKarmaOptions;
14
+ const node_module_1 = require("node:module");
15
+ const node_path_1 = __importDefault(require("node:path"));
16
+ function getBaseKarmaOptions(options, context) {
17
+ // Determine project name from builder context target
18
+ const projectName = context.target?.project;
19
+ if (!projectName) {
20
+ throw new Error(`The 'karma' builder requires a target to be specified.`);
21
+ }
22
+ const karmaOptions = options.karmaConfig
23
+ ? {}
24
+ : getBuiltInKarmaConfig(context.workspaceRoot, projectName);
25
+ const singleRun = !options.watch;
26
+ karmaOptions.singleRun = singleRun;
27
+ // Workaround https://github.com/angular/angular-cli/issues/28271, by clearing context by default
28
+ // for single run executions. Not clearing context for multi-run (watched) builds allows the
29
+ // Jasmine Spec Runner to be visible in the browser after test execution.
30
+ karmaOptions.client ??= {};
31
+ karmaOptions.client.clearContext ??= singleRun;
32
+ // Convert browsers from a string to an array
33
+ if (options.browsers) {
34
+ karmaOptions.browsers = options.browsers;
35
+ }
36
+ if (options.reporters) {
37
+ karmaOptions.reporters = options.reporters;
38
+ }
39
+ return karmaOptions;
40
+ }
41
+ function getBuiltInKarmaConfig(workspaceRoot, projectName) {
42
+ let coverageFolderName = projectName.charAt(0) === '@' ? projectName.slice(1) : projectName;
43
+ coverageFolderName = coverageFolderName.toLowerCase();
44
+ const workspaceRootRequire = (0, node_module_1.createRequire)(workspaceRoot + '/');
45
+ // Any changes to the config here need to be synced to: packages/schematics/angular/config/files/karma.conf.js.template
46
+ return {
47
+ basePath: '',
48
+ frameworks: ['jasmine'],
49
+ plugins: [
50
+ 'karma-jasmine',
51
+ 'karma-chrome-launcher',
52
+ 'karma-jasmine-html-reporter',
53
+ 'karma-coverage',
54
+ ].map((p) => workspaceRootRequire(p)),
55
+ jasmineHtmlReporter: {
56
+ suppressAll: true, // removes the duplicated traces
57
+ },
58
+ coverageReporter: {
59
+ dir: node_path_1.default.join(workspaceRoot, 'coverage', coverageFolderName),
60
+ subdir: '.',
61
+ reporters: [{ type: 'html' }, { type: 'text-summary' }],
62
+ },
63
+ reporters: ['progress', 'kjhtml'],
64
+ browsers: ['Chrome'],
65
+ customLaunchers: {
66
+ // Chrome configured to run in a bazel sandbox.
67
+ // Disable the use of the gpu and `/dev/shm` because it causes Chrome to
68
+ // crash on some environments.
69
+ // See:
70
+ // https://github.com/puppeteer/puppeteer/blob/v1.0.0/docs/troubleshooting.md#tips
71
+ // https://stackoverflow.com/questions/50642308/webdriverexception-unknown-error-devtoolsactiveport-file-doesnt-exist-while-t
72
+ ChromeHeadlessNoSandbox: {
73
+ base: 'ChromeHeadless',
74
+ flags: ['--no-sandbox', '--headless', '--disable-gpu', '--disable-dev-shm-usage'],
75
+ },
76
+ },
77
+ restartOnFileChange: true,
78
+ };
79
+ }
@@ -0,0 +1,13 @@
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 { FilePattern, InlinePluginDef } from 'karma';
9
+ export declare class AngularPolyfillsPlugin {
10
+ static readonly $inject: string[];
11
+ static readonly NAME = "angular-polyfills";
12
+ static createPlugin(polyfillsFile: FilePattern, jasmineCleanupFiles: FilePattern, scriptsFiles: FilePattern[]): InlinePluginDef;
13
+ }
@@ -0,0 +1,74 @@
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.AngularPolyfillsPlugin = void 0;
11
+ const node_module_1 = require("node:module");
12
+ const localResolve = (0, node_module_1.createRequire)(__filename).resolve;
13
+ class AngularPolyfillsPlugin {
14
+ static $inject = ['config.files'];
15
+ static NAME = 'angular-polyfills';
16
+ static createPlugin(polyfillsFile, jasmineCleanupFiles, scriptsFiles) {
17
+ return {
18
+ // This has to be a "reporter" because reporters run _after_ frameworks
19
+ // and karma-jasmine-html-reporter injects additional scripts that may
20
+ // depend on Jasmine but aren't modules - which means that they would run
21
+ // _before_ all module code (including jasmine).
22
+ [`reporter:${AngularPolyfillsPlugin.NAME}`]: [
23
+ 'factory',
24
+ Object.assign((files) => {
25
+ // The correct order is zone.js -> jasmine -> zone.js/testing.
26
+ // Jasmine has to see the patched version of the global `setTimeout`
27
+ // function so it doesn't cache the unpatched version. And /testing
28
+ // needs to see the global `jasmine` object so it can patch it.
29
+ const polyfillsIndex = 0;
30
+ files.splice(polyfillsIndex, 0, polyfillsFile);
31
+ // Insert just before test_main.js.
32
+ const zoneTestingIndex = files.findIndex((f) => {
33
+ if (typeof f === 'string') {
34
+ return false;
35
+ }
36
+ return f.pattern.endsWith('/test_main.js');
37
+ });
38
+ if (zoneTestingIndex === -1) {
39
+ throw new Error('Could not find test entrypoint file.');
40
+ }
41
+ files.splice(zoneTestingIndex, 0, jasmineCleanupFiles);
42
+ // We need to ensure that all files are served as modules, otherwise
43
+ // the order in the files list gets really confusing: Karma doesn't
44
+ // set defer on scripts, so all scripts with type=js will run first,
45
+ // even if type=module files appeared earlier in `files`.
46
+ for (const f of files) {
47
+ if (typeof f === 'string') {
48
+ throw new Error(`Unexpected string-based file: "${f}"`);
49
+ }
50
+ if (f.included === false) {
51
+ // Don't worry about files that aren't included on the initial
52
+ // page load. `type` won't affect them.
53
+ continue;
54
+ }
55
+ if (f.pattern.endsWith('.js') && 'js' === (f.type ?? 'js')) {
56
+ f.type = 'module';
57
+ }
58
+ }
59
+ // Add "scripts" option files as classic scripts
60
+ files.unshift(...scriptsFiles);
61
+ // Add browser sourcemap support as a classic script
62
+ files.unshift({
63
+ pattern: localResolve('source-map-support/browser-source-map-support.js'),
64
+ included: true,
65
+ watched: false,
66
+ });
67
+ // Karma needs a return value for a factory and Karma's multi-reporter expects an `adapters` array
68
+ return { adapters: [] };
69
+ }, AngularPolyfillsPlugin),
70
+ ],
71
+ };
72
+ }
73
+ }
74
+ exports.AngularPolyfillsPlugin = AngularPolyfillsPlugin;
@@ -0,0 +1,17 @@
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 { BuilderOutput } from '@angular-devkit/architect';
9
+ import type { Config, ConfigOptions } from 'karma';
10
+ import type { ReadableStreamController } from 'node:stream/web';
11
+ import type { ApplicationBuilderInternalOptions } from '../application/options';
12
+ import type { Result } from '../application/results';
13
+ interface BuildOptions extends ApplicationBuilderInternalOptions {
14
+ outputPath: string;
15
+ }
16
+ export declare function injectKarmaReporter(buildOptions: BuildOptions, buildIterator: AsyncIterator<Result>, karmaConfig: Config & ConfigOptions, controller: ReadableStreamController<BuilderOutput>): void;
17
+ export {};
@@ -0,0 +1,73 @@
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.injectKarmaReporter = injectKarmaReporter;
11
+ const test_files_1 = require("../../utils/test-files");
12
+ const results_1 = require("../application/results");
13
+ const LATEST_BUILD_FILES_TOKEN = 'angularLatestBuildFiles';
14
+ function injectKarmaReporter(buildOptions, buildIterator, karmaConfig, controller) {
15
+ const reporterName = 'angular-progress-notifier';
16
+ class ProgressNotifierReporter {
17
+ emitter;
18
+ latestBuildFiles;
19
+ static $inject = ['emitter', LATEST_BUILD_FILES_TOKEN];
20
+ constructor(emitter, latestBuildFiles) {
21
+ this.emitter = emitter;
22
+ this.latestBuildFiles = latestBuildFiles;
23
+ this.startWatchingBuild();
24
+ }
25
+ startWatchingBuild() {
26
+ void (async () => {
27
+ // This is effectively "for await of but skip what's already consumed".
28
+ let isDone = false; // to mark the loop condition as "not constant".
29
+ while (!isDone) {
30
+ const { done, value: buildOutput } = await buildIterator.next();
31
+ if (done) {
32
+ isDone = true;
33
+ break;
34
+ }
35
+ if (buildOutput.kind === results_1.ResultKind.Failure) {
36
+ controller.enqueue({ success: false, message: 'Build failed' });
37
+ }
38
+ else if (buildOutput.kind === results_1.ResultKind.Incremental ||
39
+ buildOutput.kind === results_1.ResultKind.Full) {
40
+ if (buildOutput.kind === results_1.ResultKind.Full) {
41
+ this.latestBuildFiles.files = buildOutput.files;
42
+ }
43
+ else {
44
+ this.latestBuildFiles.files = {
45
+ ...this.latestBuildFiles.files,
46
+ ...buildOutput.files,
47
+ };
48
+ }
49
+ await (0, test_files_1.writeTestFiles)(buildOutput.files, buildOptions.outputPath);
50
+ this.emitter.refreshFiles();
51
+ }
52
+ }
53
+ })();
54
+ }
55
+ onRunComplete = function (_browsers, results) {
56
+ if (results.exitCode === 0) {
57
+ controller.enqueue({ success: true });
58
+ }
59
+ else {
60
+ controller.enqueue({ success: false });
61
+ }
62
+ };
63
+ }
64
+ karmaConfig.reporters ??= [];
65
+ karmaConfig.reporters.push(reporterName);
66
+ karmaConfig.plugins ??= [];
67
+ karmaConfig.plugins.push({
68
+ [`reporter:${reporterName}`]: [
69
+ 'factory',
70
+ Object.assign((...args) => new ProgressNotifierReporter(...args), ProgressNotifierReporter),
71
+ ],
72
+ });
73
+ }
@@ -0,0 +1,17 @@
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 type { NormalizedKarmaBuilderOptions } from './options';
10
+ export declare function getProjectSourceRoot(context: BuilderContext): Promise<string>;
11
+ export declare function normalizePolyfills(polyfills?: string[]): [polyfills: string[], jasmineCleanup: string[]];
12
+ export declare function collectEntrypoints(options: NormalizedKarmaBuilderOptions, context: BuilderContext, projectSourceRoot: string): Promise<Map<string, string>>;
13
+ export declare function hasChunkOrWorkerFiles(files: Record<string, unknown>): boolean;
14
+ /** Returns the first item yielded by the given generator and cancels the execution. */
15
+ export declare function first<T>(generator: AsyncIterable<T>, { cancel }: {
16
+ cancel: boolean;
17
+ }): Promise<[T, AsyncIterator<T> | null]>;
@@ -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.dev/license
8
+ */
9
+ Object.defineProperty(exports, "__esModule", { value: true });
10
+ exports.getProjectSourceRoot = getProjectSourceRoot;
11
+ exports.normalizePolyfills = normalizePolyfills;
12
+ exports.collectEntrypoints = collectEntrypoints;
13
+ exports.hasChunkOrWorkerFiles = hasChunkOrWorkerFiles;
14
+ exports.first = first;
15
+ const node_module_1 = require("node:module");
16
+ const project_metadata_1 = require("../../utils/project-metadata");
17
+ const find_tests_1 = require("./find-tests");
18
+ const localResolve = (0, node_module_1.createRequire)(__filename).resolve;
19
+ async function getProjectSourceRoot(context) {
20
+ // We have already validated that the project name is set before calling this function.
21
+ const projectName = context.target?.project;
22
+ if (!projectName) {
23
+ return context.workspaceRoot;
24
+ }
25
+ const projectMetadata = await context.getProjectMetadata(projectName);
26
+ const { projectSourceRoot } = (0, project_metadata_1.getProjectRootPaths)(context.workspaceRoot, projectMetadata);
27
+ return projectSourceRoot;
28
+ }
29
+ function normalizePolyfills(polyfills = []) {
30
+ const jasmineGlobalEntryPoint = localResolve('./polyfills/jasmine_global.js');
31
+ const jasmineGlobalCleanupEntrypoint = localResolve('./polyfills/jasmine_global_cleanup.js');
32
+ const sourcemapEntrypoint = localResolve('./polyfills/init_sourcemaps.js');
33
+ const zoneTestingEntryPoint = 'zone.js/testing';
34
+ const polyfillsExludingZoneTesting = polyfills.filter((p) => p !== zoneTestingEntryPoint);
35
+ return [
36
+ polyfillsExludingZoneTesting.concat([jasmineGlobalEntryPoint, sourcemapEntrypoint]),
37
+ polyfillsExludingZoneTesting.length === polyfills.length
38
+ ? [jasmineGlobalCleanupEntrypoint]
39
+ : [jasmineGlobalCleanupEntrypoint, zoneTestingEntryPoint],
40
+ ];
41
+ }
42
+ async function collectEntrypoints(options, context, projectSourceRoot) {
43
+ // Glob for files to test.
44
+ const testFiles = await (0, find_tests_1.findTests)(options.include, options.exclude, context.workspaceRoot, projectSourceRoot);
45
+ return (0, find_tests_1.getTestEntrypoints)(testFiles, { projectSourceRoot, workspaceRoot: context.workspaceRoot });
46
+ }
47
+ function hasChunkOrWorkerFiles(files) {
48
+ return Object.keys(files).some((filename) => {
49
+ return /(?:^|\/)(?:worker|chunk)[^/]+\.js$/.test(filename);
50
+ });
51
+ }
52
+ /** Returns the first item yielded by the given generator and cancels the execution. */
53
+ async function first(generator, { cancel }) {
54
+ if (!cancel) {
55
+ const iterator = generator[Symbol.asyncIterator]();
56
+ const firstValue = await iterator.next();
57
+ if (firstValue.done) {
58
+ throw new Error('Expected generator to emit at least once.');
59
+ }
60
+ return [firstValue.value, iterator];
61
+ }
62
+ for await (const value of generator) {
63
+ return [value, null];
64
+ }
65
+ throw new Error('Expected generator to emit at least once.');
66
+ }
@@ -98,11 +98,15 @@ Object.defineProperty(exports, "__esModule", { value: true });
98
98
  exports.execute = execute;
99
99
  const architect_1 = require("@angular-devkit/architect");
100
100
  const node_assert_1 = __importDefault(require("node:assert"));
101
+ const promises_1 = require("node:fs/promises");
102
+ const node_path_1 = __importDefault(require("node:path"));
101
103
  const virtual_module_plugin_1 = require("../../tools/esbuild/virtual-module-plugin");
102
104
  const error_1 = require("../../utils/error");
105
+ const test_files_1 = require("../../utils/test-files");
103
106
  const application_1 = require("../application");
104
107
  const results_1 = require("../application/results");
105
108
  const options_1 = require("./options");
109
+ const dependency_checker_1 = require("./runners/dependency-checker");
106
110
  async function loadTestRunner(runnerName) {
107
111
  // Harden against directory traversal
108
112
  if (!/^[a-zA-Z0-9-]+$/.test(runnerName)) {
@@ -148,7 +152,8 @@ function prepareBuildExtensions(virtualFiles, projectSourceRoot, extensions) {
148
152
  }
149
153
  return extensions;
150
154
  }
151
- async function* runBuildAndTest(executor, applicationBuildOptions, context, extensions) {
155
+ async function* runBuildAndTest(executor, applicationBuildOptions, context, dumpDirectory, extensions) {
156
+ let consecutiveErrorCount = 0;
152
157
  for await (const buildResult of (0, application_1.buildApplicationInternal)(applicationBuildOptions, context, extensions)) {
153
158
  if (buildResult.kind === results_1.ResultKind.Failure) {
154
159
  yield { success: false };
@@ -159,65 +164,156 @@ async function* runBuildAndTest(executor, applicationBuildOptions, context, exte
159
164
  node_assert_1.default.fail('A full and/or incremental build result is required from the application builder.');
160
165
  }
161
166
  (0, node_assert_1.default)(buildResult.files, 'Builder did not provide result files.');
167
+ if (dumpDirectory) {
168
+ if (buildResult.kind === results_1.ResultKind.Full) {
169
+ // Full build, so clean the directory
170
+ await (0, promises_1.rm)(dumpDirectory, { recursive: true, force: true });
171
+ }
172
+ else {
173
+ // Incremental build, so delete removed files
174
+ for (const file of buildResult.removed) {
175
+ await (0, promises_1.rm)(node_path_1.default.join(dumpDirectory, file.path), { force: true });
176
+ }
177
+ }
178
+ await (0, test_files_1.writeTestFiles)(buildResult.files, dumpDirectory);
179
+ context.logger.info(`Build output files successfully dumped to '${dumpDirectory}'.`);
180
+ }
162
181
  // Pass the build artifacts to the executor
163
- yield* executor.execute(buildResult);
182
+ try {
183
+ yield* executor.execute(buildResult);
184
+ // Successful execution resets the failure counter
185
+ consecutiveErrorCount = 0;
186
+ }
187
+ catch (e) {
188
+ (0, error_1.assertIsError)(e);
189
+ context.logger.error(`An exception occurred during test execution:\n${e.stack ?? e.message}`);
190
+ yield { success: false };
191
+ consecutiveErrorCount++;
192
+ }
193
+ if (consecutiveErrorCount >= 3) {
194
+ context.logger.error('Test runner process has failed multiple times in a row. Please fix the configuration and restart the process.');
195
+ return;
196
+ }
164
197
  }
165
198
  }
166
199
  /**
167
200
  * @experimental Direct usage of this function is considered experimental.
168
201
  */
169
202
  async function* execute(options, context, extensions) {
170
- const env_1 = { stack: [], error: void 0, hasError: false };
203
+ // Determine project name from builder context target
204
+ const projectName = context.target?.project;
205
+ if (!projectName) {
206
+ context.logger.error(`The builder requires a target to be specified.`);
207
+ return;
208
+ }
209
+ context.logger.warn(`NOTE: The "unit-test" builder is currently EXPERIMENTAL and not ready for production use.`);
210
+ // Initialize the test runner and normalize options
211
+ let runner;
212
+ let normalizedOptions;
171
213
  try {
172
- // Determine project name from builder context target
173
- const projectName = context.target?.project;
174
- if (!projectName) {
175
- context.logger.error(`The builder requires a target to be specified.`);
176
- return;
214
+ normalizedOptions = await (0, options_1.normalizeOptions)(context, projectName, options);
215
+ runner = await loadTestRunner(normalizedOptions.runnerName);
216
+ await runner.validateDependencies?.(normalizedOptions);
217
+ }
218
+ catch (e) {
219
+ (0, error_1.assertIsError)(e);
220
+ if (e instanceof dependency_checker_1.MissingDependenciesError) {
221
+ context.logger.error(e.message);
177
222
  }
178
- context.logger.warn(`NOTE: The "unit-test" builder is currently EXPERIMENTAL and not ready for production use.`);
179
- const normalizedOptions = await (0, options_1.normalizeOptions)(context, projectName, options);
180
- const runner = await loadTestRunner(normalizedOptions.runnerName);
181
- const executor = __addDisposableResource(env_1, await runner.createExecutor(context, normalizedOptions), true);
182
- if (runner.isStandalone) {
183
- yield* executor.execute({
184
- kind: results_1.ResultKind.Full,
185
- files: {},
186
- });
187
- return;
223
+ else {
224
+ context.logger.error(`An exception occurred during initialization of the test runner:\n${e.stack ?? e.message}`);
188
225
  }
189
- // Get base build options from the buildTarget
190
- let buildTargetOptions;
226
+ yield { success: false };
227
+ return;
228
+ }
229
+ if (runner.isStandalone) {
191
230
  try {
192
- buildTargetOptions = (await context.validateOptions(await context.getTargetOptions(normalizedOptions.buildTarget), await context.getBuilderNameForTarget(normalizedOptions.buildTarget)));
231
+ const env_1 = { stack: [], error: void 0, hasError: false };
232
+ try {
233
+ const executor = __addDisposableResource(env_1, await runner.createExecutor(context, normalizedOptions, undefined), true);
234
+ yield* executor.execute({
235
+ kind: results_1.ResultKind.Full,
236
+ files: {},
237
+ });
238
+ }
239
+ catch (e_1) {
240
+ env_1.error = e_1;
241
+ env_1.hasError = true;
242
+ }
243
+ finally {
244
+ const result_1 = __disposeResources(env_1);
245
+ if (result_1)
246
+ await result_1;
247
+ }
193
248
  }
194
249
  catch (e) {
195
250
  (0, error_1.assertIsError)(e);
196
- context.logger.error(`Could not load build target options for "${(0, architect_1.targetStringFromTarget)(normalizedOptions.buildTarget)}".\n` +
197
- `Please check your 'angular.json' configuration.\n` +
198
- `Error: ${e.message}`);
199
- return;
251
+ context.logger.error(`An exception occurred during standalone test execution:\n${e.stack ?? e.message}`);
252
+ yield { success: false };
200
253
  }
201
- // Get runner-specific build options from the hook
202
- const { buildOptions: runnerBuildOptions, virtualFiles } = await runner.getBuildOptions(normalizedOptions, buildTargetOptions);
203
- const finalExtensions = prepareBuildExtensions(virtualFiles, normalizedOptions.projectSourceRoot, extensions);
204
- // Prepare and run the application build
205
- const applicationBuildOptions = {
206
- ...buildTargetOptions,
207
- ...runnerBuildOptions,
208
- watch: normalizedOptions.watch,
209
- tsConfig: normalizedOptions.tsConfig,
210
- progress: normalizedOptions.buildProgress ?? buildTargetOptions.progress,
211
- };
212
- yield* runBuildAndTest(executor, applicationBuildOptions, context, finalExtensions);
254
+ return;
213
255
  }
214
- catch (e_1) {
215
- env_1.error = e_1;
216
- env_1.hasError = true;
256
+ // Get base build options from the buildTarget
257
+ let buildTargetOptions;
258
+ try {
259
+ buildTargetOptions = (await context.validateOptions(await context.getTargetOptions(normalizedOptions.buildTarget), await context.getBuilderNameForTarget(normalizedOptions.buildTarget)));
217
260
  }
218
- finally {
219
- const result_1 = __disposeResources(env_1);
220
- if (result_1)
221
- await result_1;
261
+ catch (e) {
262
+ (0, error_1.assertIsError)(e);
263
+ context.logger.error(`Could not load build target options for "${(0, architect_1.targetStringFromTarget)(normalizedOptions.buildTarget)}".\n` +
264
+ `Please check your 'angular.json' configuration.\n` +
265
+ `Error: ${e.message}`);
266
+ yield { success: false };
267
+ return;
268
+ }
269
+ // Get runner-specific build options
270
+ let runnerBuildOptions;
271
+ let virtualFiles;
272
+ let testEntryPointMappings;
273
+ try {
274
+ ({
275
+ buildOptions: runnerBuildOptions,
276
+ virtualFiles,
277
+ testEntryPointMappings,
278
+ } = await runner.getBuildOptions(normalizedOptions, buildTargetOptions));
279
+ }
280
+ catch (e) {
281
+ (0, error_1.assertIsError)(e);
282
+ context.logger.error(`An exception occurred while getting runner-specific build options:\n${e.stack ?? e.message}`);
283
+ yield { success: false };
284
+ return;
285
+ }
286
+ try {
287
+ const env_2 = { stack: [], error: void 0, hasError: false };
288
+ try {
289
+ const executor = __addDisposableResource(env_2, await runner.createExecutor(context, normalizedOptions, testEntryPointMappings), true);
290
+ const finalExtensions = prepareBuildExtensions(virtualFiles, normalizedOptions.projectSourceRoot, extensions);
291
+ // Prepare and run the application build
292
+ const applicationBuildOptions = {
293
+ ...buildTargetOptions,
294
+ ...runnerBuildOptions,
295
+ watch: normalizedOptions.watch,
296
+ tsConfig: normalizedOptions.tsConfig,
297
+ progress: normalizedOptions.buildProgress ?? buildTargetOptions.progress,
298
+ };
299
+ const dumpDirectory = normalizedOptions.dumpVirtualFiles
300
+ ? node_path_1.default.join(normalizedOptions.cacheOptions.path, 'unit-test', 'output-files')
301
+ : undefined;
302
+ yield* runBuildAndTest(executor, applicationBuildOptions, context, dumpDirectory, finalExtensions);
303
+ }
304
+ catch (e_2) {
305
+ env_2.error = e_2;
306
+ env_2.hasError = true;
307
+ }
308
+ finally {
309
+ const result_2 = __disposeResources(env_2);
310
+ if (result_2)
311
+ await result_2;
312
+ }
313
+ }
314
+ catch (e) {
315
+ (0, error_1.assertIsError)(e);
316
+ context.logger.error(`An exception occurred while creating the test executor:\n${e.stack ?? e.message}`);
317
+ yield { success: false };
222
318
  }
223
319
  }
@@ -16,6 +16,7 @@ export declare function normalizeOptions(context: BuilderContext, projectName: s
16
16
  buildTarget: import("@angular-devkit/architect").Target;
17
17
  include: string[];
18
18
  exclude: string[] | undefined;
19
+ filter: string | undefined;
19
20
  runnerName: import("./schema").Runner;
20
21
  codeCoverage: {
21
22
  exclude: string[] | undefined;
@@ -23,11 +24,13 @@ export declare function normalizeOptions(context: BuilderContext, projectName: s
23
24
  } | undefined;
24
25
  tsConfig: string;
25
26
  buildProgress: boolean | undefined;
26
- reporters: string[] | undefined;
27
+ reporters: [string, Record<string, unknown>][] | undefined;
28
+ outputFile: string | undefined;
27
29
  browsers: string[] | undefined;
28
30
  watch: boolean;
29
31
  debug: boolean;
30
32
  providersFile: string | undefined;
31
33
  setupFiles: string[];
34
+ dumpVirtualFiles: boolean | undefined;
32
35
  }>;
33
36
  export declare function injectTestingPolyfills(polyfills?: string[]): string[];