Skip to content

Commit c75d8df

Browse files
kazuponfengmk2
andauthored
fix(test): exclude @vitest/browser/context from vendor-aliases to fix missing server export (#1110)
## Summary Fixes the missing `server` export from `@vitest/browser/context` when using `@storybook/addon-vitest` with vite-plus. resolve #1086 ## Root Cause In vitest's browser mode, `@vitest/browser/context` is a **virtual module**, the `BrowserContext` plugin intercepts the bare specifier in `resolveId` and returns dynamically generated code (via `generateContextFile()`) that includes the `server` export with command RPC proxies, config, platform info, etc. However, vite-plus injects a `vitest:vendor-aliases` plugin during re-packaging (in `build.ts`) that maps `@vitest/*` bare specifiers to static files in the dist directory. Both plugins use `enforce: 'pre'`, but `vendor-aliases` is registered **before** `BrowserContext` in the plugin array. Since Vite executes same-enforce plugins in registration order, `vendor-aliases` resolves the bare specifier to the static `context.js` file before `BrowserContext` ever sees it. The static `context.js` only exports `{ cdp, createUserEvent, locators, page, utils }` — it does **not** include `server`, because that's supposed to come from the virtual module. ### Normal flow (plain vitest — no vendor-aliases) ```mermaid sequenceDiagram participant Browser participant ViteServer as Vite Browser Server participant BC as BrowserContext Plugin<br/>(resolveId) participant Gen as generateContextFile() Browser->>ViteServer: import "@vitest/browser/context" ViteServer->>BC: resolveId("@vitest/browser/context") BC-->>ViteServer: "\0vitest/browser" (virtual module ID) ViteServer->>BC: load("\0vitest/browser") BC->>Gen: generateContextFile() Gen-->>BC: Dynamic code with server, commands, config... BC-->>ViteServer: Virtual module source ViteServer-->>Browser: Module with { server, page, cdp, ... } ``` ### Bug flow (vite-plus — vendor-aliases intercepts first) ```mermaid sequenceDiagram participant Browser participant ViteServer as Vite Browser Server participant VA as vendor-aliases Plugin<br/>(resolveId) ← runs first participant BC as BrowserContext Plugin<br/>(resolveId) ← never called Browser->>ViteServer: import "@vitest/browser/context" ViteServer->>VA: resolveId("@vitest/browser/context") VA-->>ViteServer: "/path/to/dist/@vitest/browser/context.js" (static file) Note over BC: resolveId is never called<br/>because vendor-aliases already resolved it ViteServer-->>Browser: Static file (no server export) Browser->>Browser: SyntaxError: does not provide<br/>an export named 'server' ``` ### Plugin registration order in the browser Vite server ``` 1. vitest:vendor-aliases ← enforce:'pre', registered first 2. vitest:browser ← enforce:'pre' 3. vitest:browser:tests ← enforce:'pre' 4. vitest:browser:virtual-module:context (BrowserContext) ← enforce:'pre', registered last ``` ## Fix Exclude `@vitest/browser/context` from the `vendor-aliases` plugin's `vendorMap` during re-packaging (`build.ts`). This allows the bare specifier to pass through to the `BrowserContext` plugin, which correctly returns the virtual module with the `server` export. --------- Co-authored-by: MK (fengmk2) <fengmk2@gmail.com>
1 parent 8159886 commit c75d8df

2 files changed

Lines changed: 67 additions & 1 deletion

File tree

Lines changed: 58 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,58 @@
1+
/**
2+
* Verify that the @voidzero-dev/vite-plus-test build output (dist/)
3+
* contains the expected files and that patches applied during the build
4+
* (in build.ts) produce correct artifacts.
5+
*
6+
* This is important because vite-plus re-packages vitest with custom
7+
* patches, and missing exports or incorrect patches can break
8+
* third-party integrations (e.g., @storybook/addon-vitest, #1086).
9+
*/
10+
import fs from 'node:fs';
11+
import path from 'node:path';
12+
import url from 'node:url';
13+
14+
import { describe, expect, it } from 'vitest';
15+
16+
const testPkgDir = path.resolve(path.dirname(url.fileURLToPath(import.meta.url)), '..');
17+
const distDir = path.join(testPkgDir, 'dist');
18+
19+
describe('build artifacts', () => {
20+
describe('@vitest/browser/context.js', () => {
21+
const contextPath = path.join(distDir, '@vitest/browser/context.js');
22+
23+
it('should exist', () => {
24+
expect(fs.existsSync(contextPath), `${contextPath} should exist`).toBe(true);
25+
});
26+
27+
it('should export page, cdp, and utils', () => {
28+
const content = fs.readFileSync(contextPath, 'utf-8');
29+
expect(content).toMatch(/export\s*\{[^}]*page[^}]*\}/);
30+
expect(content).toMatch(/export\s*\{[^}]*cdp[^}]*\}/);
31+
expect(content).toMatch(/export\s*\{[^}]*utils[^}]*\}/);
32+
});
33+
});
34+
35+
/**
36+
* The vitest:vendor-aliases plugin must NOT resolve @vitest/browser/context
37+
* to the static file. If it does, the BrowserContext plugin's virtual module
38+
* (which provides the `server` export) is bypassed.
39+
*
40+
* See: https://github.com/voidzero-dev/vite-plus/issues/1086
41+
*/
42+
describe('vitest:vendor-aliases plugin (regression test for #1086)', () => {
43+
const browserIndexPath = path.join(distDir, '@vitest/browser/index.js');
44+
45+
it('should not map @vitest/browser/context in vendorMap', () => {
46+
const content = fs.readFileSync(browserIndexPath, 'utf-8');
47+
// The vendorMap inside vitest:vendor-aliases should NOT contain
48+
// '@vitest/browser/context' — it must be left for BrowserContext
49+
// plugin to resolve as a virtual module.
50+
const vendorAliasesMatch = content.match(
51+
/name:\s*['"]vitest:vendor-aliases['"][\s\S]*?const vendorMap\s*=\s*\{([\s\S]*?)\}/,
52+
);
53+
expect(vendorAliasesMatch, 'vitest:vendor-aliases plugin should exist').toBeTruthy();
54+
const vendorMapContent = vendorAliasesMatch![1];
55+
expect(vendorMapContent).not.toContain("'@vitest/browser/context'");
56+
});
57+
});
58+
});

