366 lines
15 KiB
JavaScript
366 lines
15 KiB
JavaScript
const path = require('path');
|
|
|
|
// Absolute paths to the physical source files inside `packages/nx`. Mocking
|
|
// by the `nx/src/...` specifier instead routes through the pnpm
|
|
// `node_modules/nx` symlink, which jest keys as a *different* module id
|
|
// from the relative imports inside `packages/nx` — so the mock is never
|
|
// applied on CI. Using the absolute physical path here guarantees both
|
|
// resolution chains hit the same registry entry.
|
|
const nxSrcPath = (relative) => {
|
|
const base = path.resolve(__dirname, '..', 'packages/nx/src', relative);
|
|
// Resolve to the actual source file. `jest.doMock` keys mocks by the path
|
|
// that callers' module resolution produces — `.ts` for our source, or
|
|
// `index.js` for the napi binding entry — so passing a bare directory or
|
|
// extension-less path leaves callers' imports unmocked.
|
|
for (const candidate of [base, `${base}.ts`, path.join(base, 'index.js')]) {
|
|
try {
|
|
const stat = require('fs').statSync(candidate);
|
|
if (stat.isFile()) return candidate;
|
|
} catch (_) {}
|
|
}
|
|
return base;
|
|
};
|
|
const realWorkspaceRoot = path.resolve(__dirname, '..');
|
|
|
|
module.exports = () => {
|
|
/**
|
|
* When the daemon is enabled during unit tests,
|
|
* and the daemon is already running, the daemon-client.ts
|
|
* code will be used, but it will hit the already running
|
|
* daemon which is from the installed version of Nx.
|
|
*
|
|
* In the vast majority of cases, this is fine. However,
|
|
* if a new message type has been added to the daemon in
|
|
* the source code, and isn't yet in the installed version,
|
|
* any test that hits that codepath will fail. This is because
|
|
* the installed version of the daemon doesn't know how to
|
|
* handle the new message type.
|
|
*
|
|
* To prevent this, we disable the daemon during unit tests.
|
|
*/
|
|
process.env.NX_DAEMON = 'false';
|
|
|
|
const emptyProjectGraph = { nodes: {}, dependencies: {} };
|
|
const emptyProjectGraphAndMaps = {
|
|
projectGraph: emptyProjectGraph,
|
|
sourceMaps: {},
|
|
};
|
|
|
|
/**
|
|
* When `createProjectGraphAsync` is called during tests,
|
|
* if its not mocked, it will return the Nx repo's project
|
|
* graph. We don't want any unit tests to depend on the structure
|
|
* of the Nx repo, so we mock it to return an empty project graph.
|
|
*
|
|
* Skipped for `packages/nx` itself — `nx`'s own source code never imports
|
|
* from `@nx/devkit` (devkit re-exports from nx, not vice versa), and the
|
|
* relative-path mock for `nx/src/project-graph/project-graph` below
|
|
* already covers nx's internal `createProjectGraphAsync` callers. Loading
|
|
* devkit here just to spread `requireActual('@nx/devkit')` would pull
|
|
* its entire source tree into the sandbox for no callers.
|
|
*/
|
|
const isNxProject = process.env.NX_TASK_TARGET_PROJECT === 'nx';
|
|
if (!isNxProject) {
|
|
jest.doMock('@nx/devkit', () => ({
|
|
__esModule: true,
|
|
...jest.requireActual('@nx/devkit'),
|
|
createProjectGraphAsync: jest.fn(async () => emptyProjectGraph),
|
|
/**
|
|
* `ensurePackage` calls `require(pkg)` which resolves from node_modules
|
|
* (the installed version) instead of the local source code. Using
|
|
* `jest.requireActual` routes through Jest's module resolver which
|
|
* respects tsconfig paths, so it picks up the source code instead.
|
|
*/
|
|
ensurePackage: jest.fn((pkg) => jest.requireActual(pkg)),
|
|
}));
|
|
}
|
|
|
|
/**
|
|
* Code inside `packages/nx` imports graph builders via relative paths
|
|
* (`../../project-graph/project-graph`), which skip the `@nx/devkit`
|
|
* mock above. Mock the source file at its absolute physical path so
|
|
* those callers also get an empty graph.
|
|
*/
|
|
const projectGraphPath = nxSrcPath('project-graph/project-graph');
|
|
jest.doMock(projectGraphPath, () => {
|
|
const actual = jest.requireActual(projectGraphPath);
|
|
return {
|
|
__esModule: true,
|
|
...actual,
|
|
createProjectGraphAsync: jest.fn(async () => emptyProjectGraph),
|
|
createProjectGraphAndSourceMapsAsync: jest.fn(
|
|
async () => emptyProjectGraphAndMaps
|
|
),
|
|
buildProjectGraphAndSourceMapsWithoutDaemon: jest.fn(
|
|
async () => emptyProjectGraphAndMaps
|
|
),
|
|
};
|
|
});
|
|
|
|
/**
|
|
* Guard: if a unit test reaches plugin isolation pointed at the real
|
|
* workspace, it spawns a `plugin-worker.ts` subprocess that scans the
|
|
* whole monorepo and produces ~thousands of sandbox violations. Tests
|
|
* that legitimately exercise plugin isolation against a `TempFs` root
|
|
* (e.g. `getOnlyDefaultPlugins(tempFs.tempDir)`) pass through unchanged.
|
|
*/
|
|
const loadIsolatedPath = nxSrcPath(
|
|
'project-graph/plugins/isolation/load-isolated-plugin'
|
|
);
|
|
jest.doMock(loadIsolatedPath, () => {
|
|
const actual = jest.requireActual(loadIsolatedPath);
|
|
return {
|
|
__esModule: true,
|
|
...actual,
|
|
loadIsolatedNxPlugin: jest.fn((plugin, root, index) => {
|
|
if (root === realWorkspaceRoot) {
|
|
throw new Error(
|
|
'[unit-test-setup] loadIsolatedNxPlugin was called with the real ' +
|
|
'workspace root during a unit test. This spawns a real plugin ' +
|
|
'worker that scans the entire monorepo and causes sandbox ' +
|
|
'violations. Something reached real project-graph computation ' +
|
|
'without hitting the @nx/devkit or project-graph mocks. Check ' +
|
|
'the stack trace for the unmocked caller and either mock it in ' +
|
|
'the test, point the call at a TempFs root, or extend ' +
|
|
'scripts/unit-test-setup.js.'
|
|
);
|
|
}
|
|
return actual.loadIsolatedNxPlugin(plugin, root, index);
|
|
}),
|
|
};
|
|
});
|
|
|
|
/**
|
|
* Guard: short-circuit `workspace-context` helpers when they're handed the
|
|
* real workspace root. The native rust `WorkspaceContext` recursively walks
|
|
* the workspace on construction, so any test that reaches these with the
|
|
* real root scans the full monorepo and produces thousands of sandbox
|
|
* violations. Tests that pass a `TempFs` root continue to hit the real
|
|
* implementation.
|
|
*
|
|
* The actual culprit observed: `createFileMapUsingProjectGraph` reads the
|
|
* imported `workspaceRoot` constant and calls `getAllFileDataInContext` on
|
|
* it. Returning empty results for the real root gives every caller a safe
|
|
* no-op without breaking tests that have synthetic file maps.
|
|
*/
|
|
// Use plain functions (not `jest.fn`) so `jest.resetAllMocks()` in test
|
|
// suites can't wipe these implementations and turn them into `() =>
|
|
// undefined`, which would surface as "is not iterable" downstream.
|
|
const workspaceContextPath = nxSrcPath('utils/workspace-context');
|
|
jest.doMock(workspaceContextPath, () => {
|
|
// Lazily resolve the real module on each call. Capturing it in the
|
|
// factory closure produces an empty object on the first invocation
|
|
// (jest's internal loader returns the in-progress `module.exports`
|
|
// when `requireActual` re-enters the same module from inside the
|
|
// mock factory).
|
|
const realFn =
|
|
(name) =>
|
|
(...args) =>
|
|
jest.requireActual(workspaceContextPath)[name](...args);
|
|
const guarded =
|
|
(name, fallback) =>
|
|
(root, ...rest) => {
|
|
if (root === realWorkspaceRoot) return fallback();
|
|
return jest.requireActual(workspaceContextPath)[name](root, ...rest);
|
|
};
|
|
return {
|
|
__esModule: true,
|
|
setupWorkspaceContext: (root) => {
|
|
if (root === realWorkspaceRoot) return;
|
|
return jest
|
|
.requireActual(workspaceContextPath)
|
|
.setupWorkspaceContext(root);
|
|
},
|
|
getNxWorkspaceFilesFromContext: guarded(
|
|
'getNxWorkspaceFilesFromContext',
|
|
() =>
|
|
Promise.resolve({
|
|
projectFileMap: {},
|
|
globalFiles: [],
|
|
externalReferences: {},
|
|
})
|
|
),
|
|
globWithWorkspaceContext: guarded('globWithWorkspaceContext', () =>
|
|
Promise.resolve([])
|
|
),
|
|
globWithWorkspaceContextSync: guarded(
|
|
'globWithWorkspaceContextSync',
|
|
() => []
|
|
),
|
|
multiGlobWithWorkspaceContext: guarded(
|
|
'multiGlobWithWorkspaceContext',
|
|
() => Promise.resolve([])
|
|
),
|
|
hashWithWorkspaceContext: guarded('hashWithWorkspaceContext', () =>
|
|
Promise.resolve('0')
|
|
),
|
|
hashMultiGlobWithWorkspaceContext: guarded(
|
|
'hashMultiGlobWithWorkspaceContext',
|
|
() => Promise.resolve([])
|
|
),
|
|
getAllFileDataInContext: guarded('getAllFileDataInContext', () =>
|
|
Promise.resolve([])
|
|
),
|
|
getFilesInDirectoryUsingContext: guarded(
|
|
'getFilesInDirectoryUsingContext',
|
|
() => Promise.resolve([])
|
|
),
|
|
// Pass-through helpers that don't take a workspace root.
|
|
updateContextWithChangedFiles: realFn('updateContextWithChangedFiles'),
|
|
updateFilesInContext: realFn('updateFilesInContext'),
|
|
updateProjectFiles: realFn('updateProjectFiles'),
|
|
resetWorkspaceContext: realFn('resetWorkspaceContext'),
|
|
};
|
|
});
|
|
|
|
/**
|
|
* Backstop: short-circuit native rust functions that recursively walk a
|
|
* directory when they're handed the real workspace root. The
|
|
* `workspace-context` mock above catches the high-level callers, but
|
|
* `expandOutputs` / `getFilesForOutputsBatch` are called directly from
|
|
* `tasks-runner/cache.ts` (`_expandOutputs(outputs, workspaceRoot)`) and
|
|
* miss that net — they construct nothing, but `expand_outputs` drives
|
|
* `nx_walker(realWorkspaceRoot)` and surfaces as the same cross-project
|
|
* violation set.
|
|
*/
|
|
const nativePath = nxSrcPath('native');
|
|
jest.doMock(nativePath, () => {
|
|
const actual = jest.requireActual(nativePath);
|
|
const RealWorkspaceContext = actual.WorkspaceContext;
|
|
function GuardedWorkspaceContext(root, cacheDir) {
|
|
if (root === realWorkspaceRoot) {
|
|
throw new Error(
|
|
'[unit-test-setup] WorkspaceContext was constructed with the real ' +
|
|
'workspace root during a unit test. This triggers a recursive ' +
|
|
'walk of the entire monorepo and causes sandbox violations. ' +
|
|
'Check the stack trace for the caller and either mock it in the ' +
|
|
'test, point the call at a TempFs root, or extend ' +
|
|
'scripts/unit-test-setup.js.'
|
|
);
|
|
}
|
|
return new RealWorkspaceContext(root, cacheDir);
|
|
}
|
|
GuardedWorkspaceContext.prototype = RealWorkspaceContext.prototype;
|
|
const guardDirArg = (fn, fallback) =>
|
|
function (directory, ...rest) {
|
|
if (directory === realWorkspaceRoot) return fallback;
|
|
return fn(directory, ...rest);
|
|
};
|
|
return {
|
|
__esModule: true,
|
|
...actual,
|
|
WorkspaceContext: GuardedWorkspaceContext,
|
|
expandOutputs: guardDirArg(actual.expandOutputs, []),
|
|
getFilesForOutputsBatch: guardDirArg(actual.getFilesForOutputsBatch, []),
|
|
};
|
|
});
|
|
|
|
/**
|
|
* `isUsingTsSolutionSetup()` falls back to `new FsTree(workspaceRoot, false)`
|
|
* when called without a tree, which reads the real repo's `tsconfig.json` /
|
|
* `tsconfig.base.json`. That surfaces as a sandbox violation for tests that
|
|
* indirectly invoke it (cypress-preset, playwright-preset, plugin
|
|
* `createNodesV2`, executor `normalize`, etc.).
|
|
*
|
|
* Unit tests should never touch the real workspace FS, so when the function
|
|
* is called without a tree, short-circuit to `true`. `true` matches the
|
|
* de-facto behavior of hitting the real FS (the Nx repo is a TS solution
|
|
* workspace), preserving every test's existing expectations without
|
|
* reading from disk. Calls that pass an explicit (virtual) tree still run
|
|
* the real implementation.
|
|
*
|
|
* There are two copies of the function — one in `@nx/js` and one in
|
|
* `@nx/workspace` — both need to be mocked.
|
|
*/
|
|
const mockIsUsingTsSolutionSetup = (specifier) => {
|
|
// Some test configs (e.g. tools/workspace-plugin) use the default jest
|
|
// resolver, which does not read package `exports` maps. If a workspace
|
|
// package locks down its `exports` map, `@nx/<pkg>/src/...` subpath
|
|
// imports become unresolvable in those contexts. Skip the mock there —
|
|
// those tests don't import the function anyway.
|
|
try {
|
|
require.resolve(specifier);
|
|
} catch {
|
|
return;
|
|
}
|
|
jest.doMock(specifier, () => {
|
|
const actual = jest.requireActual(specifier);
|
|
return {
|
|
__esModule: true,
|
|
...actual,
|
|
isUsingTsSolutionSetup: jest.fn((tree) =>
|
|
tree ? actual.isUsingTsSolutionSetup(tree) : true
|
|
),
|
|
};
|
|
});
|
|
};
|
|
mockIsUsingTsSolutionSetup('@nx/js/internal');
|
|
mockIsUsingTsSolutionSetup(
|
|
'@nx/workspace/src/utilities/typescript/ts-solution-setup'
|
|
);
|
|
|
|
/**
|
|
* Two helpers in `packages/nx/src/utils/` probe the filesystem via
|
|
* `require.resolve` to find sibling Nx packages:
|
|
*
|
|
* - `hasNxJsPlugin(projectRoot, workspaceRoot)` (in `has-nx-js-plugin.ts`)
|
|
* — checks whether `@nx/js` is installed so it can decide whether to
|
|
* inject the implicit `nx-release-publish` target on a
|
|
* `package.json`-based project.
|
|
* - `readModulePackageJsonWithoutFallbacks(specifier, paths)` (in
|
|
* `package-json.ts`) — reads a plugin's `package.json`. Used by
|
|
* `readPluginPackageJson`, `readExecutorJson`, and target normalization.
|
|
*
|
|
* In unit tests `__dirname` falls back to the real `packages/nx/src/utils`,
|
|
* so even when callers pass a synthetic `workspaceRoot` like `/tmp/test`,
|
|
* Node's resolver walks up to the real repo's pnpm-symlinked
|
|
* `node_modules` and lands on `packages/<plugin>/package.json`. Each one
|
|
* shows up as a sandbox-violating foreign read.
|
|
*
|
|
* Pin both behaviors:
|
|
* - `hasNxJsPlugin` → always `true`, matching the de-facto answer in
|
|
* this repo (and what tests expect — they assert the implicit
|
|
* target gets added).
|
|
* - `readModulePackageJsonWithoutFallbacks` → throw MODULE_NOT_FOUND for
|
|
* `@nx/*` lookups. Production callers (`readPluginPackageJson`,
|
|
* `readExecutorJson`, target normalization) all catch MODULE_NOT_FOUND
|
|
* and degrade gracefully.
|
|
*
|
|
* Scoped to `nx:test` only — these mocks target `packages/nx/src/utils/`
|
|
* source files by absolute physical path and exist to neutralize nx's
|
|
* own plugin-resolution probing. Applying them to other projects'
|
|
* tests (rspack, webpack, jest, …) can interfere with legitimate
|
|
* `@nx/*` lookups those test paths might exercise.
|
|
*/
|
|
if (isNxProject) {
|
|
const hasNxJsPluginPath = nxSrcPath('utils/has-nx-js-plugin');
|
|
jest.doMock(hasNxJsPluginPath, () => ({
|
|
__esModule: true,
|
|
hasNxJsPlugin: () => true,
|
|
}));
|
|
|
|
const packageJsonPath = nxSrcPath('utils/package-json');
|
|
jest.doMock(packageJsonPath, () => {
|
|
const actual = jest.requireActual(packageJsonPath);
|
|
return {
|
|
__esModule: true,
|
|
...actual,
|
|
readModulePackageJsonWithoutFallbacks: (
|
|
moduleSpecifier,
|
|
requirePaths
|
|
) => {
|
|
if (moduleSpecifier && moduleSpecifier.startsWith('@nx/')) {
|
|
const err = new Error(`Cannot find module '${moduleSpecifier}'`);
|
|
err.code = 'MODULE_NOT_FOUND';
|
|
throw err;
|
|
}
|
|
return actual.readModulePackageJsonWithoutFallbacks(
|
|
moduleSpecifier,
|
|
requirePaths
|
|
);
|
|
},
|
|
};
|
|
});
|
|
}
|
|
};
|