@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
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@angular/build",
3
- "version": "21.0.0-next.3",
3
+ "version": "21.0.0-next.4",
4
4
  "description": "Official build system for Angular",
5
5
  "keywords": [
6
6
  "Angular CLI",
@@ -23,25 +23,25 @@
23
23
  "builders": "builders.json",
24
24
  "dependencies": {
25
25
  "@ampproject/remapping": "2.3.0",
26
- "@angular-devkit/architect": "0.2100.0-next.3",
26
+ "@angular-devkit/architect": "0.2100.0-next.4",
27
27
  "@babel/core": "7.28.4",
28
28
  "@babel/helper-annotate-as-pure": "7.27.3",
29
29
  "@babel/helper-split-export-declaration": "7.24.7",
30
- "@inquirer/confirm": "5.1.16",
30
+ "@inquirer/confirm": "5.1.18",
31
31
  "@vitejs/plugin-basic-ssl": "2.1.0",
32
32
  "beasties": "0.3.5",
33
- "browserslist": "^4.23.0",
33
+ "browserslist": "^4.26.0",
34
34
  "esbuild": "0.25.9",
35
35
  "https-proxy-agent": "7.0.6",
36
36
  "istanbul-lib-instrument": "6.0.3",
37
37
  "jsonc-parser": "3.3.1",
38
- "listr2": "9.0.3",
38
+ "listr2": "9.0.4",
39
39
  "magic-string": "0.30.19",
40
40
  "mrmime": "2.0.1",
41
41
  "parse5-html-rewriting-stream": "8.0.0",
42
42
  "picomatch": "4.0.3",
43
43
  "piscina": "5.1.3",
44
- "rolldown": "1.0.0-beta.36",
44
+ "rolldown": "1.0.0-beta.38",
45
45
  "sass": "1.92.1",
46
46
  "semver": "7.7.2",
47
47
  "source-map-support": "0.5.21",
@@ -60,7 +60,7 @@
60
60
  "@angular/platform-browser": "^21.0.0-next.0",
61
61
  "@angular/platform-server": "^21.0.0-next.0",
62
62
  "@angular/service-worker": "^21.0.0-next.0",
63
- "@angular/ssr": "^21.0.0-next.3",
63
+ "@angular/ssr": "^21.0.0-next.4",
64
64
  "karma": "^6.4.0",
65
65
  "less": "^4.2.0",
66
66
  "ng-packagr": "^21.0.0-next.0",
@@ -112,7 +112,7 @@
112
112
  "type": "git",
113
113
  "url": "https://github.com/angular/angular-cli.git"
114
114
  },
115
- "packageManager": "pnpm@10.15.1",
115
+ "packageManager": "pnpm@10.17.0",
116
116
  "engines": {
117
117
  "node": "^20.19.0 || ^22.12.0 || >=24.0.0",
118
118
  "npm": "^6.11.0 || ^7.5.6 || >=8.0.0",
@@ -81,7 +81,8 @@ context, extensions) {
81
81
  }
82
82
  const buildTime = Number(process.hrtime.bigint() - startTime) / 10 ** 9;
83
83
  const hasError = result.errors.length > 0;
84
- result.addLog(`Application bundle generation ${hasError ? 'failed' : 'complete'}. [${buildTime.toFixed(3)} seconds]\n`);
84
+ result.addLog(`Application bundle generation ${hasError ? 'failed' : 'complete'}.` +
85
+ ` [${buildTime.toFixed(3)} seconds] - ${new Date().toISOString()}\n`);
85
86
  }
86
87
  return result;
87
88
  }, {
@@ -6,8 +6,6 @@
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 { ResultFile } from '../application/results';
10
9
  import { Schema as KarmaBuilderOptions } from './schema';
11
10
  import type { KarmaBuilderTransformsOptions } from './index';
12
11
  export declare function execute(options: KarmaBuilderOptions, context: BuilderContext, transforms?: KarmaBuilderTransformsOptions): AsyncIterable<BuilderOutput>;
13
- export declare function writeTestFiles(files: Record<string, ResultFile>, testDir: string): Promise<void>;
@@ -44,204 +44,33 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
44
44
  };
45
45
  Object.defineProperty(exports, "__esModule", { value: true });
46
46
  exports.execute = execute;
47
- exports.writeTestFiles = writeTestFiles;
48
47
  const node_crypto_1 = require("node:crypto");
