From 71983f83ac3d535bbd5c993496a6c05885819e27 Mon Sep 17 00:00:00 2001 From: Doug Parker Date: Thu, 11 Jan 2024 10:12:29 -0800 Subject: [PATCH] refactor: add `node:` prefix to imports of Jest builder --- .../build_angular/src/builders/jest/index.ts | 55 +++++++++++++++++-- .../src/builders/jest/jest.config.mjs | 11 ++++ .../e2e/tests/jest/custom-config.ts | 53 ++++++++++++++++++ 3 files changed, 115 insertions(+), 4 deletions(-) create mode 100644 packages/angular_devkit/build_angular/src/builders/jest/jest.config.mjs create mode 100644 tests/legacy-cli/e2e/tests/jest/custom-config.ts diff --git a/packages/angular_devkit/build_angular/src/builders/jest/index.ts b/packages/angular_devkit/build_angular/src/builders/jest/index.ts index 64491c8bcb04..6a5f4affe5c3 100644 --- a/packages/angular_devkit/build_angular/src/builders/jest/index.ts +++ b/packages/angular_devkit/build_angular/src/builders/jest/index.ts @@ -7,9 +7,10 @@ */ import { BuilderContext, BuilderOutput, createBuilder } from '@angular-devkit/architect'; -import { execFile as execFileCb } from 'child_process'; -import * as path from 'path'; -import { promisify } from 'util'; +import { execFile as execFileCb } from 'node:child_process'; +import * as fs from 'node:fs/promises'; +import * as path from 'node:path'; +import { promisify } from 'node:util'; import { colors } from '../../utils/color'; import { findTestFiles } from '../../utils/test-files'; import { buildApplicationInternal } from '../application'; @@ -54,8 +55,25 @@ export default createBuilder( }; } + const [testFiles, customConfig] = await Promise.all([ + 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 testFiles = await findTestFiles(options.include, options.exclude, context.workspaceRoot); const jestGlobal = path.join(__dirname, 'jest-global.mjs'); const initTestBed = path.join(__dirname, 'init-test-bed.mjs'); const buildResult = await build(context, { @@ -85,6 +103,7 @@ export default createBuilder( jest, `--rootDir="${path.join(testOut, 'browser')}"`, + `--config=${path.join(__dirname, 'jest.config.mjs')}`, '--testEnvironment=jsdom', // TODO(dgp1130): Enable cache once we have a mechanism for properly clearing / disabling it. @@ -162,3 +181,31 @@ function resolveModule(module: string): string | undefined { return undefined; } } + +/** Returns whether or not the provided directory includes a Jest configuration file. */ +async function findCustomJestConfig(dir: string): Promise { + 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: string | undefined; + 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; +} diff --git a/packages/angular_devkit/build_angular/src/builders/jest/jest.config.mjs b/packages/angular_devkit/build_angular/src/builders/jest/jest.config.mjs new file mode 100644 index 000000000000..3fa377339eda --- /dev/null +++ b/packages/angular_devkit/build_angular/src/builders/jest/jest.config.mjs @@ -0,0 +1,11 @@ +/** + * @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.io/license + */ + +// Empty config file, everything is specified via CLI options right now. +// This file is used just so Jest doesn't accidentally inherit a custom user-specified Jest config. +export default {}; diff --git a/tests/legacy-cli/e2e/tests/jest/custom-config.ts b/tests/legacy-cli/e2e/tests/jest/custom-config.ts new file mode 100644 index 000000000000..10e481a43fcc --- /dev/null +++ b/tests/legacy-cli/e2e/tests/jest/custom-config.ts @@ -0,0 +1,53 @@ +import { deleteFile, writeFile } from '../../utils/fs'; +import { applyJestBuilder } from '../../utils/jest'; +import { ng } from '../../utils/process'; +import { updateJsonFile } from '../../utils/project'; + +export default async function (): Promise { + await applyJestBuilder(); + + { + // Users may incorrectly write a Jest config believing it to be used by Angular. + await writeFile( + 'jest.config.mjs', + ` + export default { + runner: 'does-not-exist', + }; + `.trim(), + ); + + // Should not fail from the above (broken) configuration. Shouldn't use it at all. + const { stderr } = await ng('test'); + + // Should warn that a Jest configuration was found but not used. + if (!stderr.includes('A custom Jest config was found')) { + throw new Error(`No warning about custom Jest config:\nSTDERR:\n\n${stderr}`); + } + if (!stderr.includes('jest.config.mjs')) { + throw new Error(`Warning did not call out 'jest.config.mjs':\nSTDERR:\n\n${stderr}`); + } + + await deleteFile('jest.config.mjs'); + } + + { + // Use `package.json` configuration instead of a `jest.config` file. + await updateJsonFile('package.json', (json) => { + json['jest'] = { + runner: 'does-not-exist', + }; + }); + + // Should not fail from the above (broken) configuration. Shouldn't use it at all. + const { stderr } = await ng('test'); + + // Should warn that a Jest configuration was found but not used. + if (!stderr.includes('A custom Jest config was found')) { + throw new Error(`No warning about custom Jest config:\nSTDERR:\n\n${stderr}`); + } + if (!stderr.includes('package.json')) { + throw new Error(`Warning did not call out 'package.json':\nSTDERR:\n\n${stderr}`); + } + } +}