diff --git a/packages/cli/snap-tests-global/migration-monorepo-skip-vite-peer-dependency/snap.txt b/packages/cli/snap-tests-global/migration-monorepo-skip-vite-peer-dependency/snap.txt index f50035fec4..b43ddde6c5 100644 --- a/packages/cli/snap-tests-global/migration-monorepo-skip-vite-peer-dependency/snap.txt +++ b/packages/cli/snap-tests-global/migration-monorepo-skip-vite-peer-dependency/snap.txt @@ -1,9 +1,9 @@ -> vp migrate --no-interactive # migration should check each package's peerDependencies +> vp migrate --no-interactive # migration should preserve vite peer contracts in workspace packages ◇ Migrated . to Vite+ • Node pnpm • 2 config updates applied, 1 file had imports rewritten -> cat packages/vite-plugin/src/index.ts # vite-plugin has vite in peerDeps: vite NOT rewritten, vitest rewritten +> cat packages/vite-plugin/src/index.ts # vite-plugin has vite in peerDeps: vite imports stay public, vitest rewrites import { defineConfig, type Plugin } from 'vite'; import { describe, it, expect } from 'vite-plus/test'; @@ -38,10 +38,13 @@ export default defineConfig({ } } -> cat packages/vite-plugin/package.json # has vite in peerDependencies +> cat packages/vite-plugin/package.json # vite peer range is preserved { "name": "my-vite-plugin", "peerDependencies": { "vite": "^6.0.0" + }, + "devDependencies": { + "vite-plus": "catalog:" } } diff --git a/packages/cli/snap-tests-global/migration-monorepo-skip-vite-peer-dependency/steps.json b/packages/cli/snap-tests-global/migration-monorepo-skip-vite-peer-dependency/steps.json index 4cd2e5c2d3..1345b5fe0f 100644 --- a/packages/cli/snap-tests-global/migration-monorepo-skip-vite-peer-dependency/steps.json +++ b/packages/cli/snap-tests-global/migration-monorepo-skip-vite-peer-dependency/steps.json @@ -1,8 +1,8 @@ { "commands": [ - "vp migrate --no-interactive # migration should check each package's peerDependencies", - "cat packages/vite-plugin/src/index.ts # vite-plugin has vite in peerDeps: vite NOT rewritten, vitest rewritten", + "vp migrate --no-interactive # migration should preserve vite peer contracts in workspace packages", + "cat packages/vite-plugin/src/index.ts # vite-plugin has vite in peerDeps: vite imports stay public, vitest rewrites", "cat package.json # check root package.json (no peerDependencies)", - "cat packages/vite-plugin/package.json # has vite in peerDependencies" + "cat packages/vite-plugin/package.json # vite peer range is preserved" ] } diff --git a/packages/cli/snap-tests-global/migration-skip-vite-peer-dependency/snap.txt b/packages/cli/snap-tests-global/migration-skip-vite-peer-dependency/snap.txt index 819644c0d1..a6f4e6f3ac 100644 --- a/packages/cli/snap-tests-global/migration-skip-vite-peer-dependency/snap.txt +++ b/packages/cli/snap-tests-global/migration-skip-vite-peer-dependency/snap.txt @@ -1,9 +1,9 @@ -> vp migrate --no-interactive # migration should skip rewriting vite imports when vite is in peerDependencies +> vp migrate --no-interactive # migration should preserve vite peer contracts ◇ Migrated . to Vite+ • Node pnpm • 2 config updates applied, 1 file had imports rewritten -> cat src/index.ts # vite imports should NOT be rewritten, vitest imports SHOULD be rewritten +> cat src/index.ts # vite imports stay public, vitest imports rewrite import { defineConfig, type Plugin } from 'vite'; import { describe, it, expect } from 'vite-plus/test'; @@ -26,7 +26,7 @@ export default defineConfig({ plugins: [myVitePlugin()], }); -> cat package.json # check package.json +> cat package.json # vite peer range is preserved { "name": "migration-skip-vite-peer-dependency", "peerDependencies": { diff --git a/packages/cli/snap-tests-global/migration-skip-vite-peer-dependency/steps.json b/packages/cli/snap-tests-global/migration-skip-vite-peer-dependency/steps.json index cca9527700..62c8710a1a 100644 --- a/packages/cli/snap-tests-global/migration-skip-vite-peer-dependency/steps.json +++ b/packages/cli/snap-tests-global/migration-skip-vite-peer-dependency/steps.json @@ -1,8 +1,8 @@ { "commands": [ - "vp migrate --no-interactive # migration should skip rewriting vite imports when vite is in peerDependencies", - "cat src/index.ts # vite imports should NOT be rewritten, vitest imports SHOULD be rewritten", - "cat package.json # check package.json", + "vp migrate --no-interactive # migration should preserve vite peer contracts", + "cat src/index.ts # vite imports stay public, vitest imports rewrite", + "cat package.json # vite peer range is preserved", "cat pnpm-workspace.yaml # check pnpm-workspace.yaml has overrides and catalog" ] } diff --git a/packages/cli/src/migration/__tests__/bun-catalog-file-protocol.spec.ts b/packages/cli/src/migration/__tests__/bun-catalog-file-protocol.spec.ts index 1f66f21036..e4050a529b 100644 --- a/packages/cli/src/migration/__tests__/bun-catalog-file-protocol.spec.ts +++ b/packages/cli/src/migration/__tests__/bun-catalog-file-protocol.spec.ts @@ -21,7 +21,7 @@ vi.mock('../../utils/constants.js', async (importOriginal) => { }; }); -const { rewriteMonorepo } = await import('../migrator.js'); +const { rewriteMonorepo, rewritePackageJson } = await import('../migrator.js'); function makeWorkspaceInfo(rootDir: string, packageManager: PackageManager): WorkspaceInfo { return { @@ -86,4 +86,67 @@ describe('rewriteMonorepo bun catalog with file: protocol', () => { 'file:/tmp/tgz/voidzero-dev-vite-plus-test-0.0.0.tgz', ); }); + + it('does not write file: paths into named catalogs', () => { + fs.writeFileSync( + path.join(tmpDir, 'package.json'), + JSON.stringify({ + name: 'bun-monorepo', + workspaces: { + packages: ['packages/*'], + catalogs: { + build: { + vite: '^7.0.0', + vitest: '^4.0.0', + tsdown: '^0.1.0', + }, + }, + }, + devDependencies: { vite: 'catalog:build' }, + overrides: { vite: 'catalog:build' }, + packageManager: 'bun@1.3.11', + }), + ); + + rewriteMonorepo(makeWorkspaceInfo(tmpDir, PackageManager.bun), true); + + const pkg = readJson(path.join(tmpDir, 'package.json')) as { + workspaces: { + catalog: Record; + catalogs: Record>; + }; + overrides: Record; + devDependencies: Record; + }; + expect(pkg.workspaces.catalog.vite).toBeUndefined(); + expect(pkg.workspaces.catalog.vitest).toBeUndefined(); + expect(pkg.workspaces.catalogs.build.vite).toBe('^7.0.0'); + expect(pkg.workspaces.catalogs.build.vitest).toBe('^4.0.0'); + expect(pkg.workspaces.catalogs.build.tsdown).toBeUndefined(); + expect(pkg.overrides.vite).toBe('file:/tmp/tgz/voidzero-dev-vite-plus-core-0.0.0.tgz'); + expect(pkg.devDependencies.vite).toBe('file:/tmp/tgz/voidzero-dev-vite-plus-core-0.0.0.tgz'); + }); + + it('does not write file: paths into peer dependencies', () => { + const pkg = { + peerDependencies: { + vite: '^7.0.0', + vitest: 'catalog:test', + }, + optionalDependencies: { + vite: '^7.0.0', + }, + }; + + rewritePackageJson(pkg, PackageManager.pnpm, true); + + expect(pkg.peerDependencies.vite).toBe('^7.0.0'); + expect(pkg.peerDependencies.vitest).toBe('*'); + expect(pkg.optionalDependencies.vite).toBe( + 'file:/tmp/tgz/voidzero-dev-vite-plus-core-0.0.0.tgz', + ); + expect( + (pkg as { devDependencies?: Record }).devDependencies?.['vite-plus'], + ).toBe('file:/tmp/tgz/vite-plus-0.0.0.tgz'); + }); }); diff --git a/packages/cli/src/migration/__tests__/migrator.spec.ts b/packages/cli/src/migration/__tests__/migrator.spec.ts index 729064a217..cc27497d35 100644 --- a/packages/cli/src/migration/__tests__/migrator.spec.ts +++ b/packages/cli/src/migration/__tests__/migrator.spec.ts @@ -3,6 +3,7 @@ import os from 'node:os'; import path from 'node:path'; import { afterEach, beforeEach, describe, expect, it, vi } from 'vitest'; +import { parse as parseYaml } from 'yaml'; import { PackageManager } from '../../types/index.js'; import { createMigrationReport } from '../report.js'; @@ -136,6 +137,140 @@ describe('rewritePackageJson', () => { expect(pkg).toMatchSnapshot(); }); + it('preserves named catalog dependency specs in monorepo projects', async () => { + const pkg = { + devDependencies: { + vite: 'catalog:vite7', + vitest: 'catalog:', + }, + dependencies: { + vitest: 'catalog:test', + }, + }; + + rewritePackageJson(pkg, PackageManager.pnpm, true); + + expect(pkg.devDependencies.vite).toBe('catalog:vite7'); + expect(pkg.devDependencies.vitest).toBe('catalog:'); + expect(pkg.dependencies.vitest).toBe('catalog:test'); + expect((pkg.devDependencies as Record)['vite-plus']).toBe('catalog:'); + }); + + it('uses default catalog specs for non-catalog dependency specs in monorepo projects', async () => { + const pkg = { + devDependencies: { + vite: '^7.0.0', + }, + dependencies: { + vitest: '^4.0.0', + }, + }; + + rewritePackageJson(pkg, PackageManager.yarn, true); + + expect(pkg.devDependencies.vite).toBe('catalog:'); + expect(pkg.dependencies.vitest).toBe('catalog:'); + expect((pkg.devDependencies as Record)['vite-plus']).toBe('catalog:'); + }); + + it('uses override specs for yarn optional dependencies in monorepo projects', async () => { + const pkg = { + devDependencies: { + vite: '^7.0.0', + }, + optionalDependencies: { + vite: '^7.0.0', + vitest: 'catalog:test', + }, + }; + + rewritePackageJson(pkg, PackageManager.yarn, true); + + expect(pkg.devDependencies.vite).toBe('catalog:'); + expect(pkg.optionalDependencies.vite).toBe('npm:@voidzero-dev/vite-plus-core@latest'); + expect(pkg.optionalDependencies.vitest).toBe('npm:@voidzero-dev/vite-plus-test@latest'); + expect((pkg.devDependencies as Record)['vite-plus']).toBe('catalog:'); + }); + + it('rewrites peer and optional dependency catalog specs in monorepo projects', async () => { + const pkg = { + peerDependencies: { + vite: 'catalog:vite7', + tsdown: 'catalog:build', + }, + optionalDependencies: { + vitest: 'catalog:test', + oxlint: 'catalog:build', + }, + }; + + rewritePackageJson(pkg, PackageManager.pnpm, true); + + expect(pkg.peerDependencies.vite).toBe('*'); + expect(pkg.peerDependencies).not.toHaveProperty('tsdown'); + expect(pkg.optionalDependencies.vitest).toBe('catalog:test'); + expect(pkg.optionalDependencies).not.toHaveProperty('oxlint'); + expect( + (pkg as { devDependencies?: Record }).devDependencies?.['vite-plus'], + ).toBe('catalog:'); + }); + + it('preserves peer dependency ranges', async () => { + const pkg = { + peerDependencies: { + vite: '^7.0.0', + vitest: '^4.0.0', + }, + optionalDependencies: { + vite: '^7.0.0', + }, + }; + + rewritePackageJson(pkg, PackageManager.pnpm, true); + + expect(pkg.peerDependencies.vite).toBe('^7.0.0'); + expect(pkg.peerDependencies.vitest).toBe('^4.0.0'); + expect(pkg.optionalDependencies.vite).toBe('catalog:'); + expect( + (pkg as { devDependencies?: Record }).devDependencies?.['vite-plus'], + ).toBe('catalog:'); + + const npmPkg = { + peerDependencies: { + vite: '^7.0.0', + }, + optionalDependencies: { + vite: '^7.0.0', + }, + }; + + rewritePackageJson(npmPkg, PackageManager.npm); + + expect(npmPkg.peerDependencies.vite).toBe('^7.0.0'); + expect(npmPkg.optionalDependencies.vite).toBe('npm:@voidzero-dev/vite-plus-core@latest'); + }); + + it('adds local vitest when only a peer vitest exists for vitest-adjacent packages', async () => { + const pkg = { + dependencies: { + 'vitest-browser-svelte': '^1.0.0', + }, + peerDependencies: { + vitest: '^4.0.0', + }, + }; + + rewritePackageJson(pkg, PackageManager.pnpm, true); + + expect(pkg.peerDependencies.vitest).toBe('^4.0.0'); + expect((pkg as { devDependencies?: Record }).devDependencies?.vitest).toBe( + 'catalog:', + ); + expect( + (pkg as { devDependencies?: Record }).devDependencies?.['vite-plus'], + ).toBe('catalog:'); + }); + it('should preserve playwright when removing @vitest/browser-playwright', async () => { const pkg = { devDependencies: { @@ -454,6 +589,10 @@ function readYaml(filePath: string): string { return fs.readFileSync(filePath, 'utf8'); } +function readYamlObject(filePath: string): Record { + return parseYaml(readYaml(filePath)) as Record; +} + describe('rewriteStandaloneProject pnpm workspace yaml', () => { let tmpDir: string; @@ -560,6 +699,230 @@ describe('rewriteStandaloneProject pnpm workspace yaml', () => { expect(yaml).toContain("vite: 'catalog:'"); expect(yaml).toContain("vitest: 'catalog:'"); }); + + it('rewrites named catalogs in pnpm-workspace.yaml without adding new entries', () => { + fs.writeFileSync( + path.join(tmpDir, 'package.json'), + JSON.stringify({ + name: 'test', + devDependencies: { vite: 'catalog:vite7' }, + peerDependencies: { + vite: 'catalog:vite7', + vitest: 'catalog:', + tsdown: 'catalog:test', + }, + }), + ); + fs.writeFileSync( + path.join(tmpDir, 'pnpm-workspace.yaml'), + [ + 'overrides:', + ' vite: catalog:vite7', + 'catalog:', + ' vitest: ^4.0.0', + 'catalogs:', + ' vite7:', + ' react: ^18.0.0', + ' vite: ^7.0.0', + ' vite-plus: ^0.0.0', + ' test:', + ' vitest: ^4.0.0', + ' tsdown: ^0.1.0', + '', + ].join('\n'), + ); + + rewriteStandaloneProject(tmpDir, makeWorkspaceInfo(tmpDir, PackageManager.pnpm), true, true); + + const yaml = readYamlObject(path.join(tmpDir, 'pnpm-workspace.yaml')) as { + catalog: Record; + overrides: Record; + catalogs: Record>; + }; + expect(yaml.overrides.vite).toBe('catalog:vite7'); + expect(yaml.overrides.vitest).toBe('catalog:'); + expect(yaml.catalog.vitest).toBe('npm:@voidzero-dev/vite-plus-test@latest'); + expect(yaml.catalogs.vite7.vite).toBe('npm:@voidzero-dev/vite-plus-core@latest'); + expect(yaml.catalogs.vite7.react).toBe('^18.0.0'); + expect(yaml.catalogs.vite7['vite-plus']).toBe('latest'); + expect(yaml.catalogs.test.vitest).toBe('npm:@voidzero-dev/vite-plus-test@latest'); + expect(yaml.catalogs.test.tsdown).toBeUndefined(); + expect(yaml.catalogs.test['vite-plus']).toBeUndefined(); + + const pkg = readJson(path.join(tmpDir, 'package.json')) as { + devDependencies: Record; + peerDependencies: Record; + }; + expect(pkg.devDependencies.vite).toBe('catalog:vite7'); + expect(pkg.devDependencies['vite-plus']).toBe('catalog:'); + expect(pkg.peerDependencies.vite).toBe('^7.0.0'); + expect(pkg.peerDependencies.vitest).toBe('^4.0.0'); + expect(pkg.peerDependencies).not.toHaveProperty('tsdown'); + }); + + it('preserves named pnpm overrides when moving root overrides to pnpm-workspace.yaml', () => { + fs.writeFileSync( + path.join(tmpDir, 'package.json'), + JSON.stringify({ + name: 'pnpm-monorepo', + workspaces: ['packages/*'], + devDependencies: { vite: 'catalog:vite7' }, + pnpm: { + overrides: { + vite: 'catalog:vite7', + react: '^18.0.0', + }, + }, + }), + ); + fs.writeFileSync( + path.join(tmpDir, 'pnpm-workspace.yaml'), + ['packages:', ' - packages/*', 'catalogs:', ' vite7:', ' vite: ^7.0.0', ''].join('\n'), + ); + + rewriteMonorepo(makeWorkspaceInfo(tmpDir, PackageManager.pnpm), true); + + const yaml = readYamlObject(path.join(tmpDir, 'pnpm-workspace.yaml')) as { + overrides: Record; + catalogs: Record>; + }; + expect(yaml.overrides.vite).toBe('catalog:vite7'); + expect(yaml.overrides.vitest).toBe('catalog:'); + expect(yaml.overrides.react).toBe('^18.0.0'); + expect(yaml.catalogs.vite7.vite).toBe('npm:@voidzero-dev/vite-plus-core@latest'); + + const pkg = readJson(path.join(tmpDir, 'package.json')) as { + pnpm?: unknown; + }; + expect(pkg.pnpm).toBeUndefined(); + }); + + it('preserves default pnpm catalog overrides over stale workspace named overrides', () => { + fs.writeFileSync( + path.join(tmpDir, 'package.json'), + JSON.stringify({ + name: 'pnpm-monorepo', + workspaces: ['packages/*'], + devDependencies: { vite: 'catalog:' }, + pnpm: { + overrides: { + vite: 'catalog:', + }, + }, + }), + ); + fs.writeFileSync( + path.join(tmpDir, 'pnpm-workspace.yaml'), + [ + 'packages:', + ' - packages/*', + 'overrides:', + ' vite: catalog:vite7', + 'catalogs:', + ' vite7:', + ' vite: ^7.0.0', + '', + ].join('\n'), + ); + + rewriteMonorepo(makeWorkspaceInfo(tmpDir, PackageManager.pnpm), true); + + const yaml = readYamlObject(path.join(tmpDir, 'pnpm-workspace.yaml')) as { + overrides: Record; + }; + expect(yaml.overrides.vite).toBe('catalog:'); + expect(yaml.overrides.vitest).toBe('catalog:'); + }); + + it('does not resolve peer dependency catalog specs to migrated aliases', () => { + fs.writeFileSync( + path.join(tmpDir, 'package.json'), + JSON.stringify({ + name: 'test', + peerDependencies: { + vite: 'catalog:vite7', + vitest: 'catalog:', + }, + }), + ); + fs.writeFileSync( + path.join(tmpDir, 'pnpm-workspace.yaml'), + [ + 'catalog:', + ' vitest: npm:@voidzero-dev/vite-plus-test@latest', + 'catalogs:', + ' vite7:', + ' vite: npm:@voidzero-dev/vite-plus-core@latest', + '', + ].join('\n'), + ); + + rewriteStandaloneProject(tmpDir, makeWorkspaceInfo(tmpDir, PackageManager.pnpm), true, true); + + const pkg = readJson(path.join(tmpDir, 'package.json')) as { + peerDependencies: Record; + }; + expect(pkg.peerDependencies.vite).toBe('*'); + expect(pkg.peerDependencies.vitest).toBe('*'); + }); +}); + +describe('rewriteMonorepo yarn catalog', () => { + let tmpDir: string; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vp-test-yarn-')); + }); + + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + it('rewrites named catalogs in .yarnrc.yml and keeps nodeLinker', () => { + fs.writeFileSync( + path.join(tmpDir, 'package.json'), + JSON.stringify({ + name: 'yarn-monorepo', + workspaces: ['packages/*'], + devDependencies: { vite: 'catalog:vite7' }, + peerDependencies: { vite: 'catalog:vite7', vitest: 'catalog:test' }, + packageManager: 'yarn@4.10.0', + }), + ); + fs.writeFileSync( + path.join(tmpDir, '.yarnrc.yml'), + [ + 'catalogs:', + ' vite7:', + ' react: ^18.0.0', + ' vite: ^7.0.0', + ' test:', + ' vitest: ^4.0.0', + ' oxlint: ^1.0.0', + '', + ].join('\n'), + ); + + rewriteMonorepo(makeWorkspaceInfo(tmpDir, PackageManager.yarn), true); + + const yarnrc = readYamlObject(path.join(tmpDir, '.yarnrc.yml')) as { + nodeLinker: string; + catalogs: Record>; + }; + expect(yarnrc.nodeLinker).toBe('node-modules'); + expect(yarnrc.catalogs.vite7.vite).toBe('npm:@voidzero-dev/vite-plus-core@latest'); + expect(yarnrc.catalogs.vite7.react).toBe('^18.0.0'); + expect(yarnrc.catalogs.test.vitest).toBe('npm:@voidzero-dev/vite-plus-test@latest'); + expect(yarnrc.catalogs.test.oxlint).toBeUndefined(); + + const pkg = readJson(path.join(tmpDir, 'package.json')) as { + devDependencies: Record; + peerDependencies: Record; + }; + expect(pkg.devDependencies.vite).toBe('catalog:vite7'); + expect(pkg.peerDependencies.vite).toBe('^7.0.0'); + expect(pkg.peerDependencies.vitest).toBe('^4.0.0'); + }); }); describe('rewriteMonorepo bun catalog', () => { @@ -622,6 +985,41 @@ describe('rewriteMonorepo bun catalog', () => { expect(workspaces.packages).toEqual(['packages/*']); }); + it('cleans stale top-level bun catalog when workspaces.catalog is preferred', () => { + fs.writeFileSync( + path.join(tmpDir, 'package.json'), + JSON.stringify({ + name: 'bun-monorepo', + workspaces: { + packages: ['packages/*'], + catalog: { vite: '^7.0.0' }, + }, + catalog: { + vite: '^6.0.0', + vitest: '^3.0.0', + tsdown: '^0.1.0', + react: '^19.0.0', + }, + devDependencies: { vite: '^7.0.0' }, + packageManager: 'bun@1.3.11', + }), + ); + + rewriteMonorepo(makeWorkspaceInfo(tmpDir, PackageManager.bun), true); + + const pkg = readJson(path.join(tmpDir, 'package.json')) as { + catalog: Record; + workspaces: { catalog: Record }; + }; + expect(pkg.workspaces.catalog.vite).toBe('npm:@voidzero-dev/vite-plus-core@latest'); + expect(pkg.workspaces.catalog['vite-plus']).toBe('latest'); + expect(pkg.catalog.vite).toBe('npm:@voidzero-dev/vite-plus-core@latest'); + expect(pkg.catalog.vitest).toBe('npm:@voidzero-dev/vite-plus-test@latest'); + expect(pkg.catalog.tsdown).toBeUndefined(); + expect(pkg.catalog.react).toBe('^19.0.0'); + expect(pkg.catalog['vite-plus']).toBeUndefined(); + }); + it('writes catalog to top-level when workspaces is an object without catalog', () => { fs.writeFileSync( path.join(tmpDir, 'package.json'), @@ -645,6 +1043,113 @@ describe('rewriteMonorepo bun catalog', () => { const workspaces = pkg.workspaces as { packages: string[] }; expect(workspaces.packages).toEqual(['packages/*']); }); + + it('rewrites top-level named catalogs and preserves named overrides', () => { + fs.writeFileSync( + path.join(tmpDir, 'package.json'), + JSON.stringify({ + name: 'bun-monorepo', + workspaces: ['packages/*'], + catalogs: { + build: { vite: '^7.0.0', react: '^19.0.0', tsdown: '^0.1.0' }, + test: { vitest: '^4.0.0' }, + }, + overrides: { vite: 'catalog:build' }, + devDependencies: { vite: 'catalog:build' }, + peerDependencies: { vite: 'catalog:build', vitest: 'catalog:test' }, + packageManager: 'bun@1.3.11', + }), + ); + + rewriteMonorepo(makeWorkspaceInfo(tmpDir, PackageManager.bun), true); + + const pkg = readJson(path.join(tmpDir, 'package.json')) as { + catalog: Record; + catalogs: Record>; + overrides: Record; + devDependencies: Record; + peerDependencies: Record; + }; + expect(pkg.catalog.vite).toBe('npm:@voidzero-dev/vite-plus-core@latest'); + expect(pkg.catalogs.build.vite).toBe('npm:@voidzero-dev/vite-plus-core@latest'); + expect(pkg.catalogs.build.react).toBe('^19.0.0'); + expect(pkg.catalogs.build.tsdown).toBeUndefined(); + expect(pkg.catalogs.test.vitest).toBe('npm:@voidzero-dev/vite-plus-test@latest'); + expect(pkg.overrides.vite).toBe('catalog:build'); + expect(pkg.overrides.vitest).toBe('catalog:'); + expect(pkg.devDependencies.vite).toBe('catalog:build'); + expect(pkg.peerDependencies.vite).toBe('^7.0.0'); + expect(pkg.peerDependencies.vitest).toBe('^4.0.0'); + }); + + it('rewrites workspaces named catalogs and writes default catalog beside them', () => { + fs.writeFileSync( + path.join(tmpDir, 'package.json'), + JSON.stringify({ + name: 'bun-monorepo', + workspaces: { + packages: ['packages/*'], + catalogs: { + build: { vite: '^7.0.0', oxlint: '^1.0.0' }, + test: { vitest: '^4.0.0', vite: '^7.0.0' }, + }, + }, + devDependencies: { vite: '^7.0.0' }, + packageManager: 'bun@1.3.11', + }), + ); + + rewriteMonorepo(makeWorkspaceInfo(tmpDir, PackageManager.bun), true); + + const pkg = readJson(path.join(tmpDir, 'package.json')) as { + catalog?: Record; + workspaces: { + catalog: Record; + catalogs: Record>; + }; + overrides: Record; + }; + expect(pkg.catalog).toBeUndefined(); + expect(pkg.workspaces.catalog.vite).toBe('npm:@voidzero-dev/vite-plus-core@latest'); + expect(pkg.workspaces.catalog['vite-plus']).toBe('latest'); + expect(pkg.workspaces.catalogs.build.vite).toBe('npm:@voidzero-dev/vite-plus-core@latest'); + expect(pkg.workspaces.catalogs.build.oxlint).toBeUndefined(); + expect(pkg.workspaces.catalogs.test.vitest).toBe('npm:@voidzero-dev/vite-plus-test@latest'); + expect(pkg.workspaces.catalogs.test.vite).toBe('npm:@voidzero-dev/vite-plus-core@latest'); + expect(pkg.overrides.vite).toBe('catalog:'); + }); + + it('keeps an existing top-level default catalog when workspaces named catalogs exist', () => { + fs.writeFileSync( + path.join(tmpDir, 'package.json'), + JSON.stringify({ + name: 'bun-monorepo', + workspaces: { + packages: ['packages/*'], + catalogs: { + build: { vite: '^7.0.0' }, + }, + }, + catalog: { react: '^19.0.0' }, + devDependencies: { vite: '^7.0.0' }, + packageManager: 'bun@1.3.11', + }), + ); + + rewriteMonorepo(makeWorkspaceInfo(tmpDir, PackageManager.bun), true); + + const pkg = readJson(path.join(tmpDir, 'package.json')) as { + catalog: Record; + workspaces: { + catalog?: Record; + catalogs: Record>; + }; + }; + expect(pkg.catalog.react).toBe('^19.0.0'); + expect(pkg.catalog.vite).toBe('npm:@voidzero-dev/vite-plus-core@latest'); + expect(pkg.workspaces.catalog).toBeUndefined(); + expect(pkg.workspaces.catalogs.build.vite).toBe('npm:@voidzero-dev/vite-plus-core@latest'); + }); }); describe('framework shim', () => { diff --git a/packages/cli/src/migration/migrator.ts b/packages/cli/src/migration/migrator.ts index be37e0442d..a228a1db1a 100644 --- a/packages/cli/src/migration/migrator.ts +++ b/packages/cli/src/migration/migrator.ts @@ -35,7 +35,7 @@ import { removeDeprecatedTsconfigFalseOption, } from '../utils/tsconfig.ts'; import type { NpmWorkspaces } from '../utils/workspace.ts'; -import { editYamlFile, scalarString, type YamlDocument } from '../utils/yaml.ts'; +import { editYamlFile, readYamlFile, scalarString, type YamlDocument } from '../utils/yaml.ts'; import { PRETTIER_CONFIG_FILES, PRETTIER_PACKAGE_JSON_CONFIG, @@ -87,6 +87,22 @@ const BROWSER_PROVIDER_PEER_DEPS: Record = { '@vitest/browser-webdriverio': 'webdriverio', }; +const PUBLIC_PEER_DEPENDENCY_FALLBACKS: Record = { + vite: '*', + vitest: '*', +}; + +type PackageJsonDependencyField = + | 'devDependencies' + | 'dependencies' + | 'peerDependencies' + | 'optionalDependencies'; + +type CatalogDependencyResolver = ( + catalogSpec: string, + dependencyName: string, +) => string | undefined; + function warnMigration(message: string, report?: MigrationReport) { addMigrationWarning(report, message); if (!report) { @@ -797,8 +813,11 @@ export function rewriteStandaloneProject( } const packageManager = workspaceInfo.packageManager; + const catalogDependencyResolver = createCatalogDependencyResolver(projectPath, packageManager); let extractedStagedConfig: Record | null = null; let remainingPnpmOverrides: Record | undefined; + let shouldRewritePnpmWorkspaceYaml = false; + let shouldAddPnpmWorkspaceVitePlusOverride = false; // Determined inside editJsonFile callback to avoid a redundant file read let usePnpmWorkspaceYaml = false; editJsonFile<{ @@ -806,6 +825,8 @@ export function rewriteStandaloneProject( resolutions?: Record; devDependencies?: Record; dependencies?: Record; + peerDependencies?: Record; + optionalDependencies?: Record; scripts?: Record; pnpm?: { overrides?: Record; @@ -830,14 +851,8 @@ export function rewriteStandaloneProject( // otherwise use pnpm-workspace.yaml. usePnpmWorkspaceYaml = !pkg.pnpm; if (usePnpmWorkspaceYaml) { - rewritePnpmWorkspaceYaml(projectPath); - // In force-override mode, also override vite-plus itself so transitive - // deps resolve to the local tgz instead of the published version. - if (isForceOverrideMode()) { - migratePnpmOverridesToWorkspaceYaml(projectPath, { - [VITE_PLUS_NAME]: VITE_PLUS_VERSION, - }); - } + shouldRewritePnpmWorkspaceYaml = true; + shouldAddPnpmWorkspaceVitePlusOverride = isForceOverrideMode(); } const overrideKeys = Object.keys(VITE_PLUS_OVERRIDE_PACKAGES); if (!usePnpmWorkspaceYaml) { @@ -886,6 +901,7 @@ export function rewriteStandaloneProject( packageManager, usePnpmWorkspaceYaml, skipStagedMigration, + catalogDependencyResolver, ); // ensure vite-plus is in devDependencies @@ -902,11 +918,21 @@ export function rewriteStandaloneProject( return pkg; }); + if (shouldRewritePnpmWorkspaceYaml) { + rewritePnpmWorkspaceYaml(projectPath); + } + // Move remaining non-Vite pnpm.overrides to pnpm-workspace.yaml if (remainingPnpmOverrides) { migratePnpmOverridesToWorkspaceYaml(projectPath, remainingPnpmOverrides); } + if (shouldAddPnpmWorkspaceVitePlusOverride) { + migratePnpmOverridesToWorkspaceYaml(projectPath, { + [VITE_PLUS_NAME]: VITE_PLUS_VERSION, + }); + } + if (packageManager === PackageManager.yarn) { rewriteYarnrcYml(projectPath); } @@ -942,6 +968,10 @@ export function rewriteMonorepo( silent = false, report?: MigrationReport, ): void { + const catalogDependencyResolver = createCatalogDependencyResolver( + workspaceInfo.rootDir, + workspaceInfo.packageManager, + ); // rewrite root workspace if (workspaceInfo.packageManager === PackageManager.pnpm) { rewritePnpmWorkspaceYaml(workspaceInfo.rootDir); @@ -954,6 +984,7 @@ export function rewriteMonorepo( workspaceInfo.rootDir, workspaceInfo.packageManager, skipStagedMigration, + catalogDependencyResolver, ); // rewrite packages @@ -964,6 +995,7 @@ export function rewriteMonorepo( skipStagedMigration, silent, report, + catalogDependencyResolver, ); } @@ -991,6 +1023,7 @@ export function rewriteMonorepoProject( skipStagedMigration?: boolean, silent = false, report?: MigrationReport, + catalogDependencyResolver?: CatalogDependencyResolver, ): void { cleanupDeprecatedTsconfigOptions(projectPath, silent, report); mergeViteConfigFiles(projectPath, silent, report); @@ -1005,10 +1038,18 @@ export function rewriteMonorepoProject( editJsonFile<{ devDependencies?: Record; dependencies?: Record; + peerDependencies?: Record; + optionalDependencies?: Record; scripts?: Record; }>(packageJsonPath, (pkg) => { // rewrite scripts in package.json - extractedStagedConfig = rewritePackageJson(pkg, packageManager, true, skipStagedMigration); + extractedStagedConfig = rewritePackageJson( + pkg, + packageManager, + true, + skipStagedMigration, + catalogDependencyResolver, + ); return pkg; }); @@ -1035,20 +1076,23 @@ function rewritePnpmWorkspaceYaml(projectPath: string): void { rewriteCatalog(doc); // overrides + const overrides = doc.getIn(['overrides']); for (const key of Object.keys(VITE_PLUS_OVERRIDE_PACKAGES)) { - let version = VITE_PLUS_OVERRIDE_PACKAGES[key]; - if (!version.startsWith('file:')) { - version = 'catalog:'; - } + const currentVersion = getYamlMapScalarStringValue(overrides, key); + const version = getCatalogDependencySpec( + currentVersion, + VITE_PLUS_OVERRIDE_PACKAGES[key], + true, + ); doc.setIn(['overrides', scalarString(key)], scalarString(version)); } // remove dependency selector from vite, e.g. "vite-plugin-svgr>vite": "npm:vite@7.0.12" - const overrides = doc.getIn(['overrides']) as YAMLMap, Scalar>; - for (const item of overrides.items) { + const updatedOverrides = doc.getIn(['overrides']) as YAMLMap, Scalar>; + for (const item of updatedOverrides.items) { if (item.key.value.includes('>')) { const splits = item.key.value.split('>'); if (splits[splits.length - 1].trim() === 'vite') { - overrides.delete(item.key); + updatedOverrides.delete(item.key); } } } @@ -1125,9 +1169,15 @@ function cleanupPnpmOverridesForWorkspaceYaml( overrideKeys: string[], ): Record | undefined { // Remove Vite-managed keys from pnpm.overrides + const catalogOverrides: Record = {}; + const overrides = pkg.pnpm?.overrides; for (const key of [...overrideKeys, ...REMOVE_PACKAGES]) { - if (pkg.pnpm?.overrides?.[key]) { - delete pkg.pnpm.overrides[key]; + const value = overrides?.[key]; + if (value) { + if (overrideKeys.includes(key) && value.startsWith('catalog:')) { + catalogOverrides[key] = value; + } + delete overrides[key]; } } // Remove dependency selectors targeting vite @@ -1142,8 +1192,11 @@ function cleanupPnpmOverridesForWorkspaceYaml( // Collect remaining overrides to move to pnpm-workspace.yaml then delete all // (pnpm ignores workspace-level overrides when pnpm.overrides exists in package.json) let remaining: Record | undefined; + if (Object.keys(catalogOverrides).length > 0) { + remaining = { ...catalogOverrides }; + } if (pkg.pnpm?.overrides && Object.keys(pkg.pnpm.overrides).length > 0) { - remaining = { ...pkg.pnpm.overrides }; + remaining = { ...remaining, ...pkg.pnpm.overrides }; } delete pkg.pnpm?.overrides; // Only remove Vite-managed peerDependencyRules entries, preserve custom ones @@ -1229,6 +1282,128 @@ function rewriteYarnrcYml(projectPath: string): void { * Rewrite catalog in pnpm-workspace.yaml or .yarnrc.yml * @param doc - The document to rewrite */ +function getCatalogDependencySpec( + currentValue: string | undefined, + version: string, + supportCatalog: boolean, + options?: { + dependencyField?: PackageJsonDependencyField; + dependencyName?: string; + packageManager?: PackageManager; + catalogDependencyResolver?: CatalogDependencyResolver; + }, +): string { + if (options?.dependencyField === 'peerDependencies') { + if (currentValue?.startsWith('catalog:') && options.dependencyName) { + const resolved = options.catalogDependencyResolver?.(currentValue, options.dependencyName); + if (resolved && !isVitePlusOverrideSpec(resolved)) { + return resolved; + } + return PUBLIC_PEER_DEPENDENCY_FALLBACKS[options.dependencyName] ?? currentValue; + } + return currentValue ?? version; + } + if ( + options?.dependencyField === 'optionalDependencies' && + options?.packageManager === PackageManager.yarn + ) { + return version; + } + if (!supportCatalog || version.startsWith('file:')) { + return version; + } + return currentValue?.startsWith('catalog:') ? currentValue : 'catalog:'; +} + +function isVitePlusOverrideSpec(value: string): boolean { + return ( + Object.values(VITE_PLUS_OVERRIDE_PACKAGES).includes(value) || + value.startsWith('npm:@voidzero-dev/vite-plus-') + ); +} + +function createCatalogDependencyResolver( + projectPath: string, + packageManager: PackageManager, +): CatalogDependencyResolver | undefined { + if (packageManager === PackageManager.pnpm) { + const pnpmWorkspaceYamlPath = path.join(projectPath, 'pnpm-workspace.yaml'); + if (!fs.existsSync(pnpmWorkspaceYamlPath)) { + return undefined; + } + const doc = readYamlFile(pnpmWorkspaceYamlPath) as { + catalog?: Record; + catalogs?: Record>; + } | null; + return createCatalogDependencyResolverFromCatalogs(doc?.catalog, doc?.catalogs); + } + if (packageManager === PackageManager.yarn) { + const yarnrcYmlPath = path.join(projectPath, '.yarnrc.yml'); + if (!fs.existsSync(yarnrcYmlPath)) { + return undefined; + } + const doc = readYamlFile(yarnrcYmlPath) as { + catalog?: Record; + catalogs?: Record>; + } | null; + return createCatalogDependencyResolverFromCatalogs(doc?.catalog, doc?.catalogs); + } + if (packageManager === PackageManager.bun) { + const packageJsonPath = path.join(projectPath, 'package.json'); + if (!fs.existsSync(packageJsonPath)) { + return undefined; + } + const pkg = readJsonFile(packageJsonPath) as { + workspaces?: NpmWorkspaces; + catalog?: Record; + catalogs?: Record>; + }; + const workspacesObj = + pkg.workspaces && !Array.isArray(pkg.workspaces) ? pkg.workspaces : undefined; + return (catalogSpec, dependencyName) => { + const catalogName = catalogSpec.slice('catalog:'.length); + if (catalogName) { + return ( + workspacesObj?.catalogs?.[catalogName]?.[dependencyName] ?? + pkg.catalogs?.[catalogName]?.[dependencyName] + ); + } + return workspacesObj?.catalog?.[dependencyName] ?? pkg.catalog?.[dependencyName]; + }; + } + return undefined; +} + +function createCatalogDependencyResolverFromCatalogs( + catalog: Record | undefined, + catalogs: Record> | undefined, +): CatalogDependencyResolver { + return (catalogSpec, dependencyName) => { + const catalogName = catalogSpec.slice('catalog:'.length); + if (catalogName) { + return catalogs?.[catalogName]?.[dependencyName]; + } + return catalog?.[dependencyName]; + }; +} + +function getYamlMapScalarStringValue(map: unknown, key: string): string | undefined { + if (!(map instanceof YAMLMap)) { + return undefined; + } + for (const item of map.items) { + if ( + item.key instanceof Scalar && + item.key.value === key && + item.value instanceof Scalar && + typeof item.value.value === 'string' + ) { + return item.value.value; + } + } + return undefined; +} + function rewriteCatalog(doc: YamlDocument): void { for (const [key, value] of Object.entries(VITE_PLUS_OVERRIDE_PACKAGES)) { // ERR_PNPM_CATALOG_IN_OVERRIDES  Could not resolve a catalog in the overrides: The entry for 'vite' in catalog 'default' declares a dependency using the 'file' protocol @@ -1248,7 +1423,53 @@ function rewriteCatalog(doc: YamlDocument): void { } } - // TODO: rewrite `catalogs` when OVERRIDE_PACKAGES exists in catalog + const catalogs = doc.getIn(['catalogs']); + if (!(catalogs instanceof YAMLMap)) { + return; + } + for (const item of catalogs.items) { + const catalogName = item.key instanceof Scalar ? item.key.value : undefined; + if (typeof catalogName !== 'string' || !(item.value instanceof YAMLMap)) { + continue; + } + for (const [key, value] of Object.entries(VITE_PLUS_OVERRIDE_PACKAGES)) { + const catalogPath = ['catalogs', catalogName, key]; + if (!value.startsWith('file:') && doc.hasIn(catalogPath)) { + doc.setIn(catalogPath, scalarString(value)); + } + } + const vitePlusPath = ['catalogs', catalogName, VITE_PLUS_NAME]; + if (!VITE_PLUS_VERSION.startsWith('file:') && doc.hasIn(vitePlusPath)) { + doc.setIn(vitePlusPath, scalarString(VITE_PLUS_VERSION)); + } + for (const name of REMOVE_PACKAGES) { + const catalogPath = ['catalogs', catalogName, name]; + if (doc.hasIn(catalogPath)) { + doc.deleteIn(catalogPath); + } + } + } +} + +function rewriteCatalogObject(catalog: Record, addMissing: boolean): void { + for (const [key, value] of Object.entries(VITE_PLUS_OVERRIDE_PACKAGES)) { + if (value.startsWith('file:') || (!addMissing && !(key in catalog))) { + continue; + } + catalog[key] = value; + } + if (!VITE_PLUS_VERSION.startsWith('file:') && (addMissing || VITE_PLUS_NAME in catalog)) { + catalog[VITE_PLUS_NAME] = VITE_PLUS_VERSION; + } + for (const name of REMOVE_PACKAGES) { + delete catalog[name]; + } +} + +function rewriteCatalogsObject(catalogs: Record>): void { + for (const catalog of Object.values(catalogs)) { + rewriteCatalogObject(catalog, false); + } } /** @@ -1266,39 +1487,43 @@ function rewriteBunCatalog(projectPath: string): void { editJsonFile<{ workspaces?: NpmWorkspaces; catalog?: Record; + catalogs?: Record>; overrides?: Record; }>(packageJsonPath, (pkg) => { // Bun supports catalogs in both workspaces.catalog and top-level catalog; // prefer the location the user already chose to avoid moving their config. const workspacesObj = pkg.workspaces && !Array.isArray(pkg.workspaces) ? pkg.workspaces : undefined; + const useWorkspacesCatalog = + workspacesObj?.catalog != null || (pkg.catalog == null && workspacesObj?.catalogs != null); const catalog: Record = { - ...(workspacesObj?.catalog ?? pkg.catalog), + ...(useWorkspacesCatalog ? workspacesObj?.catalog : pkg.catalog), }; - for (const [key, value] of Object.entries(VITE_PLUS_OVERRIDE_PACKAGES)) { - if (!value.startsWith('file:')) { - catalog[key] = value; - } - } - if (!VITE_PLUS_VERSION.startsWith('file:')) { - catalog[VITE_PLUS_NAME] = VITE_PLUS_VERSION; - } - - for (const name of REMOVE_PACKAGES) { - delete catalog[name]; - } + rewriteCatalogObject(catalog, true); - if (workspacesObj?.catalog != null) { + if (useWorkspacesCatalog) { workspacesObj.catalog = catalog; + if (pkg.catalog) { + rewriteCatalogObject(pkg.catalog, false); + } } else { pkg.catalog = catalog; + if (workspacesObj?.catalog) { + rewriteCatalogObject(workspacesObj.catalog, false); + } + } + if (workspacesObj?.catalogs) { + rewriteCatalogsObject(workspacesObj.catalogs); + } + if (pkg.catalogs) { + rewriteCatalogsObject(pkg.catalogs); } // bun overrides support catalog: references const overrides: Record = { ...pkg.overrides }; for (const [key, value] of Object.entries(VITE_PLUS_OVERRIDE_PACKAGES)) { - overrides[key] = value.startsWith('file:') ? value : 'catalog:'; + overrides[key] = getCatalogDependencySpec(overrides[key], value, true); } pkg.overrides = overrides; @@ -1314,6 +1539,7 @@ function rewriteRootWorkspacePackageJson( projectPath: string, packageManager: PackageManager, skipStagedMigration?: boolean, + catalogDependencyResolver?: CatalogDependencyResolver, ): void { const packageJsonPath = path.join(projectPath, 'package.json'); if (!fs.existsSync(packageJsonPath)) { @@ -1325,6 +1551,9 @@ function rewriteRootWorkspacePackageJson( resolutions?: Record; overrides?: Record; devDependencies?: Record; + dependencies?: Record; + peerDependencies?: Record; + optionalDependencies?: Record; pnpm?: { overrides?: Record; peerDependencyRules?: { @@ -1399,7 +1628,14 @@ function rewriteRootWorkspacePackageJson( } // rewrite package.json - rewriteMonorepoProject(projectPath, packageManager, skipStagedMigration); + rewriteMonorepoProject( + projectPath, + packageManager, + skipStagedMigration, + undefined, + undefined, + catalogDependencyResolver, + ); } const RULES_YAML_PATH = path.join(rulesDir, 'vite-tools.yml'); @@ -1435,10 +1671,13 @@ export function rewritePackageJson( 'lint-staged'?: Record; devDependencies?: Record; dependencies?: Record; + peerDependencies?: Record; + optionalDependencies?: Record; }, packageManager: PackageManager, isMonorepo?: boolean, skipStagedMigration?: boolean, + catalogDependencyResolver?: CatalogDependencyResolver, ): Record | null { if (pkg.scripts) { const updated = rewriteScripts( @@ -1458,38 +1697,51 @@ export function rewritePackageJson( const updated = rewriteScripts(JSON.stringify(config), readRulesYaml()); extractedStagedConfig = updated ? JSON.parse(updated) : config; } - const supportCatalog = isMonorepo && packageManager !== PackageManager.npm; + const supportCatalog = !!isMonorepo && packageManager !== PackageManager.npm; let needVitePlus = false; + const dependencyGroups: { + dependencyField: PackageJsonDependencyField; + dependencies: Record | undefined; + }[] = [ + { dependencyField: 'devDependencies', dependencies: pkg.devDependencies }, + { dependencyField: 'dependencies', dependencies: pkg.dependencies }, + { dependencyField: 'peerDependencies', dependencies: pkg.peerDependencies }, + { dependencyField: 'optionalDependencies', dependencies: pkg.optionalDependencies }, + ]; for (const [key, version] of Object.entries(VITE_PLUS_OVERRIDE_PACKAGES)) { - const value = supportCatalog && !version.startsWith('file:') ? 'catalog:' : version; - if (pkg.devDependencies?.[key]) { - pkg.devDependencies[key] = value; - needVitePlus = true; - } - if (pkg.dependencies?.[key]) { - pkg.dependencies[key] = value; - needVitePlus = true; + for (const { dependencyField, dependencies } of dependencyGroups) { + if (dependencies?.[key]) { + dependencies[key] = getCatalogDependencySpec(dependencies[key], version, supportCatalog, { + dependencyField, + dependencyName: key, + packageManager, + catalogDependencyResolver, + }); + needVitePlus = true; + } } } // remove packages that are replaced with vite-plus for (const name of REMOVE_PACKAGES) { - const wasInDevDeps = !!pkg.devDependencies?.[name]; - const wasInDeps = !!pkg.dependencies?.[name]; - if (wasInDevDeps) { - delete pkg.devDependencies![name]; - needVitePlus = true; + let wasRemoved = false; + for (const { dependencies } of dependencyGroups) { + if (dependencies?.[name]) { + delete dependencies[name]; + wasRemoved = true; + } } - if (wasInDeps) { - delete pkg.dependencies![name]; + if (wasRemoved) { needVitePlus = true; } // e.g., removing @vitest/browser-playwright should keep `playwright` in devDeps const peerDep = BROWSER_PROVIDER_PEER_DEPS[name]; if ( - (wasInDevDeps || wasInDeps) && + wasRemoved && peerDep && !pkg.devDependencies?.[peerDep] && - !pkg.dependencies?.[peerDep] + !pkg.dependencies?.[peerDep] && + !pkg.peerDependencies?.[peerDep] && + !pkg.optionalDependencies?.[peerDep] ) { pkg.devDependencies ??= {}; pkg.devDependencies[peerDep] = '*'; @@ -1507,10 +1759,17 @@ export function rewritePackageJson( // on vitest (e.g., vitest-browser-svelte). Without this, pnpm resolves the real // vitest for peer deps instead of @voidzero-dev/vite-plus-test, causing // third-party type augmentations to target the wrong module. - const allDeps = { ...pkg.dependencies, ...pkg.devDependencies }; - if (!allDeps.vitest && Object.keys(allDeps).some((name) => name.includes('vitest'))) { + const installableDeps = { + ...pkg.dependencies, + ...pkg.devDependencies, + ...pkg.optionalDependencies, + }; + if ( + !installableDeps.vitest && + Object.keys(installableDeps).some((name) => name.includes('vitest')) + ) { const ver = VITE_PLUS_OVERRIDE_PACKAGES.vitest; - pkg.devDependencies.vitest = supportCatalog && !ver.startsWith('file:') ? 'catalog:' : ver; + pkg.devDependencies.vitest = getCatalogDependencySpec(undefined, ver, supportCatalog); } } return extractedStagedConfig; diff --git a/packages/cli/src/utils/workspace.ts b/packages/cli/src/utils/workspace.ts index 0b5510b36b..c1e996e3f4 100644 --- a/packages/cli/src/utils/workspace.ts +++ b/packages/cli/src/utils/workspace.ts @@ -18,7 +18,13 @@ import { getScopeFromPackageName } from './package.ts'; import { editYamlFile, readYamlFile } from './yaml.ts'; // npm/yarn use an array; Bun catalogs and Yarn classic nohoist use an object with `packages`. -export type NpmWorkspaces = string[] | { packages?: string[]; catalog?: Record }; +export type NpmWorkspaces = + | string[] + | { + packages?: string[]; + catalog?: Record; + catalogs?: Record>; + }; export function findPackageJsonFilesFromPatterns(patterns: string[], cwd: string): string[] { if (patterns.length === 0) {