49
48
  const fs = __importStar(require("node:fs/promises"));
50
- const node_module_1 = require("node:module");
51
49
  const node_path_1 = __importDefault(require("node:path"));
52
- const tinyglobby_1 = require("tinyglobby");
53
- const bundler_context_1 = require("../../tools/esbuild/bundler-context");
54
- const utils_1 = require("../../tools/esbuild/utils");
50
+ const web_1 = require("node:stream/web");
55
51
  const virtual_module_plugin_1 = require("../../tools/esbuild/virtual-module-plugin");
56
- const project_metadata_1 = require("../../utils/project-metadata");
52
+ const test_files_1 = require("../../utils/test-files");
57
53
  const index_1 = require("../application/index");
58
54
  const results_1 = require("../application/results");
59
55
  const schema_1 = require("../application/schema");
60
- const find_tests_1 = require("./find-tests");
56
+ const assets_middleware_1 = require("./assets-middleware");
57
+ const coverage_1 = require("./coverage");
58
+ const karma_config_1 = require("./karma-config");
61
59
  const options_1 = require("./options");
62
- const localResolve = (0, node_module_1.createRequire)(__filename).resolve;
63
- const isWindows = process.platform === 'win32';
60
+ const polyfills_plugin_1 = require("./polyfills-plugin");
61
+ const progress_reporter_1 = require("./progress-reporter");
62
+ const utils_1 = require("./utils");
64
63
  class ApplicationBuildError extends Error {
65
64
  constructor(message) {
66
65
  super(message);
67
66
  this.name = 'ApplicationBuildError';
68
67
  }
69
68
  }
