Skip to content

Commit 452cbf6

Browse files
committed
fix(migrate): rewrite the catalog below pnpm 10.6.2 and keep catalog: overrides
Catalogs are supported from pnpm 9.5.0, independent of the 10.6.2 boundary where pnpm settings move to pnpm-workspace.yaml. Below 10.6.2 the settings stay in package.json, but the pnpm-workspace.yaml catalog rewrite was gated on the 10.6.2 boundary and skipped: the catalog kept its stale vite-plus-test wrappers, and package.json pnpm.overrides referencing catalog: were inlined to concrete versions (reconciled against the not-yet-rewritten catalog). Rewrite the catalog early, before the package.json overrides are reconciled, for pnpm 9.5-10.6.1, so a catalog: override resolves to the fresh value and stays catalog:, and the catalog itself is rewritten off the wrappers. Adds a varlet-shaped regression test and the migration-upgrade-pnpm9-overrides global snap, and syncs the migrate-rules and RFC docs. Claude-Session: https://claude.ai/code/session_01DQhS6o1fyQd1yjiee6W8jR
1 parent 6dc08ff commit 452cbf6

8 files changed

Lines changed: 201 additions & 4 deletions

File tree

docs/guide/migrate-rules.md

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -230,6 +230,11 @@ format Vite+ reads:
230230
`package.json#pnpm`. General workspace-setting support started in pnpm 10.5.0,
231231
but overrides required 10.5.1 and `peerDependencyRules` required 10.6.2. pnpm
232232
11 no longer reads the legacy package.json settings.
233+
- Catalogs are a separate feature, supported from pnpm 9.5.0, independent of the
234+
workspace-settings boundary. So below 10.6.2, where overrides stay in
235+
`package.json#pnpm`, migration still rewrites the workspace catalog off stale
236+
wrapper aliases and keeps `catalog:` overrides as references rather than
237+
inlining them to concrete versions.
233238
- Migration keeps dependency references, default and named catalogs, overrides,
234239
and `peerDependencyRules` consistent.
235240
- pnpm accepts the logical default catalog as either top-level `catalog` or
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
{
2+
"name": "migration-upgrade-pnpm9-overrides",
3+
"devDependencies": {
4+
"@vitest/coverage-v8": "4.1.6",
5+
"vite": "npm:@voidzero-dev/vite-plus-core@^0.1.20",
6+
"vite-plus": "^0.1.20",
7+
"vitest": "npm:@voidzero-dev/vite-plus-test@^0.1.20"
8+
},
9+
"packageManager": "pnpm@9.15.9",
10+
"pnpm": {
11+
"overrides": {
12+
"vite": "catalog:",
13+
"vitest": "catalog:"
14+
}
15+
}
16+
}
Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
packages:
2+
- .
3+
4+
catalog:
5+
vite: npm:@voidzero-dev/vite-plus-core@^0.1.20
6+
vite-plus: ^0.1.20
7+
vitest: npm:@voidzero-dev/vite-plus-test@^0.1.20
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
> vp migrate --no-interactive # pnpm 9.5-10.6.1: settings stay in package.json, catalog is still rewritten off the wrappers
2+
◇ Updated . to Vite+ <semver>
3+
• Node <semver> pnpm <semver>
4+
• Dependencies:
5+
vite-plus <semver> → <semver>
6+
vite → <semver>
7+
vitest <semver> → <semver>
8+
@vitest/coverage-v8 <semver> → <semver>
9+
• Package manager settings configured
10+
11+
> cat package.json # pnpm.overrides stay catalog: (not inlined to a version)
12+
{
13+
"name": "migration-upgrade-pnpm9-overrides",
14+
"devDependencies": {
15+
"@vitest/coverage-v8": "catalog:",
16+
"vite": "catalog:",
17+
"vite-plus": "catalog:",
18+
"vitest": "catalog:"
19+
},
20+
"packageManager": "pnpm@<semver>",
21+
"pnpm": {
22+
"overrides": {
23+
"vite": "catalog:",
24+
"vitest": "catalog:"
25+
},
26+
"peerDependencyRules": {
27+
"allowAny": [
28+
"vite",
29+
"vitest"
30+
],
31+
"allowedVersions": {
32+
"vite": "*",
33+
"vitest": "*"
34+
}
35+
}
36+
}
37+
}
38+
39+
> cat pnpm-workspace.yaml # catalog rewritten off the vite-plus-test wrapper; overrides remain in package.json (< 10.6.2)
40+
packages:
41+
- .
42+
43+
catalog:
44+
vite: npm:@voidzero-dev/vite-plus-core@<semver>
45+
vite-plus: <semver>
46+
vitest: <semver>
47+
'@vitest/coverage-v8': <semver>
Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
{
2+
"env": {
3+
"VP_OVERRIDE_PACKAGES": "{\"vite\":\"npm:@voidzero-dev/vite-plus-core@0.0.0-commit.0c515e3fbf5c140db35280d700df0bd600838617\",\"vitest\":\"4.1.9\"}",
4+
"VP_VERSION": "0.0.0-commit.0c515e3fbf5c140db35280d700df0bd600838617"
5+
},
6+
"commands": [
7+
"vp migrate --no-interactive # pnpm 9.5-10.6.1: settings stay in package.json, catalog is still rewritten off the wrappers",
8+
"cat package.json # pnpm.overrides stay catalog: (not inlined to a version)",
9+
"cat pnpm-workspace.yaml # catalog rewritten off the vite-plus-test wrapper; overrides remain in package.json (< 10.6.2)"
10+
]
11+
}

