Skip to content

Commit 0b9b3dc

Browse files
fix: resolve circular deadlock with Vite preload helper and loadShare TLA
The build was hanging due to a circular dependency where: 1. hostInit imports preload helper from a shared chunk 2. That chunk contains loadShare TLA waiting for initPromise 3. initPromise only resolves after remoteEntry.init() is called 4. hostInit can't call init() until the shared chunk loads → DEADLOCK Changes: - Isolate vite/preload-helper, vite/modulepreload-polyfill, and commonjsHelpers into a separate 'preload-helper' chunk via manualChunks - Fix shared module alias regex: patterns ending with '/' (e.g., 'react/') now correctly match only subpaths ('react/jsx-runtime') and not the base package ('react') - Use globalThis singleton for initPromise to prevent duplicate instances This ensures the preload helper is available before any TLA-blocked chunks need to load, breaking the circular dependency.
1 parent 762bd5f commit 0b9b3dc

File tree

4 files changed

+55
-42
lines changed

4 files changed

+55
-42
lines changed

src/index.ts

Lines changed: 37 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@ import {
1515
} from './utils/normalizeModuleFederationOptions';
1616
import normalizeOptimizeDepsPlugin from './utils/normalizeOptimizeDeps';
1717
import VirtualModule from './utils/VirtualModule';
18+
// wrapManualChunks not used - direct function assignment is simpler and works better
1819
import {
1920
getHostAutoInitImportId,
2021
getHostAutoInitPath,
@@ -94,6 +95,42 @@ function federation(mfUserOptions: ModuleFederationOptions): Plugin[] {
9495
config.optimizeDeps?.include?.push(virtualDir);
9596
config.optimizeDeps?.needsInterop?.push(virtualDir);
9697
config.optimizeDeps?.needsInterop?.push(getLocalSharedImportMapPath());
98+
99+
// FIX: Isolate preload helper to prevent deadlock with loadShare TLA
100+
// This prevents a circular deadlock where:
101+
// 1. hostInit imports preload helper from a shared chunk
102+
// 2. That chunk has loadShare TLA waiting for initPromise
103+
// 3. initPromise only resolves after remoteEntry.init() is called
104+
// 4. But hostInit can't call init() until the shared chunk loads → DEADLOCK
105+
if (_command === 'build') {
106+
config.build = config.build || {};
107+
config.build.rollupOptions = config.build.rollupOptions || {};
108+
config.build.rollupOptions.output = config.build.rollupOptions.output || {};
109+
110+
const output = Array.isArray(config.build.rollupOptions.output)
111+
? config.build.rollupOptions.output[0]
112+
: config.build.rollupOptions.output;
113+
114+
const existingManualChunks = output.manualChunks;
115+
output.manualChunks = (id: string, meta: any) => {
116+
// Isolate preload helper to prevent deadlock
117+
if (
118+
id.includes('vite/preload-helper') ||
119+
id.includes('vite/modulepreload-polyfill') ||
120+
id.includes('commonjsHelpers')
121+
) {
122+
return 'preload-helper';
123+
}
124+
// Call existing manualChunks if it exists
125+
if (typeof existingManualChunks === 'function') {
126+
return existingManualChunks(id, meta);
127+
}
128+
if (existingManualChunks && (existingManualChunks as any)[id]) {
129+
return (existingManualChunks as any)[id];
130+
}
131+
return undefined;
132+
};
133+
}
97134
},
98135
},
99136
...pluginManifest(),

