@angular/build 20.2.0 → 21.0.0-next.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.
Files changed (42) hide show
  1. package/.browserslistrc +5 -5
  2. package/package.json +15 -15
  3. package/src/builders/dev-server/builder.js +2 -2
  4. package/src/builders/dev-server/vite/hmr.d.ts +25 -0
  5. package/src/builders/dev-server/vite/hmr.js +113 -0
  6. package/src/builders/dev-server/vite/index.d.ts +21 -0
  7. package/src/builders/dev-server/{vite-server.js → vite/index.js} +19 -359
  8. package/src/builders/dev-server/vite/server.d.ts +15 -0
  9. package/src/builders/dev-server/vite/server.js +229 -0
  10. package/src/builders/dev-server/vite/utils.d.ts +36 -0
  11. package/src/builders/dev-server/vite/utils.js +76 -0
  12. package/src/builders/unit-test/builder.d.ts +1 -1
  13. package/src/builders/unit-test/builder.js +187 -289
  14. package/src/builders/unit-test/options.d.ts +1 -0
  15. package/src/builders/unit-test/options.js +2 -1
  16. package/src/builders/unit-test/runners/api.d.ts +47 -0
  17. package/src/builders/unit-test/runners/api.js +9 -0
  18. package/src/builders/unit-test/runners/karma/executor.d.ts +17 -0
  19. package/src/builders/unit-test/runners/karma/executor.js +93 -0
  20. package/src/builders/unit-test/runners/karma/index.d.ts +13 -0
  21. package/src/builders/unit-test/runners/karma/index.js +26 -0
  22. package/src/builders/unit-test/runners/vitest/browser-provider.d.ts +11 -0
  23. package/src/builders/unit-test/runners/vitest/browser-provider.js +69 -0
  24. package/src/builders/unit-test/runners/vitest/build-options.d.ts +11 -0
  25. package/src/builders/unit-test/runners/vitest/build-options.js +87 -0
  26. package/src/builders/unit-test/runners/vitest/executor.d.ts +22 -0
  27. package/src/builders/unit-test/runners/vitest/executor.js +160 -0
  28. package/src/builders/unit-test/runners/vitest/index.d.ts +13 -0
  29. package/src/builders/unit-test/runners/vitest/index.js +30 -0
  30. package/src/builders/unit-test/schema.d.ts +4 -0
  31. package/src/builders/unit-test/schema.json +4 -0
  32. package/src/builders/unit-test/test-discovery.d.ts +8 -0
  33. package/src/builders/unit-test/test-discovery.js +14 -0
  34. package/src/private.d.ts +1 -1
  35. package/src/private.js +2 -2
  36. package/src/utils/environment-options.d.ts +43 -0
  37. package/src/utils/environment-options.js +84 -30
  38. package/src/utils/normalize-cache.js +1 -1
  39. package/src/utils/version.js +1 -1
  40. package/src/builders/dev-server/vite-server.d.ts +0 -42
  41. package/src/builders/unit-test/karma-bridge.d.ts +0 -10
  42. package/src/builders/unit-test/karma-bridge.js +0 -82
@@ -6,320 +6,218 @@
6
6
  * Use of this source code is governed by an MIT-style license that can be
7
7
  * found in the LICENSE file at https://angular.dev/license
8
8
  */
