@angular-devkit/build-angular
Version:
Angular Webpack Build Facade
162 lines (161 loc) • 6.87 kB
JavaScript
/**
* @license
* Copyright Google LLC All Rights Reserved.
*
* Use of this source code is governed by an MIT-style license that can be
* found in the LICENSE file at https://angular.dev/license
*/
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const private_1 = require("@angular/build/private");
const architect_1 = require("@angular-devkit/architect");
const node_crypto_1 = require("node:crypto");
const promises_1 = __importDefault(require("node:fs/promises"));
const node_module_1 = require("node:module");
const node_path_1 = __importDefault(require("node:path"));
const test_files_1 = require("../../utils/test-files");
const schema_1 = require("../browser-esbuild/schema");
const builder_status_warnings_1 = require("./builder-status-warnings");
const options_1 = require("./options");
const write_test_files_1 = require("./write-test-files");
exports.default = (0, architect_1.createBuilder)(async (schema, ctx) => {
ctx.logger.warn('NOTE: The Web Test Runner builder is currently EXPERIMENTAL and not ready for production use.');
(0, builder_status_warnings_1.logBuilderStatusWarnings)(schema, ctx);
// Dynamic import `@web/test-runner` from the user's workspace. As an optional peer dep, it may not be installed
// and may not be resolvable from `@angular-devkit/build-angular`.
const require = (0, node_module_1.createRequire)(`${ctx.workspaceRoot}/`);
let wtr;
try {
wtr = require('@web/test-runner');
}
catch {
return {
success: false,
// TODO(dgp1130): Display a more accurate message for non-NPM users.
error: 'Web Test Runner is not installed, most likely you need to run `npm install @web/test-runner --save-dev` in your project.',
};
}
const options = (0, options_1.normalizeOptions)(schema);
const testDir = node_path_1.default.join(ctx.workspaceRoot, 'dist/test-out', (0, node_crypto_1.randomUUID)());
// Parallelize startup work.
const [testFiles] = await Promise.all([
// Glob for files to test.
(0, test_files_1.findTestFiles)(options.include, options.exclude, ctx.workspaceRoot),
// Clean build output path.
promises_1.default.rm(testDir, { recursive: true, force: true }),
]);
// Build the tests and abort on any build failure.
const buildOutput = await buildTests(testFiles, testDir, options, ctx);
if (buildOutput.kind === private_1.ResultKind.Failure) {
return { success: false };
}
else if (buildOutput.kind !== private_1.ResultKind.Full) {
return {
success: false,
error: 'A full build result is required from the application builder.',
};
}
// Write test files
await (0, write_test_files_1.writeTestFiles)(buildOutput.files, testDir);
// Run the built tests.
return await runTests(wtr, testDir, options);
});
/** Build all the given test files and write the result to the given output path. */
async function buildTests(testFiles, outputPath, options, ctx) {
const entryPoints = new Set([
...testFiles,
'jasmine-core/lib/jasmine-core/jasmine.js',
'@angular-devkit/build-angular/src/builders/web-test-runner/jasmine_runner.js',
]);
// Extract `zone.js/testing` to a separate entry point because it needs to be loaded after Jasmine.
const [polyfills, hasZoneTesting] = extractZoneTesting(options.polyfills);
if (hasZoneTesting) {
entryPoints.add('zone.js/testing');
}
// Build tests with `application` builder, using test files as entry points.
// Also bundle in Jasmine and the Jasmine runner script, which need to share chunked dependencies.
const buildOutput = await first((0, private_1.buildApplicationInternal)({
entryPoints,
tsConfig: options.tsConfig,
outputPath,
aot: options.aot,
index: false,
outputHashing: schema_1.OutputHashing.None,
optimization: false,
externalDependencies: [
// Resolved by `@web/test-runner` at runtime with dynamically generated code.
'@web/test-runner-core',
],
sourceMap: {
scripts: true,
styles: true,
vendor: true,
},
polyfills,
}, ctx));
return buildOutput;
}
function extractZoneTesting(polyfills) {
const polyfillsWithoutZoneTesting = polyfills.filter((polyfill) => polyfill !== 'zone.js/testing');
const hasZoneTesting = polyfills.length !== polyfillsWithoutZoneTesting.length;
return [polyfillsWithoutZoneTesting, hasZoneTesting];
}
/** Run Web Test Runner on the given directory of bundled JavaScript tests. */
async function runTests(wtr, testDir, options) {
const testPagePath = node_path_1.default.resolve(__dirname, 'test_page.html');
const testPage = await promises_1.default.readFile(testPagePath, 'utf8');
const runner = await wtr.startTestRunner({
config: {
rootDir: testDir,
files: [
`${testDir}/**/*.js`,
`!${testDir}/polyfills.js`,
`!${testDir}/chunk-*.js`,
`!${testDir}/jasmine.js`,
`!${testDir}/jasmine_runner.js`,
`!${testDir}/testing.js`, // `zone.js/testing`
],
testFramework: {
config: {
defaultTimeoutInterval: 5_000,
},
},
nodeResolve: true,
port: 9876,
watch: options.watch ?? false,
testRunnerHtml: (_testFramework, _config) => testPage,
},
readCliArgs: false,
readFileConfig: false,
autoExitProcess: false,
});
if (!runner) {
throw new Error('Failed to start Web Test Runner.');
}
// Wait for the tests to complete and stop the runner.
const passed = (await once(runner, 'finished'));
await runner.stop();
// No need to return error messages because Web Test Runner already printed them to the console.
return { success: passed };
}
/** Returns the first item yielded by the given generator and cancels the execution. */
async function first(generator) {
for await (const value of generator) {
return value;
}
throw new Error('Expected generator to emit at least once.');
}
/** Listens for a single emission of an event and returns the value emitted. */
// eslint-disable-next-line @typescript-eslint/no-explicit-any
function once(emitter, event) {
return new Promise((resolve) => {
const onEmit = (arg) => {
emitter.off(event, onEmit);
resolve(arg);
};
emitter.on(event, onEmit);
});
}
;