From c35ba024ed10421ec788f5252535e6f2049a7133 Mon Sep 17 00:00:00 2001 From: Nicolas Charpentier Date: Mon, 27 Jul 2026 15:13:54 -0400 Subject: [PATCH] fix(migrate): tolerate nested npm overrides during bootstrap detection --- .../src/migration/__tests__/migrator.spec.ts | 34 +++++++++++++++++++ .../migration/migrator/vite-plus-bootstrap.ts | 5 ++- 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/migration/__tests__/migrator.spec.ts b/packages/cli/src/migration/__tests__/migrator.spec.ts index bcefe8a9f5..2bbc39d2bd 100644 --- a/packages/cli/src/migration/__tests__/migrator.spec.ts +++ b/packages/cli/src/migration/__tests__/migrator.spec.ts @@ -1961,6 +1961,40 @@ describe('ensureVitePlusBootstrap', () => { expect(detectVitePlusBootstrapPending(tmpDir, PackageManager.npm)).toBe(false); }); + it('tolerates nested npm override objects under managed keys', () => { + fs.writeFileSync( + path.join(tmpDir, 'package.json'), + JSON.stringify({ + name: 'test', + devDependencies: { 'vite-plus': 'latest' }, + // Nested npm override objects: user overrides scoped UNDER the managed + // keys. Neither aliases the package itself. + overrides: { + vite: { rollup: '^4.0.0' }, + vitest: { '@vitest/expect': '4.0.0' }, + }, + devEngines: { + packageManager: { name: 'npm', version: '10.33.0', onFail: 'download' }, + }, + }), + ); + + expect(detectVitePlusBootstrapPending(tmpDir, PackageManager.npm)).toBe(true); + const result = ensureVitePlusBootstrap(makeWorkspaceInfo(tmpDir, PackageManager.npm)); + + expect(result.changed).toBe(true); + expect(detectVitePlusBootstrapPending(tmpDir, PackageManager.npm)).toBe(false); + const pkg = readJson(path.join(tmpDir, 'package.json')) as { + overrides: Record; + }; + // The nested object under `vite` does not satisfy the managed alias, so it + // is replaced with the managed override. + expect(pkg.overrides.vite).toContain('@voidzero-dev/vite-plus-core'); + // The project does NOT use vitest directly, so `vitest` is not managed; the + // user override scoped under it is left intact rather than removed. + expect(pkg.overrides.vitest).toEqual({ '@vitest/expect': '4.0.0' }); + }); + it('replaces protocol-pinned migration targets in force-override mode', () => { const savedForceMigrate = process.env.VP_FORCE_MIGRATE; process.env.VP_FORCE_MIGRATE = '1'; diff --git a/packages/cli/src/migration/migrator/vite-plus-bootstrap.ts b/packages/cli/src/migration/migrator/vite-plus-bootstrap.ts index ffdf0d776e..65ef65087a 100644 --- a/packages/cli/src/migration/migrator/vite-plus-bootstrap.ts +++ b/packages/cli/src/migration/migrator/vite-plus-bootstrap.ts @@ -124,7 +124,10 @@ function overrideSpecSatisfiesVitePlus( spec: string | undefined, catalogDependencyResolver?: CatalogDependencyResolver, ): boolean { - if (!spec) { + // npm/bun `overrides` may hold a nested object value — a user override scoped + // UNDER the dependency (e.g. `{"vite": {"rollup": "..."}}`) that does not + // alias the dependency itself, so it never satisfies the managed override. + if (typeof spec !== 'string' || !spec) { return false; } if (isSemanticVitePlusOverrideSpec(dependencyName, spec)) {