UNPKG

@nx/jest

Version:

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

932 lines (931 loc) 41.4 kB
"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.createNodesV2 = exports.createNodes = void 0; const internal_1 = require("@nx/devkit/internal"); const devkit_1 = require("@nx/devkit"); const minimatch_1 = require("minimatch"); const node_fs_1 = require("node:fs"); const node_path_1 = require("node:path"); 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 installation_directory_1 = require("nx/src/utils/installation-directory"); const plugins_1 = require("nx/src/utils/plugins"); const workspace_context_1 = require("nx/src/utils/workspace-context"); const js_1 = require("@nx/js"); const internal_2 = require("@nx/js/internal"); const versions_1 = require("../utils/versions"); const REPORTER_BUILTINS = new Set(['default', 'github-actions', 'summary']); const jestConfigGlob = '**/jest.config.{cjs,mjs,js,cts,mts,ts}'; exports.createNodes = [ jestConfigGlob, async (configFiles, options, context) => { const optionsHash = (0, devkit_internals_1.hashObject)(options); const cachePath = (0, node_path_1.join)(cache_directory_1.workspaceDataDirectory, `jest-${optionsHash}.hash`); const targetsCache = new internal_1.PluginCache(cachePath); // Cache jest preset(s) to avoid penalties of module load times. Most of jest configs will use the same preset. const presetCache = {}; // Cache tsconfig reads + isolatedModules resolution. Many projects share // the same base tsconfig in their extends chain — read each file once. const tsconfigJsonCache = new Map(); const tsconfigExistsCache = new Map(); const isolatedModulesCache = new Map(); const isInPackageManagerWorkspaces = buildPackageJsonWorkspacesMatcher(context.workspaceRoot); options = normalizeOptions(options); const packageManager = (0, devkit_1.detectPackageManager)(context.workspaceRoot); const pmc = (0, devkit_1.getPackageManagerCommand)(packageManager); const { roots: projectRoots, configFiles: validConfigFiles } = configFiles.reduce((acc, configFile) => { const potentialRoot = (0, node_path_1.dirname)(configFile); if (checkIfConfigFileShouldBeProject(configFile, potentialRoot, isInPackageManagerWorkspaces, context)) { acc.roots.push(potentialRoot); acc.configFiles.push(configFile); } return acc; }, { roots: [], configFiles: [], }); const lockFilePattern = (0, js_1.getLockFileName)(packageManager); // Load configs in parallel. `loadConfigFile` calls `registerTsProject`, // whose transpiler dedup is refcounted: serial register/unregister cycles // drop refCount to 0 between iterations and recreate a fresh ts-node // service each time (ts-node has no cleanup — see // packages/nx/src/plugins/js/utils/register.js), stacking N services in // `require.extensions` and OOM'ing under NX_PREFER_TS_NODE. Parallel // loads keep all registrations alive concurrently so the dedup holds and // a single transpiler instance is shared. let requireCacheCleared = false; const loadedConfigs = await Promise.all(validConfigFiles.map(async (configFilePath, i) => { const projectRoot = projectRoots[i]; const absConfigFilePath = (0, node_path_1.resolve)(context.workspaceRoot, configFilePath); if (!requireCacheCleared && require.cache[absConfigFilePath]) { (0, internal_1.clearRequireCache)(); requireCacheCleared = true; } const rawConfig = await (0, internal_1.loadConfigFile)(absConfigFilePath, [ 'tsconfig.spec.json', 'tsconfig.test.json', 'tsconfig.jest.json', 'tsconfig.json', ]); const { externalFiles, needsDtsInputs } = await collectExternalFileReferences(rawConfig, absConfigFilePath, projectRoot, context.workspaceRoot, { presetCache, tsconfigJsonCache, tsconfigExistsCache, isolatedModulesCache, }); return { rawConfig, externalFiles, needsDtsInputs }; })); const hashes = await (0, internal_1.calculateHashesForCreateNodes)(projectRoots, options, context, loadedConfigs.map(({ externalFiles }) => [ lockFilePattern, ...externalFiles, ])); try { return await (0, devkit_1.createNodesFromFiles)(async (configFilePath, options, context, idx) => { const projectRoot = projectRoots[idx]; const hash = hashes[idx]; const { rawConfig, needsDtsInputs } = loadedConfigs[idx]; if (!targetsCache.has(hash)) { targetsCache.set(hash, await buildJestTargets(rawConfig, needsDtsInputs, configFilePath, projectRoot, options, context, presetCache, pmc)); } const { targets, metadata } = targetsCache.get(hash); return { projects: { [projectRoot]: { root: projectRoot, targets, metadata, }, }, }; }, validConfigFiles, options, context); } finally { targetsCache.writeToDisk(); } }, ]; exports.createNodesV2 = exports.createNodes; function buildPackageJsonWorkspacesMatcher(workspaceRoot) { if (process.env.NX_INFER_ALL_PACKAGE_JSONS === 'true') { return () => true; } const packageManagerWorkspacesGlob = (0, globs_1.combineGlobPatterns)((0, package_json_1.getGlobPatternsFromPackageManagerWorkspaces)(workspaceRoot)); return (path) => (0, minimatch_1.minimatch)(path, packageManagerWorkspacesGlob); } function checkIfConfigFileShouldBeProject(configFilePath, projectRoot, isInPackageManagerWorkspaces, context) { // Do not create a project if package.json and project.json isn't there. const siblingFiles = (0, node_fs_1.readdirSync)((0, node_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 = isInPackageManagerWorkspaces(path); if (!isPackageJsonProject) { return false; } } const jestConfigContent = (0, node_fs_1.readFileSync)((0, node_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(rawConfig, needsDtsInputs, configFilePath, projectRoot, options, context, presetCache, pmc) { const absConfigFilePath = (0, node_path_1.resolve)(context.workspaceRoot, configFilePath); const targets = {}; const namedInputs = (0, internal_1.getNamedInputs)(projectRoot, context); const tsNodeCompilerOptions = JSON.stringify({ moduleResolution: 'node10', module: 'commonjs', customConditions: null, }); const env = { TS_NODE_COMPILER_OPTIONS: tsNodeCompilerOptions, }; 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, }, 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 useJestResolver = options.useJestResolver ?? !disableJestRuntime; // Jest defaults rootDir to the config file's directory, but allows overrides const rootDir = rawConfig.rootDir ? (0, node_path_1.resolve)((0, node_path_1.dirname)(absConfigFilePath), rawConfig.rootDir) : (0, node_path_1.resolve)(context.workspaceRoot, projectRoot); const cache = (target.cache = true); const inputs = (target.inputs = await getInputs(namedInputs, rawConfig, rootDir, projectRoot, context.workspaceRoot, presetCache, needsDtsInputs, useJestResolver)); let metadata; const groupName = options?.ciGroupName ?? (0, plugins_1.deriveGroupNameFromTarget)(options?.ciTargetName); if (disableJestRuntime) { const outputs = (target.outputs = getOutputs(projectRoot, rawConfig.coverageDirectory ? (0, node_path_1.join)(context.workspaceRoot, projectRoot, rawConfig.coverageDirectory) : undefined, undefined, context)); if (options?.ciTargetName) { const { specs, testMatch } = await getTestPaths(projectRoot, rawConfig, rootDir, 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 specs) { const relativePath = (0, devkit_1.normalizePath)((0, node_path_1.relative)((0, node_path_1.join)(context.workspaceRoot, projectRoot), testPath)); if (relativePath.includes('../')) { throw new Error('@nx/jest/plugin attempted to run tests outside of the project root. This is not supported and should not happen. Please open an issue at https://github.com/nrwl/nx/issues/new/choose with the following information:\n\n' + `\n\n${JSON.stringify({ projectRoot, relativePath, specs, context, testMatch, }, null, 2)}`); } if (specIgnoreRegexes?.some((regex) => regex.test(relativePath))) { continue; } const targetName = `${options.ciTargetName}--${relativePath}`; dependsOn.push({ target: targetName, params: 'forward', options: 'forward', }); targets[targetName] = { command: `jest ${relativePath}`, cache, inputs, outputs, options: { cwd: projectRoot, env, }, 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, node_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 = getJestMajorVersion(); const specs = jestVersion >= 30 ? await source.getTestPaths(config.globalConfig, config.projectConfig) : // @ts-expect-error Jest v29 doesn't have the projectConfig parameter await source.getTestPaths(config.globalConfig); // Sort to keep atomized target name insertion order stable. // jest.SearchSource.getTestPaths walks via jest-haste-map's // parallel workers, so its output order isn't guaranteed. const testPaths = new Set(specs.tests.map(({ path }) => path).sort()); if (testPaths.size > 0) { const targetGroup = []; metadata = { targetGroups: { [groupName]: targetGroup, }, }; const dependsOn = []; for (const testPath of testPaths) { const relativePath = (0, devkit_1.normalizePath)((0, node_path_1.relative)((0, node_path_1.join)(context.workspaceRoot, projectRoot), testPath)); if (relativePath.includes('../')) { throw new Error('@nx/jest/plugin attempted to run tests outside of the project root. This is not supported and should not happen. Please open an issue at https://github.com/nrwl/nx/issues/new/choose with the following information:\n\n' + `\n\n${JSON.stringify({ projectRoot, relativePath, testPaths, context, }, null, 2)}`); } const targetName = `${options.ciTargetName}--${relativePath}`; dependsOn.push({ target: targetName, params: 'forward', options: 'forward', }); targets[targetName] = { command: `jest ${relativePath}`, cache, inputs, outputs, options: { cwd: projectRoot, env, }, metadata: { technologies: ['jest'], description: `Run Jest Tests in ${relativePath}`, help: { command: `${pmc.exec} jest --help`, example: { options: { coverage: true, }, }, }, }, }; targetGroup.push(targetName); } 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); } } } return { targets, metadata }; } async function getInputs(namedInputs, rawConfig, rootDir, projectRoot, workspaceRoot, presetCache, needsDtsInputs, useJestResolver) { const inputs = [ ...('production' in namedInputs ? ['default', '^production'] : ['default', '^default']), ]; const externalDependencies = ['jest']; const resolvedModulePaths = rawConfig.modulePaths?.map((p) => replaceRootDirInPath(rootDir, p)); const jestResolve = useJestResolver ? requireJestUtil('jest-resolve', projectRoot, workspaceRoot).default : null; const presetInput = jestResolve ? resolvePresetInputWithJestResolver(rawConfig.preset, rootDir, projectRoot, workspaceRoot, jestResolve, rawConfig.moduleDirectories, resolvedModulePaths) : resolvePresetInputWithoutJestResolver(rawConfig.preset, rootDir, projectRoot, workspaceRoot); // Track preset file path to avoid duplicating it with config-derived inputs let presetInputPath = null; if (presetInput) { if (typeof presetInput !== 'string' && 'externalDependencies' in presetInput) { externalDependencies.push(...presetInput.externalDependencies); } else { presetInputPath = presetInput; inputs.push(presetInput); } } const resolveFilePath = jestResolve ? createJestResolveFilePathResolver(rootDir, projectRoot, workspaceRoot, jestResolve, rawConfig.moduleDirectories, resolvedModulePaths) : createFilePathResolverWithoutJest(rootDir, projectRoot, workspaceRoot); const configInputs = await getConfigFileInputs(rawConfig, rootDir, presetCache, resolveFilePath); for (const fileInput of configInputs.fileInputs) { if (fileInput !== presetInputPath) { inputs.push(fileInput); } } for (const dep of configInputs.externalDeps) { if (!externalDependencies.includes(dep)) { externalDependencies.push(dep); } } inputs.push({ externalDependencies }); // When ts-jest runs without isolatedModules, it creates a TypeScript // Language Service that reads .d.ts files from dependency projects. // Declare these as dependentTasksOutputFiles so changes to dependency // type declarations correctly invalidate the test cache. if (needsDtsInputs) { inputs.push({ dependentTasksOutputFiles: '**/*.d.ts', transitive: true, }); } return inputs; } function resolvePresetInputWithoutJestResolver(presetValue, rootDir, projectRoot, workspaceRoot) { if (!presetValue) return null; const presetPath = replaceRootDirInPath(rootDir, presetValue); const isNpmLike = !presetValue.startsWith('.') && !(0, node_path_1.isAbsolute)(presetPath); if (isNpmLike) { return { externalDependencies: [extractPackageName(presetValue)] }; } const absoluteProjectRoot = (0, node_path_1.resolve)(workspaceRoot, projectRoot); return classifyResolvedPath((0, node_path_1.resolve)(rootDir, presetPath), absoluteProjectRoot, workspaceRoot); } // preset resolution adapted from: // https://github.com/jestjs/jest/blob/c54bccd657fb4cf060898717c09f633b4da3eec4/packages/jest-config/src/normalize.ts#L122 function resolvePresetInputWithJestResolver(presetValue, rootDir, projectRoot, workspaceRoot, jestResolve, moduleDirectories, modulePaths) { if (!presetValue) return null; let presetPath = replaceRootDirInPath(rootDir, presetValue); presetPath = presetPath.startsWith('.') ? presetPath : (0, node_path_1.join)(presetPath, 'jest-preset'); const presetModule = jestResolve.findNodeModule(presetPath, { basedir: rootDir, extensions: ['.json', '.js', '.cjs', '.mjs'], moduleDirectory: moduleDirectories, paths: modulePaths, }); if (!presetModule) { return null; } return classifyResolvedPath(presetModule, (0, node_path_1.resolve)(workspaceRoot, projectRoot), workspaceRoot); } // 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, node_path_1.resolve)(rootDir, (0, node_path_1.normalize)(`./${filePath.slice('<rootDir>'.length)}`)); } function classifyResolvedPath(absolutePath, absoluteProjectRoot, workspaceRoot) { const relToWorkspace = (0, devkit_1.normalizePath)((0, node_path_1.relative)(workspaceRoot, absolutePath)); if (relToWorkspace.includes('node_modules/')) { const nmIndex = relToWorkspace.lastIndexOf('node_modules/'); const afterNm = relToWorkspace.slice(nmIndex + 'node_modules/'.length); return { externalDependencies: [extractPackageName(afterNm)] }; } const relToProject = (0, devkit_1.normalizePath)((0, node_path_1.relative)(absoluteProjectRoot, absolutePath)); if (relToProject.startsWith('..')) { return (0, devkit_1.joinPathFragments)('{workspaceRoot}', relToWorkspace); } return (0, devkit_1.joinPathFragments)('{projectRoot}', relToProject); } function extractPackageName(value) { const parts = value.split('/'); return value.startsWith('@') ? `${parts[0]}/${parts[1]}` : parts[0]; } function createFilePathResolverWithoutJest(rootDir, projectRoot, workspaceRoot) { const absoluteProjectRoot = (0, node_path_1.resolve)(workspaceRoot, projectRoot); return (filePath) => { const resolvedPath = replaceRootDirInPath(rootDir, filePath); if (looksLikePackageName(filePath, resolvedPath)) { return { externalDependencies: [extractPackageName(filePath)] }; } return classifyResolvedPath((0, node_path_1.resolve)(rootDir, resolvedPath), absoluteProjectRoot, workspaceRoot); }; } function resolveWithPrefixFallback(filePath, prefix, resolver) { if (!filePath.startsWith('.') && !filePath.startsWith('<rootDir>') && !(0, node_path_1.isAbsolute)(filePath) && !filePath.startsWith(prefix)) { const prefixed = resolver(`${prefix}${filePath}`); if (prefixed) return prefixed; } return resolver(filePath); } function looksLikePackageName(filePath, resolvedPath) { return (!filePath.startsWith('.') && !filePath.startsWith('<rootDir>') && !(0, node_path_1.isAbsolute)(resolvedPath)); } function createJestResolveFilePathResolver(rootDir, projectRoot, workspaceRoot, jestResolve, moduleDirectories, modulePaths) { const absoluteProjectRoot = (0, node_path_1.resolve)(workspaceRoot, projectRoot); const cache = new Map(); return (filePath) => { if (cache.has(filePath)) { return cache.get(filePath); } const resolvedPath = replaceRootDirInPath(rootDir, filePath); let result; const absolutePath = jestResolve.findNodeModule(resolvedPath, { basedir: rootDir, extensions: ['.js', '.json', '.cjs', '.mjs', '.mts', '.cts', '.node'], moduleDirectory: moduleDirectories, paths: modulePaths, }); if (!absolutePath) { if (looksLikePackageName(filePath, resolvedPath)) { result = { externalDependencies: [extractPackageName(filePath)] }; } else { result = null; } } else { result = classifyResolvedPath(absolutePath, absoluteProjectRoot, workspaceRoot); } cache.set(filePath, result); return result; }; } function getOutputs(projectRoot, coverageDirectory, outputFile, context) { function getOutput(path) { const relativePath = (0, node_path_1.relative)((0, node_path_1.join)(context.workspaceRoot, projectRoot), path); if (relativePath.startsWith('..')) { return (0, node_path_1.join)('{workspaceRoot}', (0, node_path_1.join)(projectRoot, relativePath)); } else { return (0, node_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, ...(0, installation_directory_1.getNxRequirePaths)(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, node_path_1.dirname)(jestPath)], }); } return require(require.resolve(packageName, { paths: [(0, node_path_1.dirname)(resolvedJestCorePaths[jestPath])], })); } async function getTestPaths(projectRoot, rawConfig, rootDir, context, presetCache) { const testMatch = await getJestOption(rawConfig, rootDir, '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, node_path_1.join)(projectRoot, pattern)), []); const testRegex = await getJestOption(rawConfig, rootDir, '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 { specs: paths, testMatch }; } /** * Collects workspace-relative paths to files whose CONTENT the plugin reads * when computing inferred targets and that live OUTSIDE the project root. * * Only two kinds of files qualify: * - The jest preset (loaded to read its `transform`, etc.) * - Tsconfig files in the extends chain referenced by ts-jest (read to * determine `isolatedModules`); only walked when ts-jest is not already * known to be in isolated mode. * * Other config references (setup files, custom resolvers, transformers, * etc.) are NOT collected — the plugin only resolves their paths and emits * them as task inputs; their content is not read by the plugin, so changes * to them don't influence inference. */ async function collectExternalFileReferences(rawConfig, absConfigFilePath, projectRoot, workspaceRoot, caches) { const { presetCache, tsconfigJsonCache, tsconfigExistsCache, isolatedModulesCache, } = caches; const absWorkspaceRoot = (0, node_path_1.resolve)(workspaceRoot); const rootDir = rawConfig.rootDir ? (0, node_path_1.resolve)((0, node_path_1.dirname)(absConfigFilePath), rawConfig.rootDir) : (0, node_path_1.resolve)(absWorkspaceRoot, projectRoot); const absoluteProjectRoot = (0, node_path_1.resolve)(absWorkspaceRoot, projectRoot); const externalFiles = new Set(); const addIfExternal = (absolutePath) => { if (!absolutePath) return; const rel = (0, devkit_1.normalizePath)((0, node_path_1.relative)(absWorkspaceRoot, absolutePath)); if (rel.startsWith('..') || (0, node_path_1.isAbsolute)(rel)) return; // outside workspace if (rel.includes('node_modules/')) return; // covered by lockfile const relToProject = (0, devkit_1.normalizePath)((0, node_path_1.relative)(absoluteProjectRoot, absolutePath)); if (!relToProject.startsWith('..') && !(0, node_path_1.isAbsolute)(relToProject)) return; // inside project root externalFiles.add(rel); }; // Preset path (content is loaded by the plugin to merge with rawConfig) if (typeof rawConfig.preset === 'string') { const replaced = replaceRootDirInPath(rootDir, rawConfig.preset); if (replaced.startsWith('.') || (0, node_path_1.isAbsolute)(replaced)) { addIfExternal((0, node_path_1.resolve)(rootDir, replaced)); } } // ts-jest tsconfig extends chain — only walked when ts-jest is in // non-isolated mode (otherwise the chain doesn't influence the output). // Loading the preset is required to merge its transform with the raw // config, since presets are the common source of ts-jest configuration. const presetConfig = await loadPresetConfig(rawConfig, rootDir, presetCache); const transform = { ...(presetConfig?.transform ?? {}), ...(rawConfig.transform ?? {}), }; let needsDtsInputs = false; for (const value of Object.values(transform)) { let transformPath; let transformOptions; if (Array.isArray(value)) { transformPath = value[0]; transformOptions = value[1]; } else if (typeof value === 'string') { transformPath = value; } else { continue; } if (!isTsJestTransformer(transformPath)) continue; if (transformOptions?.isolatedModules === true) { const tsJestMajor = getTsJestMajorVersion(); if (tsJestMajor !== null && tsJestMajor < 30) continue; } const tsconfigAbsPath = transformOptions?.tsconfig ? (0, node_path_1.resolve)(rootDir, replaceRootDirInPath(rootDir, transformOptions.tsconfig)) : findNearestTsconfig(rootDir, absWorkspaceRoot, tsconfigExistsCache); if (!tsconfigAbsPath) { // No tsconfig found — ts-jest defaults to non-isolated mode needsDtsInputs = true; continue; } const { value: isolatedValue, visitedFiles } = resolveIsolatedModules(tsconfigAbsPath, tsconfigJsonCache, isolatedModulesCache); for (const visitedFile of visitedFiles) { addIfExternal(visitedFile); } if (isolatedValue !== true) needsDtsInputs = true; } return { externalFiles: [...externalFiles], needsDtsInputs }; } async function loadPresetConfig(rawConfig, rootDir, presetCache) { if (!rawConfig.preset) return null; const presetValue = replaceRootDirInPath(rootDir, rawConfig.preset); let presetPath; if (presetValue.startsWith('.') || (0, node_path_1.isAbsolute)(presetValue)) { presetPath = (0, node_path_1.resolve)(rootDir, presetValue); } else { try { presetPath = require.resolve((0, node_path_1.join)(presetValue, 'jest-preset'), { paths: [rootDir], }); } catch { return null; } } try { if (!presetCache[presetPath]) { presetCache[presetPath] = (0, internal_1.loadConfigFile)(presetPath); } return (await presetCache[presetPath]); } 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 null; } } async function getConfigFileInputs(rawConfig, rootDir, presetCache, resolveFilePath) { const fileInputs = new Set(); const externalDeps = new Set(); const preset = await loadPresetConfig(rawConfig, rootDir, presetCache); function addInput(input) { if (!input) return; if (typeof input === 'string') { fileInputs.add(input); } else { input.externalDependencies.forEach((d) => externalDeps.add(d)); } } // Replaced properties: rawConfig[prop] wins over preset[prop] for (const prop of [ 'resolver', 'globalSetup', 'globalTeardown', 'snapshotResolver', 'testResultsProcessor', ]) { const value = rawConfig[prop] ?? preset?.[prop]; if (typeof value === 'string') { addInput(resolveFilePath(value)); } } // runner uses jest-runner- prefix resolution const runner = rawConfig.runner ?? preset?.runner; if (typeof runner === 'string') { addInput(resolveWithPrefixFallback(runner, 'jest-runner-', resolveFilePath)); } // Concatenated properties: preset values + config values for (const prop of ['setupFiles', 'setupFilesAfterEnv']) { const values = [ ...(preset?.[prop] ?? []), ...(rawConfig[prop] ?? []), ]; for (const value of values) { if (typeof value === 'string') { addInput(resolveFilePath(value)); } } } // Merged: moduleNameMapper — config keys win (skip values with backreferences like $1) const moduleNameMapper = { ...(preset?.moduleNameMapper ?? {}), ...(rawConfig.moduleNameMapper ?? {}), }; for (const value of Object.values(moduleNameMapper)) { const values = Array.isArray(value) ? value : [value]; for (const v of values) { if (typeof v !== 'string') continue; if (/\$\d/.test(v)) continue; addInput(resolveFilePath(v)); } } // Merged: transform — config keys win const transform = { ...(preset?.transform ?? {}), ...(rawConfig.transform ?? {}), }; for (const value of Object.values(transform)) { let transformPath; if (Array.isArray(value)) { transformPath = value[0]; } else if (typeof value === 'string') { transformPath = value; } else { continue; } addInput(resolveFilePath(transformPath)); } // Replaced: snapshotSerializers, reporters, watchPlugins const snapshotSerializers = rawConfig.snapshotSerializers ?? preset?.snapshotSerializers ?? []; for (const value of snapshotSerializers) { if (typeof value === 'string') { addInput(resolveFilePath(value)); } } const reporters = rawConfig.reporters ?? preset?.reporters ?? []; for (const entry of reporters) { const name = Array.isArray(entry) ? entry[0] : entry; if (typeof name !== 'string') continue; if (REPORTER_BUILTINS.has(name)) continue; addInput(resolveFilePath(name)); } // watchPlugins uses jest-watch- prefix resolution const watchPlugins = rawConfig.watchPlugins ?? preset?.watchPlugins ?? []; for (const entry of watchPlugins) { const name = Array.isArray(entry) ? entry[0] : entry; if (typeof name !== 'string') continue; addInput(resolveWithPrefixFallback(name, 'jest-watch-', resolveFilePath)); } return { fileInputs: [...fileInputs], externalDeps: [...externalDeps], }; } /** * Walks up from `startDir` looking for `tsconfig.json`, stopping at * `stopDir` (inclusive). Mirrors `ts.findConfigFile` but capped at the * workspace root to avoid escaping the monorepo. */ function findNearestTsconfig(startDir, stopDir, existsCache) { let dir = startDir; while (true) { const candidate = (0, node_path_1.join)(dir, 'tsconfig.json'); let exists = existsCache.get(candidate); if (exists === undefined) { exists = (0, node_fs_1.existsSync)(candidate); existsCache.set(candidate, exists); } if (exists) return candidate; if (dir === stopDir) return null; const parent = (0, node_path_1.dirname)(dir); if (parent === dir) return null; dir = parent; } } function isTsJestTransformer(value) { if (value === 'ts-jest' || value.startsWith('ts-jest/')) { return true; } // Handle resolved paths (e.g. from require.resolve('ts-jest') in configs) const normalized = (0, devkit_1.normalizePath)(value); return (normalized.includes('/node_modules/ts-jest/') || normalized.endsWith('/node_modules/ts-jest')); } /** * Returns the effective `compilerOptions.isolatedModules` for a tsconfig, * walking the `extends` chain via `walkTsconfigExtendsChain`. Tri-state: * `value` is `undefined` when the option is not set anywhere in the chain. */ function resolveIsolatedModules(tsconfigPath, jsonCache, resultCache) { const cached = resultCache.get(tsconfigPath); if (cached) return cached; let value; const visitedFiles = new Set(); (0, internal_2.walkTsconfigExtendsChain)(tsconfigPath, (absPath, rawJson) => { visitedFiles.add(absPath); const opts = rawJson?.compilerOptions; if (opts?.isolatedModules !== undefined) { value = opts.isolatedModules === true; return 'stop'; } // verbatimModuleSyntax: true implies isolatedModules: true (TS 5.0+, // the minimum TS version Nx supports) if (opts?.verbatimModuleSyntax === true) { value = true; return 'stop'; } return 'continue'; }, { jsonCache }); const result = { value, visitedFiles }; resultCache.set(tsconfigPath, result); return result; } // Module-level memoization: package versions don't change during a process // lifetime, so it's safe to cache across createNodes invocations. let cachedJestMajorVersion; function getJestMajorVersion() { if (cachedJestMajorVersion === undefined) { cachedJestMajorVersion = (0, versions_1.getInstalledJestMajorVersion)(); } return cachedJestMajorVersion; } let cachedTsJestMajorVersion; function getTsJestMajorVersion() { if (cachedTsJestMajorVersion !== undefined) { return cachedTsJestMajorVersion; } try { const { major } = require('semver'); const version = require('ts-jest/package.json').version; cachedTsJestMajorVersion = major(version); } catch { cachedTsJestMajorVersion = null; } return cachedTsJestMajorVersion; } async function getJestOption(rawConfig, rootDir, optionName, presetCache) { if (rawConfig[optionName]) return rawConfig[optionName]; const preset = await loadPresetConfig(rawConfig, rootDir, presetCache); if (preset?.[optionName]) return preset[optionName]; return undefined; }