UNPKG

@nx/jest

Version:

The Nx Plugin for Jest contains executors and generators allowing your workspace to use the powerful Jest testing capabilities.

506 lines (505 loc) • 22.9 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.createNodes = exports.createNodesV2 = void 0; const devkit_1 = require("@nx/devkit"); const calculate_hash_for_create_nodes_1 = require("@nx/devkit/src/utils/calculate-hash-for-create-nodes"); const config_utils_1 = require("@nx/devkit/src/utils/config-utils"); const get_named_inputs_1 = require("@nx/devkit/src/utils/get-named-inputs"); const fs_1 = require("fs"); const minimatch_1 = require("minimatch"); const devkit_internals_1 = require("nx/src/devkit-internals"); const package_json_1 = require("nx/src/plugins/package-json"); const cache_directory_1 = require("nx/src/utils/cache-directory"); const globs_1 = require("nx/src/utils/globs"); const path_1 = require("path"); const version_utils_1 = require("../utils/version-utils"); const workspace_context_1 = require("nx/src/utils/workspace-context"); const node_path_1 = require("node:path"); const pmc = (0, devkit_1.getPackageManagerCommand)(); function readTargetsCache(cachePath) { return (0, fs_1.existsSync)(cachePath) ? (0, devkit_1.readJsonFile)(cachePath) : {}; } function writeTargetsToCache(cachePath, results) { (0, devkit_1.writeJsonFile)(cachePath, results); } const jestConfigGlob = '**/jest.config.{cjs,mjs,js,cts,mts,ts}'; exports.createNodesV2 = [ jestConfigGlob, async (configFiles, options, context) => { const optionsHash = (0, devkit_internals_1.hashObject)(options); const cachePath = (0, path_1.join)(cache_directory_1.workspaceDataDirectory, `jest-${optionsHash}.hash`); const targetsCache = readTargetsCache(cachePath); // Cache jest preset(s) to avoid penalties of module load times. Most of jest configs will use the same preset. const presetCache = {}; const packageManagerWorkspacesGlob = (0, globs_1.combineGlobPatterns)((0, package_json_1.getGlobPatternsFromPackageManagerWorkspaces)(context.workspaceRoot)); options = normalizeOptions(options); const { roots: projectRoots, configFiles: validConfigFiles } = configFiles.reduce((acc, configFile) => { const potentialRoot = (0, path_1.dirname)(configFile); if (checkIfConfigFileShouldBeProject(configFile, potentialRoot, packageManagerWorkspacesGlob, context)) { acc.roots.push(potentialRoot); acc.configFiles.push(configFile); } return acc; }, { roots: [], configFiles: [], }); const hashes = await (0, calculate_hash_for_create_nodes_1.calculateHashesForCreateNodes)(projectRoots, options, context); try { return await (0, devkit_1.createNodesFromFiles)(async (configFilePath, options, context, idx) => { const projectRoot = projectRoots[idx]; const hash = hashes[idx]; targetsCache[hash] ??= await buildJestTargets(configFilePath, projectRoot, options, context, presetCache); const { targets, metadata } = targetsCache[hash]; return { projects: { [projectRoot]: { root: projectRoot, targets, metadata, }, }, }; }, validConfigFiles, options, context); } finally { writeTargetsToCache(cachePath, targetsCache); } }, ]; /** * @deprecated This is replaced with {@link createNodesV2}. Update your plugin to export its own `createNodesV2` function that wraps this one instead. * This function will change to the v2 function in Nx 20. */ exports.createNodes = [ jestConfigGlob, async (configFilePath, options, context) => { devkit_1.logger.warn('`createNodes` is deprecated. Update your plugin to utilize createNodesV2 instead. In Nx 20, this will change to the createNodesV2 API.'); const projectRoot = (0, path_1.dirname)(configFilePath); const packageManagerWorkspacesGlob = (0, globs_1.combineGlobPatterns)((0, package_json_1.getGlobPatternsFromPackageManagerWorkspaces)(context.workspaceRoot)); if (!checkIfConfigFileShouldBeProject(configFilePath, projectRoot, packageManagerWorkspacesGlob, context)) { return {}; } options = normalizeOptions(options); const { targets, metadata } = await buildJestTargets(configFilePath, projectRoot, options, context, {}); return { projects: { [projectRoot]: { root: projectRoot, targets, metadata, }, }, }; }, ]; function checkIfConfigFileShouldBeProject(configFilePath, projectRoot, packageManagerWorkspacesGlob, context) { // Do not create a project if package.json and project.json isn't there. const siblingFiles = (0, fs_1.readdirSync)((0, path_1.join)(context.workspaceRoot, projectRoot)); if (!siblingFiles.includes('package.json') && !siblingFiles.includes('project.json')) { return false; } else if (!siblingFiles.includes('project.json') && siblingFiles.includes('package.json')) { const path = (0, devkit_1.joinPathFragments)(projectRoot, 'package.json'); const isPackageJsonProject = (0, minimatch_1.minimatch)(path, packageManagerWorkspacesGlob); if (!isPackageJsonProject) { return false; } } const jestConfigContent = (0, fs_1.readFileSync)((0, path_1.resolve)(context.workspaceRoot, configFilePath), 'utf-8'); if (jestConfigContent.includes('getJestProjectsAsync()')) { // The `getJestProjectsAsync` function uses the project graph, which leads to a // circular dependency. We can skip this since it's no intended to be used for // an Nx project. return false; } return true; } async function buildJestTargets(configFilePath, projectRoot, options, context, presetCache) { const absConfigFilePath = (0, path_1.resolve)(context.workspaceRoot, configFilePath); if (require.cache[absConfigFilePath]) (0, config_utils_1.clearRequireCache)(); const rawConfig = await (0, config_utils_1.loadConfigFile)(absConfigFilePath); const targets = {}; const namedInputs = (0, get_named_inputs_1.getNamedInputs)(projectRoot, context); const existingTsNodeCompilerOptions = process.env['TS_NODE_COMPILER_OPTIONS']; const tsNodeCompilerOptions = JSON.stringify({ ...(existingTsNodeCompilerOptions ? JSON.parse(existingTsNodeCompilerOptions) : {}), moduleResolution: 'node10', customConditions: null, }); const target = (targets[options.targetName] = { command: 'jest', options: { cwd: projectRoot, // Jest registers ts-node with module CJS https://github.com/SimenB/jest/blob/v29.6.4/packages/jest-config/src/readConfigFileAndSetRootDir.ts#L117-L119 // We want to support of ESM via 'module':'nodenext', we need to override the resolution until Jest supports it. env: { TS_NODE_COMPILER_OPTIONS: tsNodeCompilerOptions }, }, metadata: { technologies: ['jest'], description: 'Run Jest Tests', help: { command: `${pmc.exec} jest --help`, example: { options: { coverage: true, }, }, }, }, }); // Not normalizing it here since also affects options for convert-to-inferred. const disableJestRuntime = options.disableJestRuntime !== false; const cache = (target.cache = true); const inputs = (target.inputs = getInputs(namedInputs, rawConfig.preset, projectRoot, context.workspaceRoot, disableJestRuntime)); let metadata; const groupName = options?.ciGroupName ?? deductGroupNameFromTarget(options?.ciTargetName); if (disableJestRuntime) { const outputs = (target.outputs = getOutputs(projectRoot, rawConfig.coverageDirectory ? (0, path_1.join)(context.workspaceRoot, projectRoot, rawConfig.coverageDirectory) : undefined, undefined, context)); if (options?.ciTargetName) { const testPaths = await getTestPaths(projectRoot, rawConfig, absConfigFilePath, context, presetCache); const targetGroup = []; const dependsOn = []; metadata = { targetGroups: { [groupName]: targetGroup, }, }; const specIgnoreRegexes = rawConfig.testPathIgnorePatterns?.map((p) => new RegExp(replaceRootDirInPath(projectRoot, p))); for (const testPath of testPaths) { const relativePath = (0, devkit_1.normalizePath)((0, path_1.relative)((0, path_1.join)(context.workspaceRoot, projectRoot), testPath)); if (specIgnoreRegexes?.some((regex) => regex.test(relativePath))) { continue; } const targetName = `${options.ciTargetName}--${relativePath}`; dependsOn.push(targetName); targets[targetName] = { command: `jest ${relativePath}`, cache, inputs, outputs, options: { cwd: projectRoot, env: { TS_NODE_COMPILER_OPTIONS: tsNodeCompilerOptions }, }, metadata: { technologies: ['jest'], description: `Run Jest Tests in ${relativePath}`, help: { command: `${pmc.exec} jest --help`, example: { options: { coverage: true, }, }, }, }, }; targetGroup.push(targetName); } if (targetGroup.length > 0) { targets[options.ciTargetName] = { executor: 'nx:noop', cache: true, inputs, outputs, dependsOn, metadata: { technologies: ['jest'], description: 'Run Jest Tests in CI', nonAtomizedTarget: options.targetName, help: { command: `${pmc.exec} jest --help`, example: { options: { coverage: true, }, }, }, }, }; targetGroup.unshift(options.ciTargetName); } } } else { const { readConfig } = requireJestUtil('jest-config', projectRoot, context.workspaceRoot); let config; try { config = await readConfig({ _: [], $0: undefined, }, rawConfig, undefined, (0, path_1.dirname)(absConfigFilePath)); } catch (e) { console.error(e); throw e; } const outputs = (target.outputs = getOutputs(projectRoot, config.globalConfig?.coverageDirectory, config.globalConfig?.outputFile, context)); if (options?.ciTargetName) { // nx-ignore-next-line const { default: Runtime } = requireJestUtil('jest-runtime', projectRoot, context.workspaceRoot); const jestContext = await Runtime.createContext(config.projectConfig, { maxWorkers: 1, watchman: false, }); const jest = require(resolveJestPath(projectRoot, context.workspaceRoot)); const source = new jest.SearchSource(jestContext); const jestVersion = (0, version_utils_1.getInstalledJestMajorVersion)(); const specs = jestVersion >= 30 ? await source.getTestPaths(config.globalConfig, config.projectConfig) : await source.getTestPaths(config.globalConfig); const testPaths = new Set(specs.tests.map(({ path }) => path)); if (testPaths.size > 0) { const targetGroup = []; metadata = { targetGroups: { [groupName]: targetGroup, }, }; const dependsOn = []; targets[options.ciTargetName] = { executor: 'nx:noop', cache: true, inputs, outputs, dependsOn, metadata: { technologies: ['jest'], description: 'Run Jest Tests in CI', nonAtomizedTarget: options.targetName, help: { command: `${pmc.exec} jest --help`, example: { options: { coverage: true, }, }, }, }, }; targetGroup.push(options.ciTargetName); for (const testPath of testPaths) { const relativePath = (0, devkit_1.normalizePath)((0, path_1.relative)((0, path_1.join)(context.workspaceRoot, projectRoot), testPath)); const targetName = `${options.ciTargetName}--${relativePath}`; dependsOn.push(targetName); targets[targetName] = { command: `jest ${relativePath}`, cache, inputs, outputs, options: { cwd: projectRoot, env: { TS_NODE_COMPILER_OPTIONS: tsNodeCompilerOptions }, }, metadata: { technologies: ['jest'], description: `Run Jest Tests in ${relativePath}`, help: { command: `${pmc.exec} jest --help`, example: { options: { coverage: true, }, }, }, }, }; targetGroup.push(targetName); } } } } return { targets, metadata }; } function getInputs(namedInputs, preset, projectRoot, workspaceRoot, disableJestRuntime) { const inputs = [ ...('production' in namedInputs ? ['default', '^production'] : ['default', '^default']), ]; const externalDependencies = ['jest']; const presetInput = disableJestRuntime ? resolvePresetInputWithoutJestResolver(preset, projectRoot, workspaceRoot) : resolvePresetInputWithJestResolver(preset, projectRoot, workspaceRoot); if (presetInput) { if (typeof presetInput !== 'string' && 'externalDependencies' in presetInput) { externalDependencies.push(...presetInput.externalDependencies); } else { inputs.push(presetInput); } } inputs.push({ externalDependencies }); return inputs; } function resolvePresetInputWithoutJestResolver(presetValue, projectRoot, workspaceRoot) { if (!presetValue) return null; const presetPath = replaceRootDirInPath(projectRoot, presetValue); const isNpmPackage = !presetValue.startsWith('.') && !(0, path_1.isAbsolute)(presetPath); if (isNpmPackage) { return { externalDependencies: [presetValue] }; } if (presetPath.startsWith('..')) { const relativePath = (0, path_1.relative)(workspaceRoot, (0, path_1.join)(projectRoot, presetPath)); return (0, path_1.join)('{workspaceRoot}', relativePath); } else { const relativePath = (0, path_1.relative)(projectRoot, presetPath); return (0, path_1.join)('{projectRoot}', relativePath); } } // preset resolution adapted from: // https://github.com/jestjs/jest/blob/c54bccd657fb4cf060898717c09f633b4da3eec4/packages/jest-config/src/normalize.ts#L122 function resolvePresetInputWithJestResolver(presetValue, projectRoot, workspaceRoot) { if (!presetValue) return null; let presetPath = replaceRootDirInPath(projectRoot, presetValue); const isNpmPackage = !presetValue.startsWith('.') && !(0, path_1.isAbsolute)(presetPath); presetPath = presetPath.startsWith('.') ? presetPath : (0, path_1.join)(presetPath, 'jest-preset'); const { default: jestResolve } = requireJestUtil('jest-resolve', projectRoot, workspaceRoot); const presetModule = jestResolve.findNodeModule(presetPath, { basedir: projectRoot, extensions: ['.json', '.js', '.cjs', '.mjs'], }); if (!presetModule) { return null; } if (isNpmPackage) { return { externalDependencies: [presetValue] }; } const relativePath = (0, path_1.relative)((0, path_1.join)(workspaceRoot, projectRoot), presetModule); return relativePath.startsWith('..') ? (0, path_1.join)('{workspaceRoot}', (0, path_1.join)(projectRoot, relativePath)) : (0, path_1.join)('{projectRoot}', relativePath); } // Adapted from here https://github.com/jestjs/jest/blob/c13bca3/packages/jest-config/src/utils.ts#L57-L69 function replaceRootDirInPath(rootDir, filePath) { if (!filePath.startsWith('<rootDir>')) { return filePath; } return (0, path_1.resolve)(rootDir, (0, node_path_1.normalize)(`./${filePath.slice('<rootDir>'.length)}`)); } function getOutputs(projectRoot, coverageDirectory, outputFile, context) { function getOutput(path) { const relativePath = (0, path_1.relative)((0, path_1.join)(context.workspaceRoot, projectRoot), path); if (relativePath.startsWith('..')) { return (0, path_1.join)('{workspaceRoot}', (0, path_1.join)(projectRoot, relativePath)); } else { return (0, path_1.join)('{projectRoot}', relativePath); } } const outputs = []; for (const outputOption of [coverageDirectory, outputFile]) { if (outputOption) { outputs.push(getOutput(outputOption)); } } return outputs; } function normalizeOptions(options) { options ??= {}; options.targetName ??= 'test'; return options; } let resolvedJestPaths; function resolveJestPath(projectRoot, workspaceRoot) { resolvedJestPaths ??= {}; if (resolvedJestPaths[projectRoot]) { return resolvedJestPaths[projectRoot]; } resolvedJestPaths[projectRoot] = require.resolve('jest', { paths: [projectRoot, workspaceRoot, __dirname], }); return resolvedJestPaths[projectRoot]; } let resolvedJestCorePaths; /** * Resolves a jest util package version that `jest` is using. */ function requireJestUtil(packageName, projectRoot, workspaceRoot) { const jestPath = resolveJestPath(projectRoot, workspaceRoot); resolvedJestCorePaths ??= {}; if (!resolvedJestCorePaths[jestPath]) { // nx-ignore-next-line resolvedJestCorePaths[jestPath] = require.resolve('@jest/core', { paths: [(0, path_1.dirname)(jestPath)], }); } return require(require.resolve(packageName, { paths: [(0, path_1.dirname)(resolvedJestCorePaths[jestPath])], })); } async function getTestPaths(projectRoot, rawConfig, absConfigFilePath, context, presetCache) { const testMatch = await getJestOption(rawConfig, absConfigFilePath, 'testMatch', presetCache); let paths = await (0, workspace_context_1.globWithWorkspaceContext)(context.workspaceRoot, (testMatch || [ // Default copied from https://github.com/jestjs/jest/blob/d1a2ed7/packages/jest-config/src/Defaults.ts#L84 '**/__tests__/**/*.?([mc])[jt]s?(x)', '**/?(*.)+(spec|test).?([mc])[jt]s?(x)', ]).map((pattern) => (0, path_1.join)(projectRoot, pattern)), []); const testRegex = await getJestOption(rawConfig, absConfigFilePath, 'testRegex', presetCache); if (testRegex) { const testRegexes = Array.isArray(rawConfig.testRegex) ? rawConfig.testRegex.map((r) => new RegExp(r)) : [new RegExp(rawConfig.testRegex)]; paths = paths.filter((path) => testRegexes.some((r) => r.test(path))); } return paths; } async function getJestOption(rawConfig, absConfigFilePath, optionName, presetCache) { if (rawConfig[optionName]) return rawConfig[optionName]; if (rawConfig.preset) { const dir = (0, path_1.dirname)(absConfigFilePath); const presetPath = (0, path_1.resolve)(dir, rawConfig.preset); try { let preset = presetCache[presetPath]; if (!preset) { preset = await (0, config_utils_1.loadConfigFile)(presetPath); presetCache[presetPath] = preset; } if (preset[optionName]) return preset[optionName]; } catch { // If preset fails to load, ignore the error and continue. // This is safe and less jarring for users. They will need to fix the // preset for Jest to run, and at that point we can read in the correct // value. } } return undefined; } /** * Helper that tries to deduct the name of the CI group, based on the related target name. * * This will work well, when the CI target name follows the documented naming convention or similar (for e.g `test-ci`, `e2e-ci`, `ny-e2e-ci`, etc). * * For example, `test-ci` => `TEST (CI)`, `e2e-ci` => `E2E (CI)`, `my-e2e-ci` => `MY E2E (CI)` * * * @param ciTargetName name of the CI target * @returns the deducted group name or `${ciTargetName.toUpperCase()} (CI)` if cannot be deducted automatically */ function deductGroupNameFromTarget(ciTargetName) { if (!ciTargetName) { return null; } const parts = ciTargetName.split('-').map((v) => v.toUpperCase()); if (parts.length > 1) { return `${parts.slice(0, -1).join(' ')} (${parts[parts.length - 1]})`; } return `${parts[0]} (CI)`; // default group name when there is a single segment }