9
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ var desc = Object.getOwnPropertyDescriptor(m, k);
12
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
13
+ desc = { enumerable: true, get: function() { return m[k]; } };
14
+ }
15
+ Object.defineProperty(o, k2, desc);
16
+ }) : (function(o, m, k, k2) {
17
+ if (k2 === undefined) k2 = k;
18
+ o[k2] = m[k];
19
+ }));
20
+ var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
21
+ Object.defineProperty(o, "default", { enumerable: true, value: v });
22
+ }) : function(o, v) {
23
+ o["default"] = v;
24
+ });
25
+ var __importStar = (this && this.__importStar) || (function () {
26
+ var ownKeys = function(o) {
27
+ ownKeys = Object.getOwnPropertyNames || function (o) {
28
+ var ar = [];
29
+ for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
30
+ return ar;
31
+ };
32
+ return ownKeys(o);
33
+ };
34
+ return function (mod) {
35
+ if (mod && mod.__esModule) return mod;
36
+ var result = {};
37
+ if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
38
+ __setModuleDefault(result, mod);
39
+ return result;
40
+ };
41
+ })();
42
+ var __addDisposableResource = (this && this.__addDisposableResource) || function (env, value, async) {
43
+ if (value !== null && value !== void 0) {
44
+ if (typeof value !== "object" && typeof value !== "function") throw new TypeError("Object expected.");
45
+ var dispose, inner;
46
+ if (async) {
47
+ if (!Symbol.asyncDispose) throw new TypeError("Symbol.asyncDispose is not defined.");
48
+ dispose = value[Symbol.asyncDispose];
49
+ }
50
+ if (dispose === void 0) {
51
+ if (!Symbol.dispose) throw new TypeError("Symbol.dispose is not defined.");
52
+ dispose = value[Symbol.dispose];
53
+ if (async) inner = dispose;
54
+ }
55
+ if (typeof dispose !== "function") throw new TypeError("Object not disposable.");
56
+ if (inner) dispose = function() { try { inner.call(this); } catch (e) { return Promise.reject(e); } };
57
+ env.stack.push({ value: value, dispose: dispose, async: async });
58
+ }
59
+ else if (async) {
60
+ env.stack.push({ async: true });
61
+ }
62
+ return value;
63
+ };
64
+ var __disposeResources = (this && this.__disposeResources) || (function (SuppressedError) {
65
+ return function (env) {
66
+ function fail(e) {
67
+ env.error = env.hasError ? new SuppressedError(e, env.error, "An error was suppressed during disposal.") : e;
68
+ env.hasError = true;
69
+ }
70
+ var r, s = 0;
71
+ function next() {
72
+ while (r = env.stack.pop()) {
73
+ try {
74
+ if (!r.async && s === 1) return s = 0, env.stack.push(r), Promise.resolve().then(next);
75
+ if (r.dispose) {
76
+ var result = r.dispose.call(r.value);
77
+ if (r.async) return s |= 2, Promise.resolve(result).then(next, function(e) { fail(e); return next(); });
78
+ }
79
+ else s |= 1;
80
+ }
81
+ catch (e) {
82
+ fail(e);
83
+ }
84
+ }
85
+ if (s === 1) return env.hasError ? Promise.reject(env.error) : Promise.resolve();
86
+ if (env.hasError) throw env.error;
87
+ }
88
+ return next();
89
+ };
90
+ })(typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
91
+ var e = new Error(message);
92
+ return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
93
+ });
9
94
  var __importDefault = (this && this.__importDefault) || function (mod) {
10
95
  return (mod && mod.__esModule) ? mod : { "default": mod };
11
96
  };
12
97
  Object.defineProperty(exports, "__esModule", { value: true });
13
98
  exports.execute = execute;
99
+ const architect_1 = require("@angular-devkit/architect");
14
100
  const node_assert_1 = __importDefault(require("node:assert"));
15
- const node_crypto_1 = require("node:crypto");
16
- const node_module_1 = require("node:module");
17
- const node_path_1 = __importDefault(require("node:path"));
18
101
  const virtual_module_plugin_1 = require("../../tools/esbuild/virtual-module-plugin");
19
102
  const error_1 = require("../../utils/error");
20
- const load_esm_1 = require("../../utils/load-esm");
21
- const path_1 = require("../../utils/path");
22
103
  const application_1 = require("../application");
23
104
  const results_1 = require("../application/results");
24
- const schema_1 = require("../application/schema");
25
- const application_builder_1 = require("../karma/application_builder");
26
- const find_tests_1 = require("../karma/find-tests");
27
- const karma_bridge_1 = require("./karma-bridge");
28
105
  const options_1 = require("./options");
