Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
{
"name": "playwright-test-app",
"private": true,
"devDependencies": {
"@playwright/test": "1.60.0",
"@vitest/browser-playwright": "4.1.10",
"@vitest/coverage-v8": "4.1.10",
"vite": "catalog:",
"vite-plus": "catalog:",
"vitest": "catalog:"
},
"devEngines": {
"packageManager": {
"name": "pnpm",
"version": "11.1.1",
"onFail": "download"
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
packages:
- .
catalog:
vite: npm:@voidzero-dev/vite-plus-core@latest
vite-plus: latest
vitest: 4.1.10
overrides:
vite: 'catalog:'
vitest: 'catalog:'
peerDependencyRules:
allowAny:
- vite
- vitest
allowedVersions:
vite: '*'
vitest: '*'
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
[[case]]
name = "migration_playwright_test"
vp = "global"
steps = [
{ argv = ["vp", "migrate", "--no-interactive"], comment = "existing Vite+ upgrade: @playwright/test should remain without adding a direct playwright dependency", continue-on-failure = true },
{ argv = ["vpt", "print-file", "package.json"], comment = "@vitest/browser-playwright and @vitest/coverage-v8 should become catalog:", continue-on-failure = true },
{ argv = ["vpt", "print-file", "pnpm-workspace.yaml"], comment = "the default catalog should own the aligned @vitest/* packages", continue-on-failure = true },
]
Original file line number Diff line number Diff line change
@@ -0,0 +1,70 @@
# migration_playwright_test

## `vp migrate --no-interactive`

existing Vite+ upgrade: @playwright/test should remain without adding a direct playwright dependency

```
VITE+ - The Unified Toolchain for the Web

◇ Updated . to Vite+ <version>
• Node <version> pnpm <version>
• Dependencies:
vite-plus latest → <version>
vite → <version>
vitest 4.1.10 → <version>
@vitest/browser-playwright 4.1.10 → <version>
@vitest/coverage-v8 4.1.10 → <version>
• Package manager settings configured
```

## `vpt print-file package.json`

@vitest/browser-playwright and @vitest/coverage-v8 should become catalog:

```
{
"name": "playwright-test-app",
"private": true,
"devDependencies": {
"@playwright/test": "1.60.0",
"@vitest/browser-playwright": "catalog:",
"@vitest/coverage-v8": "catalog:",
"vite": "catalog:",
"vite-plus": "catalog:",
"vitest": "catalog:"
},
"devEngines": {
"packageManager": {
"name": "pnpm",
"version": "<version>",
"onFail": "download"
}
}
}
```

## `vpt print-file pnpm-workspace.yaml`

the default catalog should own the aligned @vitest/* packages

```
packages:
- .
catalog:
vite: npm:@voidzero-dev/vite-plus-core@<version>
vite-plus: <version>
vitest: <version>
'@vitest/browser-playwright': <version>
'@vitest/coverage-v8': <version>
overrides:
vite@*: 'catalog:'
vitest@*: 'catalog:'
peerDependencyRules:
allowAny:
- vite
- vitest
allowedVersions:
vite: '*'
vitest: '*'
```
59 changes: 54 additions & 5 deletions packages/cli/src/migration/__tests__/migrator.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -632,10 +632,9 @@ describe('rewritePackageJson', () => {
expect(pkg.devDependencies).not.toHaveProperty('@vitest/browser');
});

it('pins the provider framework peer to a lockstep sibling instead of * (npmx.dev #27)', () => {
// `playwright` and `@playwright/test` release in lockstep, so a newly-added
// `playwright` peer should reuse the pinned @playwright/test version rather
// than a non-deterministic `*`.
it('does not add playwright when @playwright/test already provides it', () => {
// `@playwright/test` has a dependency on `playwright`,
// so adding a second direct dependency is redundant.
const pkg = {
devDependencies: {
'@vitest/browser-playwright': '^4.0.0',
Expand All @@ -644,7 +643,7 @@ describe('rewritePackageJson', () => {
},
};
rewritePackageJson(pkg, PackageManager.pnpm);
expect(pkg.devDependencies).toHaveProperty('playwright', '1.60.0');
expect(pkg.devDependencies).not.toHaveProperty('playwright');
});

it('injects a direct vite devDependency for an npm project that uses an opt-in browser provider', async () => {
Expand Down Expand Up @@ -3409,6 +3408,56 @@ describe('ensureVitePlusBootstrap', () => {
expect(detectVitePlusBootstrapPending(tmpDir, PackageManager.pnpm)).toBe(false);
});

it('does not add playwright on upgrade when @playwright/test already provides it', () => {
fs.writeFileSync(
path.join(tmpDir, 'package.json'),
JSON.stringify({
name: 'browser-app',
devDependencies: {
'@playwright/test': '1.60.0',
'vite-plus': 'catalog:',
},
devEngines: {
packageManager: { name: 'pnpm', version: '10.33.0', onFail: 'download' },
},
}),
);
fs.writeFileSync(
path.join(tmpDir, 'vite.config.ts'),
[
"import { defineConfig } from 'vite-plus';",
"import { playwright } from 'vite-plus/test/browser-playwright';",
'export default defineConfig({ test: { browser: { enabled: true, provider: playwright() } } });',
].join('\n'),
);
fs.writeFileSync(
path.join(tmpDir, 'pnpm-workspace.yaml'),
[
'catalog:',
' vite-plus: latest',
' vite: npm:@voidzero-dev/vite-plus-core@latest',
'overrides:',
" vite: 'catalog:'",
'peerDependencyRules:',
' allowAny: [vite]',
' allowedVersions:',
" vite: '*'",
'',
].join('\n'),
);

expect(detectVitePlusBootstrapPending(tmpDir, PackageManager.pnpm)).toBe(true);
ensureVitePlusBootstrap(makeWorkspaceInfo(tmpDir, PackageManager.pnpm));

const pkg = readJson(path.join(tmpDir, 'package.json')) as {
devDependencies: Record<string, string>;
};
expect(pkg.devDependencies).not.toHaveProperty('playwright');
expect(pkg.devDependencies.vitest).toBe('catalog:');
expect(pkg.devDependencies['@vitest/browser-playwright']).toBe('catalog:');
expect(detectVitePlusBootstrapPending(tmpDir, PackageManager.pnpm)).toBe(false);
});

it('references the catalog for an installed browser provider the injection adds to the catalog', () => {
// #2005: a workspace package uses `@vitest/browser-playwright` only through a
// Vite+ shim (no dep), so migration ADDS the provider to the default catalog.
Expand Down
5 changes: 2 additions & 3 deletions packages/cli/src/migration/migrator/package-json.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,7 +24,7 @@ import {
} from '../migrator.ts';
import {
BROWSER_PROVIDER_PEER_DEPS,
findDeclaredSpec,
hasProviderPeerDependency,
resolveProviderPeerSpec,
OPT_IN_BROWSER_PROVIDERS,
OXLINT_PLUGINS_PACKAGE,
Expand Down Expand Up @@ -280,8 +280,7 @@ export function rewritePackageJson(
);
}
const peer = BROWSER_PROVIDER_PEER_DEPS[provider]; // 'webdriverio' / 'playwright'
const peerPresent = findDeclaredSpec(pkg, peer);
if (peer && !peerPresent) {
if (!hasProviderPeerDependency(pkg, peer)) {
pkg.devDependencies ??= {};
pkg.devDependencies[peer] = resolveProviderPeerSpec(
pkg,
Expand Down
17 changes: 16 additions & 1 deletion packages/cli/src/migration/migrator/shared.ts
Original file line number Diff line number Diff line change
Expand Up @@ -82,10 +82,14 @@ export const BROWSER_PROVIDER_PEER_DEPS: Record<string, string> = {
'@vitest/browser-webdriverio': 'webdriverio',
};

// Packages that include runtime peer as a dependency.
const RUNTIME_PEER_INCLUDED_BY: Record<string, readonly string[]> = {
playwright: ['@playwright/test'],
};

// Lockstep sibling packages whose declared version a browser provider's runtime
// framework peer should reuse (they publish together). Keyed by the peer name.
export const PROVIDER_PEER_VERSION_SIBLINGS: Record<string, readonly string[]> = {
playwright: ['@playwright/test'],
webdriverio: ['@wdio/cli', '@wdio/globals'],
};

Expand All @@ -99,6 +103,17 @@ export function findDeclaredSpec(pkg: DependencyBag, name: string): string | und
);
}

// A provider's runtime peer is available when declared directly or included by
// another declared package.
export function hasProviderPeerDependency(pkg: DependencyBag, runtimePeer: string): boolean {
return (
findDeclaredSpec(pkg, runtimePeer) !== undefined ||
(RUNTIME_PEER_INCLUDED_BY[runtimePeer] ?? []).some(
(packageName) => findDeclaredSpec(pkg, packageName) !== undefined,
Comment thread
fengmk2 marked this conversation as resolved.
)
);
}

// A deterministic spec for a browser provider's framework peer instead of `*`:
// reference the catalog when it already owns the peer, otherwise reuse a declared
// lockstep sibling's version (concrete, or a catalog reference resolved to its
Expand Down
6 changes: 2 additions & 4 deletions packages/cli/src/migration/migrator/vite-plus-bootstrap.ts
Original file line number Diff line number Diff line change
Expand Up @@ -55,6 +55,7 @@ import {
import { type DependencyVersionChange, type MigrationReport } from '../report.ts';
import {
BROWSER_PROVIDER_PEER_DEPS,
hasProviderPeerDependency,
resolveProviderPeerSpec,
OPT_IN_BROWSER_PROVIDERS,
REMOVE_PACKAGES,
Expand Down Expand Up @@ -405,10 +406,7 @@ function reconcileVitePlusBootstrapPackage(
);
}
const frameworkPeer = BROWSER_PROVIDER_PEER_DEPS[provider];
const frameworkPresent = dependencyGroups.some(
(dependencies) => dependencies?.[frameworkPeer] !== undefined,
);
if (frameworkPeer && !frameworkPresent) {
if (!hasProviderPeerDependency(pkg, frameworkPeer)) {
pkg.devDependencies ??= {};
pkg.devDependencies[frameworkPeer] = resolveProviderPeerSpec(
pkg,
Expand Down
Loading