From a13b30b5bd8374a1ba012fcfef30d43b122a8df5 Mon Sep 17 00:00:00 2001 From: MK Date: Mon, 1 Jun 2026 09:18:27 +0800 Subject: [PATCH 1/2] fix(create): keep aliased vite in pnpm monorepo website so override stays effective `vp create vite:monorepo` stripped vite/vitest from apps/website for every package manager. For pnpm this removed the only direct `vite` dependency, so the pnpm-workspace.yaml `overrides.vite: catalog:` entry had no consumer and `vp why vite` resolved to upstream vite instead of @voidzero-dev/vite-plus-core, making the override look ineffective. Skip the strip for pnpm so the workspace override has a direct consumer. npm, yarn, and bun are unaffected: their root overrides/resolutions redirect the transitive/peer vite to @voidzero-dev/vite-plus-core regardless of a direct dep (verified), so they keep dropping the dead-weight keys. --- .github/workflows/test-vp-create.yml | 24 ++++-- .../new-vite-monorepo/snap.txt | 3 +- .../new-vite-monorepo/steps.json | 2 +- .../cli/src/create/__tests__/monorepo.spec.ts | 77 +++++++++++++++++++ packages/cli/src/create/templates/monorepo.ts | 56 ++++++++++---- 5 files changed, 139 insertions(+), 23 deletions(-) create mode 100644 packages/cli/src/create/__tests__/monorepo.spec.ts diff --git a/.github/workflows/test-vp-create.yml b/.github/workflows/test-vp-create.yml index ff2763d014..9c65924db9 100644 --- a/.github/workflows/test-vp-create.yml +++ b/.github/workflows/test-vp-create.yml @@ -288,7 +288,13 @@ jobs: # Issue 2: apps/website is scaffolded by create-vite which ships # `vite` (and sometimes `vitest`) in devDependencies. After # migration the scripts are rewritten to `vp ...` and `vite-plus` - # brings the runtime in transitively, so those keys must be gone. + # brings the runtime in transitively. For npm/yarn/bun those keys + # are dropped (the root overrides/resolutions redirect the + # transitive/peer vite to @voidzero-dev/vite-plus-core regardless). + # For pnpm the aliased `vite` is kept on purpose: pnpm only surfaces + # the pnpm-workspace.yaml `overrides.vite: catalog:` entry through a + # package that directly depends on `vite`, so dropping it would make + # `vp why vite` report upstream vite and the override look ineffective. node -e " const fs = require('fs'); const pm = process.env.PACKAGE_MANAGER; @@ -303,13 +309,21 @@ jobs: const appDev = app.devDependencies || {}; const utilsDev = utils.devDependencies || {}; - for (const name of ['vite', 'vitest']) { - if (appDev[name]) { - console.error('✗ apps/website devDependencies still has ' + name + ': ' + appDev[name]); + if (pm === 'pnpm') { + if (!appDev['vite']) { + console.error('✗ pnpm apps/website should keep aliased vite so the workspace override stays effective'); process.exit(1); } + console.log('✓ pnpm apps/website keeps aliased vite'); + } else { + for (const name of ['vite', 'vitest']) { + if (appDev[name]) { + console.error('✗ apps/website devDependencies still has ' + name + ': ' + appDev[name]); + process.exit(1); + } + } + console.log('✓ apps/website devDependencies has no vite/vitest'); } - console.log('✓ apps/website devDependencies has no vite/vitest'); if (!appDev['vite-plus']) { console.error('✗ apps/website missing vite-plus devDependency'); diff --git a/packages/cli/snap-tests-global/new-vite-monorepo/snap.txt b/packages/cli/snap-tests-global/new-vite-monorepo/snap.txt index 3837a993fa..42c0c5548b 100644 --- a/packages/cli/snap-tests-global/new-vite-monorepo/snap.txt +++ b/packages/cli/snap-tests-global/new-vite-monorepo/snap.txt @@ -96,7 +96,7 @@ Git initialized > ls vite-plus-monorepo/apps # check apps directory created website -> cat vite-plus-monorepo/apps/website/package.json # check website strips aliased vite/vitest +> cat vite-plus-monorepo/apps/website/package.json # check website keeps aliased vite for pnpm (workspace override stays effective) { "name": "website", "version": "0.0.0", @@ -109,6 +109,7 @@ website }, "devDependencies": { "typescript": "~6.0.2", + "vite": "catalog:", "vite-plus": "catalog:" } } diff --git a/packages/cli/snap-tests-global/new-vite-monorepo/steps.json b/packages/cli/snap-tests-global/new-vite-monorepo/steps.json index 959500cbe9..f8a47c057a 100644 --- a/packages/cli/snap-tests-global/new-vite-monorepo/steps.json +++ b/packages/cli/snap-tests-global/new-vite-monorepo/steps.json @@ -17,7 +17,7 @@ "node -e \"const { spawnSync } = require('node:child_process'); const file = '.vscode/settings.json'; const result = spawnSync('git', ['-C', 'vite-plus-monorepo', 'check-ignore', '--no-index', file], { stdio: 'ignore' }); const status = result.status; if (status === 1) { console.log(file + ' trackable'); } else { console.log(status === 0 ? 'ERROR: ' + file + ' ignored' : 'ERROR: git check-ignore failed with status ' + status); process.exit(status || 1); }\" # check VS Code settings are trackable", "node -e \"const { spawnSync } = require('node:child_process'); const file = '.vscode/extensions.json'; const result = spawnSync('git', ['-C', 'vite-plus-monorepo', 'check-ignore', '--no-index', file], { stdio: 'ignore' }); const status = result.status; if (status === 1) { console.log(file + ' trackable'); } else { console.log(status === 0 ? 'ERROR: ' + file + ' ignored' : 'ERROR: git check-ignore failed with status ' + status); process.exit(status || 1); }\" # check VS Code extensions are trackable", "ls vite-plus-monorepo/apps # check apps directory created", - "cat vite-plus-monorepo/apps/website/package.json # check website strips aliased vite/vitest", + "cat vite-plus-monorepo/apps/website/package.json # check website keeps aliased vite for pnpm (workspace override stays effective)", "cat vite-plus-monorepo/packages/utils/package.json # check utils normalizes vite-plus to catalog:", { "command": "cd vite-plus-monorepo && vp create --no-interactive vite:application # create application in non-interactive mode", diff --git a/packages/cli/src/create/__tests__/monorepo.spec.ts b/packages/cli/src/create/__tests__/monorepo.spec.ts new file mode 100644 index 0000000000..ceb747d649 --- /dev/null +++ b/packages/cli/src/create/__tests__/monorepo.spec.ts @@ -0,0 +1,77 @@ +import fs from 'node:fs'; +import os from 'node:os'; +import path from 'node:path'; + +import { afterEach, beforeEach, describe, expect, it } from 'vitest'; + +import { PackageManager } from '../../types/index.js'; +import { dropAliasedRuntimeDevDeps } from '../templates/monorepo.js'; + +describe('dropAliasedRuntimeDevDeps', () => { + let tmpDir: string; + + beforeEach(() => { + tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'vp-monorepo-strip-')); + }); + + afterEach(() => { + fs.rmSync(tmpDir, { recursive: true, force: true }); + }); + + function writeWebsitePackageJson(devDependencies: Record): void { + fs.writeFileSync( + path.join(tmpDir, 'package.json'), + JSON.stringify({ name: 'website', private: true, devDependencies }, null, 2), + ); + } + + function readDevDependencies(): Record { + const pkg = JSON.parse(fs.readFileSync(path.join(tmpDir, 'package.json'), 'utf8')) as { + devDependencies?: Record; + }; + return pkg.devDependencies ?? {}; + } + + // Regression test for "vp why vite reports the override as ineffective" in a + // freshly created pnpm monorepo: pnpm only surfaces the pnpm-workspace.yaml + // `overrides` through a package that directly depends on `vite`/`vitest`, so + // the aliased (catalog:) devDeps must survive for the override to be + // observable. Dropping them leaves `vite` resolving to upstream vite instead + // of @voidzero-dev/vite-plus-core. + it('keeps aliased vite/vitest for pnpm so the workspace override stays effective', () => { + writeWebsitePackageJson({ + vite: 'catalog:', + vitest: 'catalog:', + 'vite-plus': 'catalog:', + typescript: '~6.0.2', + }); + + dropAliasedRuntimeDevDeps(tmpDir, PackageManager.pnpm); + + const devDependencies = readDevDependencies(); + expect(devDependencies.vite).toBe('catalog:'); + expect(devDependencies.vitest).toBe('catalog:'); + expect(devDependencies['vite-plus']).toBe('catalog:'); + }); + + // npm/yarn/bun redirect the transitive/peer `vite` to + // @voidzero-dev/vite-plus-core via root overrides/resolutions regardless of a + // direct dependency, so the aliased keys are dead weight and stay dropped. + for (const packageManager of [PackageManager.npm, PackageManager.yarn, PackageManager.bun]) { + it(`drops aliased vite/vitest for ${packageManager}`, () => { + writeWebsitePackageJson({ + vite: 'npm:@voidzero-dev/vite-plus-core@latest', + vitest: 'npm:@voidzero-dev/vite-plus-test@latest', + 'vite-plus': 'latest', + typescript: '~6.0.2', + }); + + dropAliasedRuntimeDevDeps(tmpDir, packageManager); + + const devDependencies = readDevDependencies(); + expect(devDependencies.vite).toBeUndefined(); + expect(devDependencies.vitest).toBeUndefined(); + expect(devDependencies['vite-plus']).toBe('latest'); + }); + } +}); diff --git a/packages/cli/src/create/templates/monorepo.ts b/packages/cli/src/create/templates/monorepo.ts index 141f35e743..3c9856ed83 100644 --- a/packages/cli/src/create/templates/monorepo.ts +++ b/packages/cli/src/create/templates/monorepo.ts @@ -123,22 +123,9 @@ export async function executeMonorepoTemplate( undefined, options?.silent ?? false, ); - // Drop the aliased vite/vitest devDeps left by the migrator: the - // create-vite scripts were rewritten to `vp ...`, and the fresh - // template has no user `import 'vite'`, so vite-plus brings them in. - editJsonFile<{ devDependencies?: Record }>( - path.join(appProjectPath, 'package.json'), - (pkg) => { - let changed = false; - for (const name of ['vite', 'vitest']) { - if (pkg.devDependencies?.[name]) { - delete pkg.devDependencies[name]; - changed = true; - } - } - return changed ? pkg : undefined; - }, - ); + // Drop the migrator's aliased vite/vitest devDeps for npm/yarn/bun (pnpm + // keeps them so its workspace override stays effective; see the helper). + dropAliasedRuntimeDevDeps(appProjectPath, workspaceInfo.packageManager); // Automatically create a default library in packages/utils if (!options?.silent) { @@ -174,6 +161,43 @@ export async function executeMonorepoTemplate( return { exitCode: 0, projectDir: templateInfo.targetDir }; } +/** + * Drop the aliased `vite` / `vitest` devDeps that `create-vite` leaves on a + * scaffolded sub-package. After migration its scripts already use `vp ...` and + * nothing imports `'vite'` directly, so `vite-plus` provides them transitively. + * + * pnpm is the exception and keeps them: pnpm only surfaces the + * pnpm-workspace.yaml `overrides.vite: catalog:` entry through a package that + * directly depends on `vite`, so keeping the aliased devDep lets `vp why vite` + * reflect the override (resolving to @voidzero-dev/vite-plus-core). npm, yarn, + * and bun redirect the transitive/peer vite via their root + * overrides/resolutions regardless of a direct dep, so the aliased keys are + * dead weight and are dropped. + */ +export function dropAliasedRuntimeDevDeps( + appProjectPath: string, + packageManager: PackageManager, +): void { + // pnpm keeps the aliased vite/vitest so the pnpm-workspace.yaml override has + // a direct consumer to redirect; see the doc comment above. + if (packageManager === PackageManager.pnpm) { + return; + } + editJsonFile<{ devDependencies?: Record }>( + path.join(appProjectPath, 'package.json'), + (pkg) => { + let changed = false; + for (const name of ['vite', 'vitest']) { + if (pkg.devDependencies?.[name]) { + delete pkg.devDependencies[name]; + changed = true; + } + } + return changed ? pkg : undefined; + }, + ); +} + function getScopeFromPackageName(packageName: string) { if (packageName.startsWith('@')) { return packageName.split('/')[0]; From 16f7d45712660aa35f8ceffe712be2de2d8ae054 Mon Sep 17 00:00:00 2001 From: MK Date: Mon, 1 Jun 2026 09:59:25 +0800 Subject: [PATCH 2/2] docs(migrate): clarify pnpm keeps aliased vite/vitest in migration prompt The migration prompt told agents to "remove old vite and vitest dependencies" after rewriting imports. On pnpm that direct (aliased) entry is what gives the pnpm-workspace.yaml `overrides.vite: catalog:` a consumer to redirect, so removing it makes `vp why vite` report upstream vite and the override look ineffective. Reword the prompt (in both docs/guide/migrate.md and the CLI-printed copy) to keep the aliased entries on pnpm; other package managers can still drop them once import rewrites are confirmed. Regenerate the two migration snap tests that capture the printed prompt. --- docs/guide/migrate.md | 2 +- packages/cli/snap-tests-global/migration-check/snap.txt | 5 +++-- .../snap-tests-global/migration-lintstagedrc-json/snap.txt | 5 +++-- packages/cli/src/migration/bin.ts | 5 +++-- 4 files changed, 10 insertions(+), 7 deletions(-) diff --git a/docs/guide/migrate.md b/docs/guide/migrate.md index d84b010cce..5ce59a447d 100644 --- a/docs/guide/migrate.md +++ b/docs/guide/migrate.md @@ -76,7 +76,7 @@ After the migration: - Confirm `vite` imports were rewritten to `vite-plus` where needed - Confirm `vitest` imports were rewritten to `vite-plus/test` where needed -- Remove old `vite` and `vitest` dependencies only after those rewrites are confirmed +- On pnpm, keep the `vite` / `vitest` entries that `vp migrate` aliased to the Vite+ packages so the workspace override stays effective; with other package managers you can remove them once those rewrites are confirmed - Move remaining tool-specific config into the appropriate blocks in `vite.config.ts` Command mapping to keep in mind: diff --git a/packages/cli/snap-tests-global/migration-check/snap.txt b/packages/cli/snap-tests-global/migration-check/snap.txt index 8c5e069cc4..54acfa0aaf 100644 --- a/packages/cli/snap-tests-global/migration-check/snap.txt +++ b/packages/cli/snap-tests-global/migration-check/snap.txt @@ -39,8 +39,9 @@ Migration Prompt: After the migration: - Confirm `vite` imports were rewritten to `vite-plus` where needed - Confirm `vitest` imports were rewritten to `vite-plus/test` where needed - - Remove old `vite` and `vitest` dependencies only after those rewrites - are confirmed + - On pnpm, keep the `vite` / `vitest` entries that `vp migrate` aliased to + the Vite+ packages so the workspace override stays effective; with other + package managers you can remove them once those rewrites are confirmed - Move remaining tool-specific config into the appropriate blocks in `vite.config.ts` diff --git a/packages/cli/snap-tests-global/migration-lintstagedrc-json/snap.txt b/packages/cli/snap-tests-global/migration-lintstagedrc-json/snap.txt index e2ad6ee309..c3d4d437b4 100644 --- a/packages/cli/snap-tests-global/migration-lintstagedrc-json/snap.txt +++ b/packages/cli/snap-tests-global/migration-lintstagedrc-json/snap.txt @@ -39,8 +39,9 @@ Migration Prompt: After the migration: - Confirm `vite` imports were rewritten to `vite-plus` where needed - Confirm `vitest` imports were rewritten to `vite-plus/test` where needed - - Remove old `vite` and `vitest` dependencies only after those rewrites - are confirmed + - On pnpm, keep the `vite` / `vitest` entries that `vp migrate` aliased to + the Vite+ packages so the workspace override stays effective; with other + package managers you can remove them once those rewrites are confirmed - Move remaining tool-specific config into the appropriate blocks in `vite.config.ts` diff --git a/packages/cli/src/migration/bin.ts b/packages/cli/src/migration/bin.ts index 06d9ce7f08..b5394ee597 100644 --- a/packages/cli/src/migration/bin.ts +++ b/packages/cli/src/migration/bin.ts @@ -252,8 +252,9 @@ const helpMessage = renderCliDoc({ ' After the migration:', ' - Confirm `vite` imports were rewritten to `vite-plus` where needed', ' - Confirm `vitest` imports were rewritten to `vite-plus/test` where needed', - ' - Remove old `vite` and `vitest` dependencies only after those rewrites', - ' are confirmed', + ' - On pnpm, keep the `vite` / `vitest` entries that `vp migrate` aliased to', + ' the Vite+ packages so the workspace override stays effective; with other', + ' package managers you can remove them once those rewrites are confirmed', ' - Move remaining tool-specific config into the appropriate blocks in', ' `vite.config.ts`', '',