Skip to content

Commit 6dc08ff

Browse files
committed
fix(migrate): pin browser-provider framework peers instead of writing *
When migration keeps an opt-in browser provider (@vitest/browser-playwright / @vitest/browser-webdriverio) and adds its runtime framework peer, it wrote `playwright`/`webdriverio` as `*` (non-deterministic). Reuse a declared lockstep sibling's version instead (e.g. @playwright/test for playwright), resolving a `catalog:` sibling to its concrete value, referencing the catalog when it already owns the peer, and falling back to `*` only when nothing resolves (npmx.dev #27). Also extracts a shared findDeclaredSpec helper (the four-field dependency scan) and drops an unreachable peer branch in the REMOVE_PACKAGES loop. Claude-Session: https://claude.ai/code/session_01DQhS6o1fyQd1yjiee6W8jR
1 parent 8498463 commit 6dc08ff

4 files changed

Lines changed: 76 additions & 20 deletions

File tree

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

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -584,6 +584,21 @@ describe('rewritePackageJson', () => {
584584
expect(pkg.devDependencies).not.toHaveProperty('@vitest/browser');
585585
});
586586

587+
it('pins the provider framework peer to a lockstep sibling instead of * (npmx.dev #27)', () => {
588+
// `playwright` and `@playwright/test` release in lockstep, so a newly-added
589+
// `playwright` peer should reuse the pinned @playwright/test version rather
590+
// than a non-deterministic `*`.
591+
const pkg = {
592+
devDependencies: {
593+
'@vitest/browser-playwright': '^4.0.0',
594+
'@playwright/test': '1.60.0',
595+
vitest: '^4.0.0',
596+
},
597+
};
598+
rewritePackageJson(pkg, PackageManager.pnpm);
599+
expect(pkg.devDependencies).toHaveProperty('playwright', '1.60.0');
600+
});
601+
587602
it('injects a direct vite devDependency for an npm project that uses an opt-in browser provider', async () => {
588603
// npm's flat node_modules cannot dedupe the provider's own
589604
// `@vitest/browser → @vitest/mocker` subtree against the one vite-plus

packages/cli/src/migration/migrator/package-json.ts

Lines changed: 9 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ import {
2323
} from '../migrator.ts';
2424
import {
2525
BROWSER_PROVIDER_PEER_DEPS,
26+
findDeclaredSpec,
27+
resolveProviderPeerSpec,
2628
OPT_IN_BROWSER_PROVIDERS,
2729
REMOVE_PACKAGES,
2830
VITEST_BROWSER_DEP_NAMES,
@@ -192,19 +194,6 @@ export function rewritePackageJson(
192194
if (wasRemoved) {
193195
needVitePlus = true;
194196
}
195-
// e.g., removing @vitest/browser-playwright should keep `playwright` in devDeps
196-
const peerDep = BROWSER_PROVIDER_PEER_DEPS[name];
197-
if (
198-
wasRemoved &&
199-
peerDep &&
200-
!pkg.devDependencies?.[peerDep] &&
201-
!pkg.dependencies?.[peerDep] &&
202-
!pkg.peerDependencies?.[peerDep] &&
203-
!pkg.optionalDependencies?.[peerDep]
204-
) {
205-
pkg.devDependencies ??= {};
206-
pkg.devDependencies[peerDep] = '*';
207-
}
208197
}
209198
// The browser providers (webdriverio, playwright) are opt-in: vite-plus no
210199
// longer bundles them at runtime (each drags a heavy non-optional framework
@@ -262,14 +251,15 @@ export function rewritePackageJson(
262251
);
263252
}
264253
const peer = BROWSER_PROVIDER_PEER_DEPS[provider]; // 'webdriverio' / 'playwright'
265-
const peerPresent =
266-
pkg.dependencies?.[peer] ??
267-
pkg.devDependencies?.[peer] ??
268-
pkg.peerDependencies?.[peer] ??
269-
pkg.optionalDependencies?.[peer];
254+
const peerPresent = findDeclaredSpec(pkg, peer);
270255
if (peer && !peerPresent) {
271256
pkg.devDependencies ??= {};
272-
pkg.devDependencies[peer] = '*';
257+
pkg.devDependencies[peer] = resolveProviderPeerSpec(
258+
pkg,
259+
peer,
260+
supportCatalog,
261+
catalogDependencyResolver,
262+
);
273263
}
274264
needVitePlus = true;
275265
}

packages/cli/src/migration/migrator/shared.ts

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -82,6 +82,51 @@ export const BROWSER_PROVIDER_PEER_DEPS: Record<string, string> = {
8282
'@vitest/browser-webdriverio': 'webdriverio',
8383
};
8484

85+
// Lockstep sibling packages whose declared version a browser provider's runtime
86+
// framework peer should reuse (they publish together). Keyed by the peer name.
87+
export const PROVIDER_PEER_VERSION_SIBLINGS: Record<string, readonly string[]> = {
88+
playwright: ['@playwright/test'],
89+
webdriverio: ['@wdio/cli', '@wdio/globals'],
90+
};
91+
92+
// A package's declared spec across all four dependency fields, or undefined.
93+
export function findDeclaredSpec(pkg: DependencyBag, name: string): string | undefined {
94+
return (
95+
pkg.dependencies?.[name] ??
96+
pkg.devDependencies?.[name] ??
97+
pkg.peerDependencies?.[name] ??
98+
pkg.optionalDependencies?.[name]
99+
);
100+
}
101+
102+
// A deterministic spec for a browser provider's framework peer instead of `*`:
103+
// reference the catalog when it already owns the peer, otherwise reuse a declared
104+
// lockstep sibling's version (concrete, or a catalog reference resolved to its
105+
// concrete value), falling back to `*` only when there is no sibling. See
106+
// npmx.dev #27.
107+
export function resolveProviderPeerSpec(
108+
pkg: DependencyBag,
109+
peer: string,
110+
supportCatalog: boolean,
111+
catalogDependencyResolver?: CatalogDependencyResolver,
112+
): string {
113+
if (supportCatalog && catalogDependencyResolver?.('catalog:', peer) !== undefined) {
114+
return 'catalog:';
115+
}
116+
for (const sibling of PROVIDER_PEER_VERSION_SIBLINGS[peer] ?? []) {
117+
const spec = findDeclaredSpec(pkg, sibling);
118+
const resolved = spec?.startsWith('catalog:')
119+
? catalogDependencyResolver?.(spec, sibling)
120+
: spec;
121+
// Only reuse a concrete version: a `catalog:` entry may itself alias another
122+
// protocol, and npm:/workspace:/file: specs aren't versions to copy.
123+
if (resolved && !resolved.includes(':')) {
124+
return resolved;
125+
}
126+
}
127+
return '*';
128+
}
129+
85130
// Browser-provider package names that, when present in the user's deps
86131
// before migration, signal vitest browser mode even if no source file
87132
// imports them. This covers config-only browser-mode setups (e.g.

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

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ import {
5555
import { type DependencyVersionChange, type MigrationReport } from '../report.ts';
5656
import {
5757
BROWSER_PROVIDER_PEER_DEPS,
58+
resolveProviderPeerSpec,
5859
OPT_IN_BROWSER_PROVIDERS,
5960
REMOVE_PACKAGES,
6061
VITEST_IS_MANAGED_OVERRIDE,
@@ -387,7 +388,12 @@ function reconcileVitePlusBootstrapPackage(
387388
);
388389
if (frameworkPeer && !frameworkPresent) {
389390
pkg.devDependencies ??= {};
390-
pkg.devDependencies[frameworkPeer] = '*';
391+
pkg.devDependencies[frameworkPeer] = resolveProviderPeerSpec(
392+
pkg,
393+
frameworkPeer,
394+
supportCatalog,
395+
catalogDependencyResolver,
396+
);
391397
}
392398
}
393399

0 commit comments

Comments
 (0)