29
- /**
30
- * @experimental Direct usage of this function is considered experimental.
31
- */
32
- // eslint-disable-next-line max-lines-per-function
33
- async function* execute(options, context, extensions = {}) {
34
- // Determine project name from builder context target
35
- const projectName = context.target?.project;
36
- if (!projectName) {
37
- context.logger.error(`The "${context.builder.builderName}" builder requires a target to be specified.`);
38
- return;
39
- }
40
- context.logger.warn(`NOTE: The "${context.builder.builderName}" builder is currently EXPERIMENTAL and not ready for production use.`);
41
- const normalizedOptions = await (0, options_1.normalizeOptions)(context, projectName, options);
42
- const { projectSourceRoot, workspaceRoot, runnerName } = normalizedOptions;
43
- // Translate options and use karma builder directly if specified
44
- if (runnerName === 'karma') {
45
- const karmaBridge = await (0, karma_bridge_1.useKarmaBuilder)(context, normalizedOptions);
46
- yield* karmaBridge;
47
- return;
48
- }
49
- if (runnerName !== 'vitest') {
50
- context.logger.error('Unknown test runner: ' + runnerName);
51
- return;
52
- }
53
- // Find test files
54
- const testFiles = await (0, find_tests_1.findTests)(normalizedOptions.include, normalizedOptions.exclude, workspaceRoot, projectSourceRoot);
55
- if (testFiles.length === 0) {
56
- context.logger.error('No tests found.');
57
- return { success: false };
106
+ async function loadTestRunner(runnerName) {
107
+ // Harden against directory traversal
108
+ if (!/^[a-zA-Z0-9-]+$/.test(runnerName)) {
109
+ throw new Error(`Invalid runner name "${runnerName}". Runner names can only contain alphanumeric characters and hyphens.`);
58
110
  }
59
- const entryPoints = (0, find_tests_1.getTestEntrypoints)(testFiles, { projectSourceRoot, workspaceRoot });
60
- entryPoints.set('init-testbed', 'angular:test-bed-init');
61
- let vitestNodeModule;
111
+ let runnerModule;
62
112
  try {
63
- vitestNodeModule = await (0, load_esm_1.loadEsmModule)('vitest/node');
113
+ runnerModule = await Promise.resolve(`${`./runners/${runnerName}/index`}`).then(s => __importStar(require(s)));
64
114
  }
65
- catch (error) {
66
- (0, error_1.assertIsError)(error);
67
- if (error.code !== 'ERR_MODULE_NOT_FOUND') {
68
- throw error;
115
+ catch (e) {
116
+ (0, error_1.assertIsError)(e);
117
+ if (e.code === 'ERR_MODULE_NOT_FOUND') {
118
+ throw new Error(`Unknown test runner "${runnerName}".`);
69
119
  }
70
- context.logger.error('The `vitest` package was not found. Please install the package and rerun the test command.');
71
- return;
120
+ throw new Error(`Failed to load the '${runnerName}' test runner. The package may be corrupted or improperly installed.\n` +
121
+ `Error: ${e.message}`);
72
122
  }
73
- const { startVitest } = vitestNodeModule;
74
- // Setup test file build options based on application build target options
75
- const buildTargetOptions = (await context.validateOptions(await context.getTargetOptions(normalizedOptions.buildTarget), await context.getBuilderNameForTarget(normalizedOptions.buildTarget)));
76
- buildTargetOptions.polyfills = (0, options_1.injectTestingPolyfills)(buildTargetOptions.polyfills);
77
- const outputPath = (0, path_1.toPosixPath)(node_path_1.default.join(context.workspaceRoot, generateOutputPath()));
78
- const buildOptions = {
79
- ...buildTargetOptions,
80
- watch: normalizedOptions.watch,
81
- incrementalResults: normalizedOptions.watch,
82
- outputPath,
83
- index: false,
84
- browser: undefined,
85
- server: undefined,
86
- outputMode: undefined,
87
- localize: false,
88
- budgets: [],
89
- serviceWorker: false,
90
- appShell: false,
91
- ssr: false,
92
- prerender: false,
93
- sourceMap: { scripts: true, vendor: false, styles: false },
94
- outputHashing: schema_1.OutputHashing.None,
95
- optimization: false,
96
- tsConfig: normalizedOptions.tsConfig,
97
- entryPoints,
98
- externalDependencies: [
99
- 'vitest',
100
- '@vitest/browser/context',
101
- ...(buildTargetOptions.externalDependencies ?? []),
102
- ],
103
- };
104
- extensions ??= {};
105
- extensions.codePlugins ??= [];
106
- const virtualTestBedInit = (0, virtual_module_plugin_1.createVirtualModulePlugin)({
107
- namespace: 'angular:test-bed-init',
108
- loadContent: async () => {
109
- const contents = [
110
- // Initialize the Angular testing environment
111
- `import { NgModule } from '@angular/core';`,
112
- `import { getTestBed, ɵgetCleanupHook as getCleanupHook } from '@angular/core/testing';`,
113
- `import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing';`,
114
- '',
115
- normalizedOptions.providersFile
116
- ? `import providers from './${(0, path_1.toPosixPath)(node_path_1.default
117
- .relative(projectSourceRoot, normalizedOptions.providersFile)
118
- .replace(/.[mc]?ts$/, ''))}'`
119
- : 'const providers = [];',
120
- '',
121
- // Same as https://github.com/angular/angular/blob/05a03d3f975771bb59c7eefd37c01fa127ee2229/packages/core/testing/src/test_hooks.ts#L21-L29
122
- `beforeEach(getCleanupHook(false));`,
123
- `afterEach(getCleanupHook(true));`,
124
- '',
125
- `@NgModule({`,
126
- ` providers,`,
127
- `})`,
128
- `export class TestModule {}`,
129
- '',
130
- `getTestBed().initTestEnvironment([BrowserTestingModule, TestModule], platformBrowserTesting(), {`,
131
- ` errorOnUnknownElements: true,`,
132
- ` errorOnUnknownProperties: true,`,
133
- '});',
134
- ];
135
- return {
136
- contents: contents.join('\n'),
137
- loader: 'js',
138
- resolveDir: projectSourceRoot,
139
- };
140
- },
141
- });
142
- extensions.codePlugins.unshift(virtualTestBedInit);
143
- let instance;
144
- // Setup vitest browser options if configured
145
- const { browser, errors } = setupBrowserConfiguration(normalizedOptions.browsers, normalizedOptions.debug, projectSourceRoot);
146
- if (errors?.length) {
147
- errors.forEach((error) => context.logger.error(error));
148
- return { success: false };
123
+ const runner = runnerModule.default;
124
+ if (!runner ||
125
+ typeof runner.getBuildOptions !== 'function' ||
126
+ typeof runner.createExecutor !== 'function') {
127
+ throw new Error(`The loaded test runner '${runnerName}' does not appear to be a valid TestRunner implementation.`);
149
128
  }
150
- // Add setup file entries for TestBed initialization and project polyfills
151
- const setupFiles = ['init-testbed.js', ...normalizedOptions.setupFiles];
152
- if (buildTargetOptions?.polyfills?.length) {
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');
129
+ return runner;
130
+ }
131
+ function prepareBuildExtensions(virtualFiles, projectSourceRoot, extensions) {
132
+ if (!virtualFiles) {
133
+ return extensions;
156
134
  }
157
- const debugOptions = normalizedOptions.debug
158
- ? {
159
- inspectBrk: true,
160
- isolate: false,
161
- fileParallelism: false,
162
- }
163
- : {};
164
- try {
165
- for await (const result of (0, application_1.buildApplicationInternal)(buildOptions, context, extensions)) {
166
- if (result.kind === results_1.ResultKind.Failure) {
167
- continue;
168
- }
169
- else if (result.kind !== results_1.ResultKind.Full && result.kind !== results_1.ResultKind.Incremental) {
170
- node_assert_1.default.fail('A full and/or incremental build result is required from the application builder.');
171
- }
172
- (0, node_assert_1.default)(result.files, 'Builder did not provide result files.');
173
- await (0, application_builder_1.writeTestFiles)(result.files, outputPath);
174
- instance ??= await startVitest('test', undefined /* cliFilters */, {
175
- // Disable configuration file resolution/loading
176
- config: false,
177
- root: workspaceRoot,
178
- project: ['base', projectName],
179
- name: 'base',
180
- include: [],
181
- reporters: normalizedOptions.reporters ?? ['default'],
182
- watch: normalizedOptions.watch,
183
- coverage: generateCoverageOption(normalizedOptions.codeCoverage, workspaceRoot, outputPath),
184
- ...debugOptions,
185
- }, {
186
- plugins: [
187
- {
188
- name: 'angular:project-init',
189
- async configureVitest(context) {
190
- // Create a subproject that can be configured with plugins for browser mode.
191
- // Plugins defined directly in the vite overrides will not be present in the
192
- // browser specific Vite instance.
193
- const [project] = await context.injectTestProjects({
194
- test: {
195
- name: projectName,
196
- root: outputPath,
197
- globals: true,
198
- setupFiles,
199
- // Use `jsdom` if no browsers are explicitly configured.
200
- // `node` is effectively no "environment" and the default.
201
- environment: browser ? 'node' : 'jsdom',
202
- browser,
203
- },
204
- plugins: [
205
- {
206
- name: 'angular:html-index',
207
- transformIndexHtml() {
208
- // Add all global stylesheets
209
- return (Object.entries(result.files)
210
- // TODO: Expand this to all configured global stylesheets
211
- .filter(([file]) => file === 'styles.css')
212
- .map(([styleUrl]) => ({
213
- tag: 'link',
214
- attrs: {
215
- 'href': styleUrl,
216
- 'rel': 'stylesheet',
217
- },
218
- injectTo: 'head',
219
- })));
220
- },
221
- },
222
- ],
223
- });
224
- // Adjust coverage excludes to not include the otherwise automatically inserted included unit tests.
225
- // Vite does this as a convenience but is problematic for the bundling strategy employed by the
226
- // builder's test setup. To workaround this, the excludes are adjusted here to only automaticallyAdd commentMore actions
227
- // exclude the TypeScript source test files.
228
- project.config.coverage.exclude = [
229
- ...(normalizedOptions.codeCoverage?.exclude ?? []),
230
- '**/*.{test,spec}.?(c|m)ts',
231
- ];
232
- },
233
- },
234
- ],
235
- });
236
- // Check if all the tests pass to calculate the result
237
- const testModules = instance.state.getTestModules();
238
- yield { success: testModules.every((testModule) => testModule.ok()) };
135
+ extensions ??= {};
136
+ extensions.codePlugins ??= [];
137
+ for (const [namespace, contents] of Object.entries(virtualFiles)) {
138
+ extensions.codePlugins.push((0, virtual_module_plugin_1.createVirtualModulePlugin)({
139
+ namespace,
140
+ loadContent: () => {
141
+ return {
142
+ contents,
143
+ loader: 'js',
144
+ resolveDir: projectSourceRoot,
145
+ };
146
+ },
147
+ }));
148
+ }
149
+ return extensions;
150
+ }
151
+ async function* runBuildAndTest(executor, applicationBuildOptions, context, extensions) {
152
+ for await (const buildResult of (0, application_1.buildApplicationInternal)(applicationBuildOptions, context, extensions)) {
153
+ if (buildResult.kind === results_1.ResultKind.Failure) {
154
+ yield { success: false };
155
+ continue;
239
156
  }
240
- }
241
- finally {
242
- if (normalizedOptions.watch) {
243
- // Vitest will automatically close if not using watch mode
244
- await instance?.close();
157
+ else if (buildResult.kind !== results_1.ResultKind.Full &&
158
+ buildResult.kind !== results_1.ResultKind.Incremental) {
159
+ node_assert_1.default.fail('A full and/or incremental build result is required from the application builder.');
245
160
  }
161
+ (0, node_assert_1.default)(buildResult.files, 'Builder did not provide result files.');
162
+ // Pass the build artifacts to the executor
163
+ yield* executor.execute(buildResult);
246
164
  }
247
165
  }
248
- function findBrowserProvider(projectResolver) {
249
- // One of these must be installed in the project to use browser testing
250
- const vitestBuiltinProviders = ['playwright', 'webdriverio'];
251
- for (const providerName of vitestBuiltinProviders) {
166
+ /**
167
+ * @experimental Direct usage of this function is considered experimental.
168
+ */
169
+ async function* execute(options, context, extensions) {
170
+ const env_1 = { stack: [], error: void 0, hasError: false };
171
+ 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;
177
+ }
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;
188
+ }
189
+ // Get base build options from the buildTarget
190
+ let buildTargetOptions;
252
191
  try {
253
- projectResolver(providerName);
254
- return providerName;
192
+ buildTargetOptions = (await context.validateOptions(await context.getTargetOptions(normalizedOptions.buildTarget), await context.getBuilderNameForTarget(normalizedOptions.buildTarget)));
255
193
  }
256
- catch { }
257
- }
258
- }
259
- function normalizeBrowserName(browserName) {
260
- // Normalize browser names to match Vitest's expectations for headless but also supports karma's names
261
- // e.g., 'ChromeHeadless' -> 'chrome', 'FirefoxHeadless'
262
- // and 'Chrome' -> 'chrome', 'Firefox' -> 'firefox'.
263
- const normalized = browserName.toLowerCase();
264
- return normalized.replace(/headless$/, '');
265
- }
266
- function setupBrowserConfiguration(browsers, debug, projectSourceRoot) {
267
- if (browsers === undefined) {
268
- return {};
269
- }
270
- const projectResolver = (0, node_module_1.createRequire)(projectSourceRoot + '/').resolve;
271
- let errors;
272
- try {
273
- projectResolver('@vitest/browser');
274
- }
275
- catch {
276
- errors ??= [];
277
- errors.push('The "browsers" option requires the "@vitest/browser" package to be installed within the project.' +
278
- ' Please install this package and rerun the test command.');
279
- }
280
- const provider = findBrowserProvider(projectResolver);
281
- if (!provider) {
282
- errors ??= [];
283
- errors.push('The "browsers" option requires either "playwright" or "webdriverio" to be installed within the project.' +
284
- ' Please install one of these packages and rerun the test command.');
285
- }
286
- // Vitest current requires the playwright browser provider to use the inspect-brk option used by "debug"
287
- if (debug && provider !== 'playwright') {
288
- errors ??= [];
289
- errors.push('Debugging browser mode tests currently requires the use of "playwright".' +
290
- ' Please install this package and rerun the test command.');
194
+ catch (e) {
195
+ (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;
200
+ }
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);
291
213
  }
292
- if (errors) {
293
- return { errors };
214
+ catch (e_1) {
215
+ env_1.error = e_1;
216
+ env_1.hasError = true;
294
217
  }
295
- const browser = {
296
- enabled: true,
297
- provider,
298
- headless: browsers.some((name) => name.toLowerCase().includes('headless')),
299
- instances: browsers.map((browserName) => ({
300
- browser: normalizeBrowserName(browserName),
301
- })),
302
- };
303
- return { browser };
304
- }
305
- function generateOutputPath() {
306
- const datePrefix = new Date().toISOString().replaceAll(/[-:.]/g, '');
307
- const uuidSuffix = (0, node_crypto_1.randomUUID)().slice(0, 8);
308
- return node_path_1.default.join('dist', 'test-out', `${datePrefix}-${uuidSuffix}`);
309
- }
310
- function generateCoverageOption(codeCoverage, workspaceRoot, outputPath) {
311
- if (!codeCoverage) {
312
- return {
313
- enabled: false,
314
- };
218
+ finally {
219
+ const result_1 = __disposeResources(env_1);
220
+ if (result_1)
221
+ await result_1;
315
222
  }
316
- return {
317
- enabled: true,
318
- excludeAfterRemap: true,
319
- include: [`${(0, path_1.toPosixPath)(node_path_1.default.relative(workspaceRoot, outputPath))}/**`],
320
- // Special handling for `reporter` due to an undefined value causing upstream failures
321
- ...(codeCoverage.reporters
322
- ? { reporter: codeCoverage.reporters }
323
- : {}),
324
- };
325
223
  }
@@ -22,6 +22,7 @@ export declare function normalizeOptions(context: BuilderContext, projectName: s
22
22
  reporters: [string, Record<string, unknown>][] | undefined;
23
23
  } | undefined;
24
24
  tsConfig: string;
25
+ buildProgress: boolean | undefined;
25
26
  reporters: string[] | undefined;
26
27
  browsers: string[] | undefined;
27
28
  watch: boolean;
@@ -28,7 +28,7 @@ async function normalizeOptions(context, projectName, options) {
28
28
  // Target specifier defaults to the current project's build target using a development configuration
29
29
  const buildTargetSpecifier = options.buildTarget ?? `::development`;
30
30
  const buildTarget = (0, architect_1.targetFromTargetString)(buildTargetSpecifier, projectName, 'build');
31
- const { tsConfig, runner, reporters, browsers } = options;
31
+ const { tsConfig, runner, reporters, browsers, progress } = options;
32
32
  return {
33
33
  // Project/workspace information
34
34
  workspaceRoot,
@@ -49,6 +49,7 @@ async function normalizeOptions(context, projectName, options) {
49
49
  }
50
50
  : undefined,
51
51
  tsConfig,
52
+ buildProgress: progress,
52
53
  reporters,
53
54
  browsers,
54
55
  watch: options.watch ?? (0, tty_1.isTTY)(),
@@ -0,0 +1,47 @@
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, BuilderOutput } from '@angular-devkit/architect';
9
+ import type { ApplicationBuilderInternalOptions } from '../../application/options';
10
+ import type { FullResult, IncrementalResult } from '../../application/results';
11
+ import type { NormalizedUnitTestBuilderOptions } from '../options';
12
+ export interface RunnerOptions {
13
+ buildOptions: Partial<ApplicationBuilderInternalOptions>;
14
+ virtualFiles?: Record<string, string>;
15
+ }
16
+ /**
17
+ * Represents a stateful test execution session.
18
+ * An instance of this is created for each `ng test` command.
19
+ */
20
+ export interface TestExecutor {
21
+ /**
22
+ * Executes tests using the artifacts from a specific build.
23
+ * This method can be called multiple times in watch mode.
24
+ *
25
+ * @param buildResult The output from the application builder.
26
+ * @returns An async iterable builder output stream.
27
+ */
28
+ execute(buildResult: FullResult | IncrementalResult): AsyncIterable<BuilderOutput>;
29
+ [Symbol.asyncDispose](): Promise<void>;
30
+ }
31
+ /**
32
+ * Represents the metadata and hooks for a specific test runner.
33
+ */
34
+ export interface TestRunner {
35
+ readonly name: string;
36
+ readonly isStandalone?: boolean;
37
+ getBuildOptions(options: NormalizedUnitTestBuilderOptions, baseBuildOptions: Partial<ApplicationBuilderInternalOptions>): RunnerOptions | Promise<RunnerOptions>;
38
+ /**
39
+ * Creates a stateful executor for a test session.
40
+ * This is called once at the start of the `ng test` command.
41
+ *
42
+ * @param context The Architect builder context.
43
+ * @param options The normalized unit test options.
44
+ * @returns A TestExecutor instance that will handle the test runs.
45
+ */
46
+ createExecutor(context: BuilderContext, options: NormalizedUnitTestBuilderOptions): Promise<TestExecutor>;
47
+ }
@@ -0,0 +1,9 @@
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 });
@@ -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, BuilderOutput } from '@angular-devkit/architect';
9
+ import { NormalizedUnitTestBuilderOptions } from '../../options';
10
+ import type { TestExecutor } from '../api';
11
+ export declare class KarmaExecutor implements TestExecutor {
12
+ private context;
13
+ private options;
14
+ constructor(context: BuilderContext, options: NormalizedUnitTestBuilderOptions);
15
+ execute(): AsyncIterable<BuilderOutput>;
16
+ [Symbol.asyncDispose](): Promise<void>;
17
+ }