diff --git a/.changeset/fix-esbuild-import-maps.md b/.changeset/fix-esbuild-import-maps.md new file mode 100644 index 00000000000..67cfe50a9e7 --- /dev/null +++ b/.changeset/fix-esbuild-import-maps.md @@ -0,0 +1,7 @@ +--- +"@module-federation/esbuild": patch +--- + +fix: fix import map registration for ESM remote modules + +The import map registration was failing because the runtimePlugin was looking for moduleMap in the host scope instead of fetching it from the remote module exports. This fix adds explicit synchronous import map registration in the host initialization code that runs after initializeSharing but before any code that imports from remotes. diff --git a/apps/esbuild/build/build-common.js b/apps/esbuild/build/build-common.js index dffd6cbd3b6..ae684292baf 100644 --- a/apps/esbuild/build/build-common.js +++ b/apps/esbuild/build/build-common.js @@ -11,7 +11,7 @@ async function buildProject(projectName, watch) { fs.rmSync(outputPath, { force: true, recursive: true }); - await esbuild.build({ + const buildOptions = { entryPoints: [path.join(projectName, 'main.ts')], outdir: outputPath, bundle: true, @@ -28,8 +28,15 @@ async function buildProject(projectName, watch) { require(path.join('../', projectName, 'federation.config.js')), ), ], - watch, - }); + }; + + if (watch) { + const ctx = await esbuild.context(buildOptions); + await ctx.watch(); + console.log(`Watching ${projectName} for changes...`); + } else { + await esbuild.build(buildOptions); + } ['index.html', 'favicon.ico', 'styles.css'].forEach((file) => { fs.copyFileSync(path.join(projectName, file), path.join(outputPath, file)); diff --git a/packages/esbuild/src/adapters/lib/containerReference.ts b/packages/esbuild/src/adapters/lib/containerReference.ts index 653f91575d4..484b2b0acb4 100644 --- a/packages/esbuild/src/adapters/lib/containerReference.ts +++ b/packages/esbuild/src/adapters/lib/containerReference.ts @@ -59,28 +59,38 @@ export const buildFederationHost = (config: NormalizedFederationConfig) => { const runtimePlugin = () => ({ name: 'import-maps-plugin', async init(args) { - const remotePrefetch = args.options.remotes.map(async (remote) => { - console.log('remote', remote); - if (remote.type === 'esm') { - await import(remote.entry); - } - }); - - await Promise.all(remotePrefetch); - if (typeof moduleMap !== 'undefined') { - const map = Object.keys(moduleMap).reduce((acc, expose) => { - const importMap = importShim.getImportMap().imports; - const key = args.origin.name + expose.replace('.', ''); - if (!importMap[key]) { - const encodedModule = encodeInlineESM( - createVirtualRemoteModule(args.origin.name, key, moduleMap[expose].exports) - ); - acc[key] = encodedModule; + // Load all remotes and collect their moduleMaps + const remotesWithModuleMaps = await Promise.all( + args.options.remotes.map(async (remote) => { + console.log('remote', remote); + if (remote.type === 'esm') { + const remoteModule = await import(remote.entry); + return { remote, moduleMap: remoteModule.moduleMap }; + } + return { remote, moduleMap: null }; + }) + ); + + // Build import map entries for all remote modules + const allImports = {}; + for (const { remote, moduleMap } of remotesWithModuleMaps) { + if (moduleMap && typeof moduleMap === 'object') { + for (const expose of Object.keys(moduleMap)) { + const currentImportMap = importShim.getImportMap().imports; + // Use remote name + expose path (e.g., "mfe1/component") + const key = remote.name + expose.replace('.', ''); + if (!currentImportMap[key] && !allImports[key]) { + const encodedModule = encodeInlineESM( + createVirtualRemoteModule(remote.name, key, moduleMap[expose].exports) + ); + allImports[key] = encodedModule; + } } - return acc; - }, {}); + } + } - await importShim.addImportMap({ imports: map }); + if (Object.keys(allImports).length > 0) { + await importShim.addImportMap({ imports: allImports }); } return args; @@ -96,6 +106,33 @@ export const buildFederationHost = (config: NormalizedFederationConfig) => { await Promise.all(mfHoZJ92.initializeSharing('default', 'version-first')); + // Ensure import maps are registered before any code that uses remotes runs + const remotesList = ${remoteConfigs}; + const allImports = {}; + for (const remote of remotesList) { + if (remote.type === 'esm') { + try { + const remoteModule = await import(remote.entry); + const moduleMap = remoteModule.moduleMap; + if (moduleMap && typeof moduleMap === 'object') { + for (const expose of Object.keys(moduleMap)) { + const key = remote.name + expose.replace('.', ''); + if (!allImports[key]) { + const encodedModule = encodeInlineESM( + createVirtualRemoteModule(remote.name, key, moduleMap[expose].exports) + ); + allImports[key] = encodedModule; + } + } + } + } catch (e) { + console.error('Failed to load remote:', remote.name, e); + } + } + } + if (Object.keys(allImports).length > 0) { + await importShim.addImportMap({ imports: allImports }); + } `; };