@nx/vitest 23.2.0-beta.0 → 23.2.0-beta.10

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.
@@ -0,0 +1,7 @@
1
+ /**
2
+ * The Oxlint plugins a Vitest-tested project needs. Declared here so the linter
3
+ * does not have to know what a test runner requires; `@nx/js`'s
4
+ * `addLintingToProject` reads it through `ensurePackage`.
5
+ */
6
+ export declare const oxlintPlugins: string[];
7
+ export { createOrEditViteConfig, ViteConfigFileOptions, } from './src/utils/generator-utils';
@@ -0,0 +1,16 @@
1
+ "use strict";
2
+ // Semi-private surface for first-party Nx packages.
3
+ //
4
+ // External plugins should NOT import from here — this entry is curated for
5
+ // internal consumers and may change without semver protection. Mirrors
6
+ // `@nx/devkit/internal`.
7
+ Object.defineProperty(exports, "__esModule", { value: true });
8
+ exports.createOrEditViteConfig = exports.oxlintPlugins = void 0;
9
+ /**
10
+ * The Oxlint plugins a Vitest-tested project needs. Declared here so the linter
11
+ * does not have to know what a test runner requires; `@nx/js`'s
12
+ * `addLintingToProject` reads it through `ensurePackage`.
13
+ */
14
+ exports.oxlintPlugins = ['vitest'];
15
+ var generator_utils_1 = require("./src/utils/generator-utils");
16
+ Object.defineProperty(exports, "createOrEditViteConfig", { enumerable: true, get: function () { return generator_utils_1.createOrEditViteConfig; } });
@@ -7,7 +7,7 @@ const devkit_1 = require("@nx/devkit");
7
7
  const options_utils_1 = require("../../../utils/options-utils");
8
8
  const path_1 = require("path");
9
9
  const executor_utils_1 = require("../../../utils/executor-utils");
