Skip to content

Commit 381b6e2

Browse files
Brooooooklynclaude
andcommitted
fix(migration): emit bunfig.toml with peer = false to unblock bun installs
vitest@4.1.5 declares a `vite^6/^7/^8` peer dep. When the user's project overrides `vite` to `@voidzero-dev/vite-plus-core` (version 0.0.0 in dev and 0.1.21 on npm — neither matches), bun aborts with: error: vite@^6.0.0 || ^7.0.0 || ^8.0.0 failed to resolve pnpm/yarn/npm tolerate this redirect; bun does not, and offers no `peerDependencyRules`-style escape hatch — only `[install] peer = false` in `bunfig.toml`. vite-plus already provides the vite surface the user needs, so disabling bun's auto-install of *missing* peers is safe here: the redirected vite is the only one that ever fails, and other vitest peers (jsdom, happy-dom, @vitest/*, etc.) are upstream-optional and either pulled in transitively or user-installed. `ensureBunfigPeerSuppression` writes/merges `bunfig.toml` whenever the migrator touches a bun project — both the monorepo path (via `rewriteBunCatalog`) and the standalone path (in `rewriteStandaloneProject`'s post-package.json branch). Honors any pre-existing `peer =` setting so a user who deliberately set `peer = true` keeps it. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 343d09a commit 381b6e2

3 files changed

Lines changed: 99 additions & 0 deletions

File tree

packages/cli/snap-tests-global/new-vite-monorepo-bun/snap.txt

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,7 @@
33
AGENTS.md
44
README.md
55
apps
6+
bunfig.toml
67
package.json
78
packages
89
tsconfig.json

packages/cli/src/migration/__tests__/bun-catalog-file-protocol.spec.ts

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,64 @@ describe('rewriteMonorepo bun catalog with file: protocol', () => {
123123
expect(pkg.devDependencies.vite).toBe('file:/tmp/tgz/voidzero-dev-vite-plus-core-0.0.0.tgz');
124124
});
125125

126+
it('writes bunfig.toml with `peer = false` so vitest peer-dep on vite does not break install', () => {
127+
// vitest@4.1.5 declares peer vite^6/^7/^8. With overrides.vite pointing at
128+
// file:vite-plus-core@0.0.0 (whose package.json version does not match),
129+
// bun aborts the install. pnpm/yarn/npm tolerate this; bun has no equivalent
130+
// to pnpm's peerDependencyRules and only respects the `[install] peer = false`
131+
// setting in bunfig.toml. The migrator must emit that file or every bun
132+
// user hits `error: vite@^6.0.0 || ^7.0.0 || ^8.0.0 failed to resolve`.
133+
fs.writeFileSync(
134+
path.join(tmpDir, 'package.json'),
135+
JSON.stringify({
136+
name: 'bun-monorepo',
137+
workspaces: ['packages/*'],
138+
packageManager: 'bun@1.3.11',
139+
}),
140+
);
141+
rewriteMonorepo(makeWorkspaceInfo(tmpDir, PackageManager.bun), true);
142+
143+
const bunfigPath = path.join(tmpDir, 'bunfig.toml');
144+
expect(fs.existsSync(bunfigPath)).toBe(true);
145+
expect(fs.readFileSync(bunfigPath, 'utf8')).toMatch(/^\[install\][\s\S]*peer\s*=\s*false/m);
146+
});
147+
148+
it('preserves an existing bunfig.toml `peer` setting (does not overwrite user intent)', () => {
149+
fs.writeFileSync(
150+
path.join(tmpDir, 'package.json'),
151+
JSON.stringify({
152+
name: 'bun-monorepo',
153+
workspaces: ['packages/*'],
154+
packageManager: 'bun@1.3.11',
155+
}),
156+
);
157+
fs.writeFileSync(path.join(tmpDir, 'bunfig.toml'), '[install]\npeer = true\n');
158+
rewriteMonorepo(makeWorkspaceInfo(tmpDir, PackageManager.bun), true);
159+
160+
// User's explicit `peer = true` should remain — we don't silently flip it.
161+
expect(fs.readFileSync(path.join(tmpDir, 'bunfig.toml'), 'utf8')).toMatch(/peer\s*=\s*true/);
162+
});
163+
164+
it('appends `peer = false` under an existing [install] section without `peer` setting', () => {
165+
fs.writeFileSync(
166+
path.join(tmpDir, 'package.json'),
167+
JSON.stringify({
168+
name: 'bun-monorepo',
169+
workspaces: ['packages/*'],
170+
packageManager: 'bun@1.3.11',
171+
}),
172+
);
173+
fs.writeFileSync(
174+
path.join(tmpDir, 'bunfig.toml'),
175+
'[install]\nregistry = "https://registry.npmjs.org/"\n',
176+
);
177+
rewriteMonorepo(makeWorkspaceInfo(tmpDir, PackageManager.bun), true);
178+
179+
const bunfig = fs.readFileSync(path.join(tmpDir, 'bunfig.toml'), 'utf8');
180+
expect(bunfig).toMatch(/registry\s*=\s*"https:\/\/registry\.npmjs\.org\/"/);
181+
expect(bunfig).toMatch(/peer\s*=\s*false/);
182+
});
183+
126184
it('does not write file: paths into peer dependencies', () => {
127185
const pkg = {
128186
peerDependencies: {

packages/cli/src/migration/migrator.ts

Lines changed: 40 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1005,6 +1005,8 @@ export function rewriteStandaloneProject(
10051005

10061006
if (packageManager === PackageManager.yarn) {
10071007
rewriteYarnrcYml(projectPath);
1008+
} else if (packageManager === PackageManager.bun) {
1009+
ensureBunfigPeerSuppression(projectPath);
10081010
}
10091011

10101012
// Merge extracted staged config into vite.config.ts, then remove lint-staged from package.json
@@ -1572,6 +1574,42 @@ function rewriteCatalogsObject(catalogs: Record<string, Record<string, string>>)
15721574
}
15731575
}
15741576

1577+
/**
1578+
* Bun rejects vitest@4.1.5's `vite^6/^7/^8` peer-dep when the user's project
1579+
* overrides `vite` to `@voidzero-dev/vite-plus-core` (whose package.json version
1580+
* does not match those ranges). pnpm/yarn/npm all tolerate this redirect; bun
1581+
* does not, and there is no `peerDependencyRules`-style escape hatch — only the
1582+
* `[install] peer = false` setting in `bunfig.toml`.
1583+
*
1584+
* `vite-plus`/`@voidzero-dev/vite-plus-core` already provide the vite surface
1585+
* the user needs, so disabling bun's auto-install of *missing* peers is safe in
1586+
* this configuration: any vitest peer that's not already pulled in transitively
1587+
* (jsdom, happy-dom, etc.) is marked optional upstream anyway.
1588+
*
1589+
* Writes/merges `bunfig.toml` at `projectPath` so the suppression applies on
1590+
* the migration's reinstall AND every subsequent `bun install` the user runs.
1591+
*/
1592+
function ensureBunfigPeerSuppression(projectPath: string): void {
1593+
const bunfigPath = path.join(projectPath, 'bunfig.toml');
1594+
const block = '[install]\npeer = false\n';
1595+
if (!fs.existsSync(bunfigPath)) {
1596+
fs.writeFileSync(bunfigPath, block);
1597+
return;
1598+
}
1599+
const existing = fs.readFileSync(bunfigPath, 'utf8');
1600+
// Already configured? Leave the user's setting alone — they may have set
1601+
// `peer = true` deliberately for some other reason and we shouldn't override.
1602+
if (/^\s*peer\s*=\s*(true|false)\s*$/m.test(existing)) {
1603+
return;
1604+
}
1605+
// Append under existing [install] section, or add a new section.
1606+
const installSectionRe = /^\[install\][^[]*/m;
1607+
const next = installSectionRe.test(existing)
1608+
? existing.replace(installSectionRe, (section) => `${section.trimEnd()}\npeer = false\n`)
1609+
: `${existing.trimEnd()}\n\n${block}`;
1610+
fs.writeFileSync(bunfigPath, next);
1611+
}
1612+
15751613
/**
15761614
* Write catalog entries to root package.json for bun.
15771615
* Bun stores catalogs in package.json under the `catalog` key,
@@ -1639,6 +1677,8 @@ function rewriteBunCatalog(projectPath: string): void {
16391677

16401678
return pkg;
16411679
});
1680+
1681+
ensureBunfigPeerSuppression(projectPath);
16421682
}
16431683

16441684
/**

0 commit comments

Comments
 (0)