Skip to content

Commit a33e566

Browse files
committed
fix(test): resolve ws CJS named export error with cloudflare preset
closes #831 When Nuxt's cloudflare preset is active, vitest passes `--conditions browser` to worker processes. This caused `ws` to resolve to its browser stub (`ws/browser.js`) instead of the ESM wrapper (`ws/wrapper.mjs`), because the test package's `node` export condition loaded `index-node.js` which imports `@vitest/browser/index.js` → `ws`. Fix by adding a `browser` export condition before `node` in the test package's main export. When `--conditions browser` is active, `browser` matches first and resolves to `dist/index.js` (which doesn't pull in `@vitest/browser`), avoiding the ws resolution issue. Also fix ecosystem-ci `patch-project.ts` to apply pnpm overrides for projects that already use Vite+ (previously skipped by `vp migrate`), and add the reproduction repo as an e2e test case.
1 parent fdd44d0 commit a33e566

5 files changed

Lines changed: 70 additions & 7 deletions

File tree

.github/workflows/e2e-test.yml

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -246,6 +246,10 @@ jobs:
246246
vp run type-check:tsgo
247247
vp run build
248248
vp run test navigation-utils.test.ts real-browser-flicker.test.tsx workflow-parallel-limit.test.tsx
249+
- name: viteplus-ws-repro
250+
node-version: 24
251+
command: |
252+
vp test run
249253
exclude:
250254
# frm-stack uses Docker (testcontainers) which doesn't work the same way on Windows
251255
- os: windows-latest

ecosystem-ci/patch-project.ts

Lines changed: 51 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,5 @@
11
import { execSync } from 'node:child_process';
2+
import { existsSync } from 'node:fs';
23
import { readFile, writeFile } from 'node:fs/promises';
34
import { join } from 'node:path';
45

@@ -21,6 +22,14 @@ const cwd = directory ? join(repoRoot, directory) : repoRoot;
2122
// run vp migrate
2223
const cli = process.env.VITE_PLUS_CLI_BIN ?? 'vp';
2324

