Skip to content
Closed
Show file tree
Hide file tree
Changes from 1 commit
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
Prev Previous commit
Next Next commit
fix: Vite 7/Rolldown compatibility - use ESM in build mode
## Problem
Vite 7 (Rolldown) wraps CJS modules in __commonJSMin() arrow function,
making top-level await invalid ('await is only valid in async functions').

## Solution
- Dev mode: Keep original CJS syntax (backward compatible with Vite 5)
- Build mode: Use ESM (import/export) with top-level await
- runtimeInitStatus: Dual exports to support both modes

## Testing
Added new CI job to test production builds (vite build + preview),
which catches the Vite 7/Rolldown CJS wrapper issue.

Fixes #320
  • Loading branch information
DallasCarraher committed Dec 10, 2025
commit 762bd5fa8396e25aa73563a0cffb49507f549151
60 changes: 53 additions & 7 deletions .github/workflows/e2e-test.yml
Original file line number Diff line number Diff line change
Expand Up @@ -12,8 +12,9 @@ permissions:
pull-requests: read

jobs:
run-playwright-tests:
name: Playwright Tests
# Dev mode tests - uses vite dev server
run-playwright-tests-dev:
name: Playwright Tests (Dev Mode)
runs-on: ubuntu-latest
container: node:20
steps:
Expand All @@ -31,19 +32,64 @@ jobs:
- name: Install Chromium Browser
run: pnpm playwright install --with-deps chromium

- name: Build Projects
- name: Build Plugin
run: pnpm build

- name: Start Application multi-example
- name: Start Application (Dev Mode)
run: nohup pnpm run multi-example & pnpm exec wait-on http://localhost:5173;

- name: Run Playwright Tests
run: pnpm playwright test
- name: Run Playwright Tests (Dev)
run: pnpm playwright test --project=multi-example

- name: Upload Artifacts on Failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: test-results
name: test-results-dev
path: reports/e2e/output
retention-days: 3

# Build/Preview mode tests - validates production builds work correctly
# This catches issues like Vite 7/Rolldown CJS wrapper problems
run-playwright-tests-preview:
name: Playwright Tests (Build/Preview Mode)
runs-on: ubuntu-latest
container: node:20
steps:
- name: Checkout
uses: actions/checkout@v5

- name: Enable Corepack and Setup PNPM
run: |
corepack enable
corepack prepare pnpm@9.1.3 --activate

- name: Install Dependencies
run: pnpm install --frozen-lockfile

- name: Install Chromium Browser
run: pnpm playwright install --with-deps chromium

- name: Build Plugin
run: pnpm build

- name: Build Example Apps
run: pnpm --filter "multi-example-*" run build

- name: Start Application (Preview Mode)
run: |
cd examples/vite-webpack-rspack/host
nohup pnpm preview --port 4173 &
cd ../../..
pnpm exec wait-on http://localhost:4173

- name: Run Playwright Tests (Preview)
run: pnpm playwright test --project=multi-example-preview