10
- const devkit_internals_1 = require("nx/src/devkit-internals");
10
+ const internal_1 = require("@nx/devkit/internal");
11
11
  async function getOptions(options, context, projectRoot) {
12
12
  // Allows ESM to be required in CJS modules. Vite will be published as ESM in the future.
13
13
  const { loadConfigFromFile } = await (0, executor_utils_1.loadViteDynamicImport)();
@@ -57,7 +57,7 @@ async function getOptions(options, context, projectRoot) {
57
57
  // `nx run-many`/`affected` don't hang. An explicit CLI --watch/--no-watch or a
58
58
  // config `test.watch` still takes precedence.
59
59
  const uiRequested = passThroughOptions.ui === true;
60
- const watchForUi = uiRequested && !!process.stdin.isTTY && !(0, devkit_internals_1.isCI)();
60
+ const watchForUi = uiRequested && !!process.stdin.isTTY && !(0, internal_1.isCI)();
61
61
  return {
62
62
  watch: watch ??
63
63
  resolved?.config?.['test']?.watch ??
@@ -17,6 +17,17 @@ const versions_1 = require("../../utils/versions");
17
17
  const assert_supported_vitest_version_1 = require("../../utils/assert-supported-vitest-version");
18
18
  const semver_1 = require("semver");
19
19
  let ts;
20
+ // Must match `vitestWorkspaceFiles` in ../../plugins/plugin.ts, or the guards
21
+ // here and inference disagree about what counts as an existing workspace file.
22
+ const WORKSPACE_FILE_EXTENSIONS = [
23
+ 'ts',
24
+ 'mts',
25
+ 'cts',
26
+ 'js',
27
+ 'mjs',
28
+ 'cjs',
29
+ 'json',
30
+ ];
20
31
  /**
21
32
  * Determines whether to use vitest.config.mts instead of vite.config.mts.
22
33
  * Returns true for new non-framework projects that don't already have a vite.config.
@@ -121,6 +132,7 @@ getTestBed().initTestEnvironment(
121
132
  imports: [`import angular from '@analogjs/vite-plugin-angular'`],
122
133
  plugins: ['angular()'],
123
134
  setupFile: relativeTestSetupPath,
135
+ passWithNoTests: schema.passWithNoTests,
124
136
  useEsmExtension: true,
125
137
  }, true, { skipPackageJson: schema.skipPackageJson });
126
138
  }
@@ -141,7 +153,10 @@ getTestBed().initTestEnvironment(
141
153
  : `import react from '@vitejs/plugin-react'`,
142
154
  ],
143
155
  plugins: ['react()'],
144
- coverageProvider: schema.coverageProvider,
156
+ coverageProvider: schema.coverageProvider === 'none'
157
+ ? undefined
158
+ : schema.coverageProvider,
159
+ passWithNoTests: schema.passWithNoTests,
145
160
  useEsmExtension: true,
146
161
  }, true, { skipPackageJson: schema.skipPackageJson });
147
162
  }
@@ -186,7 +201,7 @@ getTestBed().initTestEnvironment(
186
201
  const projectGlobs = `'**/vite.config.{mjs,js,ts,mts}', '**/vitest.config.{mjs,js,ts,mts}'`;
187
202
  const vitestMajorVersion = (0, versions_1.getInstalledVitestMajorVersion)(tree);
188
203
  if (vitestMajorVersion === null || vitestMajorVersion >= 4) {
189
- const hasWorkspaceFile = ['ts', 'js', 'json'].some((ext) => tree.exists(`vitest.workspace.${ext}`) ||
204
+ const hasWorkspaceFile = WORKSPACE_FILE_EXTENSIONS.some((ext) => tree.exists(`vitest.workspace.${ext}`) ||
190
205
  tree.exists(`vitest.projects.${ext}`));
191
206
  const rootVitestConfig = findRootConfig(tree, 'vitest.config');
192
207
  const rootViteConfig = findRootConfig(tree, 'vite.config');
@@ -239,7 +254,9 @@ getTestBed().initTestEnvironment(
239
254
  // root vite.config added later; exclude both so neither is resolved as
240
255
  // an extra project that, carrying no `include`, re-runs every spec via
241
256
  // the default glob.
242
- tree.write('vitest.config.ts', `import { defineConfig } from 'vitest/config';
257
+ // `.mts` keeps it ESM whatever the root package.json `type` is; a
258
+ // CommonJS-loaded config trips Vite's `configLoader: 'native'` warning.
259
+ tree.write('vitest.config.mts', `import { defineConfig } from 'vitest/config';
243
260
 
244
261
  export default defineConfig({
245
262
  test: {
@@ -249,13 +266,9 @@ export default defineConfig({
249
266
  `);
250
267
  }
251
268
  }
252
- else if (!tree.exists(`vitest.workspace.ts`) &&
253
- !tree.exists(`vitest.workspace.js`) &&
254
- !tree.exists(`vitest.workspace.json`) &&
255
- !tree.exists(`vitest.projects.ts`) &&
256
- !tree.exists(`vitest.projects.js`) &&
257
- !tree.exists(`vitest.projects.json`)) {
258
- tree.write('vitest.workspace.ts', `export default [${projectGlobs}];`);
269
+ else if (!WORKSPACE_FILE_EXTENSIONS.some((ext) => tree.exists(`vitest.workspace.${ext}`) ||
270
+ tree.exists(`vitest.projects.${ext}`))) {
271
+ tree.write('vitest.workspace.mts', `export default [${projectGlobs}];`);
259
272
  }
260
273
  }
261
274
  if (!schema.skipFormat) {
@@ -373,6 +386,8 @@ function getCoverageProviderDependency(tree, coverageProvider) {
373
386
  return {
374
387
  '@vitest/coverage-istanbul': vitestCoverageIstanbulVersion,
375
388
  };
389
+ case 'none':
390
+ return {};
376
391
  default:
377
392
  return {
378
393
  '@vitest/coverage-v8': vitestCoverageV8Version,
@@ -1,7 +1,7 @@
1
1
  export interface VitestGeneratorSchema {
2
2
  project: string;
3
3
  uiFramework?: 'angular' | 'react' | 'vue' | 'none';
4
- coverageProvider: 'v8' | 'istanbul' | 'custom';
4
+ coverageProvider: 'v8' | 'istanbul' | 'custom' | 'none';
5
5
  inSourceTests?: boolean;
6
6
  skipViteConfig?: boolean;
7
7
  testTarget?: string;
@@ -11,6 +11,7 @@ export interface VitestGeneratorSchema {
11
11
  addPlugin?: boolean;
12
12
  runtimeTsconfigFileName?: string;
13
13
  compiler?: 'babel' | 'swc'; // default: babel
14
+ passWithNoTests?: boolean;
14
15
  // internal options
15
16
  projectType?: 'application' | 'library';
16
17
  viteVersion?: 5 | 6 | 7 | 8;
@@ -30,7 +30,7 @@
30
30
  },
31
31
  "coverageProvider": {
32
32
  "type": "string",
33
- "enum": ["v8", "istanbul", "custom"],
33
+ "enum": ["v8", "istanbul", "custom", "none"],
34
34
  "default": "v8",
35
35
  "description": "Coverage provider to use."
36
36
  },
@@ -66,6 +66,12 @@
66
66
  "description": "Do not add dependencies to `package.json`.",
67
67
  "x-priority": "internal"
68
68
  },
69
+ "passWithNoTests": {
70
+ "type": "boolean",
71
+ "default": false,
72
+ "description": "Exit with a zero status when the project has no test files.",
73
+ "x-priority": "internal"
74
+ },
69
75
  "zoneless": {
70
76
  "type": "boolean",
71
77
  "description": "Whether the Angular project is zoneless. When not provided, it is auto-detected from the project configuration.",
@@ -0,0 +1,3 @@
1
+ import { type Tree } from '@nx/devkit';
2
+ export default function useImportMetaDirname(tree: Tree): Promise<void>;
3
+ export declare function rewriteDirname(source: string): string;
@@ -0,0 +1,108 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.default = useImportMetaDirname;
4
+ exports.rewriteDirname = rewriteDirname;
5
+ const devkit_1 = require("@nx/devkit");
6
+ // Only ESM-only extensions. A `.ts`/`.js` config can still be loaded as CJS,
7
+ // where `import.meta` is a syntax error.
8
+ const CONFIG_FILE_PATTERN = /(^|\/)(vite|vitest)\.config\.(mts|mjs)$/;
9
+ let ts;
10
+ async function useImportMetaDirname(tree) {
11
+ let touchedCount = 0;
12
+ (0, devkit_1.visitNotIgnoredFiles)(tree, '.', (filePath) => {
13
+ if (!CONFIG_FILE_PATTERN.test(filePath)) {
14
+ return;
15
+ }
16
+ const original = tree.read(filePath, 'utf-8');
17
+ if (!original?.includes('__dirname')) {
18
+ return;
19
+ }
20
+ const updated = rewriteDirname(original);
21
+ if (updated !== original) {
22
+ tree.write(filePath, updated);
23
+ touchedCount += 1;
24
+ }
25
+ });
26
+ if (touchedCount > 0) {
27
+ devkit_1.logger.info(`Replaced \`__dirname\` with \`import.meta.dirname\` in ${touchedCount} Vite config file(s).`);
28
+ }
29
+ await (0, devkit_1.formatFiles)(tree);
30
+ }
31
+ function rewriteDirname(source) {
32
+ ts ??= (0, devkit_1.ensurePackage)('typescript', '*');
33
+ const sourceFile = ts.createSourceFile('tmp.mts', source, ts.ScriptTarget.Latest,
34
+ /* setParentNodes */ true, ts.ScriptKind.TS);
35
+ // A config TypeScript can only error-recover on may not mean what its AST
36
+ // says, so leave it for a human rather than rewrite it blind.
37
+ if (sourceFile.parseDiagnostics?.length) {
38
+ return source;
39
+ }
40
+ const references = [];
41
+ let bailOut = false;
42
+ const visit = (node) => {
43
+ if (ts.isIdentifier(node) && node.text === '__dirname') {
44
+ const kind = classify(node);
45
+ if (kind === 'bail') {
46
+ bailOut = true;
47
+ }
48
+ else if (kind === 'reference') {
49
+ references.push(node);
50
+ }
51
+ }
52
+ ts.forEachChild(node, visit);
53
+ };
54
+ visit(sourceFile);
55
+ if (bailOut || references.length === 0) {
56
+ return source;
57
+ }
58
+ let updated = source;
59
+ for (const node of references.reverse()) {
60
+ const start = node.getStart(sourceFile);
61
+ updated =
62
+ updated.slice(0, start) +
63
+ 'import.meta.dirname' +
64
+ updated.slice(node.getEnd());
65
+ }
66
+ return updated;
67
+ }
68
+ /**
69
+ * `bail` abandons the whole file: something binds `__dirname` in scope, so the
70
+ * identifier no longer means the CJS global, or it is a shorthand property
71
+ * where key and value are one token. `skip` leaves a single identifier alone.
72
+ *
73
+ * The name-position test is inverted on purpose - anything sitting in a
74
+ * parent's `name` or `propertyName` slot is excluded by default, so node kinds
75
+ * nobody enumerated (class fields, accessors, enum members) cannot be rewritten
76
+ * into invalid syntax.
77
+ */
78
+ function classify(node) {
79
+ const parent = node.parent;
80
+ if (!parent) {
81
+ return 'reference';
82
+ }
83
+ if (ts.isShorthandPropertyAssignment(parent)) {
84
+ return 'bail';
85
+ }
86
+ if (bindsDirname(parent, node)) {
87
+ return 'bail';
88
+ }
89
+ if (parent.name === node || parent.propertyName === node) {
90
+ return 'skip';
91
+ }
92
+ return 'reference';
93
+ }
94
+ /** Declarations that put a new `__dirname` in scope, shadowing the global. */
95
+ function bindsDirname(parent, node) {
96
+ if (parent.name !== node) {
97
+ return false;
98
+ }
99
+ return (ts.isVariableDeclaration(parent) ||
100
+ ts.isParameter(parent) ||
101
+ ts.isBindingElement(parent) ||
102
+ ts.isFunctionDeclaration(parent) ||
103
+ ts.isClassDeclaration(parent) ||
104
+ ts.isImportClause(parent) ||
105
+ ts.isImportSpecifier(parent) ||
106
+ ts.isNamespaceImport(parent) ||
107
+ ts.isExportSpecifier(parent));
108
+ }
@@ -0,0 +1,36 @@
1
+ #### Replace `__dirname` with `import.meta.dirname` in Vite config files
2
+
3
+ Vite 8 warns when a config uses features that its `configLoader: 'native'` mode cannot support, and `__dirname` is one of them:
4
+
5
+ ```
6
+ (!) Your Vite config uses features that are unsupported by `configLoader: 'native'`, which is planned to become the default in a future major version of Vite:
7
+ - `__dirname` (packages/utils/vitest.config.mts:4:9). Use `import.meta.dirname` instead
8
+ ```
9
+
10
+ This migration rewrites `__dirname` to `import.meta.dirname` in every `vite.config.mts`, `vite.config.mjs`, `vitest.config.mts`, and `vitest.config.mjs` file in your workspace.
11
+
12
+ #### Sample Code Changes
13
+
14
+ ##### Before
15
+
16
+ ```ts
17
+ export default defineConfig(() => ({
18
+ root: __dirname,
19
+ }));
20
+ ```
21
+
22
+ ##### After
23
+
24
+ ```ts
25
+ export default defineConfig(() => ({
26
+ root: import.meta.dirname,
27
+ }));
28
+ ```
29
+
30
+ #### What is not rewritten
31
+
32
+ `.ts` and `.js` configs are left alone, since those extensions can still be loaded as CommonJS, where `import.meta` is a syntax error. Configs that declare their own `__dirname` (usually `const __dirname = path.dirname(fileURLToPath(import.meta.url))`) are also left alone - that idiom already works under the native config loader.
33
+
34
+ No migration renames an existing config to `.mts`, because other tooling may reference it by path. So a workspace whose config is `.ts` keeps the companion warning about ESM syntax in a CommonJS-loaded file. Rename it yourself, or set `"type": "module"` in the closest `package.json`, to clear that one. Newly generated configs use `.mts` and are unaffected.
35
+
36
+ One generated case does put `import.meta.dirname` in a `.ts` config: `@nx/nuxt` falls back to `.ts` on workspaces still using eslintrc, because the legacy `@nuxt/eslint-config` cannot parse `.mts`. Vite's default config loader bundles that file, so it works today, but the file is not loadable under `configLoader: 'native'` - its own `import` statements already make it so.
@@ -15,6 +15,27 @@ export interface VitestPluginOptions {
15
15
  * - 'run': Tests run once and exit
16
16
  */
17
17
  testMode?: 'watch' | 'run';
18
+ /**
19
+ * How atomized test files (the `ciTargetName` targets) are discovered.
20
+ * - 'glob' (default): enumerate specs with a glob that mirrors Vitest's own
21
+ * resolution instead of booting Vitest per project. Booting Vitest starts a
22
+ * Vite dev server and runs the config's plugin hooks, so the glob is faster
23
+ * during graph creation. The glob reads the Nx workspace file index, so
24
+ * files ignored by `.gitignore`/`.nxignore` are never enumerated even when
25
+ * Vitest itself would run them.
26
+ * - 'vitest': always enumerate through Vitest.
27
+ *
28
+ * Configs a glob cannot reproduce faithfully still boot Vitest automatically
29
+ * even under 'glob': `test.projects`/`test.workspace` (inline or an
30
+ * auto-loaded `vitest.workspace.*`/`vitest.projects.*` sibling file), plugins
31
+ * with a `configureVitest` hook, `test.changed`/`test.related`, enabled
32
+ * browser `instances` that set their own include/exclude/includeSource/dir,
33
+ * and `include`, `exclude`, `includeSource`, or `typecheck` include/exclude
34
+ * patterns the workspace glob reads differently (absolute paths, a trailing
35
+ * `/`, or an `!(...)` extglob, each optionally negated).
36
+ * @default 'glob'
37
+ */
38
+ discoverTestFiles?: 'glob' | 'vitest';
18
39
  }
19
40
  /**
20
41
  * @deprecated The 'createDependencies' function is now a no-op. This functionality is included in 'createNodesV2'.
@@ -6,10 +6,8 @@ const devkit_1 = require("@nx/devkit");
6
6
  const js_1 = require("@nx/js");
7
7
  const internal_2 = require("@nx/js/internal");
8
8
  const node_fs_1 = require("node:fs");
9
+ const promises_1 = require("node:fs/promises");
9
10
  const node_path_1 = require("node:path");
10
- const file_hasher_1 = require("nx/src/hasher/file-hasher");
11
- const cache_directory_1 = require("nx/src/utils/cache-directory");
12
- const plugins_1 = require("nx/src/utils/plugins");
13
11
  const executor_utils_1 = require("../utils/executor-utils");
14
12
  /**
15
13
  * @deprecated The 'createDependencies' function is now a no-op. This functionality is included in 'createNodesV2'.
@@ -23,9 +21,9 @@ exports.createNodes = [
23
21
  vitestConfigGlob,
24
22
  async (configFilePaths, options, context) => {
25
23
  const pmc = (0, devkit_1.getPackageManagerCommand)((0, devkit_1.detectPackageManager)(context.workspaceRoot));
26
- const optionsHash = (0, file_hasher_1.hashObject)(options);
24
+ const optionsHash = (0, internal_1.hashObject)(options);
27
25
  const normalizedOptions = normalizeOptions(options);
28
- const cachePath = (0, node_path_1.join)(cache_directory_1.workspaceDataDirectory, `vitest-${optionsHash}.hash`);
26
+ const cachePath = (0, node_path_1.join)(internal_1.workspaceDataDirectory, `vitest-${optionsHash}.hash`);
29
27
  const targetsCache = new internal_1.PluginCache(cachePath);
30
28
  const { roots: projectRoots, configFiles: validConfigFiles } = configFilePaths.reduce((acc, configFile) => {
31
29
  const potentialRoot = (0, node_path_1.dirname)(configFile);
@@ -129,9 +127,7 @@ async function buildVitestTargets(configFilePath, projectRoot, options, context,
129
127
  // entirely so it does not register a project rooted at the workspace root (which
130
128
  // would, for example, make `nx format` treat the whole workspace as one project).
131
129
  const isWorkspaceRoot = projectRoot === '.';
132
- // TODO(jack): Remove this cast when @nx/vitest switches to moduleResolution:
133
- // "nodenext". Vite 8's rolldown types break vitest's test augmentation.
134
- const hasProjectsProperty = Array.isArray(viteBuildConfig?.test?.projects);
130
+ const hasProjectsProperty = Array.isArray(viteBuildConfig.test?.projects);
135
131
  if (isWorkspaceRoot && hasProjectsProperty) {
136
132
  return null;
137
133
  }
@@ -141,11 +137,10 @@ async function buildVitestTargets(configFilePath, projectRoot, options, context,
141
137
  const targets = {};
142
138
  // if file is vitest.config or vite.config has definition for test, create targets for test and/or atomized tests
143
139
  if (configFilePath.includes('vitest.config') || hasTest) {
144
- const isTypecheckEnabled = !!viteBuildConfig?.test?.typecheck
145
- ?.enabled;
140
+ const isTypecheckEnabled = !!viteBuildConfig.test?.typecheck?.enabled;
146
141
  targets[options.testTargetName] = await testTarget(namedInputs, testOutputs, projectRoot, options.testMode, pmc, isTypecheckEnabled, tsconfigInputs);
147
142
  if (options.ciTargetName) {
148
- const groupName = options.ciGroupName ?? (0, plugins_1.deriveGroupNameFromTarget)(options.ciTargetName);
143
+ const groupName = options.ciGroupName ?? (0, internal_1.deriveGroupNameFromTarget)(options.ciTargetName);
149
144
  const targetGroup = [];
150
145
  const dependsOn = [];
151
146
  metadata = {
@@ -153,7 +148,24 @@ async function buildVitestTargets(configFilePath, projectRoot, options, context,
153
148
  [groupName]: targetGroup,
154
149
  },
155
150
  };
156
- const projectRootRelativeTestPaths = await getTestPathsRelativeToProjectRoot(projectRoot, context.workspaceRoot);
151
+ // Not normalizing in normalizeOptions since it also affects the options
152
+ // computed for convert-to-inferred.
153
+ const useGlobDiscovery = (options.discoverTestFiles ?? 'glob') !== 'vitest';
154
+ // Both discovery paths read the serve-resolved config: Vitest runs tests
155
+ // through a Vite server (the `serve` command), so `apply: 'serve'`
156
+ // plugins and command-sensitive `test` options (include/exclude and
157
+ // `dir`) are absent from the build resolution used above for outputs.
158
+ // Resolve under `mode: 'test'` to match Vitest, which defaults the Vite
159
+ // mode to 'test'; a config that branches on `command`/`mode` would
160
+ // otherwise enumerate a different spec set here than at test time.
161
+ const viteServeConfig = await resolveConfig({
162
+ configFile: absoluteConfigFilePath,
163
+ mode: 'test',
164
+ }, 'serve');
165
+ const projectRootRelativeTestPaths = await getTestPathsRelativeToProjectRoot(projectRoot, context.workspaceRoot,
166
+ // Only the glob path reads this config; the opt-out path forces the
167
+ // runtime by receiving no config, and takes `test.dir` separately.
168
+ useGlobDiscovery ? viteServeConfig : undefined, viteServeConfig.test?.dir);
157
169
  for (const relativePath of projectRootRelativeTestPaths) {
158
170
  if (relativePath.includes('../')) {
159
171
  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' +
@@ -403,12 +415,205 @@ function checkIfConfigFileShouldBeProject(projectRoot, context) {
403
415
  }
404
416
  return true;
405
417
  }
406
- async function getTestPathsRelativeToProjectRoot(projectRoot, workspaceRoot) {
418
+ async function getTestPathsRelativeToProjectRoot(projectRoot, workspaceRoot, viteConfig,
419
+ // Serve-resolved `test.dir` (the command Vitest runs under). Only used on the
420
+ // opt-out path below, which receives no `viteConfig` to read it from.
421
+ optOutTestDir) {
407
422
  const fullProjectRoot = (0, node_path_1.join)(workspaceRoot, projectRoot);
423
+ // `viteConfig` is resolved only when glob discovery is requested; its absence
424
+ // means the runtime path was selected (`discoverTestFiles: 'vitest'`).
425
+ if (viteConfig) {
426
+ const test = viteConfig.test ?? {};
427
+ if (!configRequiresVitestRuntime(test, viteConfig, fullProjectRoot)) {
428
+ return globTestPathsRelativeToProjectRoot(test, workspaceRoot, projectRoot, fullProjectRoot);
429
+ }
430
+ return getTestPathsViaVitestRuntime(fullProjectRoot, projectRoot, test.dir);
431
+ }
432
+ return getTestPathsViaVitestRuntime(fullProjectRoot, projectRoot, optOutTestDir);
433
+ }
434
+ /**
435
+ * The directory Vitest enumerates from: `test.dir` when set, else the project
436
+ * root. Vitest resolves a relative `test.dir` against the working directory,
437
+ * which for both the `test` and atomized targets is the project root.
438
+ */
439
+ function resolveTestDir(fullProjectRoot, testDir) {
440
+ return testDir ? (0, node_path_1.resolve)(fullProjectRoot, testDir) : fullProjectRoot;
441
+ }
442
+ /**
443
+ * Enumerates a project's test files by mirroring Vitest's own resolution with
444
+ * a glob. Vitest globs `test.include` and `test.includeSource` (both skipped
445
+ * when typecheck is enabled with `only`, the latter also kept only when the
446
+ * file contains an in-source test) plus `test.typecheck.include` (when
447
+ * typecheck is enabled), each minus the relevant `exclude`, from `test.dir`
448
+ * when set and the project root otherwise. Defaults come from the installed
449
+ * Vitest so they track the user's version. Globbing goes through the Nx
450
+ * workspace context (the daemon-cached file index), so files ignored by
451
+ * `.gitignore`/`.nxignore` are never candidates. Callers must first confirm the
452
+ * config is reproducible with a glob via `configRequiresVitestRuntime`.
453
+ */
454
+ async function globTestPathsRelativeToProjectRoot(test, workspaceRoot, projectRoot, fullProjectRoot) {
455
+ const { configDefaults } = await (0, executor_utils_1.loadVitestConfigDynamicImport)();
456
+ const exclude = test.exclude ?? configDefaults.exclude;
457
+ const typecheck = test.typecheck;
458
+ const typecheckOnly = !!(typecheck?.enabled && typecheck?.only);
459
+ // The workspace context matches workspace-relative paths, while the config's
460
+ // patterns are relative to the directory Vitest enumerates from; anchor them
461
+ // to it.
462
+ const scanRoot = test.dir
463
+ ? (0, devkit_1.normalizePath)((0, node_path_1.relative)(workspaceRoot, resolveTestDir(fullProjectRoot, test.dir)))
464
+ : projectRoot;
465
+ // The workspace context only reads `!` at index 0, so a negated pattern has
466
+ // to be re-prefixed rather than anchored as-is.
467
+ const anchor = (pattern) => isNegatedPattern(pattern)
468
+ ? `!${(0, devkit_1.joinPathFragments)(scanRoot, pattern.slice(1))}`
469
+ : (0, devkit_1.joinPathFragments)(scanRoot, pattern);
470
+ const globProjectFiles = (include, ignore) => {
471
+ // The workspace context treats an all-negated (or empty) include set as
472
+ // "match everything" (it inverts to the exclude set), while Vitest
473
+ // enumerates nothing. Match Vitest: without a positive entry there is
474
+ // nothing to enumerate.
475
+ if (!include.some((pattern) => !isNegatedPattern(pattern))) {
476
+ return Promise.resolve([]);
477
+ }
478
+ return (0, internal_1.globWithWorkspaceContext)(workspaceRoot, include.map(anchor),
479
+ // Vitest discards a negated `exclude` entry; forwarding it would turn the
480
+ // exclude set into an allowlist.
481
+ ignore.filter((pattern) => !isNegatedPattern(pattern)).map(anchor));
482
+ };
483
+ // Regular and type tests are independent walks; run them together.
484
+ const globJobs = [];
485
+ // Typecheck enabled with `only` makes Vitest run only type tests, so skip
486
+ // regular tests.
487
+ if (!typecheckOnly) {
488
+ const include = test.include ?? configDefaults.include;
489
+ globJobs.push(globProjectFiles(include, exclude));
490
+ }
491
+ if (typecheck?.enabled) {
492
+ const include = typecheck.include ?? configDefaults.typecheck.include;
493
+ const ignore = typecheck.exclude ?? configDefaults.typecheck.exclude;
494
+ globJobs.push(globProjectFiles(include, ignore));
495
+ }
496
+ const matches = new Set();
497
+ for (const files of await Promise.all(globJobs)) {
498
+ for (const file of files)
499
+ matches.add(file);
500
+ }
501
+ // In-source tests: only files that actually contain a test are included.
502
+ // Typecheck enabled with `only` makes Vitest run only type tests, so skip
503
+ // these too.
504
+ if (!typecheckOnly && test.includeSource?.length) {
505
+ const sourceFiles = await globProjectFiles(test.includeSource, exclude);
506
+ // The candidate set can be the whole `src` tree, so read in bounded
507
+ // batches; an unbounded Promise.all over every file risks EMFILE.
508
+ const readConcurrency = 25;
509
+ for (let i = 0; i < sourceFiles.length; i += readConcurrency) {
510
+ const inSourceMatches = await Promise.all(sourceFiles.slice(i, i + readConcurrency).map(async (file) => {
511
+ // Vitest tolerates unreadable in-source candidates and skips them;
512
+ // match that so a permission error or TOCTOU race can't abort graph
513
+ // creation.
514
+ try {
515
+ const content = await (0, promises_1.readFile)((0, node_path_1.join)(workspaceRoot, file), 'utf-8');
516
+ return content.includes('import.meta.vitest') ? file : null;
517
+ }
518
+ catch {
519
+ return null;
520
+ }
521
+ }));
522
+ for (const file of inSourceMatches) {
523
+ if (file)
524
+ matches.add(file);
525
+ }
526
+ }
527
+ }
528
+ // The workspace context returns workspace-relative paths, so re-relativize
529
+ // them to the project root. An `include` pattern (`../lib2/**`) or an
530
+ // out-of-project `test.dir` can land outside the project root, so drop what
531
+ // does, matching the runtime path.
532
+ return [...matches]
533
+ .map((file) => (0, devkit_1.normalizePath)((0, node_path_1.relative)(projectRoot, file)))
534
+ .filter((file) => !file.startsWith('../'))
535
+ .sort();
536
+ }
537
+ // Vitest reads a leading `!` as a negation, except when it opens an extglob
538
+ // (`!(...)`), which the runtime gate routes away before discovery reaches here.
539
+ function isNegatedPattern(pattern) {
540
+ return pattern.startsWith('!') && !pattern.startsWith('!(');
541
+ }
542
+ /**
543
+ * Whether the workspace context resolves a pattern differently than Vitest.
544
+ * Anchoring rewrites an absolute pattern into a project-relative one that
545
+ * enumerates the wrong location; a trailing `/` becomes a recursive directory
546
+ * match where Vitest (globbing with `expandDirectories: false`) matches
547
+ * nothing; and `!(...)` converts to an include plus literal exclusions that
548
+ * reproduces Vitest's extglob only for some shapes. A leading `!` is stripped
549
+ * first so a negated form of any of these still routes to the runtime.
550
+ */
551
+ function patternRequiresVitestRuntime(pattern) {
552
+ const bare = isNegatedPattern(pattern) ? pattern.slice(1) : pattern;
553
+ return (0, node_path_1.isAbsolute)(bare) || bare.endsWith('/') || bare.includes('!(');
554
+ }
555
+ // Sibling files Vitest 3 auto-loads to define sub-projects even when the config
556
+ // object declares none. Removed in Vitest 4 in favor of inline `test.projects`.
557
+ const vitestWorkspaceFiles = ['vitest.workspace', 'vitest.projects'].flatMap((name) => ['ts', 'mts', 'cts', 'js', 'mjs', 'cjs', 'json'].map((ext) => `${name}.${ext}`));
558
+ /**
559
+ * Whether a config's test discovery cannot be faithfully reproduced with a
560
+ * glob, so enumeration must go through Vitest itself.
561
+ */
562
+ function configRequiresVitestRuntime(
563
+ // `workspace` (removed in Vitest 4) and the CLI-only `changed`/`related`
564
+ // filters are not part of the config-file `InlineConfig` type.
565
+ test, viteConfig, projectDir) {
566
+ // Multi-project configs resolve include/exclude per sub-project.
567
+ if (Array.isArray(test?.projects) || test?.workspace)
568
+ return true;
569
+ // Vitest 3 auto-loads a `vitest.workspace.*`/`vitest.projects.*` sibling file
570
+ // to define sub-projects even when the config object declares none; the glob
571
+ // resolves only the single config, so it cannot reproduce that.
572
+ if (vitestWorkspaceFiles.some((file) => (0, node_fs_1.existsSync)((0, node_path_1.join)(projectDir, file)))) {
573
+ return true;
574
+ }
575
+ // Vitest filters specs by VCS/graph state, which a glob cannot know.
576
+ if (test?.changed || test?.related)
577
+ return true;
578
+ // Vitest's own defaults carry none of these shapes, so only a config that
579
+ // spells one out reaches the runtime for this reason.
580
+ const configuredPatterns = [
581
+ test?.include,
582
+ test?.exclude,
583
+ test?.includeSource,
584
+ test?.typecheck?.include,
585
+ test?.typecheck?.exclude,
586
+ ]
587
+ .filter(Array.isArray)
588
+ .flat();
589
+ if (configuredPatterns.some(patternRequiresVitestRuntime))
590
+ return true;
591
+ // Browser mode: an instance can override include/exclude/includeSource, and
592
+ // `dir` (the base directory Vitest scans), so a top-level glob enumerates a
593
+ // different spec set than the instance would. Vitest ignores `instances`
594
+ // while browser mode is off, and the atomized target runs `vitest run <file>`
595
+ // without a browser flag, so the resolved `enabled` matches the run.
596
+ const browserInstances = test?.browser?.instances ?? [];
597
+ if (test?.browser?.enabled &&
598
+ browserInstances.some((instance) => instance &&
599
+ (instance.include?.length ||
600
+ instance.exclude?.length ||
601
+ instance.includeSource?.length ||
602
+ instance.dir))) {
603
+ return true;
604
+ }
605
+ // A plugin can inject or reshape projects through this Vitest-only hook.
606
+ const plugins = viteConfig?.plugins ?? [];
607
+ return plugins.some((plugin) => plugin && typeof plugin === 'object' && 'configureVitest' in plugin);
608
+ }
609
+ async function getTestPathsViaVitestRuntime(fullProjectRoot, projectRoot, testDir) {
408
610
  const { createVitest } = await import('vitest/node');
409
611
  const vitest = await createVitest('test', {
410
612
  root: fullProjectRoot,
411
- dir: fullProjectRoot,
613
+ // `dir` defaults to `root`, and a relative `test.dir` would resolve against
614
+ // the working directory, which during graph creation is the workspace root
615
+ // rather than the project root.
616
+ dir: resolveTestDir(fullProjectRoot, testDir),
412
617
  filesOnly: true,
413
618
  watch: false,
414
619
  });
@@ -1,2 +1,3 @@
1
- export declare function loadViteDynamicImport(): Promise<any>;
1
+ export declare function loadViteDynamicImport(): Promise<typeof import('vite')>;
2
2
  export declare function loadVitestDynamicImport(): Promise<typeof import('vitest/node')>;
3
+ export declare function loadVitestConfigDynamicImport(): Promise<typeof import('vitest/config')>;
@@ -2,12 +2,13 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.loadViteDynamicImport = loadViteDynamicImport;
4
4
  exports.loadVitestDynamicImport = loadVitestDynamicImport;
5
- // TODO(jack): Remove this cast when @nx/vitest switches to moduleResolution:
6
- // "nodenext". Vite 8 ships ESM-only type declarations (.d.mts) not resolvable
7
- // under moduleResolution: "node".
5
+ exports.loadVitestConfigDynamicImport = loadVitestConfigDynamicImport;
8
6
  function loadViteDynamicImport() {
9
7
  return Function('return import("vite")')();
10
8
  }
11
9
  function loadVitestDynamicImport() {
12
10
  return Function('return import("vitest/node")')();
13
11
  }
12
+ function loadVitestConfigDynamicImport() {
13
+ return Function('return import("vitest/config")')();
14
+ }
@@ -4,7 +4,7 @@ export type TargetFlags = Partial<Record<Target, boolean>>;
4
4
  export interface VitestGeneratorSchema {
5
5
  project: string;
6
6
  uiFramework?: 'angular' | 'react' | 'vue' | 'none';
7
- coverageProvider: 'v8' | 'istanbul' | 'custom';
7
+ coverageProvider: 'v8' | 'istanbul' | 'custom' | 'none';
8
8
  inSourceTests?: boolean;
9
9
  skipViteConfig?: boolean;
10
10
  testTarget?: string;
@@ -22,10 +22,18 @@ export interface ViteConfigFileOptions {
22
22
  includeVitest?: boolean;
23
23
  inSourceTests?: boolean;
24
24
  testEnvironment?: 'node' | 'jsdom' | 'happy-dom' | 'edge-runtime' | string;
25
+ testInclude?: string[];
26
+ /**
27
+ * Aliases to emit under `resolve.alias`, as alias -> project-relative path.
28
+ * Only applies when a new config file is written; an existing config is left
29
+ * untouched.
30
+ */
31
+ resolveAlias?: Record<string, string>;
25
32
  rolldownOptionsExternal?: string[];
26
33
  imports?: string[];
27
34
  plugins?: string[];
28
- coverageProvider?: 'v8' | 'istanbul' | 'custom';
35
+ coverageProvider?: 'v8' | 'istanbul' | 'custom' | 'none';
36
+ passWithNoTests?: boolean;
29
37
  setupFile?: string;
30
38
  useEsmExtension?: boolean;
31
39
  port?: number;
@@ -35,4 +43,5 @@ export declare function createOrEditViteConfig(tree: Tree, options: ViteConfigFi
35
43
  projectAlreadyHasViteTargets?: TargetFlags;
36
44
  skipPackageJson?: boolean;
37
45
  vitestFileName?: boolean;
46
+ skipNxPlugins?: boolean;
38
47
  }): void;
@@ -9,14 +9,25 @@ const deprecation_1 = require("./deprecation");
9
9
  const versions_1 = require("./versions");
10
10
  function addOrChangeTestTarget(tree, options, hasPlugin) {
11
11
  const nxJson = (0, devkit_1.readNxJson)(tree);
12
- hasPlugin = nxJson.plugins?.some((p) => typeof p === 'string'
13
- ? p === '@nx/vitest'
14
- : p.plugin === '@nx/vitest' || hasPlugin);
12
+ const target = options.testTarget ?? 'test';
13
+ // The plugin only infers the target names it is registered for, so a request
14
+ // for any other name still needs an explicit target.
15
+ hasPlugin ||=
16
+ nxJson.plugins?.some((p) => {
17
+ if (typeof p === 'string') {
18
+ return p === '@nx/vitest' && target === 'test';
19
+ }
20
+ if (p.plugin !== '@nx/vitest') {
21
+ return false;
22
+ }
23
+ const pluginOptions = p.options;
24
+ return ((pluginOptions?.testTargetName ?? 'test') === target ||
25
+ pluginOptions?.ciTargetName === target);
26
+ }) ?? false;
15
27
  if (hasPlugin) {
16
28
  return;
17
29
  }
18
30
  const project = (0, devkit_1.readProjectConfiguration)(tree, options.project);
19
- const target = options.testTarget ?? 'test';
20
31
  const reportsDirectory = (0, devkit_1.joinPathFragments)('coverage', project.root === '.' ? options.project : project.root);
21
32
  const testOptions = {
22
33
  reportsDirectory,
@@ -35,6 +46,14 @@ function addOrChangeTestTarget(tree, options, hasPlugin) {
35
46
  }
36
47
  (0, devkit_1.updateProjectConfiguration)(tree, options.project, project);
37
48
  }
49
+ // Escape a value for emission inside a single-quoted source literal.
50
+ function escapeLiteral(value) {
51
+ return value
52
+ .replace(/\\/g, '\\\\')
53
+ .replace(/'/g, "\\'")
54
+ .replace(/\n/g, '\\n')
55
+ .replace(/\r/g, '\\r');
56
+ }
38
57
  function createOrEditViteConfig(tree, options, onlyVitest, extraOptions = {}) {
39
58
  const { root: projectRoot } = (0, devkit_1.readProjectConfiguration)(tree, options.project);
40
59
  const extension = options.useEsmExtension ? 'mts' : 'ts';
@@ -86,7 +105,7 @@ function createOrEditViteConfig(tree, options, onlyVitest, extraOptions = {}) {
86
105
  if (!onlyVitest && options.includeLib && !isTsSolutionSetup) {
87
106
  imports.push(`import dts from 'vite-plugin-dts'`, `import * as path from 'path'`);
88
107
  }
89
- if (!isTsSolutionSetup) {
108
+ if (!isTsSolutionSetup && !extraOptions.skipNxPlugins) {
90
109
  imports.push(`import { nxViteTsPaths } from '@nx/vite/plugins/nx-tsconfig-paths.plugin'`, `import { nxCopyAssetsPlugin } from '@nx/vite/plugins/nx-copy-assets.plugin'`);
91
110
  plugins.push(`nxViteTsPaths()`, `nxCopyAssetsPlugin(['*.md'])`);
92
111
  if (!extraOptions.skipPackageJson) {
@@ -94,31 +113,39 @@ function createOrEditViteConfig(tree, options, onlyVitest, extraOptions = {}) {
94
113
  }
95
114
  }
96
115
  if (!onlyVitest && options.includeLib) {
97
- plugins.push(`dts({ entryRoot: 'src', tsconfigPath: path.join(__dirname, 'tsconfig.lib.json')${!isTsSolutionSetup ? ', pathsToAliases: false' : ''} })`);
116
+ plugins.push(`dts({ entryRoot: 'src', tsconfigPath: path.join(import.meta.dirname, 'tsconfig.lib.json')${!isTsSolutionSetup ? ', pathsToAliases: false' : ''} })`);
98
117
  }
99
118
  const reportsDirectory = isTsSolutionSetup
100
119
  ? './test-output/vitest/coverage'
101
120
  : projectRoot === '.'
102
121
  ? `./coverage/${options.project}`
103
122
  : `${(0, devkit_1.offsetFromRoot)(projectRoot)}coverage/${projectRoot}`;
123
+ const testInclude = options.testInclude ?? [
124
+ '{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
125
+ ];
104
126
  const testOption = options.includeVitest
105
127
  ? ` test: {
106
128
  name: '${options.project}',
107
129
  watch: false,
108
130
  globals: true,
109
131
  environment: '${options.testEnvironment ?? 'jsdom'}',
110
- include: ['{src,tests}/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
132
+ include: [${testInclude
133
+ .map((pattern) => `'${escapeLiteral(pattern)}'`)
134
+ .join(', ')}],
135
+ ${options.passWithNoTests ? ` passWithNoTests: true,\n` : ''}\
111
136
  ${options.setupFile ? ` setupFiles: ['${options.setupFile}'],\n` : ''}\
112
137
  ${options.inSourceTests
113
138
  ? ` includeSource: ['src/**/*.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],\n`
114
139
  : ''}\
115
- reporters: ['default'],
140
+ reporters: ['default']${options.coverageProvider !== 'none'
141
+ ? `,
116
142
  coverage: {
117
143
  reportsDirectory: '${reportsDirectory}',
118
144
  provider: ${options.coverageProvider
119
- ? `'${options.coverageProvider}' as const`
120
- : `'v8' as const`},
121
- }
145
+ ? `'${options.coverageProvider}' as const`
146
+ : `'v8' as const`},
147
+ }`
148
+ : ''}
122
149
  },`
123
150
  : '';
124
151
  const defineOption = options.inSourceTests
@@ -151,19 +178,34 @@ ${options.inSourceTests
151
178
  // worker: {
152
179
  // plugins: () => [ nxViteTsPaths() ],
153
180
  // },`;
181
+ const aliasEntries = Object.entries(options.resolveAlias ?? {});
182
+ const resolveOption = aliasEntries.length
183
+ ? ` resolve: {
184
+ alias: {
185
+ ${aliasEntries
186
+ .map(([alias, target]) => ` '${escapeLiteral(alias)}': join(import.meta.dirname, '${escapeLiteral(target)}'),`)
187
+ .join('\n')}
188
+ },
189
+ },`
190
+ : '';
154
191
  const cacheDir = `cacheDir: '${normalizedJoinPaths((0, devkit_1.offsetFromRoot)(projectRoot), 'node_modules', '.vite', projectRoot === '.' ? options.project : projectRoot)}',`;
155
192
  if (tree.exists(viteConfigPath)) {
193
+ if (aliasEntries.length) {
194
+ devkit_1.logger.warn(`${viteConfigPath} already exists; the requested resolve.alias entries were not added to it.`);
195
+ }
156
196
  handleViteConfigFileExists(tree, viteConfigPath, options, buildOption, buildOutDir, imports, plugins, testOption, reportsDirectory, cacheDir, projectRoot, (0, devkit_1.offsetFromRoot)(projectRoot), extraOptions.projectAlreadyHasViteTargets);
157
197
  return;
158
198
  }
159
- // When using vitest.config, use vitest/config import and skip vite-specific options
199
+ if (aliasEntries.length) {
200
+ imports.push(`import { join } from 'node:path'`);
201
+ }
160
202
  const viteConfigContent = extraOptions.vitestFileName
161
203
  ? `import { defineConfig } from 'vitest/config';
162
204
  ${imports.join(';\n')}${imports.length ? ';' : ''}
163
205
 
164
206
  export default defineConfig(() => ({
165
- root: __dirname,
166
- ${printOptions(cacheDir, plugins.length ? ` plugins: [${plugins.join(', ')}],` : '', defineOption, testOption)}
207
+ root: import.meta.dirname,
208
+ ${printOptions(cacheDir, plugins.length ? ` plugins: [${plugins.join(', ')}],` : '', resolveOption, defineOption, testOption)}
167
209
  }));
168
210
  `.replace(/\s+(?=(\n|$))/gm, '\n')
169
211
  : `/// <reference types='vitest' />
@@ -171,8 +213,8 @@ import { defineConfig } from 'vite';
171
213
  ${imports.join(';\n')}${imports.length ? ';' : ''}
172
214
 
173
215
  export default defineConfig(() => ({
174
- root: __dirname,
175
- ${printOptions(cacheDir, devServerOption, previewServerOption, ` plugins: [${plugins.join(', ')}],`, workerOption, buildOption, defineOption, testOption)}
216
+ root: import.meta.dirname,
217
+ ${printOptions(cacheDir, devServerOption, previewServerOption, ` plugins: [${plugins.join(', ')}],`, resolveOption, workerOption, buildOption, defineOption, testOption)}
176
218
  }));
177
219
  `.replace(/\s+(?=(\n|$))/gm, '\n');
178
220
  tree.write(viteConfigPath, viteConfigContent);
@@ -215,7 +257,10 @@ function handleViteConfigFileExists(tree, viteConfigPath, options, buildOption,
215
257
  const testOptionObject = {
216
258
  globals: true,
217
259
  environment: options.testEnvironment ?? 'jsdom',
218
- include: ['src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}'],
260
+ include: options.testInclude ?? [
261
+ 'src/**/*.{test,spec}.{js,mjs,cjs,ts,mts,cts,jsx,tsx}',
262
+ ],
263
+ ...(options.passWithNoTests ? { passWithNoTests: true } : {}),
219
264
  reporters: ['default'],
220
265
  coverage: {
221
266
  reportsDirectory: reportsDirectory,
@@ -1,4 +1,3 @@
1
1
  import { type Tree } from '@nx/devkit';
2
2
  export declare function ignoreVitestTempFiles(tree: Tree, projectRoot?: string | undefined): Promise<void>;
3
3
  export declare function addVitestTempFilesToGitIgnore(tree: Tree): void;
4
- export declare function isEslintInstalled(tree: Tree): boolean;
@@ -2,8 +2,8 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.ignoreVitestTempFiles = ignoreVitestTempFiles;
4
4
  exports.addVitestTempFilesToGitIgnore = addVitestTempFilesToGitIgnore;
5
- exports.isEslintInstalled = isEslintInstalled;
6
5
  const devkit_1 = require("@nx/devkit");
6
+ const internal_1 = require("@nx/js/internal");
7
7
  const versions_1 = require("./versions");
8
8
  async function ignoreVitestTempFiles(tree, projectRoot) {
9
9
  addVitestTempFilesToGitIgnore(tree);
@@ -20,7 +20,9 @@ function addVitestTempFilesToGitIgnore(tree) {
20
20
  tree.write('.gitignore', gitIgnoreContents);
21
21
  }
22
22
  async function ignoreVitestTempFilesInEslintConfig(tree, projectRoot) {
23
- if (!isEslintInstalled(tree)) {
23
+ // Checked before `ensurePackage` so an Oxlint workspace does not install
24
+ // `@nx/eslint` only for `isEslintConfigSupported` to send it straight back.
25
+ if (!(0, internal_1.detectLinters)(tree).includes('eslint')) {
24
26
  return;
25
27
  }
26
28
  (0, devkit_1.ensurePackage)('@nx/eslint', versions_1.nxVersion);
@@ -45,15 +47,3 @@ async function ignoreVitestTempFilesInEslintConfig(tree, projectRoot) {
45
47
  const directory = isUsingFlatConfig ? '' : (projectRoot ?? '');
46
48
  addIgnoresToLintConfig(tree, directory, ['**/vitest.config.*.timestamp*']);
47
49
  }
48
- function isEslintInstalled(tree) {
49
- try {
50
- require('eslint');
51
- return true;
52
- }
53
- catch { }
54
- // it might not be installed yet, but it might be in the tree pending install
55
- const { devDependencies, dependencies } = tree.exists('package.json')
56
- ? (0, devkit_1.readJson)(tree, 'package.json')
57
- : {};
58
- return !!devDependencies?.['eslint'] || !!dependencies?.['eslint'];
59
- }
@@ -1,3 +1,3 @@
1
- import type { Tree } from 'nx/src/generators/tree';
1
+ import { type Tree } from '@nx/devkit';
2
2
  export declare function getInstalledViteVersion(tree: Tree): string;
3
3
  export declare function getInstalledViteMajorVersion(tree: Tree): 5 | 6 | 7 | 8 | undefined;
package/migrations.json CHANGED
@@ -28,6 +28,12 @@
28
28
  "description": "Rename imports of `createNodesV2` from `@nx/vitest` to the canonical `createNodes` export.",
29
29
  "implementation": "./dist/src/migrations/update-23-0-0/migrate-create-nodes-v2-to-create-nodes",
30
30
  "documentation": "./dist/src/migrations/update-23-0-0/migrate-create-nodes-v2-to-create-nodes.md"
31
+ },
32
+ "update-23-2-0-use-import-meta-dirname": {
33
+ "version": "23.2.0-beta.6",
34
+ "description": "Replace `__dirname` with `import.meta.dirname` in `vite.config.mts`/`vitest.config.mts` files so they work with Vite's `configLoader: 'native'`.",
35
+ "implementation": "./dist/src/migrations/update-23-2-0/use-import-meta-dirname",
36
+ "documentation": "./dist/src/migrations/update-23-2-0/use-import-meta-dirname.md"
31
37
  }
32
38
  },
33
39
  "packageJsonUpdates": {
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-beta.0",
4
+ "version": "23.2.0-beta.10",
5
5
  "type": "commonjs",
6
6
  "files": [
7
7
  "dist",
@@ -37,6 +37,9 @@
37
37
  "generators": [
38
38
  "dist/generators.d.ts"
39
39
  ],
40
+ "internal": [
41
+ "dist/internal.d.ts"
42
+ ],
40
43
  "executors": [
41
44
  "dist/executors.d.ts"
42
45
  ]
@@ -57,6 +60,11 @@
57
60
  "types": "./dist/generators.d.ts",
58
61
  "default": "./dist/generators.js"
59
62
  },
63
+ "./internal": {
64
+ "@nx/nx-source": "./internal.ts",
65
+ "types": "./dist/internal.d.ts",
66
+ "default": "./dist/internal.js"
67
+ },
60
68
  "./executors": {
61
69
  "@nx/nx-source": "./executors.ts",
62
70
  "types": "./dist/executors.d.ts",
@@ -73,13 +81,13 @@
73
81
  "tslib": "^2.3.0",
74
82
  "semver": "^7.6.3",
75
83
  "@phenomnomnominal/tsquery": "~6.2.0",
76
- "@nx/devkit": "23.2.0-beta.0",
77
- "@nx/js": "23.2.0-beta.0"
84
+ "@nx/devkit": "23.2.0-beta.10",
85
+ "@nx/js": "23.2.0-beta.10"
78
86
  },
79
87
  "peerDependencies": {
80
88
  "vitest": "^3.0.0 || ^4.0.0",
81
89
  "vite": "^5.0.0 || ^6.0.0 || ^7.0.0 || ^8.0.0",
82
- "@nx/eslint": "23.2.0-beta.0"
90
+ "@nx/eslint": "23.2.0-beta.10"
83
91
  },
84
92
  "peerDependenciesMeta": {
85
93
  "@nx/eslint": {
@@ -93,6 +101,6 @@
93
101
  }
94
102
  },
95
103
  "devDependencies": {
96
- "nx": "23.2.0-beta.0"
104
+ "nx": "23.2.0-beta.10"
97
105
  }
98
106
  }