Skip to content

Commit e99fffc

Browse files
author
Angular Builds
committed
54594b5 feat(@angular-devkit/build-angular): support karma with esbuild
1 parent 4366e7e commit e99fffc

15 files changed

+641
-233
lines changed

package.json

Lines changed: 8 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,16 +1,16 @@
11
{
22
"name": "@angular-devkit/build-angular",
3-
"version": "19.0.0-next.7+sha-4179bf2",
3+
"version": "19.0.0-next.7+sha-54594b5",
44
"description": "Angular Webpack Build Facade",
55
"main": "src/index.js",
66
"typings": "src/index.d.ts",
77
"builders": "builders.json",
88
"dependencies": {
99
"@ampproject/remapping": "2.3.0",
10-
"@angular-devkit/architect": "github:angular/angular-devkit-architect-builds#4179bf2",
11-
"@angular-devkit/build-webpack": "github:angular/angular-devkit-build-webpack-builds#4179bf2",
12-
"@angular-devkit/core": "github:angular/angular-devkit-core-builds#4179bf2",
13-
"@angular/build": "github:angular/angular-build-builds#4179bf2",
10+
"@angular-devkit/architect": "github:angular/angular-devkit-architect-builds#54594b5",
11+
"@angular-devkit/build-webpack": "github:angular/angular-devkit-build-webpack-builds#54594b5",
12+
"@angular-devkit/core": "github:angular/angular-devkit-core-builds#54594b5",
13+
"@angular/build": "github:angular/angular-build-builds#54594b5",
1414
"@babel/core": "7.25.2",
1515
"@babel/generator": "7.25.6",
1616
"@babel/helper-annotate-as-pure": "7.24.7",
@@ -21,7 +21,7 @@
2121
"@babel/preset-env": "7.25.4",
2222
"@babel/runtime": "7.25.6",
2323
"@discoveryjs/json-ext": "0.6.1",
24-
"@ngtools/webpack": "github:angular/ngtools-webpack-builds#4179bf2",
24+
"@ngtools/webpack": "github:angular/ngtools-webpack-builds#54594b5",
2525
"@vitejs/plugin-basic-ssl": "1.1.0",
2626
"ansi-colors": "4.1.3",
2727
"autoprefixer": "10.4.20",
@@ -77,7 +77,7 @@
7777
"@angular/localize": "^19.0.0-next.0",
7878
"@angular/platform-server": "^19.0.0-next.0",
7979
"@angular/service-worker": "^19.0.0-next.0",
80-
"@angular/ssr": "github:angular/angular-ssr-builds#4179bf2",
80+
"@angular/ssr": "github:angular/angular-ssr-builds#54594b5",
8181
"@web/test-runner": "^0.19.0",
8282
"browser-sync": "^3.0.2",
8383
"jest": "^29.5.0",
@@ -98,7 +98,7 @@
9898
"@angular/service-worker": {
9999
"optional": true
100100
},
101-
"@angular/ssr": "github:angular/angular-ssr-builds#4179bf2",
101+
"@angular/ssr": "github:angular/angular-ssr-builds#54594b5",
102102
"@web/test-runner": {
103103
"optional": true
104104
},
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
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 { ResultFile } from '@angular/build/private';
9+
import { BuilderContext, BuilderOutput } from '@angular-devkit/architect';
10+
import type { ConfigOptions } from 'karma';
11+
import { Observable } from 'rxjs';
12+
import { Configuration } from 'webpack';
13+
import { ExecutionTransformer } from '../../transforms';
14+
import { Schema as KarmaBuilderOptions } from './schema';
15+
export declare function execute(options: KarmaBuilderOptions, context: BuilderContext, karmaOptions: ConfigOptions, transforms?: {
16+
webpackConfiguration?: ExecutionTransformer<Configuration>;
17+
karmaOptions?: (options: ConfigOptions) => ConfigOptions;
18+
}): Observable<BuilderOutput>;
19+
export declare function writeTestFiles(files: Record<string, ResultFile>, testDir: string): Promise<void>;
Lines changed: 217 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,217 @@
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 __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 (mod) {
26+
if (mod && mod.__esModule) return mod;
27+
var result = {};
28+
if (mod != null) for (var k in mod) if (k !== "default" && Object.prototype.hasOwnProperty.call(mod, k)) __createBinding(result, mod, k);
29+
__setModuleDefault(result, mod);
30+
return result;
31+
};
32+
Object.defineProperty(exports, "__esModule", { value: true });
33+
exports.execute = execute;
34+
exports.writeTestFiles = writeTestFiles;
35+
const build_1 = require("@angular/build");
36+
const private_1 = require("@angular/build/private");
37+
const crypto_1 = require("crypto");
38+
const fs = __importStar(require("fs/promises"));
39+
const path = __importStar(require("path"));
40+
const rxjs_1 = require("rxjs");
41+
const read_tsconfig_1 = require("../../utils/read-tsconfig");
42+
const schema_1 = require("../browser-esbuild/schema");
43+
const find_tests_1 = require("./find-tests");
44+
class ApplicationBuildError extends Error {
45+
constructor(message) {
46+
super(message);
47+
this.name = 'ApplicationBuildError';
48+
}
49+
}
50+
function execute(options, context, karmaOptions, transforms = {}) {
51+
return (0, rxjs_1.from)(initializeApplication(options, context, karmaOptions, transforms)).pipe((0, rxjs_1.switchMap)(([karma, karmaConfig]) => new rxjs_1.Observable((subscriber) => {
52+
// Complete the observable once the Karma server returns.
53+
const karmaServer = new karma.Server(karmaConfig, (exitCode) => {
54+
subscriber.next({ success: exitCode === 0 });
55+
subscriber.complete();
56+
});
57+
const karmaStart = karmaServer.start();
58+
// Cleanup, signal Karma to exit.
59+
return () => {
60+
void karmaStart.then(() => karmaServer.stop());
61+
};
62+
})), (0, rxjs_1.catchError)((err) => {
63+
if (err instanceof ApplicationBuildError) {
64+
return (0, rxjs_1.of)({ success: false, message: err.message });
65+
}
66+
throw err;
67+
}), (0, rxjs_1.defaultIfEmpty)({ success: false }));
68+
}
69+
async function getProjectSourceRoot(context) {
70+
// We have already validated that the project name is set before calling this function.
71+
const projectName = context.target?.project;
72+
if (!projectName) {
73+
return context.workspaceRoot;
74+
}
75+
const projectMetadata = await context.getProjectMetadata(projectName);
76+
const sourceRoot = (projectMetadata.sourceRoot ?? projectMetadata.root ?? '');
77+
return path.join(context.workspaceRoot, sourceRoot);
78+
}
79+
async function collectEntrypoints(options, context) {
80+
const projectSourceRoot = await getProjectSourceRoot(context);
81+
// Glob for files to test.
82+
const testFiles = await (0, find_tests_1.findTests)(options.include ?? [], options.exclude ?? [], context.workspaceRoot, projectSourceRoot);
83+
const entryPoints = new Set([
84+
...testFiles,
85+
'@angular-devkit/build-angular/src/builders/karma/init_test_bed.js',
86+
]);
87+
// Extract `zone.js/testing` to a separate entry point because it needs to be loaded after Jasmine.
88+
const [polyfills, hasZoneTesting] = extractZoneTesting(options.polyfills);
89+
if (hasZoneTesting) {
90+
entryPoints.add('zone.js/testing');
91+
}
92+
const tsConfigPath = path.resolve(context.workspaceRoot, options.tsConfig);
93+
const tsConfig = await (0, read_tsconfig_1.readTsconfig)(tsConfigPath);
94+
const localizePackageInitEntryPoint = '@angular/localize/init';
95+
const hasLocalizeType = tsConfig.options.types?.some((t) => t === '@angular/localize' || t === localizePackageInitEntryPoint);
96+
if (hasLocalizeType) {
97+
polyfills.push(localizePackageInitEntryPoint);
98+
}
99+
return [entryPoints, polyfills];
100+
}
101+
async function initializeApplication(options, context, karmaOptions, transforms = {}) {
102+
if (transforms.webpackConfiguration) {
103+
context.logger.warn(`This build is using the application builder but transforms.webpackConfiguration was provided. The transform will be ignored.`);
104+
}
105+
const testDir = path.join(context.workspaceRoot, 'dist/test-out', (0, crypto_1.randomUUID)());
106+
const [karma, [entryPoints, polyfills]] = await Promise.all([
107+
Promise.resolve().then(() => __importStar(require('karma'))),
108+
collectEntrypoints(options, context),
109+
fs.rm(testDir, { recursive: true, force: true }),
110+
]);
111+
const outputPath = testDir;
112+
// Build tests with `application` builder, using test files as entry points.
113+
const buildOutput = await first((0, private_1.buildApplicationInternal)({
114+
entryPoints,
115+
tsConfig: options.tsConfig,
116+
outputPath,
117+
aot: false,
118+
index: false,
119+
outputHashing: schema_1.OutputHashing.None,
120+
optimization: false,
121+
sourceMap: {
122+
scripts: true,
123+
styles: true,
124+
vendor: true,
125+
},
126+
styles: options.styles,
127+
polyfills,
128+
webWorkerTsConfig: options.webWorkerTsConfig,
129+
}, context));
130+
if (buildOutput.kind === private_1.ResultKind.Failure) {
131+
throw new ApplicationBuildError('Build failed');
132+
}
133+
else if (buildOutput.kind !== private_1.ResultKind.Full) {
134+
throw new ApplicationBuildError('A full build result is required from the application builder.');
135+
}
136+
// Write test files
137+
await writeTestFiles(buildOutput.files, testDir);
138+
karmaOptions.files ??= [];
139+
karmaOptions.files.push(
140+
// Serve polyfills first.
141+
{ pattern: `${testDir}/polyfills.js`, type: 'module' },
142+
// Allow loading of chunk-* files but don't include them all on load.
143+
{ pattern: `${testDir}/chunk-*.js`, type: 'module', included: false },
144+
// Allow loading of worker-* files but don't include them all on load.
145+
{ pattern: `${testDir}/worker-*.js`, type: 'module', included: false },
146+
// `zone.js/testing`, served but not included on page load.
147+
{ pattern: `${testDir}/testing.js`, type: 'module', included: false },
148+
// Serve remaining JS on page load, these are the test entrypoints.
149+
{ pattern: `${testDir}/*.js`, type: 'module' });
150+
if (options.styles?.length) {
151+
// Serve CSS outputs on page load, these are the global styles.
152+
karmaOptions.files.push({ pattern: `${testDir}/*.css`, type: 'css' });
153+
}
154+
const parsedKarmaConfig = await karma.config.parseConfig(options.karmaConfig && path.resolve(context.workspaceRoot, options.karmaConfig), transforms.karmaOptions ? transforms.karmaOptions(karmaOptions) : karmaOptions, { promiseConfig: true, throwErrors: true });
155+
// Remove the webpack plugin/framework:
156+
// Alternative would be to make the Karma plugin "smart" but that's a tall order
157+
// with managing unneeded imports etc..
158+
const pluginLengthBefore = (parsedKarmaConfig.plugins ?? []).length;
159+
parsedKarmaConfig.plugins = (parsedKarmaConfig.plugins ?? []).filter((plugin) => {
160+
if (typeof plugin === 'string') {
161+
return plugin !== 'framework:@angular-devkit/build-angular';
162+
}
163+
return !plugin['framework:@angular-devkit/build-angular'];
164+
});
165+
parsedKarmaConfig.frameworks = parsedKarmaConfig.frameworks?.filter((framework) => framework !== '@angular-devkit/build-angular');
166+
const pluginLengthAfter = (parsedKarmaConfig.plugins ?? []).length;
167+
if (pluginLengthBefore !== pluginLengthAfter) {
168+
context.logger.warn(`Ignoring framework "@angular-devkit/build-angular" from karma config file because it's not compatible with the application builder.`);
169+
}
170+
// When using code-coverage, auto-add karma-coverage.
171+
// This was done as part of the karma plugin for webpack.
172+
if (options.codeCoverage &&
173+
!parsedKarmaConfig.reporters?.some((r) => r === 'coverage' || r === 'coverage-istanbul')) {
174+
parsedKarmaConfig.reporters = (parsedKarmaConfig.reporters ?? []).concat(['coverage']);
175+
}
176+
return [karma, parsedKarmaConfig];
177+
}
178+
async function writeTestFiles(files, testDir) {
179+
const directoryExists = new Set();
180+
// Writes the test related output files to disk and ensures the containing directories are present
181+
await (0, private_1.emitFilesToDisk)(Object.entries(files), async ([filePath, file]) => {
182+
if (file.type !== build_1.BuildOutputFileType.Browser && file.type !== build_1.BuildOutputFileType.Media) {
183+
return;
184+
}
185+
const fullFilePath = path.join(testDir, filePath);
186+
// Ensure output subdirectories exist
187+
const fileBasePath = path.dirname(fullFilePath);
188+
if (fileBasePath && !directoryExists.has(fileBasePath)) {
189+
await fs.mkdir(fileBasePath, { recursive: true });
190+
directoryExists.add(fileBasePath);
191+
}
192+
if (file.origin === 'memory') {
193+
// Write file contents
194+
await fs.writeFile(fullFilePath, file.contents);
195+
}
196+
else {
197+
// Copy file contents
198+
await fs.copyFile(file.inputPath, fullFilePath, fs.constants.COPYFILE_FICLONE);
199+
}
200+
});
201+
}
202+
function extractZoneTesting(polyfills) {
203+
if (typeof polyfills === 'string') {
204+
polyfills = [polyfills];
205+
}
206+
polyfills ??= [];
207+
const polyfillsWithoutZoneTesting = polyfills.filter((polyfill) => polyfill !== 'zone.js/testing');
208+
const hasZoneTesting = polyfills.length !== polyfillsWithoutZoneTesting.length;
209+
return [polyfillsWithoutZoneTesting, hasZoneTesting];
210+
}
211+
/** Returns the first item yielded by the given generator and cancels the execution. */
212+
async function first(generator) {
213+
for await (const value of generator) {
214+
return value;
215+
}
216+
throw new Error('Expected generator to emit at least once.');
217+
}
Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,21 @@
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 { BuilderContext, BuilderOutput } from '@angular-devkit/architect';
9+
import type { ConfigOptions } from 'karma';
10+
import { Observable } from 'rxjs';
11+
import { Configuration } from 'webpack';
12+
import { ExecutionTransformer } from '../../transforms';
13+
import { Schema as KarmaBuilderOptions } from './schema';
14+
export type KarmaConfigOptions = ConfigOptions & {
15+
buildWebpack?: unknown;
16+
configFile?: string;
17+
};
18+
export declare function execute(options: KarmaBuilderOptions, context: BuilderContext, karmaOptions: KarmaConfigOptions, transforms?: {
19+
webpackConfiguration?: ExecutionTransformer<Configuration>;
20+
karmaOptions?: (options: KarmaConfigOptions) => KarmaConfigOptions;
21+
}): Observable<BuilderOutput>;

0 commit comments

Comments
 (0)