packages/test/build.ts

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1458,8 +1458,16 @@ async function patchVitestBrowserPackage() {
14581458

14591459
// 1. Inject vitest:vendor-aliases plugin into BrowserPlugin return array
14601460
// This allows imports like @vitest/runner to be resolved to our copied @vitest files
1461+
// Exclude @vitest/browser/context from vendor-aliases so that BrowserContext
1462+
// plugin's resolveId can intercept the bare specifier and return the virtual
1463+
// module (which includes the dynamically generated `server` export).
1464+
// Without this, vendor-aliases resolves the bare specifier to the static
1465+
// context.js file (which has no `server`), bypassing BrowserContext entirely.
1466+
// See: https://github.com/voidzero-dev/vite-plus/issues/1086
1467+
const VENDOR_ALIASES_EXCLUDE = new Set(['@vitest/browser/context']);
1468+
14611469
const mappingEntries = Object.entries(VITEST_PACKAGE_TO_PATH)
1462-
.filter(([pkg]) => pkg.startsWith('@vitest/'))
1470+
.filter(([pkg]) => pkg.startsWith('@vitest/') && !VENDOR_ALIASES_EXCLUDE.has(pkg))
14631471
.map(([pkg, file]) => `'${pkg}': resolve(packageRoot, '${file}')`)
14641472
.join(',\n ');
14651473

0 commit comments

Comments
 (0)