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

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 (51) hide show
  1. package/package.json +11 -11
  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 +44 -352
  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/find-tests.d.ts +1 -9
  10. package/src/builders/karma/find-tests.js +6 -106
  11. package/src/builders/karma/karma-config.d.ts +11 -0
  12. package/src/builders/karma/karma-config.js +79 -0
  13. package/src/builders/karma/polyfills-plugin.d.ts +13 -0
  14. package/src/builders/karma/polyfills-plugin.js +74 -0
  15. package/src/builders/karma/progress-reporter.d.ts +17 -0
  16. package/src/builders/karma/progress-reporter.js +73 -0
  17. package/src/builders/karma/utils.d.ts +17 -0
  18. package/src/builders/karma/utils.js +66 -0
  19. package/src/builders/unit-test/builder.js +150 -44
  20. package/src/builders/unit-test/options.d.ts +5 -1
  21. package/src/builders/unit-test/options.js +12 -5
  22. package/src/builders/unit-test/runners/api.d.ts +19 -1
  23. package/src/builders/unit-test/runners/dependency-checker.d.ts +43 -0
  24. package/src/builders/unit-test/runners/dependency-checker.js +82 -0
  25. package/src/builders/unit-test/runners/karma/executor.js +26 -2
  26. package/src/builders/unit-test/runners/karma/index.js +17 -0
  27. package/src/builders/unit-test/runners/vitest/browser-provider.d.ts +3 -2
  28. package/src/builders/unit-test/runners/vitest/build-options.js +6 -4
  29. package/src/builders/unit-test/runners/vitest/executor.d.ts +4 -5
  30. package/src/builders/unit-test/runners/vitest/executor.js +23 -135
  31. package/src/builders/unit-test/runners/vitest/index.js +19 -2
  32. package/src/builders/unit-test/runners/vitest/plugins.d.ts +23 -0
  33. package/src/builders/unit-test/runners/vitest/plugins.js +131 -0
  34. package/src/builders/unit-test/schema.d.ts +59 -30
  35. package/src/builders/unit-test/schema.js +1 -1
  36. package/src/builders/unit-test/schema.json +70 -16
  37. package/src/builders/unit-test/test-discovery.d.ts +25 -1
  38. package/src/builders/unit-test/test-discovery.js +194 -5
  39. package/src/tools/angular/transformers/jit-bootstrap-transformer.js +1 -1
  40. package/src/tools/angular/transformers/jit-resource-transformer.js +1 -1
  41. package/src/tools/esbuild/application-code-bundle.js +4 -7
  42. package/src/tools/esbuild/stylesheets/less-language.js +2 -26
  43. package/src/tools/esbuild/stylesheets/stylesheet-plugin-factory.js +2 -1
  44. package/src/tools/vite/middlewares/assets-middleware.d.ts +2 -0
  45. package/src/tools/vite/middlewares/assets-middleware.js +31 -0
  46. package/src/tools/vite/utils.js +0 -1
  47. package/src/utils/normalize-cache.js +1 -1
  48. package/src/utils/supported-browsers.js +7 -3
  49. package/src/utils/test-files.d.ts +17 -0
  50. package/src/utils/test-files.js +82 -0
  51. package/.browserslistrc +0 -7