25+
const overridePackages: Record<string, string> = {
26+
vite: `file:${tgzDir}/voidzero-dev-vite-plus-core-0.0.0.tgz`,
27+
vitest: `file:${tgzDir}/voidzero-dev-vite-plus-test-0.0.0.tgz`,
28+
'@voidzero-dev/vite-plus-core': `file:${tgzDir}/voidzero-dev-vite-plus-core-0.0.0.tgz`,
29+
'@voidzero-dev/vite-plus-test': `file:${tgzDir}/voidzero-dev-vite-plus-test-0.0.0.tgz`,
30+
};
31+
const vitePlusVersion = `file:${tgzDir}/vite-plus-0.0.0.tgz`;
32+
2433
if (project === 'rollipop') {
2534
const oxfmtrc = await readFile(join(repoRoot, '.oxfmtrc.json'), 'utf-8');
2635
await writeFile(
@@ -35,12 +44,47 @@ execSync(`${cli} migrate --no-agent --no-interactive`, {
3544
stdio: 'inherit',
3645
env: {
3746
...process.env,
38-
VITE_PLUS_OVERRIDE_PACKAGES: JSON.stringify({
39-
vite: `file:${tgzDir}/voidzero-dev-vite-plus-core-0.0.0.tgz`,
40-
vitest: `file:${tgzDir}/voidzero-dev-vite-plus-test-0.0.0.tgz`,
41-
'@voidzero-dev/vite-plus-core': `file:${tgzDir}/voidzero-dev-vite-plus-core-0.0.0.tgz`,
42-
'@voidzero-dev/vite-plus-test': `file:${tgzDir}/voidzero-dev-vite-plus-test-0.0.0.tgz`,
43-
}),
44-
VITE_PLUS_VERSION: `file:${tgzDir}/vite-plus-0.0.0.tgz`,
47+
VITE_PLUS_OVERRIDE_PACKAGES: JSON.stringify(overridePackages),
48+
VITE_PLUS_VERSION: vitePlusVersion,
4549
},
4650
});
51+
52+
// vp migrate skips override injection for projects already using Vite+.
53+
// Apply overrides so e2e tests use the locally built packages.
54+
const workspaceYamlPath = join(cwd, 'pnpm-workspace.yaml');
55+
if (existsSync(workspaceYamlPath)) {
56+
// pnpm catalogs don't support the file: protocol, so we replace
57+
// workspace-level overrides (which point to "catalog:") with direct
58+
// file: tgz paths, and add overrides for transitive deps.
59+
let yaml = await readFile(workspaceYamlPath, 'utf-8');
60+
// Replace existing "catalog:" overrides with direct tgz paths
61+
yaml = yaml.replace(/^(\s+)(vite|vitest):\s*"catalog:"$/gm, (_, indent, pkg) => {
62+
const tgzPath = pkg === 'vite' ? overridePackages.vite : overridePackages.vitest;
63+
return `${indent}${pkg}: ${tgzPath}`;
64+
});
65+
// Add overrides for transitive deps and vite-plus itself
66+
const additionalOverrides = {
67+
'@voidzero-dev/vite-plus-core': overridePackages['@voidzero-dev/vite-plus-core'],
68+
'@voidzero-dev/vite-plus-test': overridePackages['@voidzero-dev/vite-plus-test'],
69+
'vite-plus': vitePlusVersion,
70+
};
71+
const overridesLines = Object.entries(additionalOverrides)
72+
.map(([name, version]) => {
73+
const key = /[/@]/.test(name) ? `"${name}"` : name;
74+
return ` ${key}: ${version}`;
75+
})
76+
.join('\n');
77+
yaml = yaml.replace(/^overrides:\n/m, `overrides:\n${overridesLines}\n`);
78+
await writeFile(workspaceYamlPath, yaml, 'utf-8');
79+
} else {
80+
// No workspace config — apply pnpm overrides in package.json directly
81+
const pkgPath = join(cwd, 'package.json');
82+
const pkg = JSON.parse(await readFile(pkgPath, 'utf-8'));
83+
pkg.pnpm = pkg.pnpm || {};
84+
pkg.pnpm.overrides = pkg.pnpm.overrides || {};
85+
const allOverrides = { ...overridePackages, 'vite-plus': vitePlusVersion };
86+
for (const [name, version] of Object.entries(allOverrides)) {
87+
pkg.pnpm.overrides[name] = version;
88+
}
89+
await writeFile(pkgPath, JSON.stringify(pkg, null, 2) + '\n', 'utf-8');
90+
}

ecosystem-ci/repo.json

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -54,5 +54,10 @@
5454
"repository": "https://github.com/fengmk2/vite-vue-vercel.git",
5555
"branch": "main",
5656
"hash": "f2bf9fc40880c6a80f5d89bff70641c2eeaf77ef"
57+
},
58+
"viteplus-ws-repro": {
59+
"repository": "https://github.com/Charles5277/viteplus-ws-repro.git",
60+
"branch": "main",
61+
"hash": "451925ad7c07750a23de1d6ed454825d0eb14092"
5762
}
5863
}

packages/test/build.ts

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -302,9 +302,18 @@ async function mergePackageJson(pluginExports: Array<{ exportPath: string; shimF
302302
// browser-provider exports. Browser code uses index.js which is safe.
303303
// This separation prevents Node.js-only code (like __vite__injectQuery) from being
304304
// loaded in the browser, which would cause "Identifier already declared" errors.
305+
//
306+
// IMPORTANT: The 'browser' condition must come BEFORE 'node' because vitest passes
307+
// custom --conditions (like 'browser') to worker processes when frameworks like Nuxt
308+
// set edge/cloudflare presets. Without the 'browser' condition here, Node.js would
309+
// match 'node' first, loading index-node.js which imports @vitest/browser/index.js,
310+
// which imports 'ws'. With --conditions browser active, 'ws' resolves to its browser
311+
// stub (ws/browser.js) that doesn't export WebSocketServer, causing a SyntaxError.
312+
// See: https://github.com/voidzero-dev/vite-plus/issues/831
305313
if (destPkg.exports['.'] && destPkg.exports['.'].import) {
306314
destPkg.exports['.'].import = {
307315
types: destPkg.exports['.'].import.types,
316+
browser: destPkg.exports['.'].import.default,
308317
node: './dist/index-node.js',
309318
default: destPkg.exports['.'].import.default,
310319
};

packages/test/package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
".": {
2727
"import": {
2828
"types": "./dist/index.d.ts",
29+
"browser": "./dist/index.js",
2930
"node": "./dist/index-node.js",
3031
"default": "./dist/index.js"
3132
},

0 commit comments

Comments
 (0)