UNPKG

@angular-devkit/build-angular

Version:
162 lines (161 loc) 6.87 kB
"use strict"; /** * @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); }); }