@nx/vitest 23.3.0-beta.4 → 23.3.0-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -9,6 +9,7 @@ const node_fs_1 = require("node:fs");
9
9
  const promises_1 = require("node:fs/promises");
10
10
  const node_path_1 = require("node:path");
11
11
  const executor_utils_1 = require("../utils/executor-utils");
12
+ const setup_file_inputs_1 = require("./setup-file-inputs");
12
13
  /**
13
14
  * @deprecated The 'createDependencies' function is now a no-op. This functionality is included in 'createNodesV2'.
14
15
  */
@@ -42,6 +43,7 @@ exports.createNodes = [
42
43
  lockfile,
43
44
  ...(tsconfigChainsByProjectRoot.get(root) ?? []),
44
45
  ]));
46
+ const setupTsconfigJsonCache = new Map();
45
47
  try {
46
48
  return await (0, devkit_1.createNodesFromFiles)(async (configFile, _, context, idx) => {
47
49
  const projectRoot = (0, node_path_1.dirname)(configFile);
@@ -50,7 +52,10 @@ exports.createNodes = [
50
52
  // Adding the config file path to the hash ensures that the final hash value is different
51
53
  // for different config files.
52
54
  const hash = hashes[idx] + configFile;
53
- if (!targetsCache.has(hash)) {
55
+ const cachedTargets = targetsCache.get(hash);
56
+ // Older cache entries predate `setupFileCandidates`.
57
+ if (cachedTargets === undefined ||
58
+ (cachedTargets && !cachedTargets.setupFileCandidates)) {
54
59
  const result = await buildVitestTargets(configFile, projectRoot, normalizedOptions, context, pmc, tsconfigChainsByProjectRoot.get(projectRoot) ?? []);
55
60
  // Cache the result even when it's null (a root orchestrator config)
56
61
  // so the config isn't re-resolved on every project-graph build.
@@ -62,10 +67,12 @@ exports.createNodes = [
62
67
  if (!cached) {
63
68
  return { projects: {} };
64
69
  }
65
- const { metadata, targets } = cached;
70
+ const { metadata, targets, setupFileCandidates } = cached;
66
71
  const project = {
67
72
  root: projectRoot,
68
- targets,
73
+ // Resolved on every run: the cache key does not cover setup files
74
+ // or their tsconfigs.
75
+ targets: withSetupFileInputs(targets, normalizedOptions.testTargetName, setupFileCandidates, projectRoot, context.workspaceRoot, setupTsconfigJsonCache),
69
76
  metadata,
70
77
  };
71
78
  return {
@@ -105,9 +112,25 @@ async function buildVitestTargets(configFilePath, projectRoot, options, context,
105
112
  // vitest/node not available or not needed, ignore
106
113
  }
107
114
  const { resolveConfig } = await (0, executor_utils_1.loadViteDynamicImport)();
115
+ // Vite fills a missing `root` with `process.cwd()`, which at graph time is
116
+ // the workspace root - not where the task runs. Capture what the config
117
+ // actually authored so a relative path can be resolved the way Vitest will.
118
+ let authoredViteRoot;
108
119
  const viteBuildConfig = await (0, internal_1.retryOnRequireEsmRace)(() => resolveConfig({
109
120
  configFile: absoluteConfigFilePath,
110
121
  mode: 'development',
122
+ plugins: [
123
+ {
124
+ name: 'nx-capture-authored-vitest-root',
125
+ enforce: 'post',
126
+ config: {
127
+ order: 'post',
128
+ handler(config) {
129
+ authoredViteRoot = config.root;
130
+ },
131
+ },
132
+ },
133
+ ],
111
134
  }, 'build'));
112
135
  // A root config that aggregates project configs via `test.projects` is just an
113
136
  // orchestrator; the actual tests live in the individual project configs. Skip it
@@ -122,9 +145,11 @@ async function buildVitestTargets(configFilePath, projectRoot, options, context,
122
145
  const { testOutputs, hasTest } = getOutputs(viteBuildConfig, projectRoot, context.workspaceRoot);
123
146
  const namedInputs = (0, internal_1.getNamedInputs)(projectRoot, context);
124
147
  const targets = {};
148
+ let setupFileCandidates = [];
125
149
  // if file is vitest.config or vite.config has definition for test, create targets for test and/or atomized tests
126
150
  if (configFilePath.includes('vitest.config') || hasTest) {
127
151
  const isTypecheckEnabled = !!viteBuildConfig.test?.typecheck?.enabled;
152
+ setupFileCandidates = (0, setup_file_inputs_1.resolveSetupFileCandidates)(viteBuildConfig, projectRoot, context.workspaceRoot, authoredViteRoot);
128
153
  targets[options.testTargetName] = await testTarget(namedInputs, testOutputs, projectRoot, options.testMode, pmc, isTypecheckEnabled, tsconfigInputs);
129
154
  if (options.ciTargetName) {
130
155
  const groupName = options.ciGroupName ?? (0, internal_1.deriveGroupNameFromTarget)(options.ciTargetName);
@@ -288,7 +313,28 @@ async function buildVitestTargets(configFilePath, projectRoot, options, context,
288
313
  }
289
314
  }
290
315
  }
291
- return { targets, metadata };
316
+ return { targets, metadata, setupFileCandidates };
317
+ }
318
+ function withSetupFileInputs(targets, testTargetName, setupFileCandidates, projectRoot, workspaceRoot, jsonCache) {
319
+ if (setupFileCandidates.length === 0)
320
+ return targets;
321
+ const { files, tsconfigs } = (0, setup_file_inputs_1.collectSetupFileInputs)(setupFileCandidates, projectRoot, workspaceRoot, jsonCache);
322
+ if (files.length === 0 && tsconfigs.length === 0)
323
+ return targets;
324
+ const setupInputs = [
325
+ ...tsconfigs.map((f) => ({
326
+ json: `{workspaceRoot}/${f}`,
327
+ fields: ['compilerOptions'],
328
+ })),
329
+ // Whole-file, not just `compilerOptions`: these are sources Vitest
330
+ // executes, so any change to them changes the run.
331
+ ...files.map((f) => `{workspaceRoot}/${f}`),
332
+ ];
333
+ const inputs = [...targets[testTargetName].inputs, ...setupInputs];
334
+ return Object.fromEntries(Object.entries(targets).map(([name, target]) => [
335
+ name,
336
+ { ...target, inputs },
337
+ ]));
292
338
  }
293
339
  async function testTarget(namedInputs, outputs, projectRoot, testMode = 'watch', pmc, isTypecheckEnabled, tsconfigInputs) {
294
340
  const command = testMode === 'run' ? 'vitest run' : 'vitest';
@@ -0,0 +1,24 @@
1
+ import type { RawTsconfigJsonCache } from '@nx/js/internal';
2
+ import type { ResolvedConfig } from 'vite';
3
+ /**
4
+ * Resolves the files Vitest loads from outside the project root - `setupFiles`
5
+ * and `globalSetup`. `collectSetupFileInputs` declares the ones that exist,
6
+ * together with the tsconfigs Vite reads to transform them.
7
+ *
8
+ * Reads the build-resolved config, as the inferred target's inputs are built
9
+ * from it. Setup files contributed by an `apply: 'serve'` plugin are therefore
10
+ * invisible here - the same build/serve divergence the atomized path documents.
11
+ *
12
+ * Solution-style tsconfigs are deliberately not followed. When the nearest
13
+ * tsconfig of a setup file is a solution file, Vite walks its `references` and
14
+ * reads every project's tsconfig; declaring those would attach one input per
15
+ * project in the workspace. Keep the setup file inside a project (or beside a
16
+ * leaf tsconfig) instead.
17
+ */
18
+ export declare function resolveSetupFileCandidates(viteConfig: ResolvedConfig, projectRoot: string, workspaceRoot: string,
19
+ /** `root` as the config authored it, before vite defaults it to `cwd`. */
20
+ authoredViteRoot?: string): string[];
21
+ export declare function collectSetupFileInputs(setupFileCandidates: string[], projectRoot: string, workspaceRoot: string, jsonCache: RawTsconfigJsonCache): {
22
+ files: string[];
23
+ tsconfigs: string[];
24
+ };
@@ -0,0 +1,103 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.resolveSetupFileCandidates = resolveSetupFileCandidates;
4
+ exports.collectSetupFileInputs = collectSetupFileInputs;
5
+ const internal_1 = require("@nx/js/internal");
6
+ const js_1 = require("@nx/js");
7
+ const node_fs_1 = require("node:fs");
8
+ const node_path_1 = require("node:path");
9
+ /**
10
+ * Resolves the files Vitest loads from outside the project root - `setupFiles`
11
+ * and `globalSetup`. `collectSetupFileInputs` declares the ones that exist,
12
+ * together with the tsconfigs Vite reads to transform them.
13
+ *
14
+ * Reads the build-resolved config, as the inferred target's inputs are built
15
+ * from it. Setup files contributed by an `apply: 'serve'` plugin are therefore
16
+ * invisible here - the same build/serve divergence the atomized path documents.
17
+ *
18
+ * Solution-style tsconfigs are deliberately not followed. When the nearest
19
+ * tsconfig of a setup file is a solution file, Vite walks its `references` and
20
+ * reads every project's tsconfig; declaring those would attach one input per
21
+ * project in the workspace. Keep the setup file inside a project (or beside a
22
+ * leaf tsconfig) instead.
23
+ */
24
+ function resolveSetupFileCandidates(viteConfig, projectRoot, workspaceRoot,
25
+ /** `root` as the config authored it, before vite defaults it to `cwd`. */
26
+ authoredViteRoot) {
27
+ // At the workspace root everything is already covered by `default`.
28
+ if (projectRoot === '.')
29
+ return [];
30
+ const entries = [
31
+ viteConfig.test?.setupFiles,
32
+ viteConfig.test?.globalSetup,
33
+ ].flatMap((value) => typeof value === 'string' ? [value] : Array.isArray(value) ? value : []);
34
+ if (entries.length === 0)
35
+ return [];
36
+ // Vitest resolves both options against its root, and the inferred target
37
+ // runs with `cwd` set to the project root - so a config that authors no
38
+ // root resolves them against the project, not `viteConfig.root` (which vite
39
+ // has already defaulted to the graph process's cwd). Mirrors
40
+ // `effectiveVitestRoot` in plugin.ts.
41
+ const fullProjectRoot = (0, node_path_1.resolve)(workspaceRoot, projectRoot);
42
+ const authoredRoot = viteConfig.test?.root ?? authoredViteRoot;
43
+ const configRoot = !authoredRoot
44
+ ? fullProjectRoot
45
+ : (0, node_path_1.isAbsolute)(authoredRoot)
46
+ ? authoredRoot
47
+ : (0, node_path_1.resolve)(fullProjectRoot, authoredRoot);
48
+ const candidates = new Set();
49
+ for (const entry of entries) {
50
+ const absolutePath = (0, node_path_1.isAbsolute)(entry) ? entry : (0, node_path_1.resolve)(configRoot, entry);
51
+ const wsRelative = declarable(absolutePath, projectRoot, workspaceRoot);
52
+ if (wsRelative)
53
+ candidates.add(wsRelative);
54
+ }
55
+ return [...candidates];
56
+ }
57
+ function collectSetupFileInputs(setupFileCandidates, projectRoot, workspaceRoot, jsonCache) {
58
+ const rootTsConfigName = (0, js_1.getRootTsConfigFileName)();
59
+ const files = [];
60
+ const tsconfigs = new Set();
61
+ for (const file of setupFileCandidates) {
62
+ const absolutePath = (0, node_path_1.join)(workspaceRoot, file);
63
+ if (!(0, node_fs_1.existsSync)(absolutePath))
64
+ continue;
65
+ files.push(file);
66
+ // The tsconfig Vite resolves for the file: the nearest one walking up,
67
+ // plus its extends chain.
68
+ let dir = (0, node_path_1.dirname)(absolutePath);
69
+ while (dir.startsWith(workspaceRoot)) {
70
+ const candidate = (0, node_path_1.join)(dir, 'tsconfig.json');
71
+ if ((0, node_fs_1.existsSync)(candidate)) {
72
+ (0, internal_1.walkTsconfigExtendsChain)(candidate, (absPath) => {
73
+ const relativePath = declarable(absPath, projectRoot, workspaceRoot);
74
+ if (relativePath && relativePath !== rootTsConfigName) {
75
+ tsconfigs.add(relativePath);
76
+ }
77
+ return 'continue';
78
+ }, { jsonCache });
79
+ break;
80
+ }
81
+ const parent = (0, node_path_1.dirname)(dir);
82
+ if (parent === dir)
83
+ break;
84
+ dir = parent;
85
+ }
86
+ }
87
+ return { files, tsconfigs: [...tsconfigs] };
88
+ }
89
+ /** Workspace-relative path, or null when it is not ours to declare. */
90
+ function declarable(absolutePath, projectRoot, workspaceRoot) {
91
+ const wsRelative = (0, node_path_1.relative)(workspaceRoot, absolutePath).split(node_path_1.sep).join('/');
92
+ // Outside the workspace → cannot be expressed as an input.
93
+ if (wsRelative.startsWith('../') || wsRelative === '..')
94
+ return null;
95
+ // Inside node_modules → invalidated via the lockfile.
96
+ if (wsRelative.startsWith('node_modules/') ||
97
+ wsRelative.includes('/node_modules/'))
98
+ return null;
99
+ // Inside the project → covered by `default`.
100
+ if (wsRelative === projectRoot || wsRelative.startsWith(`${projectRoot}/`))
101
+ return null;
102
+ return wsRelative;
103
+ }
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@nx/vitest",
3
3
  "description": "The Nx Plugin for Vitest to enable fast unit testing with Vitest.",
4
- "version": "23.3.0-beta.4",
4
+ "version": "23.3.0-beta.6",
5
5
  "type": "commonjs",
6
6
  "files": [
7
7
  "dist",
@@ -78,16 +78,16 @@
78
78
  "executors": "./executors.json",
79
79
  "generators": "./generators.json",
80
80
  "dependencies": {
81
+ "@nx/devkit": "23.3.0-beta.6",
82
+ "@nx/js": "23.3.0-beta.6",
81
83
  "tslib": "^2.3.0",
82
84
  "semver": "^7.6.3",
83
- "@phenomnomnominal/tsquery": "~6.2.0",
84
- "@nx/devkit": "23.3.0-beta.4",
85
- "@nx/js": "23.3.0-beta.4"
85
+ "@phenomnomnominal/tsquery": "~6.2.0"
86
86
  },
87
87
  "peerDependencies": {
88
+ "@nx/eslint": "23.3.0-beta.6",
88
89
  "vitest": "^3.0.0 || ^4.0.0 || ^5.0.0",
89
- "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0",
90
- "@nx/eslint": "23.3.0-beta.4"
90
+ "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0"
91
91
  },
92
92
  "peerDependenciesMeta": {
93
93
  "@nx/eslint": {
@@ -101,6 +101,6 @@
101
101
  }
102
102
  },
103
103
  "devDependencies": {
104
- "nx": "23.3.0-beta.4"
104
+ "nx": "23.3.0-beta.6"
105
105
  }
106
106
  }
@@ -0,0 +1,18 @@
1
+ <p style="text-align: center;">
2
+ <picture>
3
+ <source media="(prefers-color-scheme: dark)" srcset="https://raw.githubusercontent.com/nrwl/nx/master/images/nx-dark.svg">
4
+ <img alt="Nx - Smart Monorepos · Fast Builds" src="https://raw.githubusercontent.com/nrwl/nx/master/images/nx-light.svg" width="100%">
5
+ </picture>
6
+ </p>
7
+
8
+ {{links}}
9
+
10
+ <hr>
11
+
12
+ # Nx: Smart Monorepos · Fast Builds
13
+
14
+ Get to green PRs in half the time. Nx optimizes your builds, scales your CI, and fixes failed PRs. Built for developers and AI agents.
15
+
16
+ This package is a [Vitest plugin for Nx](https://nx.dev/nx-api/vitest).
17
+
18
+ {{content}}