@nx/vitest 23.2.0-rc.0 → 23.2.0-rc.2

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.
@@ -157,14 +157,70 @@ async function buildVitestTargets(configFilePath, projectRoot, options, context,
157
157
  // Resolve under `mode: 'test'` to match Vitest, which defaults the Vite
158
158
  // mode to 'test'; a config that branches on `command`/`mode` would
159
159
  // otherwise enumerate a different spec set here than at test time.
160
+ // Capture the raw root after user hooks: graph construction and the
161
+ // atom run resolve it against different cwds.
162
+ let configuredViteRoot;
160
163
  const viteServeConfig = await resolveConfig({
161
164
  configFile: absoluteConfigFilePath,
162
165
  mode: 'test',
166
+ plugins: [
167
+ {
168
+ // Promotes test.root before user hooks as Vitest does, so a
169
+ // later hook can override it. No options.root: atoms pass no --root.
170
+ name: 'nx-promote-vitest-root',
171
+ enforce: 'pre',
172
+ config(config) {
173
+ if (config.test?.root) {
174
+ return { root: config.test.root };
175
+ }
176
+ },
177
+ },
178
+ {
179
+ name: 'nx-capture-vitest-root',
180
+ enforce: 'post',
181
+ config: {
182
+ order: 'post',
183
+ handler(config) {
184
+ configuredViteRoot = config.root;
185
+ },
186
+ },
187
+ },
188
+ ],
163
189
  }, 'serve');
164
190
  const projectRootRelativeTestPaths = await getTestPathsRelativeToProjectRoot(projectRoot, context.workspaceRoot,
165
191
  // Only the glob path reads this config; the opt-out path forces the
166
192
  // runtime by receiving no config, and takes `test.dir` separately.
167
193
  useGlobDiscovery ? viteServeConfig : undefined, viteServeConfig.test?.dir);
194
+ // Each atom writes coverage to its own directory or the reports would
195
+ // overwrite one another. The nested flag never enables coverage.
196
+ const coverageReportsDirectory = viteServeConfig.test?.coverage?.reportsDirectory || 'coverage';
197
+ // Atom directories mirror spec paths. A shared base outside the project
198
+ // gets a project-root prefix unless it already ends with it, as the
199
+ // generated `<offset>/coverage/<projectRoot>` configs do.
200
+ const fullProjectRoot = (0, node_path_1.resolve)(context.workspaceRoot, projectRoot);
201
+ // A relative reportsDirectory resolves against the Vitest root, mapped
202
+ // here as the atom run would (cwd is the project root). A root computed
203
+ // from runtime state such as `process.cwd()` cannot be mapped and is
204
+ // not supported.
205
+ const effectiveVitestRoot = !configuredViteRoot
206
+ ? fullProjectRoot
207
+ : (0, node_path_1.isAbsolute)(configuredViteRoot)
208
+ ? configuredViteRoot
209
+ : (0, node_path_1.resolve)(fullProjectRoot, configuredViteRoot);
210
+ const resolvedReportsDirectory = (0, node_path_1.resolve)(effectiveVitestRoot, coverageReportsDirectory);
211
+ // Vitest refuses a reports directory equal to its root or cwd, and redirecting
212
+ // under it would point each atom at its own spec path for coverage to delete.
213
+ const vitestRejectsReportsDirectory = (0, node_path_1.relative)(effectiveVitestRoot, resolvedReportsDirectory) === '' ||
214
+ (0, node_path_1.relative)(fullProjectRoot, resolvedReportsDirectory) === '';
215
+ const atomSubfolderPrefix = isPathOutside((0, node_path_1.relative)(fullProjectRoot, resolvedReportsDirectory)) &&
216
+ !endsWithProjectRoot(resolvedReportsDirectory, projectRoot)
217
+ ? projectRoot
218
+ : '';
219
+ // The cache only accepts outputs inside the workspace. A base outside it
220
+ // cannot be declared, and a cache hit would then replay without writing
221
+ // coverage, so the atoms and their parent are not cached either.
222
+ const isCoverageCacheable = !isPathOutside((0, node_path_1.relative)(context.workspaceRoot, resolvedReportsDirectory));
223
+ const atomOutputs = [];
168
224
  for (const relativePath of projectRootRelativeTestPaths) {
169
225
  if (relativePath.includes('../')) {
170
226
  throw new Error('@nx/vitest 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' +
@@ -175,14 +231,28 @@ async function buildVitestTargets(configFilePath, projectRoot, options, context,
175
231
  context,
176
232
  }, null, 2)}`);
177
233
  }
234
+ const outputSubfolder = atomSubfolderPrefix
235
+ ? (0, devkit_1.joinPathFragments)(atomSubfolderPrefix, relativePath)
236
+ : relativePath;
237
+ // joinPathFragments strips Windows drive letters, so an absolute
238
+ // reports directory must be joined with the OS path helper.
239
+ const atomCoverageDirectory = (0, node_path_1.isAbsolute)(coverageReportsDirectory)
240
+ ? (0, node_path_1.join)(coverageReportsDirectory, outputSubfolder)
241
+ : (0, devkit_1.joinPathFragments)(coverageReportsDirectory, outputSubfolder);
178
242
  const targetName = `${options.ciTargetName}--${relativePath}`;
179
243
  dependsOn.push(targetName);
180
244
  targets[targetName] = {
181
245
  // It does not make sense to run atomized tests in watch mode as they are intended to be run in CI
182
- command: `vitest run ${relativePath}`,
183
- cache: targets[options.testTargetName].cache,
246
+ command: `vitest run ${(0, internal_1.quoteShellArg)(relativePath)}${vitestRejectsReportsDirectory
247
+ ? ''
248
+ : ` --coverage.reportsDirectory=${(0, internal_1.quoteShellArg)(atomCoverageDirectory)}`}`,
249
+ cache: isCoverageCacheable && targets[options.testTargetName].cache,
184
250
  inputs: targets[options.testTargetName].inputs,
185
- outputs: targets[options.testTargetName].outputs,
251
+ outputs: isCoverageCacheable && !vitestRejectsReportsDirectory
252
+ ? [
253
+ normalizeAtomOutputPath((0, node_path_1.join)(resolvedReportsDirectory, outputSubfolder), fullProjectRoot, context.workspaceRoot),
254
+ ]
255
+ : [],
186
256
  options: {
187
257
  cwd: projectRoot,
188
258
  env: targets[options.testTargetName].options.env,
@@ -200,14 +270,17 @@ async function buildVitestTargets(configFilePath, projectRoot, options, context,
200
270
  },
201
271
  },
202
272
  };
273
+ atomOutputs.push(...targets[targetName].outputs);
203
274
  targetGroup.push(targetName);
204
275
  }
205
276
  if (targetGroup.length > 0) {
206
277
  targets[options.ciTargetName] = {
207
278
  executor: 'nx:noop',
208
- cache: true,
279
+ cache: isCoverageCacheable,
209
280
  inputs: targets[options.testTargetName].inputs,
210
- outputs: targets[options.testTargetName].outputs,
281
+ // Exactly the atom directories: a cache restore replaces each declared
282
+ // directory, and any wider one can hold other projects' coverage.
283
+ outputs: atomOutputs,
211
284
  dependsOn,
212
285
  metadata: {
213
286
  technologies: ['vitest'],
@@ -292,6 +365,38 @@ function getOutputs(viteBuildConfig, projectRoot, workspaceRoot) {
292
365
  hasTest: !!test,
293
366
  };
294
367
  }
368
+ /**
369
+ * Whether a `relative()` result leaves the base directory. A directory named
370
+ * `..coverage` is a child, not traversal.
371
+ */
372
+ function isPathOutside(relativePath) {
373
+ return (relativePath === '..' ||
374
+ relativePath.startsWith(`..${node_path_1.sep}`) ||
375
+ (0, node_path_1.isAbsolute)(relativePath));
376
+ }
377
+ /**
378
+ * Maps an atom's absolute coverage directory, which must be inside the
379
+ * workspace, to the narrowest Nx root token.
380
+ */
381
+ function normalizeAtomOutputPath(absoluteOutputPath, fullProjectRoot, workspaceRoot) {
382
+ const relativeToProject = (0, node_path_1.relative)(fullProjectRoot, absoluteOutputPath);
383
+ if (!isPathOutside(relativeToProject)) {
384
+ return (0, devkit_1.joinPathFragments)('{projectRoot}', relativeToProject);
385
+ }
386
+ return (0, devkit_1.joinPathFragments)('{workspaceRoot}', (0, node_path_1.relative)(workspaceRoot, absoluteOutputPath));
387
+ }
388
+ /**
389
+ * Whether the reports directory ends with the project root's segments. The
390
+ * root project has none, so it always holds.
391
+ */
392
+ function endsWithProjectRoot(resolvedPath, projectRoot) {
393
+ const pathSegments = resolvedPath.split(/[\\/]/).filter(Boolean);
394
+ const rootSegments = projectRoot
395
+ .split('/')
396
+ .filter((segment) => segment && segment !== '.');
397
+ return (pathSegments.length >= rootSegments.length &&
398
+ rootSegments.every((segment, i) => pathSegments[pathSegments.length - rootSegments.length + i] === segment));
399
+ }
295
400
  function normalizeOutputPath(outputPath, projectRoot, workspaceRoot, path) {
296
401
  if (!outputPath) {
297
402
  if (projectRoot === '.') {
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.2.0-rc.0",
4
+ "version": "23.2.0-rc.2",
5
5
  "type": "commonjs",
6
6
  "files": [
7
7
  "dist",
@@ -81,13 +81,13 @@
81
81
  "tslib": "^2.3.0",
82
82
  "semver": "^7.6.3",
83
83
  "@phenomnomnominal/tsquery": "~6.2.0",
84
- "@nx/devkit": "23.2.0-rc.0",
85
- "@nx/js": "23.2.0-rc.0"
84
+ "@nx/devkit": "23.2.0-rc.2",
85
+ "@nx/js": "23.2.0-rc.2"
86
86
  },
87
87
  "peerDependencies": {
88
88
  "vitest": "^3.0.0 || ^4.0.0",
89
89
  "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0",
90
- "@nx/eslint": "23.2.0-rc.0"
90
+ "@nx/eslint": "23.2.0-rc.2"
91
91
  },
92
92
  "peerDependenciesMeta": {
93
93
  "@nx/eslint": {
@@ -101,6 +101,6 @@
101
101
  }
102
102
  },
103
103
  "devDependencies": {
104
- "nx": "23.2.0-rc.0"
104
+ "nx": "23.2.0-rc.2"
105
105
  }
106
106
  }