packages/cli/src/migration/__tests__/migrator.spec.ts

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2579,6 +2579,63 @@ describe('ensureVitePlusBootstrap', () => {
25792579
expect(pkg.devDependencies['@types/node']).toBe('catalog:');
25802580
});
25812581

2582+
it('rewrites the catalog and keeps pnpm.overrides catalog: below pnpm 10.6.2 (varlet #10)', () => {
2583+
// pnpm 9.5-10.6.1: catalogs work (>= 9.5.0) but settings stay in package.json
2584+
// (< 10.6.2). The catalog must still be rewritten off the stale vite-plus-test
2585+
// wrapper, and package.json pnpm.overrides that reference it stay `catalog:`.
2586+
const appDir = path.join(tmpDir, 'packages/app');
2587+
fs.mkdirSync(appDir, { recursive: true });
2588+
fs.writeFileSync(
2589+
path.join(tmpDir, 'package.json'),
2590+
JSON.stringify({
2591+
name: 'varlet',
2592+
private: true,
2593+
devDependencies: { 'vite-plus': 'catalog:', vitest: 'catalog:' },
2594+
pnpm: { overrides: { vite: 'catalog:', vitest: 'catalog:' } },
2595+
devEngines: { packageManager: { name: 'pnpm', version: '9.15.9', onFail: 'download' } },
2596+
}),
2597+
);
2598+
fs.writeFileSync(
2599+
path.join(appDir, 'package.json'),
2600+
JSON.stringify({ name: 'app', devDependencies: { vitest: 'catalog:' } }),
2601+
);
2602+
fs.writeFileSync(
2603+
path.join(tmpDir, 'pnpm-workspace.yaml'),
2604+
[
2605+
'packages:',
2606+
' - packages/*',
2607+
'catalog:',
2608+
" vite: 'npm:@voidzero-dev/vite-plus-core@0.1.18'",
2609+
" vitest: 'npm:@voidzero-dev/vite-plus-test@0.1.18'",
2610+
' vite-plus: 0.1.18',
2611+
'',
2612+
].join('\n'),
2613+
);
2614+
const workspaceInfo = {
2615+
...makeWorkspaceInfo(tmpDir, PackageManager.pnpm),
2616+
isMonorepo: true,
2617+
workspacePatterns: ['packages/*'],
2618+
packages: [{ name: 'app', path: 'packages/app' }],
2619+
};
2620+
workspaceInfo.downloadPackageManager = {
2621+
...workspaceInfo.downloadPackageManager,
2622+
version: '9.15.9',
2623+
};
2624+
ensureVitePlusBootstrap(workspaceInfo);
2625+
2626+
const rootPkg = readJson(path.join(tmpDir, 'package.json')) as {
2627+
pnpm?: { overrides?: Record<string, string> };
2628+
};
2629+
const workspace = readYamlObject(path.join(tmpDir, 'pnpm-workspace.yaml')) as {
2630+
catalog?: Record<string, string>;
2631+
};
2632+
// The vite override stays `catalog:` (settings remain in package.json below 10.6.2).
2633+
expect(rootPkg.pnpm?.overrides?.vite).toBe('catalog:');
2634+
// The catalog entries are rewritten off the stale 0.1.18 wrappers.
2635+
expect(workspace.catalog?.vite).toBe('npm:@voidzero-dev/vite-plus-core@latest');
2636+
expect(workspace.catalog?.['vite-plus']).toBe('latest');
2637+
});
2638+
25822639
it('does not align deprecated @vitest/coverage-c8 to a nonexistent Vitest 4 version', () => {
25832640
fs.writeFileSync(
25842641
path.join(tmpDir, 'package.json'),

packages/cli/src/migration/migrator/vite-plus-bootstrap.ts

Lines changed: 50 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -952,6 +952,35 @@ export function ensureVitePlusBootstrap(
952952
);
953953
let movedPnpmSettings: Record<string, unknown> | undefined;
954954

955+
// Below pnpm 10.6.2 the pnpm settings (overrides) stay in package.json, so the
956+
// `usePnpmWorkspaceYaml` block below skips the pnpm-workspace.yaml rewrite. But
957+
// catalogs work from 9.5.0, so rewrite the catalog NOW — before the package.json
958+
// overrides are reconciled — so a `catalog:` override resolves to the fresh
959+
// toolchain value (not the stale vite-plus-test wrapper) and stays `catalog:`,
960+
// and the catalog itself is rewritten off the wrappers. Catalog only
961+
// (`writeWorkspaceSettings: false`); never creates a missing pnpm-workspace.yaml.
962+
const pnpmWorkspaceCatalogBefore =
963+
workspaceInfo.packageManager === PackageManager.pnpm &&
964+
fs.existsSync(path.join(projectPath, 'pnpm-workspace.yaml'))
965+
? fs.readFileSync(path.join(projectPath, 'pnpm-workspace.yaml'), 'utf-8')
966+
: undefined;
967+
if (
968+
workspaceInfo.packageManager === PackageManager.pnpm &&
969+
!usePnpmWorkspaceYaml &&
970+
supportCatalog &&
971+
pnpmWorkspaceCatalogBefore !== undefined
972+
) {
973+
rewritePnpmWorkspaceYaml(
974+
projectPath,
975+
pnpmMajorVersion,
976+
shouldAllowBrowserBuilds,
977+
usesVitest,
978+
vitestEcosystemPackages,
979+
false,
980+
providerCatalogAdditions,
981+
);
982+
}
983+
955984
editJsonFile<
956985
BootstrapPackageJson & {
957986
workspaces?: NpmWorkspaces;
@@ -998,7 +1027,14 @@ export function ensureVitePlusBootstrap(
9981027
}
9991028
} else if (workspaceInfo.packageManager === PackageManager.pnpm && !usePnpmWorkspaceYaml) {
10001029
pkg.pnpm ??= {};
1001-
const ensured = ensureOverrideEntries(pkg.pnpm.overrides, usesVitest);
1030+
// Below pnpm 10.6.2 the settings stay here, but catalogs (>= 9.5.0) still
1031+
// work: keep `catalog:` overrides referencing the workspace catalog
1032+
// (rewritten below) instead of inlining them, mirroring the bun branch.
1033+
const ensured = ensureOverrideEntries(
1034+
pkg.pnpm.overrides,
1035+
usesVitest,
1036+
supportCatalog ? readPnpmWorkspaceCatalogDependencyResolver(projectPath) : undefined,
1037+
);
10021038
if (ensured.changed) {
10031039
pkg.pnpm.overrides = ensured.overrides;
10041040
packageJsonChanged = true;
@@ -1123,9 +1159,19 @@ export function ensureVitePlusBootstrap(
11231159
? fs.readFileSync(pnpmWorkspaceYamlPath, 'utf-8')
11241160
: undefined;
11251161
result.packageManagerConfig = before !== after;
1126-
} else if (ensurePnpmWorkspaceExoticSubdepsSetting(projectPath)) {
1127-
ensurePnpmWorkspacePackages(projectPath, workspaceInfo.workspacePatterns);
1128-
result.packageManagerConfig = true;
1162+
} else {
1163+
// The catalog was already rewritten before the reconcile (above). Only the
1164+
// exotic-subdeps setting and packages field remain; track the net change
1165+
// against the pre-rewrite snapshot.
1166+
const exoticChanged = ensurePnpmWorkspaceExoticSubdepsSetting(projectPath);
1167+
const pnpmWorkspaceYamlPath = path.join(projectPath, 'pnpm-workspace.yaml');
1168+
if (fs.existsSync(pnpmWorkspaceYamlPath)) {
1169+
ensurePnpmWorkspacePackages(projectPath, workspaceInfo.workspacePatterns);
1170+
}
1171+
const after = fs.existsSync(pnpmWorkspaceYamlPath)
1172+
? fs.readFileSync(pnpmWorkspaceYamlPath, 'utf-8')
1173+
: undefined;
1174+
result.packageManagerConfig = exoticChanged || pnpmWorkspaceCatalogBefore !== after;
11291175
}
11301176
} else if (workspaceInfo.packageManager === PackageManager.yarn) {
11311177
const yarnrcYmlPath = path.join(projectPath, '.yarnrc.yml');

rfcs/migrate-existing-projects.md

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,6 +48,13 @@ When PnP is active, interactive migration prints the incompatibility and asks wh
4848

4949
Force-override/CI mode (`VP_OVERRIDE_PACKAGES`) is respected: when `vitest` is not a managed key there, the project's own `vitest` is never stripped and its `@vitest/*` ecosystem dependencies are not realigned. Object-valued nested npm/Bun overrides are user-owned scopes rather than managed version pins and are preserved.
5050

51+
Catalogs are a separate pnpm feature from workspace settings, supported since
52+
pnpm 9.5.0, so they are independent of the 10.6.2 settings boundary. Below 10.6.2,
53+
where overrides stay in `package.json#pnpm`, migration still rewrites the
54+
workspace catalog off stale `@voidzero-dev/vite-plus-test` wrappers and keeps
55+
`package.json` `catalog:` overrides as references rather than inlining them to
56+
concrete versions.
57+
5158
Legacy browser-provider usage must be detected before source imports are
5259
rewritten. Projects that aliased `vitest` to the removed
5360
`@voidzero-dev/vite-plus-test` package can import Playwright or WebdriverIO from
@@ -160,6 +167,7 @@ Covered by unit tests in `migrator.spec.ts` (vitest removal, required-peer provi
160167
| Standalone Yarn writes catalog specs in one pass and is idempotent | `migration-standalone-yarn4-idempotent` |
161168
| pnpm preserves `catalogs.default` without adding top-level `catalog` | `migration-upgrade-pnpm-catalogs-default` |
162169
| pnpm reuses a named-only managed toolchain catalog during pkg.pr.new migration | `migration-upgrade-pnpm-named-catalog` |
170+
| pnpm below 10.6.2 keeps `package.json` `catalog:` overrides while rewriting the catalog | `migration-upgrade-pnpm9-overrides` |
163171
| Unmanaged exact-peer Vitest ecosystem versions remain aligned with user-owned Vitest | `migration-vitest-unmanaged-override` |
164172
| Nuxt packages preserve all upstream `vitest` imports without affecting sibling packages | `migration-upgrade-nuxt-test-utils`, `migration-upgrade-nuxt-test-utils-monorepo` |
165173

0 commit comments

Comments
 (0)