@@ -98,11 +98,16 @@ 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");
110
+ const test_discovery_1 = require("./test-discovery");
106
111
  async function loadTestRunner(runnerName) {
107
112
  // Harden against directory traversal
108
113
  if (!/^[a-zA-Z0-9-]+$/.test(runnerName)) {
@@ -148,7 +153,8 @@ function prepareBuildExtensions(virtualFiles, projectSourceRoot, extensions) {
148
153
  }
149
154
  return extensions;
150
155
  }
151
- async function* runBuildAndTest(executor, applicationBuildOptions, context, extensions) {
156
+ async function* runBuildAndTest(executor, applicationBuildOptions, context, dumpDirectory, extensions) {
157
+ let consecutiveErrorCount = 0;
152
158
  for await (const buildResult of (0, application_1.buildApplicationInternal)(applicationBuildOptions, context, extensions)) {
153
159
  if (buildResult.kind === results_1.ResultKind.Failure) {
154
160
  yield { success: false };
@@ -159,65 +165,165 @@ async function* runBuildAndTest(executor, applicationBuildOptions, context, exte
159
165
  node_assert_1.default.fail('A full and/or incremental build result is required from the application builder.');
160
166
  }
161
167
  (0, node_assert_1.default)(buildResult.files, 'Builder did not provide result files.');
168
+ if (dumpDirectory) {
169
+ if (buildResult.kind === results_1.ResultKind.Full) {
170
+ // Full build, so clean the directory
171
+ await (0, promises_1.rm)(dumpDirectory, { recursive: true, force: true });
172
+ }
173
+ else {
174
+ // Incremental build, so delete removed files
175
+ for (const file of buildResult.removed) {
176
+ await (0, promises_1.rm)(node_path_1.default.join(dumpDirectory, file.path), { force: true });
177
+ }
178
+ }
179
+ await (0, test_files_1.writeTestFiles)(buildResult.files, dumpDirectory);
180
+ context.logger.info(`Build output files successfully dumped to '${dumpDirectory}'.`);
181
+ }
162
182
  // Pass the build artifacts to the executor
163
- yield* executor.execute(buildResult);
183
+ try {
184
+ yield* executor.execute(buildResult);
185
+ // Successful execution resets the failure counter
186
+ consecutiveErrorCount = 0;
187
+ }
188
+ catch (e) {
189
+ (0, error_1.assertIsError)(e);
190
+ context.logger.error(`An exception occurred during test execution:\n${e.stack ?? e.message}`);
191
+ yield { success: false };
192
+ consecutiveErrorCount++;
193
+ }
194
+ if (consecutiveErrorCount >= 3) {
195
+ context.logger.error('Test runner process has failed multiple times in a row. Please fix the configuration and restart the process.');
196
+ return;
197
+ }
164
198
  }
165
199
  }
166
200
  /**
167
201
  * @experimental Direct usage of this function is considered experimental.
168
202
  */
169
203
  async function* execute(options, context, extensions) {
170
- const env_1 = { stack: [], error: void 0, hasError: false };
204
+ // Determine project name from builder context target
205
+ const projectName = context.target?.project;
206
+ if (!projectName) {
207
+ context.logger.error(`The builder requires a target to be specified.`);
208
+ return;
209
+ }
210
+ context.logger.warn(`NOTE: The "unit-test" builder is currently EXPERIMENTAL and not ready for production use.`);
211
+ // Initialize the test runner and normalize options
212
+ let runner;
213
+ let normalizedOptions;
171
214
  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;
215
+ normalizedOptions = await (0, options_1.normalizeOptions)(context, projectName, options);
216
+ runner = await loadTestRunner(normalizedOptions.runnerName);
217
+ await runner.validateDependencies?.(normalizedOptions);
218
+ }
219
+ catch (e) {
220
+ (0, error_1.assertIsError)(e);
221
+ if (e instanceof dependency_checker_1.MissingDependenciesError) {
222
+ context.logger.error(e.message);
177
223
  }
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;
224
+ else {
225
+ context.logger.error(`An exception occurred during initialization of the test runner:\n${e.stack ?? e.message}`);
188
226
  }
189
- // Get base build options from the buildTarget
190
- let buildTargetOptions;
227
+ yield { success: false };
228
+ return;
229
+ }
230
+ if (normalizedOptions.listTests) {
231
+ const testFiles = await (0, test_discovery_1.findTests)(normalizedOptions.include, normalizedOptions.exclude ?? [], normalizedOptions.workspaceRoot, normalizedOptions.projectSourceRoot);
232
+ context.logger.info('Discovered test files:');
233
+ for (const file of testFiles) {
234
+ context.logger.info(` ${node_path_1.default.relative(normalizedOptions.workspaceRoot, file)}`);
235
+ }
236
+ yield { success: true };
237
+ return;
238
+ }
239
+ if (runner.isStandalone) {
191
240
  try {
192
- buildTargetOptions = (await context.validateOptions(await context.getTargetOptions(normalizedOptions.buildTarget), await context.getBuilderNameForTarget(normalizedOptions.buildTarget)));
241
+ const env_1 = { stack: [], error: void 0, hasError: false };
242
+ try {
243
+ const executor = __addDisposableResource(env_1, await runner.createExecutor(context, normalizedOptions, undefined), true);
244
+ yield* executor.execute({
245
+ kind: results_1.ResultKind.Full,
246
+ files: {},
247
+ });
248
+ }
249
+ catch (e_1) {
250
+ env_1.error = e_1;
251
+ env_1.hasError = true;
252
+ }
253
+ finally {
254
+ const result_1 = __disposeResources(env_1);
255
+ if (result_1)
256
+ await result_1;
257
+ }
193
258
  }
194
259
  catch (e) {
195
260
  (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;
261
+ context.logger.error(`An exception occurred during standalone test execution:\n${e.stack ?? e.message}`);
262
+ yield { success: false };
200
263
  }
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);
264
+ return;
265
+ }
266
+ // Get base build options from the buildTarget
267
+ let buildTargetOptions;
268
+ try {
269
+ buildTargetOptions = (await context.validateOptions(await context.getTargetOptions(normalizedOptions.buildTarget), await context.getBuilderNameForTarget(normalizedOptions.buildTarget)));
270
+ }
271
+ catch (e) {
272
+ (0, error_1.assertIsError)(e);
273
+ context.logger.error(`Could not load build target options for "${(0, architect_1.targetStringFromTarget)(normalizedOptions.buildTarget)}".\n` +
274
+ `Please check your 'angular.json' configuration.\n` +
275
+ `Error: ${e.message}`);
276
+ yield { success: false };
277
+ return;
278
+ }
279
+ // Get runner-specific build options
280
+ let runnerBuildOptions;
281
+ let virtualFiles;
282
+ let testEntryPointMappings;
283
+ try {
284
+ ({
285
+ buildOptions: runnerBuildOptions,
286
+ virtualFiles,
287
+ testEntryPointMappings,
288
+ } = await runner.getBuildOptions(normalizedOptions, buildTargetOptions));
213
289
  }
214
- catch (e_1) {
215
- env_1.error = e_1;
216
- env_1.hasError = true;
290
+ catch (e) {
291
+ (0, error_1.assertIsError)(e);
292
+ context.logger.error(`An exception occurred while getting runner-specific build options:\n${e.stack ?? e.message}`);
293
+ yield { success: false };
294
+ return;
217
295
  }
218
- finally {
219
- const result_1 = __disposeResources(env_1);
220
- if (result_1)
221
- await result_1;
296
+ try {
297
+ const env_2 = { stack: [], error: void 0, hasError: false };
298
+ try {
299
+ const executor = __addDisposableResource(env_2, await runner.createExecutor(context, normalizedOptions, testEntryPointMappings), true);
300
+ const finalExtensions = prepareBuildExtensions(virtualFiles, normalizedOptions.projectSourceRoot, extensions);
301
+ // Prepare and run the application build
302
+ const applicationBuildOptions = {
303
+ ...buildTargetOptions,
304
+ ...runnerBuildOptions,
305
+ watch: normalizedOptions.watch,
306
+ tsConfig: normalizedOptions.tsConfig,
307
+ progress: normalizedOptions.buildProgress ?? buildTargetOptions.progress,
308
+ };
309
+ const dumpDirectory = normalizedOptions.dumpVirtualFiles
310
+ ? node_path_1.default.join(normalizedOptions.cacheOptions.path, 'unit-test', 'output-files')
311
+ : undefined;
312
+ yield* runBuildAndTest(executor, applicationBuildOptions, context, dumpDirectory, finalExtensions);
313
+ }
314
+ catch (e_2) {
315
+ env_2.error = e_2;
316
+ env_2.hasError = true;
317
+ }
318
+ finally {
319
+ const result_2 = __disposeResources(env_2);
320
+ if (result_2)
321
+ await result_2;
322
+ }
323
+ }
324
+ catch (e) {
325
+ (0, error_1.assertIsError)(e);
326
+ context.logger.error(`An exception occurred while creating the test executor:\n${e.stack ?? e.message}`);
327
+ yield { success: false };
222
328
  }
223
329
  }
@@ -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,14 @@ 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;
35
+ listTests: boolean | undefined;
32
36
  }>;
33
37
  export declare function injectTestingPolyfills(polyfills?: string[]): string[];
@@ -17,6 +17,11 @@ const node_path_1 = __importDefault(require("node:path"));
17
17
  const normalize_cache_1 = require("../../utils/normalize-cache");
18
18
  const project_metadata_1 = require("../../utils/project-metadata");
19
19
  const tty_1 = require("../../utils/tty");
20
+ function normalizeReporterOption(reporters) {
21
+ return reporters?.map((entry) => typeof entry === 'string'
22
+ ? [entry, {}]
23
+ : entry);
24
+ }
20
25
  async function normalizeOptions(context, projectName, options) {
21
26
  // Setup base paths based on workspace root and project information
22
27
  const workspaceRoot = context.workspaceRoot;
@@ -28,7 +33,7 @@ async function normalizeOptions(context, projectName, options) {
28
33
  // Target specifier defaults to the current project's build target using a development configuration
29
34
  const buildTargetSpecifier = options.buildTarget ?? `::development`;
30
35
  const buildTarget = (0, architect_1.targetFromTargetString)(buildTargetSpecifier, projectName, 'build');
31
- const { tsConfig, runner, reporters, browsers, progress } = options;
36
+ const { tsConfig, runner, browsers, progress, filter } = options;
32
37
  return {
33
38
  // Project/workspace information
34
39
  workspaceRoot,
@@ -39,18 +44,18 @@ async function normalizeOptions(context, projectName, options) {
39
44
  buildTarget,
40
45
  include: options.include ?? ['**/*.spec.ts'],
41
46
  exclude: options.exclude,
47
+ filter,
42
48
  runnerName: runner,
43
49
  codeCoverage: options.codeCoverage
44
50
  ? {
45
51
  exclude: options.codeCoverageExclude,
46
- reporters: options.codeCoverageReporters?.map((entry) => typeof entry === 'string'
47
- ? [entry, {}]
48
- : entry),
52
+ reporters: normalizeReporterOption(options.codeCoverageReporters),
49
53
  }
50
54
  : undefined,
51
55
  tsConfig,
52
56
  buildProgress: progress,
53
- reporters,
57
+ reporters: normalizeReporterOption(options.reporters),
58
+ outputFile: options.outputFile,
54
59
  browsers,
55
60
  watch: options.watch ?? (0, tty_1.isTTY)(),
56
61
  debug: options.debug ?? false,
@@ -58,6 +63,8 @@ async function normalizeOptions(context, projectName, options) {
58
63
  setupFiles: options.setupFiles
59
64
  ? options.setupFiles.map((setupFile) => node_path_1.default.join(workspaceRoot, setupFile))
60
65
  : [],
66
+ dumpVirtualFiles: options.dumpVirtualFiles,
67
+ listTests: options.listTests,
61
68
  };
62
69
  }
63
70
  function injectTestingPolyfills(polyfills = []) {
@@ -9,9 +9,25 @@ import type { BuilderContext, BuilderOutput } from '@angular-devkit/architect';
9
9
  import type { ApplicationBuilderInternalOptions } from '../../application/options';
10
10
  import type { FullResult, IncrementalResult } from '../../application/results';
11
11
  import type { NormalizedUnitTestBuilderOptions } from '../options';
12
+ /**
13
+ * Represents the options for a test runner.
14
+ */
12
15
  export interface RunnerOptions {
16
+ /**
17
+ * Partial options for the application builder.
18
+ * These will be merged with the options from the build target.
19
+ */
13
20
  buildOptions: Partial<ApplicationBuilderInternalOptions>;
21
+ /**
22
+ * A record of virtual files to be added to the build.
23
+ * The key is the file path and the value is the file content.
24
+ */
14
25
  virtualFiles?: Record<string, string>;
26
+ /**
27
+ * A map of test entry points to their corresponding test files.
28
+ * This is used to avoid re-discovering the test files in the executor.
29
+ */
30
+ testEntryPointMappings?: Map<string, string>;
15
31
  }
16
32
  /**
17
33
  * Represents a stateful test execution session.
@@ -34,6 +50,7 @@ export interface TestExecutor {
34
50
  export interface TestRunner {
35
51
  readonly name: string;
36
52
  readonly isStandalone?: boolean;
53
+ validateDependencies?(options: NormalizedUnitTestBuilderOptions): void | Promise<void>;
37
54
  getBuildOptions(options: NormalizedUnitTestBuilderOptions, baseBuildOptions: Partial<ApplicationBuilderInternalOptions>): RunnerOptions | Promise<RunnerOptions>;
38
55
  /**
39
56
  * Creates a stateful executor for a test session.
@@ -41,7 +58,8 @@ export interface TestRunner {
41
58
  *
42
59
  * @param context The Architect builder context.
43
60
  * @param options The normalized unit test options.
61
+ * @param testEntryPointMappings A map of test entry points to their corresponding test files.
44
62
  * @returns A TestExecutor instance that will handle the test runs.
45
63
  */
46
- createExecutor(context: BuilderContext, options: NormalizedUnitTestBuilderOptions): Promise<TestExecutor>;
64
+ createExecutor(context: BuilderContext, options: NormalizedUnitTestBuilderOptions, testEntryPointMappings: Map<string, string> | undefined): Promise<TestExecutor>;
47
65
  }
@@ -0,0 +1,43 @@
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
+ /**
9
+ * A custom error class to represent missing dependency errors.
10
+ * This is used to avoid printing a stack trace for this expected error.
11
+ */
12
+ export declare class MissingDependenciesError extends Error {
13
+ constructor(message: string);
14
+ }
15
+ export declare class DependencyChecker {
16
+ private readonly resolver;
17
+ private readonly missingDependencies;
18
+ constructor(projectSourceRoot: string);
19
+ /**
20
+ * Checks if a package is installed.
21
+ * @param packageName The name of the package to check.
22
+ * @returns True if the package is found, false otherwise.
23
+ */
24
+ private isInstalled;
25
+ /**
26
+ * Verifies that a package is installed and adds it to a list of missing
27
+ * dependencies if it is not.
28
+ * @param packageName The name of the package to check.
29
+ */
30
+ check(packageName: string): void;
31
+ /**
32
+ * Verifies that at least one of a list of packages is installed. If none are
33
+ * installed, a custom error message is added to the list of errors.
34
+ * @param packageNames An array of package names to check.
35
+ * @param customErrorMessage The error message to use if none of the packages are found.
36
+ */
37
+ checkAny(packageNames: string[], customErrorMessage: string): void;
38
+ /**
39
+ * Throws a `MissingDependenciesError` if any dependencies were found to be missing.
40
+ * The error message is a formatted list of all missing packages.
41
+ */
42
+ report(): void;
43
+ }
@@ -0,0 +1,82 @@
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.DependencyChecker = exports.MissingDependenciesError = void 0;
11
+ const node_module_1 = require("node:module");
12
+ /**
13
+ * A custom error class to represent missing dependency errors.
14
+ * This is used to avoid printing a stack trace for this expected error.
15
+ */
16
+ class MissingDependenciesError extends Error {
17
+ constructor(message) {
18
+ super(message);
19
+ this.name = 'MissingDependenciesError';
20
+ }
21
+ }
22
+ exports.MissingDependenciesError = MissingDependenciesError;
23
+ class DependencyChecker {
24
+ resolver;
25
+ missingDependencies = new Set();
26
+ constructor(projectSourceRoot) {
27
+ this.resolver = (0, node_module_1.createRequire)(projectSourceRoot + '/').resolve;
28
+ }
29
+ /**
30
+ * Checks if a package is installed.
31
+ * @param packageName The name of the package to check.
32
+ * @returns True if the package is found, false otherwise.
33
+ */
34
+ isInstalled(packageName) {
35
+ try {
36
+ this.resolver(packageName);
37
+ return true;
38
+ }
39
+ catch {
40
+ return false;
41
+ }
42
+ }
43
+ /**
44
+ * Verifies that a package is installed and adds it to a list of missing
45
+ * dependencies if it is not.
46
+ * @param packageName The name of the package to check.
47
+ */
48
+ check(packageName) {
49
+ if (!this.isInstalled(packageName)) {
50
+ this.missingDependencies.add(packageName);
51
+ }
52
+ }
53
+ /**
54
+ * Verifies that at least one of a list of packages is installed. If none are
55
+ * installed, a custom error message is added to the list of errors.
56
+ * @param packageNames An array of package names to check.
57
+ * @param customErrorMessage The error message to use if none of the packages are found.
58
+ */
59
+ checkAny(packageNames, customErrorMessage) {
60
+ if (packageNames.every((name) => !this.isInstalled(name))) {
61
+ // This is a custom error, so we add it directly.
62
+ // Using a Set avoids duplicate custom messages.
63
+ this.missingDependencies.add(customErrorMessage);
64
+ }
65
+ }
66
+ /**
67
+ * Throws a `MissingDependenciesError` if any dependencies were found to be missing.
68
+ * The error message is a formatted list of all missing packages.
69
+ */
70
+ report() {
71
+ if (this.missingDependencies.size === 0) {
72
+ return;
73
+ }
74
+ let message = 'The following packages are required but were not found:\n';
75
+ for (const name of this.missingDependencies) {
76
+ message += ` - ${name}\n`;
77
+ }
78
+ message += 'Please install the missing packages and rerun the test command.';
79
+ throw new MissingDependenciesError(message);
80
+ }
81
+ }
82
+ exports.DependencyChecker = DependencyChecker;
@@ -79,12 +79,36 @@ class KarmaExecutor {
79
79
  codeCoverage: !!unitTestOptions.codeCoverage,
80
80
  codeCoverageExclude: unitTestOptions.codeCoverage?.exclude,
81
81
  fileReplacements: buildTargetOptions.fileReplacements,
82
- reporters: unitTestOptions.reporters,
82
+ reporters: unitTestOptions.reporters?.map((reporter) => {
83
+ // Karma only supports string reporters.
84
+ if (Object.keys(reporter[1]).length > 0) {
85
+ context.logger.warn(`The "karma" test runner does not support options for the "${reporter[0]}" reporter. The options will be ignored.`);
86
+ }
87
+ return reporter[0];
88
+ }),
83
89
  webWorkerTsConfig: buildTargetOptions.webWorkerTsConfig,
84
90
  aot: buildTargetOptions.aot,
85
91
  };
92
+ const transformOptions = {
93
+ karmaOptions: (options) => {
94
+ if (unitTestOptions.filter) {
95
+ let filter = unitTestOptions.filter;
96
+ if (filter[0] === '/' && filter.at(-1) === '/') {
97
+ this.context.logger.warn('The `--filter` option is always a regular expression.' +
98
+ 'Leading and trailing `/` are not required and will be ignored.');
99
+ }
100
+ else {
101
+ filter = `/${filter}/`;
102
+ }
103
+ options.client ??= {};
104
+ options.client.args ??= [];
105
+ options.client.args.push('--grep', filter);
106
+ }
107
+ return options;
108
+ },
109
+ };
86
110
  const { execute } = await Promise.resolve().then(() => __importStar(require('../../../karma')));
87
- yield* execute(karmaOptions, context);
111
+ yield* execute(karmaOptions, context, transformOptions);
88
112
  }
89
113
  async [Symbol.asyncDispose]() {
90
114
  // The Karma builder handles its own teardown
@@ -7,6 +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
+ const dependency_checker_1 = require("../dependency-checker");
10
11
  const executor_1 = require("./executor");
11
12
  /**
12
13
  * A declarative definition of the Karma test runner.
@@ -14,6 +15,22 @@ const executor_1 = require("./executor");
14
15
  const KarmaTestRunner = {
15
16
  name: 'karma',
16
17
  isStandalone: true,
18
+ validateDependencies(options) {
19
+ const checker = new dependency_checker_1.DependencyChecker(options.projectSourceRoot);
20
+ checker.check('karma');
21
+ checker.check('karma-jasmine');
22
+ // Check for browser launchers
23
+ if (options.browsers?.length) {
24
+ for (const browser of options.browsers) {
25
+ const launcherName = `karma-${browser.toLowerCase().split('headless')[0]}-launcher`;
26
+ checker.check(launcherName);
27
+ }
28
+ }
29
+ if (options.codeCoverage) {
30
+ checker.check('karma-coverage');
31
+ }
32
+ checker.report();
33
+ },
17
34
  getBuildOptions() {
18
35
  return {
19
36
  buildOptions: {},
@@ -5,7 +5,8 @@
5
5
  * Use of this source code is governed by an MIT-style license that can be
6
6
  * found in the LICENSE file at https://angular.dev/license
7
7
  */
8
- export declare function setupBrowserConfiguration(browsers: string[] | undefined, debug: boolean, projectSourceRoot: string): {
8
+ export interface BrowserConfiguration {
9
9
  browser?: import('vitest/node').BrowserConfigOptions;
10
10
  errors?: string[];
11
- };
11
+ }
12
+ export declare function setupBrowserConfiguration(browsers: string[] | undefined, debug: boolean, projectSourceRoot: string): BrowserConfiguration;
@@ -16,7 +16,8 @@ const path_1 = require("../../../../utils/path");
16
16
  const schema_1 = require("../../../application/schema");
17
17
  const options_1 = require("../../options");
18
18
  const test_discovery_1 = require("../../test-discovery");
19
- function createTestBedInitVirtualFile(providersFile, projectSourceRoot) {
19
+ function createTestBedInitVirtualFile(providersFile, projectSourceRoot, polyfills = []) {
20
+ const usesZoneJS = polyfills.includes('zone.js');
20
21
  let providersImport = 'const providers = [];';
21
22
  if (providersFile) {
22
23
  const relativePath = node_path_1.default.relative(projectSourceRoot, providersFile);
@@ -26,7 +27,7 @@ function createTestBedInitVirtualFile(providersFile, projectSourceRoot) {
26
27
  }
27
28
  return `
28
29
  // Initialize the Angular testing environment
29
- import { NgModule } from '@angular/core';
30
+ import { NgModule${usesZoneJS ? ', provideZoneChangeDetection' : ''} } from '@angular/core';
30
31
  import { getTestBed, ɵgetCleanupHook as getCleanupHook } from '@angular/core/testing';
31
32
  import { BrowserTestingModule, platformBrowserTesting } from '@angular/platform-browser/testing';
32
33
  ${providersImport}
@@ -34,7 +35,7 @@ function createTestBedInitVirtualFile(providersFile, projectSourceRoot) {
34
35
  beforeEach(getCleanupHook(false));
35
36
  afterEach(getCleanupHook(true));
36
37
  @NgModule({
37
- providers,
38
+ providers: [${usesZoneJS ? 'provideZoneChangeDetection(), ' : ''}...providers],
38
39
  })
39
40
  export class TestModule {}
40
41
  getTestBed().initTestEnvironment([BrowserTestingModule, TestModule], platformBrowserTesting(), {
@@ -91,11 +92,12 @@ async function getVitestBuildOptions(options, baseBuildOptions) {
91
92
  externalDependencies: ['vitest', '@vitest/browser/context'],
92
93
  };
93
94
  buildOptions.polyfills = (0, options_1.injectTestingPolyfills)(buildOptions.polyfills);
94
- const testBedInitContents = createTestBedInitVirtualFile(providersFile, projectSourceRoot);
95
+ const testBedInitContents = createTestBedInitVirtualFile(providersFile, projectSourceRoot, buildOptions.polyfills);
95
96
  return {
96
97
  buildOptions,
97
98
  virtualFiles: {
98
99
  'angular:test-bed-init': testBedInitContents,
99
100
  },
101
+ testEntryPointMappings: entryPoints,
100
102
  };
101
103
  }
@@ -13,13 +13,12 @@ export declare class VitestExecutor implements TestExecutor {
13
13
  private vitest;
14
14
  private readonly projectName;
15
15
  private readonly options;
16
- private buildResultFiles;
17
- private testFileToEntryPoint;
18
- private entryPointToTestFile;
19
- constructor(projectName: string, options: NormalizedUnitTestBuilderOptions);
16
+ private readonly buildResultFiles;
17
+ private readonly testFileToEntryPoint;
18
+ private readonly entryPointToTestFile;
19
+ constructor(projectName: string, options: NormalizedUnitTestBuilderOptions, testEntryPointMappings: Map<string, string> | undefined);
20
20
  execute(buildResult: FullResult | IncrementalResult): AsyncIterable<BuilderOutput>;
21
21
  [Symbol.asyncDispose](): Promise<void>;
22
22
  private prepareSetupFiles;
23
- private createVitestPlugins;
24
23
  private initializeVitest;
25
24
  }