- name: Upload Artifacts on Failure
if: failure()
uses: actions/upload-artifact@v4
with:
name: test-results-preview
path: reports/e2e/output
retention-days: 3
9 changes: 9 additions & 0 deletions playwright.config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,15 @@ export default defineConfig({
browserName: 'chromium',
},
},
// Test production build via preview mode - validates Vite 7/Rolldown compatibility
{
name: 'multi-example-preview',
testDir: 'e2e/vite-webpack-rspack',
use: {
baseURL: 'http://localhost:4173',
browserName: 'chromium',
},
},
],
outputDir: 'reports/e2e/output',
reporter: [
Expand Down
33 changes: 19 additions & 14 deletions src/virtualModules/virtualRemotes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,8 +7,7 @@ const cacheRemoteMap: {
export const LOAD_REMOTE_TAG = '__loadRemote__';
export function getRemoteVirtualModule(remote: string, command: string) {
if (!cacheRemoteMap[remote]) {
// Use .mjs extension to ensure ESM treatment by bundlers
cacheRemoteMap[remote] = new VirtualModule(remote, LOAD_REMOTE_TAG, '.mjs');
cacheRemoteMap[remote] = new VirtualModule(remote, LOAD_REMOTE_TAG, '.js');
cacheRemoteMap[remote].writeSync(generateRemotes(remote, command));
}
const virtual = cacheRemoteMap[remote];
Expand All @@ -25,16 +24,22 @@ export function getUsedRemotesMap() {
return usedRemotesMap;
}
export function generateRemotes(id: string, command: string) {
// Generate ESM-compatible code to fix Vite 7/Rolldown compatibility
// The previous CJS code (require + module.exports + top-level await) caused syntax errors
// because Rolldown wraps CJS in a function where top-level await is invalid
return `
// Use dynamic import instead of require for ESM compatibility
const runtimeModule = await import("${virtualRuntimeInitStatus.getImportId()}")
const {initPromise} = runtimeModule
const res = initPromise.then(runtime => runtime.loadRemote(${JSON.stringify(id)}))
const exportModule = ${command !== 'build' ? '/*mf top-level-await placeholder replacement mf*/' : 'await '}initPromise.then(_ => res)
// Use ESM export instead of module.exports to avoid CJS wrapper issues
export default exportModule
`;
if (command === 'build') {
// Build mode: Use ESM syntax to fix Vite 7/Rolldown compatibility
// Rolldown wraps CJS (require + module.exports) in a function, breaking top-level await
return `
import { initPromise } from "${virtualRuntimeInitStatus.getImportId()}"
const res = initPromise.then(runtime => runtime.loadRemote(${JSON.stringify(id)}))
const exportModule = await initPromise.then(_ => res)
export default exportModule
`;
} else {
// Dev mode: Use original CJS syntax for compatibility with existing plugins
return `
const {initPromise} = require("${virtualRuntimeInitStatus.getImportId()}")
const res = initPromise.then(runtime => runtime.loadRemote(${JSON.stringify(id)}))
const exportModule = /*mf top-level-await placeholder replacement mf*/initPromise.then(_ => res)
module.exports = exportModule
`;
}
}
10 changes: 5 additions & 5 deletions src/virtualModules/virtualRuntimeInitStatus.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,10 @@
import VirtualModule from '../utils/VirtualModule';
// Use .mjs extension to ensure ESM treatment by bundlers
export const virtualRuntimeInitStatus = new VirtualModule('runtimeInit', '__mf_v__', '.mjs');
export const virtualRuntimeInitStatus = new VirtualModule('runtimeInit');
export function writeRuntimeInitStatus() {
// Use globalThis singleton to ensure only one initPromise exists
const globalKey = `__mf_init__${virtualRuntimeInitStatus.getImportId()}__`;
// Generate ESM-compatible code to fix Vite 7/Rolldown compatibility
// Use named ESM exports instead of module.exports for proper dynamic import support
// This module is imported by both dev and build modes
// We use a dual-export pattern that works with both CJS require() and ESM import
virtualRuntimeInitStatus.writeSync(`
const globalKey = ${JSON.stringify(globalKey)}
if (!globalThis[globalKey]) {
Expand All @@ -20,7 +19,8 @@ export function writeRuntimeInitStatus() {
initReject
}
}
// Use ESM named exports instead of module.exports for compatibility with dynamic import()
// Dual exports: CJS for dev mode (require), ESM for build mode (import)
module.exports = globalThis[globalKey]
export const initPromise = globalThis[globalKey].initPromise
export const initResolve = globalThis[globalKey].initResolve
export const initReject = globalThis[globalKey].initReject
Expand Down
57 changes: 34 additions & 23 deletions src/virtualModules/virtualShared_preBuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -32,31 +32,42 @@ export const LOAD_SHARE_TAG = '__loadShare__';
const loadShareCacheMap: Record<string, VirtualModule> = {};
export function getLoadShareModulePath(pkg: string): string {
if (!loadShareCacheMap[pkg])
// Use .mjs extension to ensure ESM treatment by bundlers
loadShareCacheMap[pkg] = new VirtualModule(pkg, LOAD_SHARE_TAG, '.mjs');
loadShareCacheMap[pkg] = new VirtualModule(pkg, LOAD_SHARE_TAG, '.js');
const filepath = loadShareCacheMap[pkg].getPath();
return filepath;
}
export function writeLoadShareModule(pkg: string, shareItem: ShareItem, command: string) {
// Generate ESM-compatible code to fix Vite 7/Rolldown compatibility
// The previous CJS code (require + module.exports + top-level await) caused syntax errors
// because Rolldown wraps CJS in a function where top-level await is invalid
loadShareCacheMap[pkg].writeSync(`
;() => import(${JSON.stringify(getPreBuildLibImportId(pkg))}).catch(() => {});
// dev uses dynamic import to separate chunks
${command !== 'build' ? `;() => import(${JSON.stringify(pkg)}).catch(() => {});` : ''}
// Use dynamic import instead of require for ESM compatibility
const runtimeModule = await import("${virtualRuntimeInitStatus.getImportId()}")
const {initPromise} = runtimeModule
const res = initPromise.then(runtime => runtime.loadShare(${JSON.stringify(pkg)}, {
customShareInfo: {shareConfig:{
singleton: ${shareItem.shareConfig.singleton},
strictVersion: ${shareItem.shareConfig.strictVersion},
requiredVersion: ${JSON.stringify(shareItem.shareConfig.requiredVersion)}
}}
}))
const exportModule = ${command !== 'build' ? '/*mf top-level-await placeholder replacement mf*/' : 'await '}res.then(factory => factory())
// Use ESM export instead of module.exports to avoid CJS wrapper issues
export default exportModule
`);
if (command === 'build') {
// Build mode: Use ESM syntax to fix Vite 7/Rolldown compatibility
// Rolldown wraps CJS (require + module.exports) in a function, breaking top-level await
loadShareCacheMap[pkg].writeSync(`
import { initPromise } from "${virtualRuntimeInitStatus.getImportId()}"
;() => import(${JSON.stringify(getPreBuildLibImportId(pkg))}).catch(() => {});
const res = initPromise.then(runtime => runtime.loadShare(${JSON.stringify(pkg)}, {
customShareInfo: {shareConfig:{
singleton: ${shareItem.shareConfig.singleton},
strictVersion: ${shareItem.shareConfig.strictVersion},
requiredVersion: ${JSON.stringify(shareItem.shareConfig.requiredVersion)}
}}
}))
const exportModule = await res.then(factory => factory())
export default exportModule
`);
} else {
// Dev mode: Use original CJS syntax for compatibility with existing plugins
loadShareCacheMap[pkg].writeSync(`
;() => import(${JSON.stringify(getPreBuildLibImportId(pkg))}).catch(() => {});
;() => import(${JSON.stringify(pkg)}).catch(() => {});
const {initPromise} = require("${virtualRuntimeInitStatus.getImportId()}")
const res = initPromise.then(runtime => runtime.loadShare(${JSON.stringify(pkg)}, {
customShareInfo: {shareConfig:{
singleton: ${shareItem.shareConfig.singleton},
strictVersion: ${shareItem.shareConfig.strictVersion},
requiredVersion: ${JSON.stringify(shareItem.shareConfig.requiredVersion)}
}}
}))
const exportModule = /*mf top-level-await placeholder replacement mf*/res.then(factory => factory())
module.exports = exportModule
`);
}
}
Loading