Files
nrwl--nx/scripts/patched-jest-resolver.js
2026-07-13 12:38:36 +08:00

246 lines
8.0 KiB
JavaScript

'use strict';
const path = require('path');
const fs = require('fs');
// Memoize results of `existsSync(...) && lstatSync(...).isFile()` for
// resolution probes inside `packages/*`. Each spec file with
// `jest.mock('@nx/devkit', ...)` (or any cross-package require) makes Jest
// re-run resolution from scratch, which calls `existsSync` and `lstatSync`
// on the same workspace files repeatedly. Those probes show up in the
// sandbox report as "unexpected reads" even though they don't read any
// file content. The map is process-wide and bounded by the set of
// workspace package entry points, so memory is trivial.
const isFileCache = new Map();
const isWorkspaceFile = (p) => {
if (isFileCache.has(p)) return isFileCache.get(p);
let result = false;
try {
result = fs.existsSync(p) && fs.lstatSync(p).isFile();
} catch (_) {}
isFileCache.set(p, result);
return result;
};
/**
* Custom resolver which will respect package exports (until Jest supports it natively
* by resolving https://github.com/facebook/jest/issues/9771)
*
* Also needed for TypeScript project references support:
* - ts-jest doesn't support TypeScript project references (https://github.com/kulshekhar/ts-jest/issues/1648)
* - When using TS project references, Jest needs to resolve imports like '@nx/devkit' to TypeScript source files
* - Without this resolver, Jest will fail to resolve these imports correctly
*/
const enhancedResolver = require('enhanced-resolve').create.sync({
conditionNames: ['@nx/nx-source', 'require', 'node', 'default'],
extensions: ['.js', '.json', '.node', '.ts', '.tsx'],
});
if (
process.argv[1].indexOf('jest-worker') > -1 ||
(process.argv.length >= 4 && process.argv[3].split(':')[1] === 'test')
) {
const root = path.join(__dirname, '..', 'tmp', 'unit');
try {
if (!fs.existsSync(root)) {
fs.mkdirSync(root);
}
} catch (_err) {}
process.env.NX_WORKSPACE_ROOT_PATH = root;
}
const excludedPackages = [
'@nx/conformance',
'@nx/owners',
'@nx/key',
'@nx/s3-cache',
'@nx/azure-cache',
'@nx/gcs-cache',
'@nx/shared-fs-cache',
];
module.exports = function (modulePath, options) {
// Skip sequencer
if (modulePath === 'jest-sequencer-@jest/test-sequencer') return;
// Only use custom resolution for workspace packages
// Let default resolver handle published packages and everything else
const workspacePackages = [
'@nx/rollup',
'@nx/eslint',
'@nx/vite',
'@nx/vitest',
'@nx/jest',
'@nx/docker',
'@nx/dotnet',
'@nx/js',
'@nx/maven',
'@nx/next',
'@nx/storybook',
'@nx/rsbuild',
'@nx/react-native',
'@nx/express',
'@nx/web',
'@nx/vue',
'@nx/workspace',
'@nx/module-federation',
'@nx/rspack',
'@nx/eslint-plugin',
'@nx/angular',
'@nx/create-nx-plugin',
'@nx/create-nx-workspace',
'@nx/cypress',
'@nx/detox',
'@nx/devkit',
'@nx/esbuild',
'@nx/expo',
'@nx/gradle',
'@nx/nest',
'@nx/node',
'@nx/nuxt',
'@nx/playwright',
'@nx/plugin',
'@nx/react',
'@nx/remix',
'@nx/webpack',
];
const isWorkspacePackage =
workspacePackages.some((pkg) => modulePath.startsWith(pkg)) ||
modulePath.startsWith('nx/');
if (!isWorkspacePackage) {
// Global modules which must be resolved by defaultResolver
if (['child_process', 'fs', 'http', 'path'].includes(modulePath)) {
return options.defaultResolver(modulePath, options);
}
return enhancedResolver(path.resolve(options.basedir), modulePath);
}
const ext = path.extname(modulePath);
// Handle CSS imports
if (ext === '.css' || ext === '.scss' || ext === '.sass' || ext === '.less') {
return require.resolve('identity-obj-proxy');
}
try {
// For nx/* imports, use @nx/nx-source condition to resolve to .ts
// source files instead of dist/*.js files.
if (modulePath.startsWith('nx/') || modulePath === 'nx') {
return options.defaultResolver(modulePath, {
...options,
conditions: ['@nx/nx-source', 'require', 'node', 'default'],
});
}
// Find workspace root - avoid filesystem lookups inside node_modules
// For PNPM workspaces, we know the structure: workspace/packages/packageName
let workspaceRoot = options.rootDir;
// If we're in a packages subdirectory, go up two levels to workspace root
if (workspaceRoot.includes('/packages/')) {
const packagesIndex = workspaceRoot.lastIndexOf('/packages/');
workspaceRoot = workspaceRoot.substring(0, packagesIndex);
} else {
// Fallback: go up directories until we find packages/ (but check pnpm-lock.yaml for validation)
while (workspaceRoot && workspaceRoot !== path.dirname(workspaceRoot)) {
const pnpmLock = path.join(workspaceRoot, 'pnpm-lock.yaml');
const packagesDir = path.join(workspaceRoot, 'packages');
if (fs.existsSync(pnpmLock) && fs.existsSync(packagesDir)) {
break;
}
workspaceRoot = path.dirname(workspaceRoot);
}
}
const packagesPath = path.join(workspaceRoot, 'packages');
// Handle main @nx/* package imports (e.g., '@nx/devkit', '@nx/js')
const nxPackageMatch = modulePath.match(/^@nx\/([^/]+)$/);
if (nxPackageMatch) {
const packageName = nxPackageMatch[1];
// Check if this package exists in workspace
const packageDir = path.join(packagesPath, packageName);
// Try different entry points based on package structure
const possibleEntries = [
path.join(packageDir, 'index.ts'),
path.join(packageDir, 'src', 'index.ts'),
];
for (const entry of possibleEntries) {
if (isWorkspaceFile(entry)) {
return entry;
}
}
}
// Handle @nx/*/src/* subpath imports (e.g., '@nx/devkit/src/utils/something')
const nxSubpathMatch = modulePath.match(/^@nx\/([^/]+)\/src\/(.+)$/);
if (nxSubpathMatch) {
const packageName = nxSubpathMatch[1];
const subpath = nxSubpathMatch[2];
// Try different patterns for subpath resolution
const possiblePaths = [
path.join(packagesPath, packageName, 'src', subpath + '.ts'), // Direct file
path.join(packagesPath, packageName, 'src', subpath, 'index.ts'), // Directory with index.ts
];
for (const possiblePath of possiblePaths) {
if (isWorkspaceFile(possiblePath)) {
return possiblePath;
}
}
}
// Handle @nx/* other subpaths (e.g., '@nx/devkit/testing', '@nx/devkit/package.json')
const nxOtherMatch = modulePath.match(/^@nx\/([^/]+)\/(.+)$/);
if (nxOtherMatch) {
const packageName = nxOtherMatch[1];
const subpath = nxOtherMatch[2];
const packageDir = path.join(packagesPath, packageName);
// Try different patterns for subpath resolution
const possiblePaths = [
path.join(packageDir, subpath), // For files like package.json
path.join(packageDir, subpath + '.ts'),
path.join(packageDir, 'src', subpath + '.ts'),
];
for (const possiblePath of possiblePaths) {
if (isWorkspaceFile(possiblePath)) {
return possiblePath;
}
}
}
// Block excluded Nx packages from auto-resolution
if (
modulePath.startsWith('@nx/') &&
!modulePath.startsWith('@nx/powerpack-') &&
!excludedPackages.some((pkg) => modulePath.startsWith(pkg))
) {
// If we get here, it means the workspace package couldn't be resolved above
// This might indicate a missing file or incorrect import
console.warn(
`[resolver] Could not resolve workspace package: ${modulePath}`
);
}
return enhancedResolver(path.resolve(options.basedir), modulePath);
} catch (e) {
// Final fallback: use default resolver for packages we can't handle
// This preserves the old behavior where packages that couldn't be resolved
// by the custom resolver would fall back to default resolution
try {
return options.defaultResolver(modulePath, options);
} catch (defaultResolverError) {
throw new Error(`[resolver] Could not resolve module: ${modulePath}`);
}
}
};