Skip to content
Open
Show file tree
Hide file tree
Changes from 3 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
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 [email protected] --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
37 changes: 37 additions & 0 deletions src/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@ import {
} from './utils/normalizeModuleFederationOptions';
import normalizeOptimizeDepsPlugin from './utils/normalizeOptimizeDeps';
import VirtualModule from './utils/VirtualModule';
// wrapManualChunks not used - direct function assignment is simpler and works better
import {
getHostAutoInitImportId,
getHostAutoInitPath,
Expand Down Expand Up @@ -94,6 +95,42 @@ function federation(mfUserOptions: ModuleFederationOptions): Plugin[] {
config.optimizeDeps?.include?.push(virtualDir);
config.optimizeDeps?.needsInterop?.push(virtualDir);
config.optimizeDeps?.needsInterop?.push(getLocalSharedImportMapPath());

// FIX: Isolate preload helper to prevent deadlock with loadShare TLA
// This prevents a circular deadlock where:
// 1. hostInit imports preload helper from a shared chunk
// 2. That chunk has loadShare TLA waiting for initPromise
// 3. initPromise only resolves after remoteEntry.init() is called
// 4. But hostInit can't call init() until the shared chunk loads → DEADLOCK
if (_command === 'build') {
config.build = config.build || {};
config.build.rollupOptions = config.build.rollupOptions || {};
config.build.rollupOptions.output = config.build.rollupOptions.output || {};

const output = Array.isArray(config.build.rollupOptions.output)
? config.build.rollupOptions.output[0]
: config.build.rollupOptions.output;

const existingManualChunks = output.manualChunks;
output.manualChunks = (id: string, meta: any) => {
// Isolate preload helper to prevent deadlock
if (
id.includes('vite/preload-helper') ||
id.includes('vite/modulepreload-polyfill') ||
id.includes('commonjsHelpers')
) {
return 'preload-helper';
}
// Call existing manualChunks if it exists
if (typeof existingManualChunks === 'function') {
return existingManualChunks(id, meta);
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I'm wondering if this part could cause breaking changes for anyone relying on manual checks since a set of vite plugins (line 118-120) won't be checked anymore.

}
if (existingManualChunks && (existingManualChunks as any)[id]) {
return (existingManualChunks as any)[id];
}
return undefined;
};
}
},
},
...pluginManifest(),
Expand Down
6 changes: 3 additions & 3 deletions src/plugins/pluginProxySharedModule_preBuild.ts
Original file line number Diff line number Diff line change
Expand Up @@ -48,9 +48,9 @@ export function proxySharedModule(options: {
config(config: UserConfig, { command }) {
(config.resolve as any).alias.push(
...Object.keys(shared).map((key) => {
const pattern = key.endsWith('/')
? `(^${key.replace(/\/$/, '')}(\/.+)?$)`
: `(^${key}$)`;
// FIX: When key ends with '/', only match subpaths (e.g., 'react/' matches 'react/jsx-runtime' but NOT 'react')
// The previous regex (/.+)? was optional, incorrectly matching the base package too
const pattern = key.endsWith('/') ? `(^${key.replace(/\/$/, '')}/.+$)` : `(^${key}$)`;
return {
// Intercept all shared requests and proxy them to loadShare
find: new RegExp(pattern),
Expand Down
24 changes: 18 additions & 6 deletions src/virtualModules/virtualRemotes.ts
Original file line number Diff line number Diff line change
Expand Up @@ -24,10 +24,22 @@ export function getUsedRemotesMap() {
return usedRemotesMap;
}
export function generateRemotes(id: string, command: string) {
return `
const {initPromise} = require("${virtualRuntimeInitStatus.getImportId()}")
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)
module.exports = 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
`;
}
}
Loading