70
- const LATEST_BUILD_FILES_TOKEN = 'angularLatestBuildFiles';
71
- class AngularAssetsMiddleware {
72
- serveFile;
73
- latestBuildFiles;
74
- static $inject = ['serveFile', LATEST_BUILD_FILES_TOKEN];
75
- static NAME = 'angular-test-assets';
76
- constructor(serveFile, latestBuildFiles) {
77
- this.serveFile = serveFile;
78
- this.latestBuildFiles = latestBuildFiles;
79
- }
80
- handle(req, res, next) {
81
- const url = new URL(`http://${req.headers['host']}${req.url}`);
82
- // Remove the leading slash from the URL path and convert to platform specific.
83
- // The latest build files will use the platform path separator.
84
- let pathname = url.pathname.slice(1);
85
- if (isWindows) {
86
- pathname = pathname.replaceAll(node_path_1.default.posix.sep, node_path_1.default.win32.sep);
87
- }
88
- const file = this.latestBuildFiles.files[pathname];
89
- if (!file) {
90
- next();
91
- return;
92
- }
93
- // Implementation of serverFile can be found here:
94
- // https://github.com/karma-runner/karma/blob/84f85e7016efc2266fa6b3465f494a3fa151c85c/lib/middleware/common.js#L10
95
- switch (file.origin) {
96
- case 'disk':
97
- this.serveFile(file.inputPath, undefined, res, undefined, undefined, /* doNotCache */ true);
98
- break;
99
- case 'memory':
100
- // Include pathname to help with Content-Type headers.
101
- this.serveFile(`/unused/${url.pathname}`, undefined, res, undefined, file.contents,
102
- /* doNotCache */ false);
103
- break;
104
- }
105
- }
106
- static createPlugin(initialFiles) {
107
- return {
108
- [LATEST_BUILD_FILES_TOKEN]: ['value', { files: { ...initialFiles.files } }],
109
- [`middleware:${AngularAssetsMiddleware.NAME}`]: [
110
- 'factory',
111
- Object.assign((...args) => {
112
- const inst = new AngularAssetsMiddleware(...args);
113
- return inst.handle.bind(inst);
114
- }, AngularAssetsMiddleware),
115
- ],
116
- };
117
- }
118
- }
119
- class AngularPolyfillsPlugin {
120
- static $inject = ['config.files'];
121
- static NAME = 'angular-polyfills';
122
- static createPlugin(polyfillsFile, jasmineCleanupFiles, scriptsFiles) {
123
- return {
124
- // This has to be a "reporter" because reporters run _after_ frameworks
125
- // and karma-jasmine-html-reporter injects additional scripts that may
126
- // depend on Jasmine but aren't modules - which means that they would run
127
- // _before_ all module code (including jasmine).
128
- [`reporter:${AngularPolyfillsPlugin.NAME}`]: [
129
- 'factory',
130
- Object.assign((files) => {
131
- // The correct order is zone.js -> jasmine -> zone.js/testing.
132
- // Jasmine has to see the patched version of the global `setTimeout`
133
- // function so it doesn't cache the unpatched version. And /testing
134
- // needs to see the global `jasmine` object so it can patch it.
135
- const polyfillsIndex = 0;
136
- files.splice(polyfillsIndex, 0, polyfillsFile);
137
- // Insert just before test_main.js.
138
- const zoneTestingIndex = files.findIndex((f) => {
139
- if (typeof f === 'string') {
140
- return false;
141
- }
142
- return f.pattern.endsWith('/test_main.js');
143
- });
144
- if (zoneTestingIndex === -1) {
145
- throw new Error('Could not find test entrypoint file.');
146
- }
147
- files.splice(zoneTestingIndex, 0, jasmineCleanupFiles);
148
- // We need to ensure that all files are served as modules, otherwise
149
- // the order in the files list gets really confusing: Karma doesn't
150
- // set defer on scripts, so all scripts with type=js will run first,
151
- // even if type=module files appeared earlier in `files`.
152
- for (const f of files) {
153
- if (typeof f === 'string') {
154
- throw new Error(`Unexpected string-based file: "${f}"`);
155
- }
156
- if (f.included === false) {
157
- // Don't worry about files that aren't included on the initial
158
- // page load. `type` won't affect them.
159
- continue;
160
- }
161
- if (f.pattern.endsWith('.js') && 'js' === (f.type ?? 'js')) {
162
- f.type = 'module';
163
- }
164
- }
165
- // Add "scripts" option files as classic scripts
166
- files.unshift(...scriptsFiles);
167
- // Add browser sourcemap support as a classic script
168
- files.unshift({
169
- pattern: localResolve('source-map-support/browser-source-map-support.js'),
170
- included: true,
171
- watched: false,
172
- });
173
- // Karma needs a return value for a factory and Karma's multi-reporter expects an `adapters` array
174
- return { adapters: [] };
175
- }, AngularPolyfillsPlugin),
176
- ],
177
- };
178
- }
179
- }
180
- function injectKarmaReporter(buildOptions, buildIterator, karmaConfig, controller) {
181
- const reporterName = 'angular-progress-notifier';
182
- class ProgressNotifierReporter {
183
- emitter;
184
- latestBuildFiles;
185
- static $inject = ['emitter', LATEST_BUILD_FILES_TOKEN];
186
- constructor(emitter, latestBuildFiles) {
187
- this.emitter = emitter;
188
- this.latestBuildFiles = latestBuildFiles;
189
- this.startWatchingBuild();
190
- }
191
- startWatchingBuild() {
192
- void (async () => {
193
- // This is effectively "for await of but skip what's already consumed".
194
- let isDone = false; // to mark the loop condition as "not constant".
195
- while (!isDone) {
196
- const { done, value: buildOutput } = await buildIterator.next();
197
- if (done) {
198
- isDone = true;
199
- break;
200
- }
201
- if (buildOutput.kind === results_1.ResultKind.Failure) {
202
- controller.enqueue({ success: false, message: 'Build failed' });
203
- }
204
- else if (buildOutput.kind === results_1.ResultKind.Incremental ||
205
- buildOutput.kind === results_1.ResultKind.Full) {
206
- if (buildOutput.kind === results_1.ResultKind.Full) {
207
- this.latestBuildFiles.files = buildOutput.files;
208
- }
209
- else {
210
- this.latestBuildFiles.files = {
211
- ...this.latestBuildFiles.files,
212
- ...buildOutput.files,
213
- };
214
- }
215
- await writeTestFiles(buildOutput.files, buildOptions.outputPath);
216
- this.emitter.refreshFiles();
217
- }
218
- }
219
- })();
220
- }
221
- onRunComplete = function (_browsers, results) {
222
- if (results.exitCode === 0) {
223
- controller.enqueue({ success: true });
224
- }
225
- else {
226
- controller.enqueue({ success: false });
227
- }
228
- };
229
- }
230
- karmaConfig.reporters ??= [];
231
- karmaConfig.reporters.push(reporterName);
232
- karmaConfig.plugins ??= [];
233
- karmaConfig.plugins.push({
234
- [`reporter:${reporterName}`]: [
235
- 'factory',
236
- Object.assign((...args) => new ProgressNotifierReporter(...args), ProgressNotifierReporter),
237
- ],
238
- });
239
- }
240
69
  function execute(options, context, transforms) {
241
70
  const normalizedOptions = (0, options_1.normalizeOptions)(context, options);
242
- const karmaOptions = getBaseKarmaOptions(normalizedOptions, context);
71
+ const karmaOptions = (0, karma_config_1.getBaseKarmaOptions)(normalizedOptions, context);
243
72
  let karmaServer;
244
- return new ReadableStream({
73
+ return new web_1.ReadableStream({
245
74
  async start(controller) {
246
75
  let init;
247
76
  try {
@@ -259,7 +88,7 @@ function execute(options, context, transforms) {
259
88
  // If `--watch` is explicitly enabled or if we are keeping the Karma
260
89
  // process running, we should hook Karma into the build.
261
90
  if (buildIterator) {
262
- injectKarmaReporter(buildOptions, buildIterator, karmaConfig, controller);
91
+ (0, progress_reporter_1.injectKarmaReporter)(buildOptions, buildIterator, karmaConfig, controller);
263
92
  }
264
93
  // Close the stream once the Karma server returns.
265
94
  karmaServer = new karma.Server(karmaConfig, (exitCode) => {
@@ -273,43 +102,18 @@ function execute(options, context, transforms) {
273
102
  },
274
103
  });
275
104
  }
276
- async function getProjectSourceRoot(context) {
277
- // We have already validated that the project name is set before calling this function.
278
- const projectName = context.target?.project;
279
- if (!projectName) {
280
- return context.workspaceRoot;
281
- }
282
- const projectMetadata = await context.getProjectMetadata(projectName);
283
- const { projectSourceRoot } = (0, project_metadata_1.getProjectRootPaths)(context.workspaceRoot, projectMetadata);
284
- return projectSourceRoot;
285
- }
286
- function normalizePolyfills(polyfills = []) {
287
- const jasmineGlobalEntryPoint = localResolve('./polyfills/jasmine_global.js');
288
- const jasmineGlobalCleanupEntrypoint = localResolve('./polyfills/jasmine_global_cleanup.js');
289
- const sourcemapEntrypoint = localResolve('./polyfills/init_sourcemaps.js');
290
- const zoneTestingEntryPoint = 'zone.js/testing';
291
- const polyfillsExludingZoneTesting = polyfills.filter((p) => p !== zoneTestingEntryPoint);
292
- return [
293
- polyfillsExludingZoneTesting.concat([jasmineGlobalEntryPoint, sourcemapEntrypoint]),
294
- polyfillsExludingZoneTesting.length === polyfills.length
295
- ? [jasmineGlobalCleanupEntrypoint]
296
- : [jasmineGlobalCleanupEntrypoint, zoneTestingEntryPoint],
297
- ];
298
- }
299
- async function collectEntrypoints(options, context, projectSourceRoot) {
300
- // Glob for files to test.
301
- const testFiles = await (0, find_tests_1.findTests)(options.include, options.exclude, context.workspaceRoot, projectSourceRoot);
302
- return (0, find_tests_1.getTestEntrypoints)(testFiles, { projectSourceRoot, workspaceRoot: context.workspaceRoot });
303
- }
304
- // eslint-disable-next-line max-lines-per-function
305
105
  async function initializeApplication(options, context, karmaOptions, transforms) {
106
+ const karma = await Promise.resolve().then(() => __importStar(require('karma')));
107
+ const projectSourceRoot = await (0, utils_1.getProjectSourceRoot)(context);
306
108
  const outputPath = node_path_1.default.join(context.workspaceRoot, 'dist/test-out', (0, node_crypto_1.randomUUID)());
307
- const projectSourceRoot = await getProjectSourceRoot(context);
308
- const [karma, entryPoints] = await Promise.all([
309
- Promise.resolve().then(() => __importStar(require('karma'))),
310
- collectEntrypoints(options, context, projectSourceRoot),
311
- fs.rm(outputPath, { recursive: true, force: true }),
312
- ]);
109
+ await fs.rm(outputPath, { recursive: true, force: true });
110
+ const { buildOptions, mainName } = await setupBuildOptions(options, context, projectSourceRoot, outputPath);
111
+ const [buildOutput, buildIterator] = await runEsbuild(buildOptions, context, projectSourceRoot);
112
+ const karmaConfig = await configureKarma(karma, context, karmaOptions, options, buildOptions, buildOutput, mainName, transforms);
113
+ return [karma, karmaConfig, buildOptions, buildIterator];
114
+ }
115
+ async function setupBuildOptions(options, context, projectSourceRoot, outputPath) {
116
+ const entryPoints = await (0, utils_1.collectEntrypoints)(options, context, projectSourceRoot);
313
117
  const mainName = 'test_main';
314
118
  if (options.main) {
315
119
  entryPoints.set(mainName, options.main);
@@ -318,9 +122,9 @@ async function initializeApplication(options, context, karmaOptions, transforms)
318
122
  entryPoints.set(mainName, 'angular:test-bed-init');
319
123
  }
320
124
  const instrumentForCoverage = options.codeCoverage
321
- ? createInstrumentationFilter(projectSourceRoot, getInstrumentationExcludedPaths(context.workspaceRoot, options.codeCoverageExclude ?? []))
125
+ ? (0, coverage_1.createInstrumentationFilter)(projectSourceRoot, (0, coverage_1.getInstrumentationExcludedPaths)(context.workspaceRoot, options.codeCoverageExclude ?? []))
322
126
  : undefined;
323
- const [polyfills, jasmineCleanup] = normalizePolyfills(options.polyfills);
127
+ const [polyfills, jasmineCleanup] = (0, utils_1.normalizePolyfills)(options.polyfills);
324
128
  for (let idx = 0; idx < jasmineCleanup.length; ++idx) {
325
129
  entryPoints.set(`jasmine-cleanup-${idx}`, jasmineCleanup[idx]);
326
130
  }
@@ -348,6 +152,9 @@ async function initializeApplication(options, context, karmaOptions, transforms)
348
152
  loader: options.loader,
349
153
  externalDependencies: options.externalDependencies,
350
154
  };
155
+ return { buildOptions, mainName };
156
+ }
157
+ async function runEsbuild(buildOptions, context, projectSourceRoot) {
351
158
  const virtualTestBedInit = (0, virtual_module_plugin_1.createVirtualModulePlugin)({
352
159
  namespace: 'angular:test-bed-init',
353
160
  loadContent: async () => {
@@ -358,7 +165,7 @@ async function initializeApplication(options, context, karmaOptions, transforms)
358
165
  `getTestBed().initTestEnvironment(BrowserTestingModule, platformBrowserTesting(), {`,
359
166
  ` errorOnUnknownElements: true,`,
360
167
  ` errorOnUnknownProperties: true,`,
361
- '});',
168
+ `});`,
362
169
  ];
363
170
  return {
364
171
  contents: contents.join('\n'),
@@ -368,7 +175,7 @@ async function initializeApplication(options, context, karmaOptions, transforms)
368
175
  },
369
176
  });
370
177
  // Build tests with `application` builder, using test files as entry points.
371
- const [buildOutput, buildIterator] = await first((0, index_1.buildApplicationInternal)(buildOptions, context, { codePlugins: [virtualTestBedInit] }), { cancel: !buildOptions.watch });
178
+ const [buildOutput, buildIterator] = await (0, utils_1.first)((0, index_1.buildApplicationInternal)(buildOptions, context, { codePlugins: [virtualTestBedInit] }), { cancel: !buildOptions.watch });
372
179
  if (buildOutput.kind === results_1.ResultKind.Failure) {
373
180
  throw new ApplicationBuildError('Build failed');
374
181
  }
@@ -376,7 +183,11 @@ async function initializeApplication(options, context, karmaOptions, transforms)
376
183
  throw new ApplicationBuildError('A full build result is required from the application builder.');
377
184
  }
378
185
  // Write test files
379
- await writeTestFiles(buildOutput.files, buildOptions.outputPath);
186
+ await (0, test_files_1.writeTestFiles)(buildOutput.files, buildOptions.outputPath);
187
+ return [buildOutput, buildIterator];
188
+ }
189
+ async function configureKarma(karma, context, karmaOptions, options, buildOptions, buildOutput, mainName, transforms) {
190
+ const outputPath = buildOptions.outputPath;
380
191
  // We need to add this to the beginning *after* the testing framework has
381
192
  // prepended its files. The output path is required for each since they are
382
193
  // added later in the test process via a plugin.
@@ -422,7 +233,7 @@ async function initializeApplication(options, context, karmaOptions, transforms)
422
233
  { pattern: `*.map`, included: false, watched: false },
423
234
  // These are the test entrypoints.
424
235
  { pattern: `spec-*.js`, type: 'module', watched: false });
425
- if (hasChunkOrWorkerFiles(buildOutput.files)) {
236
+ if ((0, utils_1.hasChunkOrWorkerFiles)(buildOutput.files)) {
426
237
  karmaOptions.files.push(
427
238
  // Allow loading of chunk-* files but don't include them all on load.
428
239
  {
@@ -462,12 +273,12 @@ async function initializeApplication(options, context, karmaOptions, transforms)
462
273
  if (pluginLengthBefore !== pluginLengthAfter) {
463
274
  context.logger.warn(`Ignoring framework "@angular-devkit/build-angular" from karma config file because it's not compatible with the application builder.`);
464
275
  }
465
- parsedKarmaConfig.plugins.push(AngularAssetsMiddleware.createPlugin(buildOutput));
276
+ parsedKarmaConfig.plugins.push(assets_middleware_1.AngularAssetsMiddleware.createPlugin(buildOutput));
466
277
  parsedKarmaConfig.middleware ??= [];
467
- parsedKarmaConfig.middleware.push(AngularAssetsMiddleware.NAME);
468
- parsedKarmaConfig.plugins.push(AngularPolyfillsPlugin.createPlugin(polyfillsFile, jasmineCleanupFiles, scriptsFiles));
278
+ parsedKarmaConfig.middleware.push(assets_middleware_1.AngularAssetsMiddleware.NAME);
279
+ parsedKarmaConfig.plugins.push(polyfills_plugin_1.AngularPolyfillsPlugin.createPlugin(polyfillsFile, jasmineCleanupFiles, scriptsFiles));
469
280
  parsedKarmaConfig.reporters ??= [];
470
- parsedKarmaConfig.reporters.push(AngularPolyfillsPlugin.NAME);
281
+ parsedKarmaConfig.reporters.push(polyfills_plugin_1.AngularPolyfillsPlugin.NAME);
471
282
  // Adjust karma junit reporter outDir location to maintain previous (devkit) behavior
472
283
  // The base path for the reporter was previously the workspace root.
473
284
  // To keep the files in the same location, the reporter's output directory is adjusted
@@ -497,128 +308,5 @@ async function initializeApplication(options, context, karmaOptions, transforms)
497
308
  !parsedKarmaConfig.reporters?.some((r) => r === 'coverage' || r === 'coverage-istanbul')) {
498
309
  parsedKarmaConfig.reporters = (parsedKarmaConfig.reporters ?? []).concat(['coverage']);
499
310
  }
500
- return [karma, parsedKarmaConfig, buildOptions, buildIterator];
501
- }
502
- function hasChunkOrWorkerFiles(files) {
503
- return Object.keys(files).some((filename) => {
504
- return /(?:^|\/)(?:worker|chunk)[^/]+\.js$/.test(filename);
505
- });
506
- }
507
- async function writeTestFiles(files, testDir) {
508
- const directoryExists = new Set();
509
- // Writes the test related output files to disk and ensures the containing directories are present
510
- await (0, utils_1.emitFilesToDisk)(Object.entries(files), async ([filePath, file]) => {
511
- if (file.type !== bundler_context_1.BuildOutputFileType.Browser && file.type !== bundler_context_1.BuildOutputFileType.Media) {
512
- return;
513
- }
514
- const fullFilePath = node_path_1.default.join(testDir, filePath);
515
- // Ensure output subdirectories exist
516
- const fileBasePath = node_path_1.default.dirname(fullFilePath);
517
- if (fileBasePath && !directoryExists.has(fileBasePath)) {
518
- await fs.mkdir(fileBasePath, { recursive: true });
519
- directoryExists.add(fileBasePath);
520
- }
521
- if (file.origin === 'memory') {
522
- // Write file contents
523
- await fs.writeFile(fullFilePath, file.contents);
524
- }
525
- else {
526
- // Copy file contents
527
- await fs.copyFile(file.inputPath, fullFilePath, fs.constants.COPYFILE_FICLONE);
528
- }
529
- });
530
- }
531
- /** Returns the first item yielded by the given generator and cancels the execution. */
532
- async function first(generator, { cancel }) {
533
- if (!cancel) {
534
- const iterator = generator[Symbol.asyncIterator]();
535
- const firstValue = await iterator.next();
536
- if (firstValue.done) {
537
- throw new Error('Expected generator to emit at least once.');
538
- }
539
- return [firstValue.value, iterator];
540
- }
541
- for await (const value of generator) {
542
- return [value, null];
543
- }
544
- throw new Error('Expected generator to emit at least once.');
545
- }
546
- function createInstrumentationFilter(includedBasePath, excludedPaths) {
547
- return (request) => {
548
- return (!excludedPaths.has(request) &&
549
- !/\.(e2e|spec)\.tsx?$|[\\/]node_modules[\\/]/.test(request) &&
550
- request.startsWith(includedBasePath));
551
- };
552
- }
553
- function getInstrumentationExcludedPaths(root, excludedPaths) {
554
- const excluded = new Set();
555
- for (const excludeGlob of excludedPaths) {
556
- const excludePath = excludeGlob[0] === '/' ? excludeGlob.slice(1) : excludeGlob;
557
- (0, tinyglobby_1.globSync)(excludePath, { cwd: root }).forEach((p) => excluded.add(node_path_1.default.join(root, p)));
558
- }
559
- return excluded;
560
- }
561
- function getBaseKarmaOptions(options, context) {
562
- // Determine project name from builder context target
563
- const projectName = context.target?.project;
564
- if (!projectName) {
565
- throw new Error(`The 'karma' builder requires a target to be specified.`);
566
- }
567
- const karmaOptions = options.karmaConfig
568
- ? {}
569
- : getBuiltInKarmaConfig(context.workspaceRoot, projectName);
570
- const singleRun = !options.watch;
571
- karmaOptions.singleRun = singleRun;
572
- // Workaround https://github.com/angular/angular-cli/issues/28271, by clearing context by default
573
- // for single run executions. Not clearing context for multi-run (watched) builds allows the
574
- // Jasmine Spec Runner to be visible in the browser after test execution.
575
- karmaOptions.client ??= {};
576
- karmaOptions.client.clearContext ??= singleRun;
577
- // Convert browsers from a string to an array
578
- if (options.browsers) {
579
- karmaOptions.browsers = options.browsers;
580
- }
581
- if (options.reporters) {
582
- karmaOptions.reporters = options.reporters;
583
- }
584
- return karmaOptions;
585
- }
586
- function getBuiltInKarmaConfig(workspaceRoot, projectName) {
587
- let coverageFolderName = projectName.charAt(0) === '@' ? projectName.slice(1) : projectName;
588
- coverageFolderName = coverageFolderName.toLowerCase();
589
- const workspaceRootRequire = (0, node_module_1.createRequire)(workspaceRoot + '/');
590
- // Any changes to the config here need to be synced to: packages/schematics/angular/config/files/karma.conf.js.template
591
- return {
592
- basePath: '',
593
- frameworks: ['jasmine'],
594
- plugins: [
595
- 'karma-jasmine',
596
- 'karma-chrome-launcher',
597
- 'karma-jasmine-html-reporter',
598
- 'karma-coverage',
599
- ].map((p) => workspaceRootRequire(p)),
600
- jasmineHtmlReporter: {
601
- suppressAll: true, // removes the duplicated traces
602
- },
603
- coverageReporter: {
604
- dir: node_path_1.default.join(workspaceRoot, 'coverage', coverageFolderName),
605
- subdir: '.',
606
- reporters: [{ type: 'html' }, { type: 'text-summary' }],
607
- },
608
- reporters: ['progress', 'kjhtml'],
609
- browsers: ['Chrome'],
610
- customLaunchers: {
611
- // Chrome configured to run in a bazel sandbox.
612
- // Disable the use of the gpu and `/dev/shm` because it causes Chrome to
613
- // crash on some environments.
614
- // See:
615
- // https://github.com/puppeteer/puppeteer/blob/v1.0.0/docs/troubleshooting.md#tips
616
- // https://stackoverflow.com/questions/50642308/webdriverexception-unknown-error-devtoolsactiveport-file-doesnt-exist-while-t
617
- ChromeHeadlessNoSandbox: {
618
- base: 'ChromeHeadless',
619
- flags: ['--no-sandbox', '--headless', '--disable-gpu', '--disable-dev-shm-usage'],
620
- },
621
- },
622
- restartOnFileChange: true,
623
- };
311
+ return parsedKarmaConfig;
624
312
  }
@@ -0,0 +1,26 @@
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 { InlinePluginDef } from 'karma';
9
+ import type { IncomingMessage, ServerResponse } from 'node:http';
10
+ import type { ResultFile } from '../application/results';
11
+ interface ServeFileFunction {
12
+ (filepath: string, rangeHeader: string | string[] | undefined, response: ServerResponse, transform?: (c: string | Uint8Array) => string | Uint8Array, content?: string | Uint8Array, doNotCache?: boolean): void;
13
+ }
14
+ export interface LatestBuildFiles {
15
+ files: Record<string, ResultFile | undefined>;
16
+ }
17
+ export declare class AngularAssetsMiddleware {
18
+ private readonly serveFile;
19
+ private readonly latestBuildFiles;
20
+ static readonly $inject: string[];
21
+ static readonly NAME = "angular-test-assets";
22
+ constructor(serveFile: ServeFileFunction, latestBuildFiles: LatestBuildFiles);
23
+ handle(req: IncomingMessage, res: ServerResponse, next: (err?: unknown) => unknown): void;
24
+ static createPlugin(initialFiles: LatestBuildFiles): InlinePluginDef;
25
+ }
26
+ export {};
@@ -0,0 +1,65 @@
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.AngularAssetsMiddleware = void 0;
14
+ const node_path_1 = __importDefault(require("node:path"));
15
+ const isWindows = process.platform === 'win32';
16
+ const LATEST_BUILD_FILES_TOKEN = 'angularLatestBuildFiles';
17
+ class AngularAssetsMiddleware {
18
+ serveFile;
19
+ latestBuildFiles;
20
+ static $inject = ['serveFile', LATEST_BUILD_FILES_TOKEN];
21
+ static NAME = 'angular-test-assets';
22
+ constructor(serveFile, latestBuildFiles) {
23
+ this.serveFile = serveFile;
24
+ this.latestBuildFiles = latestBuildFiles;
25
+ }
26
+ handle(req, res, next) {
27
+ const url = new URL(`http://${req.headers['host'] ?? ''}${req.url ?? ''}`);
28
+ // Remove the leading slash from the URL path and convert to platform specific.
29
+ // The latest build files will use the platform path separator.
30
+ let pathname = url.pathname.slice(1);
31
+ if (isWindows) {
32
+ pathname = pathname.replaceAll(node_path_1.default.posix.sep, node_path_1.default.win32.sep);
33
+ }
34
+ const file = this.latestBuildFiles.files[pathname];
35
+ if (!file) {
36
+ next();
37
+ return;
38
+ }
39
+ // Implementation of serverFile can be found here:
40
+ // https://github.com/karma-runner/karma/blob/84f85e7016efc2266fa6b3465f494a3fa151c85c/lib/middleware/common.js#L10
41
+ switch (file.origin) {
42
+ case 'disk':
43
+ this.serveFile(file.inputPath, undefined, res, undefined, undefined, /* doNotCache */ true);
44
+ break;
45
+ case 'memory':
46
+ // Include pathname to help with Content-Type headers.
47
+ this.serveFile(`/unused/${url.pathname}`, undefined, res, undefined, file.contents,
48
+ /* doNotCache */ false);
49
+ break;
50
+ }
51
+ }
52
+ static createPlugin(initialFiles) {
53
+ return {
54
+ [LATEST_BUILD_FILES_TOKEN]: ['value', { files: { ...initialFiles.files } }],
55
+ [`middleware:${AngularAssetsMiddleware.NAME}`]: [
56
+ 'factory',
57
+ Object.assign((...args) => {
58
+ const inst = new AngularAssetsMiddleware(...args);
59
+ return inst.handle.bind(inst);
60
+ }, AngularAssetsMiddleware),
61
+ ],
62
+ };
63
+ }
64
+ }
65
+ exports.AngularAssetsMiddleware = AngularAssetsMiddleware;
@@ -0,0 +1,9 @@
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
+ export declare function createInstrumentationFilter(includedBasePath: string, excludedPaths: Set<string>): (request: string) => boolean;
9
+ export declare function getInstrumentationExcludedPaths(root: string, excludedPaths: string[]): Set<string>;