diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/config_stale_vitest_alias_boundaries/npm-workspace/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/config_stale_vitest_alias_boundaries/npm-workspace/package.json new file mode 100644 index 0000000000..fde53127fb --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/config_stale_vitest_alias_boundaries/npm-workspace/package.json @@ -0,0 +1,4 @@ +{ + "name": "npm-workspace", + "workspaces": ["packages/*"] +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/config_stale_vitest_alias_boundaries/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/config_stale_vitest_alias_boundaries/package.json new file mode 100644 index 0000000000..eb1bdfbb43 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/config_stale_vitest_alias_boundaries/package.json @@ -0,0 +1,6 @@ +{ + "name": "unrelated-parent", + "devDependencies": { + "vitest": "npm:@voidzero-dev/vite-plus-test@0.1.24" + } +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/config_stale_vitest_alias_boundaries/pnpm-workspace/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/config_stale_vitest_alias_boundaries/pnpm-workspace/package.json new file mode 100644 index 0000000000..a07127694e --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/config_stale_vitest_alias_boundaries/pnpm-workspace/package.json @@ -0,0 +1,3 @@ +{ + "name": "pnpm-workspace" +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/config_stale_vitest_alias_boundaries/pnpm-workspace/pnpm-workspace.yaml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/config_stale_vitest_alias_boundaries/pnpm-workspace/pnpm-workspace.yaml new file mode 100644 index 0000000000..eefb9ee0cf --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/config_stale_vitest_alias_boundaries/pnpm-workspace/pnpm-workspace.yaml @@ -0,0 +1,5 @@ +packages: + - . +metadata: &metadata + self: *metadata + example: npm:@voidzero-dev/vite-plus-test@0.1.24 diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/config_stale_vitest_alias_boundaries/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/config_stale_vitest_alias_boundaries/snapshots.toml new file mode 100644 index 0000000000..210f203c98 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/config_stale_vitest_alias_boundaries/snapshots.toml @@ -0,0 +1,9 @@ +[[case]] +name = "config_stale_vitest_alias_boundaries" +vp = ["local", "global"] +env = { npm_lifecycle_event = "prepare" } +steps = [ + { argv = ["vp", "config", "--no-hooks", "--no-agent"], cwd = "standalone", comment = "Ignore metadata and unrelated parent package settings", continue-on-failure = true }, + { argv = ["vp", "config", "--no-hooks", "--no-agent"], cwd = "npm-workspace", comment = "Stop at the npm workspace root", continue-on-failure = true }, + { argv = ["vp", "config", "--no-hooks", "--no-agent"], cwd = "pnpm-workspace", comment = "Stop at the pnpm workspace root and ignore cyclic metadata", continue-on-failure = true }, +] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/config_stale_vitest_alias_boundaries/snapshots/config_stale_vitest_alias_boundaries.global.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/config_stale_vitest_alias_boundaries/snapshots/config_stale_vitest_alias_boundaries.global.md new file mode 100644 index 0000000000..a71ff6cfcc --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/config_stale_vitest_alias_boundaries/snapshots/config_stale_vitest_alias_boundaries.global.md @@ -0,0 +1,22 @@ +# config_stale_vitest_alias_boundaries + +## `cd standalone && vp config --no-hooks --no-agent` + +Ignore metadata and unrelated parent package settings + +``` +``` + +## `cd npm-workspace && vp config --no-hooks --no-agent` + +Stop at the npm workspace root + +``` +``` + +## `cd pnpm-workspace && vp config --no-hooks --no-agent` + +Stop at the pnpm workspace root and ignore cyclic metadata + +``` +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/config_stale_vitest_alias_boundaries/snapshots/config_stale_vitest_alias_boundaries.local.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/config_stale_vitest_alias_boundaries/snapshots/config_stale_vitest_alias_boundaries.local.md new file mode 100644 index 0000000000..a71ff6cfcc --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/config_stale_vitest_alias_boundaries/snapshots/config_stale_vitest_alias_boundaries.local.md @@ -0,0 +1,22 @@ +# config_stale_vitest_alias_boundaries + +## `cd standalone && vp config --no-hooks --no-agent` + +Ignore metadata and unrelated parent package settings + +``` +``` + +## `cd npm-workspace && vp config --no-hooks --no-agent` + +Stop at the npm workspace root + +``` +``` + +## `cd pnpm-workspace && vp config --no-hooks --no-agent` + +Stop at the pnpm workspace root and ignore cyclic metadata + +``` +``` diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/config_stale_vitest_alias_boundaries/standalone/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/config_stale_vitest_alias_boundaries/standalone/package.json new file mode 100644 index 0000000000..100ec4b172 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/config_stale_vitest_alias_boundaries/standalone/package.json @@ -0,0 +1,4 @@ +{ + "name": "standalone", + "description": "npm:@voidzero-dev/vite-plus-test@0.1.24" +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_stale_vitest_alias_startup/.gitignore b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_stale_vitest_alias_startup/.gitignore new file mode 100644 index 0000000000..c2658d7d1b --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_stale_vitest_alias_startup/.gitignore @@ -0,0 +1 @@ +node_modules/ diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_stale_vitest_alias_startup/package.json b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_stale_vitest_alias_startup/package.json new file mode 100644 index 0000000000..8518bded4a --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_stale_vitest_alias_startup/package.json @@ -0,0 +1,18 @@ +{ + "name": "migration-stale-vitest-alias-startup", + "scripts": { + "prepare": "vp config" + }, + "devDependencies": { + "vite": "catalog:", + "vite-plus": "catalog:", + "vitest": "catalog:" + }, + "devEngines": { + "packageManager": { + "name": "pnpm", + "version": "11.9.0", + "onFail": "download" + } + } +} diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_stale_vitest_alias_startup/pnpm-workspace.yaml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_stale_vitest_alias_startup/pnpm-workspace.yaml new file mode 100644 index 0000000000..2048034313 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_stale_vitest_alias_startup/pnpm-workspace.yaml @@ -0,0 +1,11 @@ +packages: + - . + +catalog: + vite: npm:@voidzero-dev/vite-plus-core@0.3.0 + vite-plus: 0.3.0 + vitest: npm:@voidzero-dev/vite-plus-test@0.1.24 + +overrides: + vite: 'catalog:' + vitest: 'catalog:' diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_stale_vitest_alias_startup/setup-local.mjs b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_stale_vitest_alias_startup/setup-local.mjs new file mode 100644 index 0000000000..5e9dfa8c2d --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_stale_vitest_alias_startup/setup-local.mjs @@ -0,0 +1,33 @@ +import fs from 'node:fs'; +import { createRequire } from 'node:module'; +import path from 'node:path'; + +const require = createRequire(import.meta.url); +const sourcePackageDir = path.dirname(require.resolve('vite-plus/package.json')); +const localPackageDir = path.join('node_modules', 'vite-plus'); + +fs.mkdirSync('node_modules', { recursive: true }); +fs.cpSync(sourcePackageDir, localPackageDir, { recursive: true, dereference: true }); + +const fakeVitestDir = path.join('node_modules', 'vitest'); +fs.mkdirSync(fakeVitestDir, { recursive: true }); +fs.writeFileSync( + path.join(fakeVitestDir, 'package.json'), + JSON.stringify({ + name: 'vitest', + version: '0.1.24', + type: 'module', + exports: { + './config': './config.js', + './package.json': './package.json', + }, + }), +); +fs.writeFileSync( + path.join(fakeVitestDir, 'config.js'), + "throw new Error('stale Vitest alias was loaded before command dispatch');\n", +); + +// This file only prepares node_modules. Remove the staged copy so migration's +// source-import pass measures the project fixture rather than its test harness. +fs.rmSync(new URL(import.meta.url)); diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_stale_vitest_alias_startup/snapshots.toml b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_stale_vitest_alias_startup/snapshots.toml new file mode 100644 index 0000000000..5de853af23 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_stale_vitest_alias_startup/snapshots.toml @@ -0,0 +1,11 @@ +[[case]] +name = "migration_stale_vitest_alias_startup" +vp = "global" +link-node-modules = true +steps = [ + { argv = ["node", "setup-local.mjs"], snapshot = false, continue-on-failure = true }, + { argv = ["vp", "config", "--no-hooks", "--no-agent"], envs = [["npm_lifecycle_event", "prepare"]], comment = "prepare should report the stale alias instead of loading its native binding", continue-on-failure = true }, + { argv = ["vp", "migrate", "--no-interactive"], envs = [["VP_SKIP_INSTALL", "1"]], comment = "migrate should start without loading the stale Vitest alias", continue-on-failure = true }, + { argv = ["vpt", "print-file", "package.json"], comment = "the stale Vitest dependency should be removed", continue-on-failure = true }, + { argv = ["vpt", "print-file", "pnpm-workspace.yaml"], comment = "the stale catalog alias and override should be removed", continue-on-failure = true }, +] diff --git a/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_stale_vitest_alias_startup/snapshots/migration_stale_vitest_alias_startup.md b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_stale_vitest_alias_startup/snapshots/migration_stale_vitest_alias_startup.md new file mode 100644 index 0000000000..f07bedca24 --- /dev/null +++ b/crates/vp_cli_snapshots/tests/cli_snapshots/fixtures/migration_stale_vitest_alias_startup/snapshots/migration_stale_vitest_alias_startup.md @@ -0,0 +1,73 @@ +# migration_stale_vitest_alias_startup + +## `node setup-local.mjs` + + +## `npm_lifecycle_event=prepare vp config --no-hooks --no-agent` + +prepare should report the stale alias instead of loading its native binding + +**Exit code:** 1 + +``` +error: Found a stale Vitest alias in pnpm-workspace.yaml that points to the removed `@voidzero-dev/vite-plus-test` package. Run `vp migrate` to update the project before installing dependencies. +``` + +## `VP_SKIP_INSTALL=1 vp migrate --no-interactive` + +migrate should start without loading the stale Vitest alias + +``` +VITE+ - The Unified Toolchain for the Web + +◇ Updated . to Vite+ +• Node pnpm +• Dependencies: + vite → +• Package manager settings configured +``` + +## `vpt print-file package.json` + +the stale Vitest dependency should be removed + +``` +{ + "name": "migration-stale-vitest-alias-startup", + "scripts": { + "prepare": "vp config" + }, + "devDependencies": { + "vite": "catalog:", + "vite-plus": "catalog:" + }, + "devEngines": { + "packageManager": { + "name": "pnpm", + "version": "", + "onFail": "download" + } + } +} +``` + +## `vpt print-file pnpm-workspace.yaml` + +the stale catalog alias and override should be removed + +``` +packages: + - . + +catalog: + vite: npm:@voidzero-dev/vite-plus-core@ + vite-plus: + +overrides: + vite@*: 'catalog:' +peerDependencyRules: + allowAny: + - vite + allowedVersions: + vite: '*' +``` diff --git a/packages/cli/src/bin.ts b/packages/cli/src/bin.ts index 5e7e2075a1..8f5865b8ad 100644 --- a/packages/cli/src/bin.ts +++ b/packages/cli/src/bin.ts @@ -21,7 +21,6 @@ import { fmt } from './resolve-fmt.ts'; import { lint } from './resolve-lint.ts'; import { pack } from './resolve-pack.ts'; import { test } from './resolve-test.ts'; -import { resolveUniversalViteConfig } from './resolve-vite-config.ts'; import { vite } from './resolve-vite.ts'; import { accent, errorMsg, log } from './utils/terminal.ts'; @@ -130,6 +129,9 @@ if (maybePrintCommandHelp(args)) { } else { // All other commands — delegate to Rust core via NAPI binding try { + // This module imports `vitest/config` through define-config. Load it here + // so `migrate` and `config` can handle stale aliases before Vitest loads. + const { resolveUniversalViteConfig } = await import('./resolve-vite-config.js'); const initInspection = inspectInitCommand(command, args.slice(1)); if ( initInspection.handled && diff --git a/packages/cli/src/config/bin.ts b/packages/cli/src/config/bin.ts index c46d7be1e0..48ca4d1d87 100644 --- a/packages/cli/src/config/bin.ts +++ b/packages/cli/src/config/bin.ts @@ -1,11 +1,12 @@ import { existsSync } from 'node:fs'; -import { join } from 'node:path'; +import { join, relative } from 'node:path'; import { parseConfigArgs } from '../../binding/index.js'; import { updateExistingAgentInstructions } from '../utils/agent.ts'; import { unwrapCliParseOutcome } from '../utils/cli-parse.ts'; +import { findLegacyVitestAliasConfig } from '../utils/legacy-vitest-alias.ts'; import { defaultInteractive, promptGitHooks } from '../utils/prompts.ts'; -import { log } from '../utils/terminal.ts'; +import { errorMsg, log } from '../utils/terminal.ts'; import { install, isGitHooksEnvDisabled, @@ -23,6 +24,15 @@ async function main() { const isLifecycleScript = lifecycleEvent === 'prepare' || lifecycleEvent === 'postinstall'; const root = process.cwd(); + const staleVitestAliasConfig = findLegacyVitestAliasConfig(root); + if (staleVitestAliasConfig) { + const relativeConfigPath = relative(root, staleVitestAliasConfig); + errorMsg( + `Found a stale Vitest alias in ${relativeConfigPath} that points to the removed \`@voidzero-dev/vite-plus-test\` package. Run \`vp migrate\` to update the project before installing dependencies.`, + ); + process.exit(1); + } + // --- Step 1: Hooks setup --- // Prefer CLI flag, then last-used dir from local git config, then default. // Check environment opt-outs before the Git-backed location lookup. diff --git a/packages/cli/src/utils/__tests__/legacy-vitest-alias.spec.ts b/packages/cli/src/utils/__tests__/legacy-vitest-alias.spec.ts new file mode 100644 index 0000000000..8d74f59472 --- /dev/null +++ b/packages/cli/src/utils/__tests__/legacy-vitest-alias.spec.ts @@ -0,0 +1,156 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import { findLegacyVitestAliasConfig } from '../legacy-vitest-alias.ts'; + +describe('findLegacyVitestAliasConfig', () => { + const legacyAlias = 'npm:@voidzero-dev/vite-plus-test@0.1.24'; + let projectDir: string; + + function writePackageJson(config: Record, directory?: string): string { + const packageDir = directory ?? projectDir; + const filePath = path.join(packageDir, 'package.json'); + fs.mkdirSync(packageDir, { recursive: true }); + fs.writeFileSync(filePath, JSON.stringify(config)); + return filePath; + } + + beforeEach(() => { + projectDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vp-legacy-vitest-alias-')); + }); + + afterEach(() => { + fs.rmSync(projectDir, { recursive: true, force: true }); + }); + + it('finds a nested package.json override alias', () => { + const packagePath = writePackageJson({ overrides: { parent: { vitest: legacyAlias } } }); + + expect(findLegacyVitestAliasConfig(projectDir)).toBe(packagePath); + }); + + it.each([ + 'dependencies', + 'devDependencies', + 'optionalDependencies', + 'peerDependencies', + 'resolutions', + 'catalog', + ])('finds aliases in package.json %s', (field) => { + const packagePath = writePackageJson({ [field]: { vitest: legacyAlias } }); + + expect(findLegacyVitestAliasConfig(projectDir)).toBe(packagePath); + }); + + it.each([ + { pnpm: { overrides: { vitest: legacyAlias } } }, + { catalogs: { testing: { vitest: legacyAlias } } }, + { workspaces: { packages: ['packages/*'], catalog: { vitest: legacyAlias } } }, + { workspaces: { packages: ['packages/*'], catalogs: { testing: { vitest: legacyAlias } } } }, + ])('finds aliases in nested package-manager settings: %j', (config) => { + const packagePath = writePackageJson(config); + + expect(findLegacyVitestAliasConfig(projectDir)).toBe(packagePath); + }); + + it.each(['standalone', 'pnpm', 'npm'])('stops at the %s project boundary', (kind) => { + const childDir = path.join(projectDir, 'child'); + const sourceDir = path.join(childDir, 'src'); + fs.mkdirSync(sourceDir, { recursive: true }); + writePackageJson({ devDependencies: { vitest: legacyAlias } }); + writePackageJson( + { + devDependencies: { vitest: '4.1.11' }, + ...(kind === 'npm' ? { workspaces: ['packages/*'] } : {}), + }, + childDir, + ); + if (kind === 'pnpm') { + fs.writeFileSync(path.join(childDir, 'pnpm-workspace.yaml'), 'packages: [.]\n'); + } + + expect(findLegacyVitestAliasConfig(sourceDir)).toBeUndefined(); + }); + + it('finds root npm overrides from a workspace member', () => { + const packageDir = path.join(projectDir, 'packages', 'app'); + writePackageJson({}, packageDir); + const packagePath = writePackageJson({ + workspaces: ['packages/*'], + overrides: { vitest: legacyAlias }, + }); + + expect(findLegacyVitestAliasConfig(packageDir)).toBe(packagePath); + }); + + it('ignores alias strings in metadata and unrelated settings', () => { + writePackageJson({ + description: legacyAlias, + config: { example: legacyAlias }, + pnpm: { metadata: legacyAlias }, + workspaces: { packages: [], metadata: legacyAlias }, + devDependencies: { vitest: '4.1.11' }, + }); + fs.writeFileSync( + path.join(projectDir, 'pnpm-workspace.yaml'), + `packages: [.]\nmetadata: ${legacyAlias}\ncatalog:\n vitest: 4.1.11\n`, + ); + + expect(findLegacyVitestAliasConfig(projectDir)).toBeUndefined(); + }); + + it.each([false, true])('handles cyclic YAML with a stale alias present: %s', (hasAlias) => { + writePackageJson({}); + fs.writeFileSync( + path.join(projectDir, 'pnpm-workspace.yaml'), + [ + 'packages: [.]', + 'metadata: &metadata', + ' self: *metadata', + 'overrides: &overrides', + ' self: *overrides', + ' list: &list [*list]', + ` vitest: ${hasAlias ? legacyAlias : '4.1.11'}`, + '', + ].join('\n'), + ); + + expect(findLegacyVitestAliasConfig(projectDir)).toBe( + hasAlias ? path.join(projectDir, 'pnpm-workspace.yaml') : undefined, + ); + }); + + it('finds a pnpm catalog alias from a workspace package', () => { + const packageDir = path.join(projectDir, 'packages', 'app'); + writePackageJson({}, packageDir); + fs.writeFileSync( + path.join(projectDir, 'pnpm-workspace.yaml'), + [ + 'packages:', + ' - packages/*', + 'catalog:', + ' vitest: npm:@voidzero-dev/vite-plus-test@0.1.24', + 'overrides:', + " vitest: 'catalog:'", + '', + ].join('\n'), + ); + + expect(findLegacyVitestAliasConfig(packageDir)).toBe( + path.join(projectDir, 'pnpm-workspace.yaml'), + ); + }); + + it('ignores current Vitest pins and malformed config files', () => { + fs.writeFileSync(path.join(projectDir, 'package.json'), '{'); + fs.writeFileSync( + path.join(projectDir, 'pnpm-workspace.yaml'), + ['catalog:', ' vitest: 4.1.11', 'overrides:', ' vitest@*: 4.1.11', ''].join('\n'), + ); + + expect(findLegacyVitestAliasConfig(projectDir)).toBeUndefined(); + }); +}); diff --git a/packages/cli/src/utils/legacy-vitest-alias.ts b/packages/cli/src/utils/legacy-vitest-alias.ts new file mode 100644 index 0000000000..8474f17293 --- /dev/null +++ b/packages/cli/src/utils/legacy-vitest-alias.ts @@ -0,0 +1,101 @@ +import fs from 'node:fs'; +import path from 'node:path'; + +import { parse as parseYaml } from 'yaml'; + +const LEGACY_VITEST_ALIAS = 'npm:@voidzero-dev/vite-plus-test'; + +function containsLegacyVitestAlias(value: unknown, visited = new Set()): boolean { + if (typeof value === 'string') { + return value === LEGACY_VITEST_ALIAS || value.startsWith(`${LEGACY_VITEST_ALIAS}@`); + } + if (value === null || typeof value !== 'object' || visited.has(value)) { + return false; + } + visited.add(value); + return Object.values(value).some((entry) => containsLegacyVitestAlias(entry, visited)); +} + +function asRecord(value: unknown): Record | undefined { + return value !== null && typeof value === 'object' && !Array.isArray(value) + ? (value as Record) + : undefined; +} + +function containsAliasInFields(config: unknown, fields: string[]): boolean { + const record = asRecord(config); + return fields.some((field) => containsLegacyVitestAlias(record?.[field])); +} + +function containsPackageAlias(config: Record | undefined): boolean { + return ( + containsAliasInFields(config, [ + 'dependencies', + 'devDependencies', + 'optionalDependencies', + 'peerDependencies', + 'overrides', + 'resolutions', + 'catalog', + 'catalogs', + ]) || + containsAliasInFields(config?.pnpm, ['overrides']) || + containsAliasInFields(config?.workspaces, ['catalog', 'catalogs']) + ); +} + +function readConfigFile(filePath: string): Record | undefined { + try { + const source = fs.readFileSync(filePath, 'utf8'); + return asRecord( + path.basename(filePath) === 'package.json' ? JSON.parse(source) : parseYaml(source), + ); + } catch { + // Configuration parsing has its own diagnostics. Do not replace them with + // the stale-alias recovery message when the file is malformed. + return undefined; + } +} + +/** + * Find a package-manager setting that still aliases Vitest to the deleted + * `@voidzero-dev/vite-plus-test` wrapper. Check the nearest package and its + * workspace root, without inspecting unrelated ancestor package settings. + */ +export function findLegacyVitestAliasConfig(startDir: string): string | undefined { + let currentDir = path.resolve(startDir); + let foundPackage = false; + while (true) { + const packagePath = path.join(currentDir, 'package.json'); + const pkg = readConfigFile(packagePath); + const isNearestPackage = !foundPackage && fs.existsSync(packagePath); + if (isNearestPackage) { + foundPackage = true; + } + + // Match workspace discovery: the nearest pnpm-workspace.yaml or + // package.json with a workspaces field defines the workspace root. + // If neither exists, only the nearest package's settings apply. + const workspacePath = path.join(currentDir, 'pnpm-workspace.yaml'); + const isWorkspaceRoot = + fs.existsSync(workspacePath) || (pkg !== undefined && Object.hasOwn(pkg, 'workspaces')); + + if ((isNearestPackage || isWorkspaceRoot) && containsPackageAlias(pkg)) { + return packagePath; + } + if (isWorkspaceRoot) { + if ( + containsAliasInFields(readConfigFile(workspacePath), ['catalog', 'catalogs', 'overrides']) + ) { + return workspacePath; + } + return undefined; + } + + const parentDir = path.dirname(currentDir); + if (parentDir === currentDir) { + return undefined; + } + currentDir = parentDir; + } +}