@nx/jest
Version:
932 lines (931 loc) • 41.4 kB
JavaScript
"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;
}