src/plugins/pluginProxySharedModule_preBuild.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -48,9 +48,9 @@ export function proxySharedModule(options: {
4848
config(config: UserConfig, { command }) {
4949
(config.resolve as any).alias.push(
5050
...Object.keys(shared).map((key) => {
51-
const pattern = key.endsWith('/')
52-
? `(^${key.replace(/\/$/, '')}(\/.+)?$)`
53-
: `(^${key}$)`;
51+
// FIX: When key ends with '/', only match subpaths (e.g., 'react/' matches 'react/jsx-runtime' but NOT 'react')
52+
// The previous regex (/.+)? was optional, incorrectly matching the base package too
53+
const pattern = key.endsWith('/') ? `(^${key.replace(/\/$/, '')}/.+$)` : `(^${key}$)`;
5454
return {
5555
// Intercept all shared requests and proxy them to loadShare
5656
find: new RegExp(pattern),

src/virtualModules/virtualRuntimeInitStatus.ts

Lines changed: 0 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ export const virtualRuntimeInitStatus = new VirtualModule('runtimeInit');
33
export function writeRuntimeInitStatus() {
44
// Use globalThis singleton to ensure only one initPromise exists
55
const globalKey = `__mf_init__${virtualRuntimeInitStatus.getImportId()}__`;
6-
// This module is imported by both dev and build modes
7-
// We use a dual-export pattern that works with both CJS require() and ESM import
86
virtualRuntimeInitStatus.writeSync(`
97
const globalKey = ${JSON.stringify(globalKey)}
108
if (!globalThis[globalKey]) {
@@ -19,10 +17,6 @@ export function writeRuntimeInitStatus() {
1917
initReject
2018
}
2119
}
22-
// Dual exports: CJS for dev mode (require), ESM for build mode (import)
2320
module.exports = globalThis[globalKey]
24-
export const initPromise = globalThis[globalKey].initPromise
25-
export const initResolve = globalThis[globalKey].initResolve
26-
export const initReject = globalThis[globalKey].initReject
2721
`);
2822
}

src/virtualModules/virtualShared_preBuild.ts

Lines changed: 15 additions & 33 deletions
Original file line numberDiff line numberDiff line change
@@ -37,37 +37,19 @@ export function getLoadShareModulePath(pkg: string): string {
3737
return filepath;
3838
}
3939
export function writeLoadShareModule(pkg: string, shareItem: ShareItem, command: string) {
40-
if (command === 'build') {
41-
// Build mode: Use ESM syntax to fix Vite 7/Rolldown compatibility
42-
// Rolldown wraps CJS (require + module.exports) in a function, breaking top-level await
43-
loadShareCacheMap[pkg].writeSync(`
44-
import { initPromise } from "${virtualRuntimeInitStatus.getImportId()}"
45-
;() => import(${JSON.stringify(getPreBuildLibImportId(pkg))}).catch(() => {});
46-
const res = initPromise.then(runtime => runtime.loadShare(${JSON.stringify(pkg)}, {
47-
customShareInfo: {shareConfig:{
48-
singleton: ${shareItem.shareConfig.singleton},
49-
strictVersion: ${shareItem.shareConfig.strictVersion},
50-
requiredVersion: ${JSON.stringify(shareItem.shareConfig.requiredVersion)}
51-
}}
52-
}))
53-
const exportModule = await res.then(factory => factory())
54-
export default exportModule
55-
`);
56-
} else {
57-
// Dev mode: Use original CJS syntax for compatibility with existing plugins
58-
loadShareCacheMap[pkg].writeSync(`
59-
;() => import(${JSON.stringify(getPreBuildLibImportId(pkg))}).catch(() => {});
60-
;() => import(${JSON.stringify(pkg)}).catch(() => {});
61-
const {initPromise} = require("${virtualRuntimeInitStatus.getImportId()}")
62-
const res = initPromise.then(runtime => runtime.loadShare(${JSON.stringify(pkg)}, {
63-
customShareInfo: {shareConfig:{
64-
singleton: ${shareItem.shareConfig.singleton},
65-
strictVersion: ${shareItem.shareConfig.strictVersion},
66-
requiredVersion: ${JSON.stringify(shareItem.shareConfig.requiredVersion)}
67-
}}
68-
}))
69-
const exportModule = /*mf top-level-await placeholder replacement mf*/res.then(factory => factory())
70-
module.exports = exportModule
71-
`);
72-
}
40+
loadShareCacheMap[pkg].writeSync(`
41+
;() => import(${JSON.stringify(getPreBuildLibImportId(pkg))}).catch(() => {});
42+
// dev uses dynamic import to separate chunks
43+
${command !== 'build' ? `;() => import(${JSON.stringify(pkg)}).catch(() => {});` : ''}
44+
const {initPromise} = require("${virtualRuntimeInitStatus.getImportId()}")
45+
const res = initPromise.then(runtime => runtime.loadShare(${JSON.stringify(pkg)}, {
46+
customShareInfo: {shareConfig:{
47+
singleton: ${shareItem.shareConfig.singleton},
48+
strictVersion: ${shareItem.shareConfig.strictVersion},
49+
requiredVersion: ${JSON.stringify(shareItem.shareConfig.requiredVersion)}
50+
}}
51+
}))
52+
const exportModule = ${command !== 'build' ? '/*mf top-level-await placeholder replacement mf*/' : 'await '}res.then(factory => factory())
53+
module.exports = exportModule
54+
`);
7355
}

0 commit comments

Comments
 (0)