UNPKG

@angular-devkit/build-angular

Version:
213 lines (212 loc) 10.3 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 __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) { if (k2 === undefined) k2 = k; var desc = Object.getOwnPropertyDescriptor(m, k); if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) { desc = { enumerable: true, get: function() { return m[k]; } }; } Object.defineProperty(o, k2, desc); }) : (function(o, m, k, k2) { if (k2 === undefined) k2 = k; o[k2] = m[k]; })); var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) { Object.defineProperty(o, "default", { enumerable: true, value: v }); }) : function(o, v) { o["default"] = v; }); var __importStar = (this && this.__importStar) || (function () { var ownKeys = function(o) { ownKeys = Object.getOwnPropertyNames || function (o) { var ar = []; for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k; return ar; }; return ownKeys(o); }; return function (mod) { if (mod && mod.__esModule) return mod; var result = {}; if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]); __setModuleDefault(result, mod); return result; }; })(); Object.defineProperty(exports, "__esModule", { value: true }); const private_1 = require("@angular/build/private"); const architect_1 = require("@angular-devkit/architect"); const node_child_process_1 = require("node:child_process"); const node_crypto_1 = require("node:crypto"); const fs = __importStar(require("node:fs/promises")); const path = __importStar(require("node:path")); const node_util_1 = require("node:util"); const color_1 = require("../../utils/color"); const test_files_1 = require("../../utils/test-files"); const schema_1 = require("../browser-esbuild/schema"); const write_test_files_1 = require("../web-test-runner/write-test-files"); const options_1 = require("./options"); const execFile = (0, node_util_1.promisify)(node_child_process_1.execFile); /** Main execution function for the Jest builder. */ exports.default = (0, architect_1.createBuilder)(async (schema, context) => { context.logger.warn('NOTE: The Jest builder is currently EXPERIMENTAL and not ready for production use.'); const options = (0, options_1.normalizeOptions)(schema); const testOut = path.join(context.workspaceRoot, 'dist/test-out', (0, node_crypto_1.randomUUID)()); // TODO(dgp1130): Hide in temp directory. // Verify Jest installation and get the path to it's binary. // We need to `node_modules/.bin/jest`, but there is no means to resolve that directly. Fortunately Jest's `package.json` exports the // same file at `bin/jest`, so we can just resolve that instead. const jest = resolveModule('jest/bin/jest'); if (!jest) { return { success: false, // TODO(dgp1130): Display a more accurate message for non-NPM users. error: 'Jest is not installed, most likely you need to run `npm install jest --save-dev` in your project.', }; } // Verify that JSDom is installed in the project. const environment = resolveModule('jest-environment-jsdom'); if (!environment) { return { success: false, // TODO(dgp1130): Display a more accurate message for non-NPM users. error: '`jest-environment-jsdom` is not installed. Install it with `npm install jest-environment-jsdom --save-dev`.', }; } const [testFiles, customConfig] = await Promise.all([ (0, test_files_1.findTestFiles)(options.include, options.exclude, context.workspaceRoot), findCustomJestConfig(context.workspaceRoot), ]); // Warn if a custom Jest configuration is found. We won't use it, so if a developer is trying to use a custom config, this hopefully // makes a better experience than silently ignoring the configuration. // Ideally, this would be a hard error. However a Jest config could exist for testing other files in the workspace outside of Angular // CLI, so we likely can't produce a hard error in this situation without an opt-out. if (customConfig) { context.logger.warn('A custom Jest config was found, but this is not supported by `@angular-devkit/build-angular:jest` and will be' + ` ignored: ${customConfig}. This is an experiment to see if completely abstracting away Jest's configuration is viable. Please` + ` consider if your use case can be met without directly modifying the Jest config. If this is a major obstacle for your use` + ` case, please post it in this issue so we can collect feedback and evaluate: https://github.com/angular/angular-cli/issues/25434.`); } // Build all the test files. const jestGlobal = path.join(__dirname, 'jest-global.mjs'); const initTestBed = path.join(__dirname, 'init-test-bed.mjs'); const buildResult = await first((0, private_1.buildApplicationInternal)({ // Build all the test files and also the `jest-global` and `init-test-bed` scripts. entryPoints: new Set([...testFiles, jestGlobal, initTestBed]), tsConfig: options.tsConfig, polyfills: options.polyfills ?? ['zone.js', 'zone.js/testing'], outputPath: testOut, aot: options.aot, index: false, outputHashing: schema_1.OutputHashing.None, outExtension: 'mjs', // Force native ESM. optimization: false, sourceMap: { scripts: true, styles: false, vendor: false, }, }, context)); if (buildResult.kind === private_1.ResultKind.Failure) { return { success: false }; } else if (buildResult.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)(buildResult.files, testOut); // Execute Jest on the built output directory. const jestProc = execFile(process.execPath, [ '--experimental-vm-modules', jest, `--rootDir="${testOut}"`, `--config=${path.join(__dirname, 'jest.config.mjs')}`, '--testEnvironment=jsdom', // TODO(dgp1130): Enable cache once we have a mechanism for properly clearing / disabling it. '--no-cache', // Run basically all files in the output directory, any excluded files were already dropped by the build. `--testMatch="<rootDir>/**/*.mjs"`, // Load polyfills and initialize the environment before executing each test file. // IMPORTANT: Order matters here. // First, we execute `jest-global.mjs` to initialize the `jest` global variable. // Second, we execute user polyfills, including `zone.js` and `zone.js/testing`. This is dependent on the Jest global so it can patch // the environment for fake async to work correctly. // Third, we initialize `TestBed`. This is dependent on fake async being set up correctly beforehand. `--setupFilesAfterEnv="<rootDir>/jest-global.mjs"`, ...(options.polyfills ? [`--setupFilesAfterEnv="<rootDir>/polyfills.mjs"`] : []), `--setupFilesAfterEnv="<rootDir>/init-test-bed.mjs"`, // Don't run any infrastructure files as tests, they are manually loaded where needed. `--testPathIgnorePatterns="<rootDir>/jest-global\\.mjs"`, ...(options.polyfills ? [`--testPathIgnorePatterns="<rootDir>/polyfills\\.mjs"`] : []), `--testPathIgnorePatterns="<rootDir>/init-test-bed\\.mjs"`, // Skip shared chunks, as they are not entry points to tests. `--testPathIgnorePatterns="<rootDir>/chunk-.*\\.mjs"`, // Optionally enable color. ...(color_1.colors.enabled ? ['--colors'] : []), ]); // Stream test output to the terminal. jestProc.child.stdout?.on('data', (chunk) => { context.logger.info(chunk); }); jestProc.child.stderr?.on('data', (chunk) => { // Write to stderr directly instead of `context.logger.error(chunk)` because the logger will overwrite Jest's coloring information. process.stderr.write(chunk); }); try { await jestProc; } catch (error) { // No need to propagate error message, already piped to terminal output. // TODO(dgp1130): Handle process spawning failures. return { success: false }; } return { success: true }; }); /** 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.'); } /** Safely resolves the given Node module string. */ function resolveModule(module) { try { return require.resolve(module); } catch { return undefined; } } /** Returns whether or not the provided directory includes a Jest configuration file. */ async function findCustomJestConfig(dir) { const entries = await fs.readdir(dir, { withFileTypes: true }); // Jest supports many file extensions (`js`, `ts`, `cjs`, `cts`, `json`, etc.) Just look // for anything with that prefix. const config = entries.find((entry) => entry.isFile() && entry.name.startsWith('jest.config.')); if (config) { return path.join(dir, config.name); } // Jest also supports a `jest` key in `package.json`, look for a config there. const packageJsonPath = path.join(dir, 'package.json'); let packageJson; try { packageJson = await fs.readFile(packageJsonPath, 'utf8'); } catch { return undefined; // No package.json, therefore no Jest configuration in it. } const json = JSON.parse(packageJson); if ('jest' in json) { return packageJsonPath; } return undefined; }