feat(esbuild): rebuild module federation plugin architecture#4389
feat(esbuild): rebuild module federation plugin architecture#4389ScriptedAlchemy wants to merge 57 commits intomainfrom
Conversation
Rewrote the esbuild plugin from the ground up for proper Module Federation support: ARCHITECTURE: - Uses @module-federation/runtime directly (no webpack runtime emulation) - Clean ESM virtual module system with esbuild namespaces - Top-level await for async boundaries (shared negotiation, remote loading) - Code splitting for shared dependency chunks SHARED MODULES: - Intercepts shared dependency imports via onResolve/onLoad hooks - Generates virtual proxy modules that call loadShare() from MF runtime - Bundled fallback versions as separate chunks via dynamic imports - Version negotiation between containers through the share scope - Supports singleton, strictVersion, requiredVersion, eager configuration REMOTE MODULES: - Intercepts remote imports (e.g., 'mfe1/component') via prefix matching - Generates virtual proxy modules that call loadRemote() from MF runtime - Runtime handles container loading, init(), and get() protocol - Default export forwarding for seamless component imports CONTAINER ENTRY (remoteEntry.js): - Generates standard MF container with get()/init() exports - get(module) dynamically imports exposed modules - init(shareScope) negotiates shared deps with host via runtime - Separate chunk generation for exposed modules RUNTIME INITIALIZATION: - Injects runtime init import at top of entry points - ESM evaluation order ensures init completes before app code - Configures remotes, shared deps, and share strategy - Initializes sharing for cross-container negotiation MANIFEST: - Generates mf-manifest.json for runtime discovery - Includes shared deps, remotes, exposes, and build metadata Also added @module-federation/runtime as dependency/peerDependency and updated exports from index.ts and build.ts.
- Shared module proxy now correctly handles subpath imports (e.g., 'react/jsx-runtime' when 'react' is shared) - Added comprehensive README with architecture docs, examples, and API reference - Improved code generation with proper subpath fallback chains
- Simplified remote module proxy to use clean default + __mfModule exports - Removed broken dynamic export generation attempts - Updated shell example to use default import pattern for remote modules - Remote exports are loaded at runtime via loadRemote() and the module object is available through the default export
|
Cursor Agent can help with this pull request. Just |
🦋 Changeset detectedLatest commit: 28a4035 The changes in this PR will be included in the next version bump. This PR includes changesets to release 1 package
Not sure what this means? Click here to learn what changesets are. Click here if you're a maintainer who wants to add another changeset to this PR |
✅ Deploy Preview for module-federation-docs ready!
To edit notification comments on pull requests, go to your Netlify project configuration. |
Bundle Size Report3 package(s) changed, 38 unchanged.
Total dist: 6.65 MB (-67821 B (-1.0%)) |
- Remove dead postProcessContainerEntry code (new container uses proper dynamic imports, no __MODULE_MAP__ placeholder needed) - Fix hook registration order: remote hooks now registered before shared hooks so remote names take priority over shared package names - Clean up subpath shared module proxy: remove unreachable try/catch - Forward initScope parameter in container init() to runtime - Remove unused createExactFilter utility function - Reduce plugin.mjs from 28.5kB to 26.5kB by removing dead code
Features added: - shareStrategy passthrough: config.shareStrategy now correctly flows to both runtime init and container init code (was hardcoded) - Eager shared modules: shared deps with eager:true now use static imports instead of dynamic imports, loaded synchronously at init time - HTTPS remote support: name@https://... format now correctly parsed (was only matching @http, missing @https) - Filename normalization: withFederation() now ensures .js extension on filename to prevent container entry matching failures Test suite (62 tests): - generateRuntimeInitCode: 11 tests covering init generation, remotes, shared config, eager modules, shareStrategy, name@url parsing - generateContainerEntryCode: 9 tests covering get/init exports, module map, shared config, multiple exposes, eager, strategy - generateSharedProxyCode: 5 tests covering loadShare, fallback, subpath imports, scoped packages - generateRemoteProxyCode: 7 tests covering loadRemote, default export, error handling, top-level await - moduleFederationPlugin: 5 tests covering plugin creation with various config shapes - esbuild integration: 6 tests running actual esbuild builds verifying host builds, container builds, format/splitting auto-set, metafile generation, runtime init injection, remote virtual modules - withFederation: 8 tests covering config normalization, filename extension handling, defaults - Edge cases: 11 tests covering scoped packages, multiple remotes, multiple shared deps, empty configs, special characters, deep paths
Removed files (no longer imported after the plugin rewrite): adapters/lib/: - containerPlugin.ts - old webpack-emulating container (replaced by plugin.ts) - containerReference.ts - old host init with import maps (replaced by plugin.ts) - linkRemotesPlugin.ts - old remote handling via externals (replaced by plugin.ts) - commonjs.ts - CJS-to-ESM transform (no longer needed) - lexer.ts - string parser used only by commonjs.ts - utils.ts - utility functions used only by commonjs.ts - transform.ts - esbuild transform wrapper (unused) - react-replacements.ts - React CJS path mappings (unused) lib/core/: - build-adapter.ts - abstract build adapter (unused) - createContainerTemplate.ts - old 181-line webpack runtime emulation (replaced) - federation-options.ts - options interface (unused) - write-federation-info.ts - old manifest writer (replaced by manifest.ts) Also cleaned: - Removed dead normalizeSharedMappings function from with-native-federation.ts - Removed unused MappedPath import from federation-config.ts - Removed dead comment block from plugin.ts - Removed 3 unused npm dependencies: @chialab/esbuild-plugin-commonjs, @hyrious/esbuild-plugin-commonjs, @module-federation/sdk Net: -1,352 lines of dead code removed.
New features implemented for maximum parity with enhanced webpack plugin: SHARE SCOPE: - Global shareScope config (defaults to 'default', configurable) - Per-shared-module shareScope override for isolated scoping - Per-remote shareScope override - shareScope flows through to initializeSharing and initShareScopeMap RUNTIME PLUGINS: - runtimePlugins config accepts array of file paths / package names - Plugins are imported and injected into the MF runtime init - Works in both host runtime init and container entry SHARED MODULE FEATURES: - import:false - disable local fallback, module must come from scope - shareKey - custom key in share scope (defaults to package name) - packageName - explicit package name for version auto-detection - Auto version detection from node_modules package.json PUBLIC PATH: - publicPath config flows to manifest generation - Defaults to 'auto' REMOTE FEATURES: - Per-remote shareScope config via NormalizedRemoteConfig object - Remote config accepts both string URLs and config objects - withFederation normalizes both formats CONFIG NORMALIZATION: - withFederation passes through all new fields - Remote config objects normalized to NormalizedRemoteConfig - All shared config fields (import, shareKey, shareScope, packageName) preserved through normalization TYPES: - NormalizedRemoteConfig interface for advanced remote config - All new fields added to NormalizedSharedConfig - NormalizedFederationConfig extended with shareScope, runtimePlugins, publicPath CODE GENERATION: - Shared config builder extracted to buildSharedCodeEntries() for reuse - Empty module namespace (mf-empty) for import:false fallback handling - Runtime plugins import/injection code generation Tests: 83 passing (was 62), covering all new features
|
@codex review pr deeply for feature parity |
There was a problem hiding this comment.
💡 Codex Review
Here are some automated review suggestions for this pull request.
Reviewed commit: 6e3d7fe01c
ℹ️ About Codex in GitHub
Your team has set up Codex to review pull requests in this repo. Reviews are triggered when you
- Open a pull request for review
- Mark a draft as ready
- Comment "@codex review".
If Codex has suggestions, it will comment; otherwise it will react with 👍.
Codex can also answer questions or update the PR. Try commenting "@codex address that feedback".
…rage Modeled after webpack enhanced plugin test patterns (configCases, unit, integration). Test categories and counts: - generateRuntimeInitCode: 27 tests - Basic init generation, container name, all remote entries - name@http and name@https parsing, plain URL fallback - Remote type:esm, per-remote shareScope - Shared config: version/scope/get, singleton/strictVersion/eager booleans - Eager static imports vs non-eager dynamic imports - import:false (no fallback), custom shareKey, per-shared shareScope - Global shareScope, shareStrategy, initializeSharing with await/try-catch - runtimePlugins injection (single, multiple, function-or-object pattern) - Empty remotes, empty shared, multiple shared deps - generateContainerEntryCode: 16 tests - get/init function exports, module map with exposes - Factory return from get(), error for unknown module - Multiple exposes (including root '.'), initShareScopeMap/initOptions - initScope forwarding, initializeSharing call - shareStrategy, custom shareScope, shared deps - Eager shared, runtimePlugins, empty exposes, import:false - generateSharedProxyCode: 13 tests - loadShare call, MF runtime import, fallback dynamic import - Default export with 'default' check, subpath imports - Catch for subpath loadShare failure - import:false (throw error, no fallback) - Custom shareKey in loadShare, scoped packages, scoped subpaths - Packages with dots, console.warn on failure - generateRemoteProxyCode: 9 tests - loadRemote call, runtime import, top-level await - Throw on null result, export default, prefer module.default - __mfModule export, deep paths, dashes/underscores in names - moduleFederationPlugin: 6 tests - Plugin name/setup, minimal config, host/remote/combined configs - Full config with all options (shareScope, runtimePlugins, publicPath, shareStrategy) - esbuild integration: 12 tests - Host build with shared deps (externalized) - Container build with exposes (absolute paths) - Auto-set format/splitting, metafile generation - Runtime init injection into entry, no injection when not needed - Remote imports as virtual modules (loadRemote in output) - Valid ESM output (no module.exports) - Container entry output contains get/init functions - Multiple shared deps build, eager shared dep build - withFederation normalization: 21 tests - Basic normalization, filename .js/.mjs handling, defaults - Shared config defaults (singleton, strictVersion, requiredVersion) - Pass-through: shareScope, shareStrategy, runtimePlugins, publicPath - Remote string vs config object normalization, array external - Shared advanced: import:false, shareKey, shareScope, packageName, eager - Edge cases: 13 tests - Container with no shared, host with no remotes, minimal config - import:false + custom shareKey combined - Multiple share scopes in same config - Version auto-detection from requiredVersion - Mixed eager/non-eager shared modules - Special characters: dot paths, scoped packages, numbers, underscores - runtimePlugins: single, multiple, function-or-object dispatch
collect-exports.ts:
- Implement re-export following (was TODO): recursively resolve
'export * from' and CJS reexports up to depth 5, with cycle detection
- Remove dead resolvePackageJson export (never imported)
- Replace console.log with console.warn for error logging
- Deduplicate export names in result
plugin.ts:
- Remove unused _fallback parameter from parseRemoteEntry()
- Replace 'any' type on getEntryPaths with proper EntryPoints union type
(string[] | {in,out}[] | Record<string,string> | undefined)
with-native-federation.ts:
- Replace Record<string, any> with proper typed remotes map
logger.ts:
- Replace @ts-ignore with @ts-expect-error with descriptive message
- Remove eslint-disable comment (no longer needed)
share-utils.ts:
- Replace console.log with logger.warn for missing entry point warning
Dead files removed: - src/lib/utils/mapped-paths.ts: zero importers since normalizeSharedMappings was removed (50 lines) - src/resolve/esm-resolver.mjs: not imported by any source, not in package.json exports (19 lines) - src/resolve/package.json: companion to esm-resolver Dead dependency removed: - json5: only used by mapped-paths.ts which is now deleted Build config cleanup: - rslib.config.ts: removed copy directive for src/resolve (no longer exists) Code cleanup: - collect-exports.ts: removed unnecessary 'export' on 'resolve' constant (only used internally by getExports, not imported elsewhere) All 15 remaining source files verified reachable from entry points. 117 tests pass, build and types clean.
…est remotes, add changeset
P1 (shared fallback path): When shareKey differs from the package name,
the fallback import path now correctly uses the actual package name
(e.g., __mf_fallback__/react) instead of the shareKey
(e.g., __mf_fallback__/my-react). loadShare() still uses the shareKey
for scope negotiation. This ensures the local fallback can always find
the installed package on disk.
P2 (manifest object remotes): When remotes are configured as objects
({ entry: '...', shareScope: '...' }) via withFederation, the manifest
now correctly extracts the entry URL from the object. Previously it
would write an empty string, breaking tooling that reads mf-manifest.json.
Also added changeset for the minor version bump.
Added 2 regression tests for P1 (119 total).
Address maintainer feedback: remote component imports now work cleanly without destructuring, matching the standard MF pattern. Before (required workaround): import RemoteModule from 'mfe1/component'; const RemoteApp = RemoteModule.App || RemoteModule; After (clean default import): import RemoteApp from 'mfe1/component'; Changes: - mfe1/app.tsx: changed to 'export default function App()' instead of named 'export function App()'. This is the standard pattern for MF exposed modules across the ecosystem. - shell/app.tsx: simplified to 'import RemoteApp from mfe1/component' with no destructuring needed. - README: updated docs explaining why default exports are recommended for exposed modules, and why named imports from remotes differ from webpack (real ESM requires static export declarations vs webpack's own module system that resolves exports dynamically at runtime).
Users can now write standard named imports from remote modules:
import { App } from 'mfe1/component';
import Default, { helper } from 'mfe1/utils';
import * as Lib from 'mfe1/lib';
The plugin automatically transforms these at build time into a pattern
esbuild can handle. For example:
import { App } from 'mfe1/component';
// is transformed to:
import { __mfModule as __mfR0 } from 'mfe1/component';
const { App } = __mfR0;
This is transparent to the user - they write standard MF imports just
like webpack, and the plugin handles the ESM static export limitation.
Implementation:
- Added transformRemoteImports() using es-module-lexer to parse source
files and rewrite import statements from remote modules
- The onLoad hook for source files now intercepts ALL files that import
from remotes (not just entry points), applies the transform, and
also injects runtime init for entry points
- Handles all import forms: named, aliased (as), default+named,
namespace (*), and correctly skips type-only imports
- The remote proxy's __mfModule export provides the full loaded module
for destructuring
Example app reverted to natural named import syntax:
import { App as RemoteApp } from 'mfe1/component';
Tests: 135 passing (+16 new: 13 transform unit tests + 3 integration)
…sues
Security (resolves 4 'Improper code sanitization' CodeQL alerts):
- Added safeStr() wrapper around JSON.stringify for all user-provided
config values embedded in generated code (package names, URLs, paths,
share keys, module specifiers). JSON.stringify is safe for JS string
embedding (escapes quotes, backslashes, control chars, unicode), but
the explicit wrapper makes the sanitization intent visible to both
the security scanner and reviewers.
Bug fixes:
- transformRemoteImports now handles re-exports: export { App } from
'remote/mod' is correctly transformed to import + local binding +
re-export. Previously this would cause an esbuild error.
- Fixed false-positive matching in the transform quick-check: now looks
for the remote name inside quote delimiters ('name/' or 'name') instead
of bare string.includes(name) which would match variable names.
- Added outdir validation warning when splitting is enabled without outdir.
- Removed unused _remoteName parameter from generateRemoteProxyCode.
135 tests passing.
@module-federation/devtools
@module-federation/cli
create-module-federation
@module-federation/data-prefetch
@module-federation/dts-plugin
@module-federation/enhanced
@module-federation/error-codes
@module-federation/esbuild
@module-federation/managers
@module-federation/manifest
@module-federation/metro
@module-federation/metro-plugin-rnc-cli
@module-federation/metro-plugin-rnef
@module-federation/modern-js
@module-federation/modern-js-v3
@module-federation/native-federation-tests
@module-federation/native-federation-typescript
@module-federation/nextjs-mf
@module-federation/node
@module-federation/retry-plugin
@module-federation/rsbuild-plugin
@module-federation/rspack
@module-federation/rspress-plugin
@module-federation/runtime
@module-federation/runtime-core
@module-federation/runtime-tools
@module-federation/sdk
@module-federation/storybook-addon
@module-federation/third-party-dts-extractor
@module-federation/treeshake-frontend
@module-federation/treeshake-server
@module-federation/typescript
@module-federation/utilities
@module-federation/webpack-bundler-runtime
@module-federation/bridge-react
@module-federation/bridge-react-webpack-plugin
@module-federation/bridge-shared
@module-federation/bridge-vue3
@module-federation/inject-external-runtime-core-plugin
commit: |
Android Release APK for all devicesNote: if the download link expires, please re-run the workflow to generate a new build. Generated at 2026-02-14T23:25:37.297Z UTC |
iOS Release APP for simulatorsNote: if the download link expires, please re-run the workflow to generate a new build. Generated at 2026-02-14T23:18:40.072Z UTC |
…-federation-plugin-2869 # Conflicts: # pnpm-lock.yaml
…-federation-plugin-2869 # Conflicts: # pnpm-lock.yaml
Resolve the dts-plugin TYPE-001 failure by correcting package entry paths for workspace dependencies and updating RawSource usage for webpack typings. Co-authored-by: Cursor <cursoragent@cursor.com>
Update pnpm-lock.yaml to match branch package specifiers so checkout-install and publish-preview no longer fail with ERR_PNPM_OUTDATED_LOCKFILE. Co-authored-by: Cursor <cursoragent@cursor.com>
Restore sdk and error-codes export paths to the filenames emitted by the current build so CI package resolution no longer fails on these branches. Co-authored-by: Cursor <cursoragent@cursor.com>
…-federation-plugin-2869 # Conflicts: # pnpm-lock.yaml
Summary
@module-federation/esbuildfor full Module Federation runtime/container support with expanded test coverageChanged Packages
@module-federation/esbuild@module-federation/bridge-react@module-federation/enhancedChangesets
.changeset/esbuild-module-federation-rebuild.md(@module-federation/esbuild: minor).changeset/real-schools-breathe.md(@module-federation/bridge-react: patch,@module-federation/enhanced: patch)Base
mainSingle-PR Review Guide (No Additional PRs)
This PR will remain a single PR. Improvements are being done in-place (no stacked or follow-up PR splitting).
Suggested Review Order
In-Place Scope Trim Checklist