diff --git a/packages/cli/BUNDLING.md b/packages/cli/BUNDLING.md index 8b561c9472..9e85463322 100644 --- a/packages/cli/BUNDLING.md +++ b/packages/cli/BUNDLING.md @@ -440,6 +440,8 @@ Provider `.d.ts` shims are NOT plain `export * from '@vitest/browser-playwright' This avoids a pnpm-edge type-identity split: when the upstream `.d.ts` is loaded by reference (`export * from '@vitest/browser-playwright'`), TypeScript resolves its internal `import { BrowserProvider } from 'vitest/node'` through the provider package's own pnpm-edge, which can be a different vitest copy than the one a user's `vite.config.ts` sees through `vite-plus`. The mismatch produces two structurally identical but nominally distinct `BrowserProvider` types, so `provider: playwright()` fails the user's typecheck. Rewriting the specifiers routes every type import through vite-plus's own subpath shims, guaranteeing a single vitest identity across the user's whole config. +The copied Playwright declarations still import `playwright` directly. Declare it as an optional peer of `vite-plus`, matching the provider's upstream peer range, so isolated dependency layouts can resolve these types from the copied files. + ### Conditional Export Handling The sync handles complex conditional exports with `import`/`require`/`node`/`types` conditions. diff --git a/packages/cli/package.json b/packages/cli/package.json index ea09ecb84f..832fbcffca 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -390,7 +390,8 @@ }, "peerDependencies": { "@vitest/browser-playwright": "catalog:", - "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0" + "@vitest/browser-webdriverio": "^5.0.0-beta.5 || >=5.0.0", + "playwright": "*" }, "peerDependenciesMeta": { "@vitest/browser-playwright": { @@ -398,6 +399,9 @@ }, "@vitest/browser-webdriverio": { "optional": true + }, + "playwright": { + "optional": true } }, "napi": { diff --git a/packages/cli/src/__tests__/exports-map.spec.ts b/packages/cli/src/__tests__/exports-map.spec.ts index 520dd7198e..a48aa0e0be 100644 --- a/packages/cli/src/__tests__/exports-map.spec.ts +++ b/packages/cli/src/__tests__/exports-map.spec.ts @@ -13,6 +13,7 @@ */ import fs from 'node:fs'; import { createRequire } from 'node:module'; +import os from 'node:os'; import path from 'node:path'; import url from 'node:url'; @@ -58,6 +59,59 @@ function typeDiagnostics(source: string, separator: string = path.sep): string[] } describe('package.json exports map', () => { + it('checks Playwright options with only declared dependencies available', () => { + const directory = fs.mkdtempSync(path.join(os.tmpdir(), 'vite-plus-provider-types-')); + try { + const pkg = JSON.parse(fs.readFileSync(cliPkgJsonPath, 'utf8')); + const installed = path.join(directory, 'node_modules/vite-plus'); + fs.mkdirSync(installed, { recursive: true }); + fs.writeFileSync(path.join(installed, 'package.json'), JSON.stringify(pkg)); + fs.cpSync(path.join(cliPkgDir, 'dist/test'), path.join(installed, 'dist/test'), { + recursive: true, + }); + // Keep the copied declarations outside the workspace so hoisted dependencies + // cannot hide missing peers, as in pnpm's global virtual store (#2854). + for (const name of Object.keys({ ...pkg.dependencies, ...pkg.peerDependencies })) { + const target = path.join(directory, 'node_modules', name); + fs.mkdirSync(path.dirname(target), { recursive: true }); + fs.symlinkSync( + fs.realpathSync(path.join(cliPkgDir, 'node_modules', name)), + target, + 'junction', + ); + } + const filename = path.join(directory, 'config.mts'); + fs.writeFileSync( + filename, + ` +import { playwright } from 'vite-plus/test/browser-playwright'; +import { playwright as alias } from 'vite-plus/test/browser/providers/playwright'; +playwright({ contextOptions: { reducedMotion: 'reduce' } }); +alias({ contextOptions: { reducedMotion: 'no-preference' } }); +// @ts-expect-error Invalid Playwright options must not become unchecked under skipLibCheck. +playwright({ contextOptions: { reducedMotion: 'bogus' } }); +// @ts-expect-error The compatibility alias must retain the same option types. +alias({ contextOptions: { reducedMotion: 'bogus' } }); +`, + ); + const program = ts.createProgram([filename], { + noEmit: true, + strict: true, + skipLibCheck: true, + types: [], + module: ts.ModuleKind.NodeNext, + target: ts.ScriptTarget.ESNext, + }); + expect( + ts + .getPreEmitDiagnostics(program) + .map((d) => ts.flattenDiagnosticMessageText(d.messageText, '\n')), + ).toEqual([]); + } finally { + fs.rmSync(directory, { recursive: true, force: true }); + } + }); + it('provides the bundled Vitest Vite peer without relying on project dependencies', () => { const pkg = JSON.parse(fs.readFileSync(cliPkgJsonPath, 'utf8')); expect(pkg.dependencies.vite).toBe('workspace:@voidzero-dev/vite-plus-core@*'); diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0ab2f49610..20bf5bc824 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -353,6 +353,9 @@ importers: oxlint-tsgolint: specifier: 'catalog:' version: 7.0.2003 + playwright: + specifier: '*' + version: 1.62.1 vite: specifier: workspace:@voidzero-dev/vite-plus-core@* version: link:../core