-
-
Notifications
You must be signed in to change notification settings - Fork 401
feat(esbuild): rebuild module federation plugin architecture #4389
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Closed
ScriptedAlchemy
wants to merge
57
commits into
main
from
cursor/esbuild-module-federation-plugin-2869
Closed
Changes from 7 commits
Commits
Show all changes
57 commits
Select commit
Hold shift + click to select a range
47cadbb
feat(esbuild): completely redesign and rebuild Module Federation plugin
cursoragent 33ad656
feat(esbuild): add subpath handling for shared modules, update README
cursoragent 52cd0ea
fix(esbuild): clean up remote proxy code generation, update example app
cursoragent 1f007ee
fix(esbuild): address review issues in plugin implementation
cursoragent 41bb05c
feat(esbuild): add missing features, comprehensive test suite (62 tests)
cursoragent 34ed493
refactor(esbuild): remove 12 dead files and 3 unused dependencies
cursoragent 6e3d7fe
feat(esbuild): implement full webpack MF parity features (83 tests)
cursoragent d2ea75a
test(esbuild): expand test suite to 117 tests with webpack-style cove…
cursoragent ecae579
fix(esbuild): address all code review comments and annotations
cursoragent f2c5e9d
refactor(esbuild): remove remaining dead code - 3 files, 1 dep
cursoragent 2ed1312
fix(esbuild): address PR review comments - P1 fallback path, P2 manif…
cursoragent 0954cf7
fix(esbuild): use default export pattern for remote components
cursoragent 60fd2fa
feat(esbuild): transform named imports from remotes to work like webpack
cursoragent a9b3b36
fix(esbuild): address security scanner warnings and remaining code is…
cursoragent 42aa318
Merge branch 'main' into cursor/esbuild-module-federation-plugin-2869
ScriptedAlchemy 1070735
fix(esbuild): harden codegen sanitization and runtime init entry matc…
ScriptedAlchemy 46fcb9a
fix(esbuild): address review findings in plugin and manifest
ScriptedAlchemy 4513fac
refactor(esbuild): remove dead native federation references and unuse…
ScriptedAlchemy 6d040a1
chore: apply queued repository updates
ScriptedAlchemy dd9e34f
Merge branch 'main' into cursor/esbuild-module-federation-plugin-2869
ScriptedAlchemy c7adba9
fix(router-host-2000): stabilize router e2e on CI
ScriptedAlchemy 6cab664
fix(router-host-2000): stabilize router e2e locally
ScriptedAlchemy 0363b1a
Merge remote-tracking branch 'origin/main' into cursor/esbuild-module…
ScriptedAlchemy 464fa57
Revert "chore: apply queued repository updates"
ScriptedAlchemy 1b2230a
Merge remote-tracking branch 'origin/main' into cursor/esbuild-module…
ScriptedAlchemy c583ab5
fix(enhanced): reduce fallback build races
ScriptedAlchemy 5bebce7
fix(router-host-2000): bind router demo servers to 127.0.0.1
ScriptedAlchemy 351daf1
fix(bridge-react): stabilize router demo e2e in CI
ScriptedAlchemy dadf174
fix(esbuild): reduce measure job nx invocation crashes
ScriptedAlchemy e39e080
Merge remote-tracking branch 'origin/main' into cursor/esbuild-module…
ScriptedAlchemy 77cec83
chore(core): add changeset coverage for pr #4389
ScriptedAlchemy 5ed2555
chore(core): add contextual bridge-react/enhanced changeset
ScriptedAlchemy 0a19ed9
chore(esbuild): merge origin/main
ScriptedAlchemy 5f488ae
test(enhanced): harden reshake fixture bootstrap
ScriptedAlchemy e623e08
test(enhanced): stabilize treeshake fixtures under load
ScriptedAlchemy edce619
Merge branch 'main' into cursor/esbuild-module-federation-plugin-2869
ScriptedAlchemy b0d8f0e
Merge remote-tracking branch 'origin/main' into cursor/esbuild-module…
ScriptedAlchemy 71ebcd7
Merge origin/main into cursor/esbuild-module-federation-plugin-2869
ScriptedAlchemy 5b977ec
Merge origin/main into cursor/esbuild-module-federation-plugin-2869
ScriptedAlchemy f9b8722
Merge origin/main into cursor/esbuild-module-federation-plugin-2869
ScriptedAlchemy 9ddfb29
Merge remote-tracking branch 'origin/main' into cursor/esbuild-module…
ScriptedAlchemy 8daf370
fix(dts-plugin): align workspace entrypoints and RawSource typing
ScriptedAlchemy a62f024
Merge branch 'main' into cursor/esbuild-module-federation-plugin-2869
ScriptedAlchemy 5543638
fix(core): sync lockfile for frozen install checks
ScriptedAlchemy 61ff018
fix(sdk): align package entrypoints with emitted artifacts
ScriptedAlchemy 82c4261
Merge remote-tracking branch 'origin/main' into cursor/esbuild-module…
ScriptedAlchemy d8bfde3
chore(esbuild): revert non-esbuild scope creep changes
ScriptedAlchemy e99f967
chore(esbuild): minimize lockfile update for runtime dependency swap
ScriptedAlchemy b11510f
Merge branch 'main' into cursor/esbuild-module-federation-plugin-2869
ScriptedAlchemy 53e87b1
Merge branch 'main' into cursor/esbuild-module-federation-plugin-2869
ScriptedAlchemy ec4929b
Merge branch 'main' into cursor/esbuild-module-federation-plugin-2869
ScriptedAlchemy 9a7b322
Merge branch 'main' into cursor/esbuild-module-federation-plugin-2869
ScriptedAlchemy 68ecb7d
Merge branch 'main' into cursor/esbuild-module-federation-plugin-2869
ScriptedAlchemy 5e82aa6
Merge branch 'main' into cursor/esbuild-module-federation-plugin-2869
ScriptedAlchemy a349431
Merge branch 'main' into cursor/esbuild-module-federation-plugin-2869
ScriptedAlchemy 31ca80f
Merge branch 'main' into cursor/esbuild-module-federation-plugin-2869
ScriptedAlchemy 28a4035
Merge branch 'main' into cursor/esbuild-module-federation-plugin-2869
ScriptedAlchemy File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -1,113 +1,269 @@ | ||
| # @module-federation/esbuild | ||
|
|
||
| This package provides an esbuild plugin for Module Federation, enabling you to easily share code between independently built and deployed applications. | ||
| Module Federation plugin for esbuild. Enables sharing code between independently built and deployed applications using the Module Federation protocol. | ||
|
|
||
| ## Installation | ||
|
|
||
| Install the package using npm: | ||
|
|
||
| ```bash | ||
| npm install @module-federation/esbuild | ||
| npm install @module-federation/esbuild @module-federation/runtime | ||
| # or | ||
| pnpm add @module-federation/esbuild @module-federation/runtime | ||
| ``` | ||
|
|
||
| ## Usage | ||
| ## Requirements | ||
|
|
||
| - **esbuild** `^0.25.0` | ||
| - **format**: `'esm'` (ESM output is required for dynamic imports and top-level await) | ||
| - **splitting**: `true` (code splitting is required for shared/exposed module chunks) | ||
| - **@module-federation/runtime** must be installed and resolvable | ||
|
|
||
| The plugin will automatically set `format: 'esm'` and `splitting: true` if not already configured. | ||
|
|
||
| To use the Module Federation plugin with esbuild, add it to your esbuild configuration: | ||
| ## Quick Start | ||
|
|
||
| ### 1. Create a Federation Config | ||
|
|
||
| ```js | ||
| // federation.config.js | ||
| const { withFederation } = require('@module-federation/esbuild/build'); | ||
|
|
||
| module.exports = withFederation({ | ||
| name: 'myApp', | ||
| filename: 'remoteEntry.js', | ||
| exposes: { | ||
| './Button': './src/components/Button', | ||
| }, | ||
| remotes: { | ||
| remoteApp: 'http://localhost:3001/remoteEntry.js', | ||
| }, | ||
| shared: { | ||
| react: { singleton: true, version: '^18.2.0' }, | ||
| 'react-dom': { singleton: true, version: '^18.2.0' }, | ||
| }, | ||
| }); | ||
| ``` | ||
|
|
||
| ### 2. Use the Plugin in Your Build | ||
|
|
||
| ```js | ||
| const esbuild = require('esbuild'); | ||
| const path = require('path'); | ||
| const { moduleFederationPlugin } = require('@module-federation/esbuild/plugin'); | ||
| const federationConfig = require('./federation.config.js'); | ||
|
|
||
| async function buildApp() { | ||
| const tsConfig = 'tsconfig.json'; | ||
| const outputPath = path.join('dist', 'host'); | ||
|
|
||
| try { | ||
| await esbuild.build({ | ||
| entryPoints: [path.join('host', 'main.ts')], | ||
| outdir: outputPath, | ||
| bundle: true, | ||
| platform: 'browser', | ||
| format: 'esm', | ||
| mainFields: ['es2020', 'browser', 'module', 'main'], | ||
| conditions: ['es2020', 'es2015', 'module'], | ||
| resolveExtensions: ['.ts', '.tsx', '.mjs', '.js'], | ||
| tsconfig: tsConfig, | ||
| splitting: true, | ||
| plugins: [moduleFederationPlugin(federationConfig)], | ||
| }); | ||
| } catch (err) { | ||
| console.error(err); | ||
| process.exit(1); | ||
| } | ||
| } | ||
| esbuild.build({ | ||
| entryPoints: ['./src/main.tsx'], | ||
| outdir: './dist', | ||
| bundle: true, | ||
| format: 'esm', | ||
| splitting: true, | ||
| plugins: [moduleFederationPlugin(federationConfig)], | ||
| }); | ||
| ``` | ||
|
|
||
| ## How It Works | ||
|
|
||
| ### Architecture | ||
|
|
||
| The plugin uses `@module-federation/runtime` directly for all Module Federation functionality. It works by intercepting module imports via esbuild's plugin hooks and replacing them with virtual modules that use the MF runtime: | ||
|
|
||
| 1. **Shared Modules**: Imports of shared dependencies (e.g., `react`) are replaced with virtual proxy modules that call `loadShare()` from the MF runtime for version negotiation between containers. | ||
|
|
||
| 2. **Remote Modules**: Imports matching remote names (e.g., `remoteApp/Button`) are replaced with virtual proxy modules that call `loadRemote()` to fetch modules from remote containers at runtime. | ||
|
|
||
| 3. **Container Entry**: When `exposes` is configured, a `remoteEntry.js` is generated with standard `get()`/`init()` exports that follow the Module Federation protocol. | ||
|
|
||
| 4. **Runtime Initialization**: Entry points are augmented with runtime initialization code that sets up the MF instance before any app code runs, using ESM top-level await. | ||
|
|
||
| 5. **Manifest**: An `mf-manifest.json` is generated for runtime discovery. | ||
|
|
||
| ### Shared Module Flow | ||
|
|
||
| ``` | ||
| ┌─────────────────────────────────────────────────┐ | ||
| │ import React from 'react' │ | ||
| │ │ │ | ||
| │ ▼ │ | ||
| │ ┌──────────────────────────┐ │ | ||
| │ │ Shared Proxy (virtual) │ │ | ||
| │ │ loadShare('react') │ │ | ||
| │ │ ├─ Share Scope found? │ │ | ||
| │ │ │ ├─ YES: use shared │ │ | ||
| │ │ │ └─ NO: use fallback │───► Bundled react │ | ||
| │ │ └─ return module │ (separate │ | ||
| │ └──────────────────────────┘ chunk) │ | ||
| └─────────────────────────────────────────────────┘ | ||
| ``` | ||
|
|
||
| ### Remote Module Flow | ||
|
|
||
| ``` | ||
| ┌─────────────────────────────────────────────────┐ | ||
| │ import Button from 'remoteApp/Button' │ | ||
| │ │ │ | ||
| │ ▼ │ | ||
| │ ┌──────────────────────────┐ │ | ||
| │ │ Remote Proxy (virtual) │ │ | ||
| │ │ loadRemote('remoteApp/ │ │ | ||
| │ │ Button') │ │ | ||
| │ │ ├─ Load remoteEntry.js │ │ | ||
| │ │ ├─ Call init(shareScope)│ │ | ||
| │ │ ├─ Call get('./Button') │ │ | ||
| │ │ └─ return module │ │ | ||
| │ └──────────────────────────┘ │ | ||
| └─────────────────────────────────────────────────┘ | ||
| ``` | ||
|
|
||
| ## Configuration | ||
|
|
||
| ### `withFederation(config)` | ||
|
|
||
| Normalizes a federation configuration object. Use this to prepare your config before passing it to `moduleFederationPlugin()`. | ||
|
|
||
| ```js | ||
| const { withFederation } = require('@module-federation/esbuild/build'); | ||
| ``` | ||
|
|
||
| #### Config Properties | ||
|
|
||
| | Property | Type | Required | Description | | ||
| |----------|------|----------|-------------| | ||
| | `name` | `string` | Yes | Unique name for this federation container | | ||
| | `filename` | `string` | No | Remote entry filename (default: `'remoteEntry.js'`) | | ||
| | `exposes` | `Record<string, string>` | No | Modules to expose to other containers | | ||
| | `remotes` | `Record<string, string>` | No | Remote containers to consume | | ||
| | `shared` | `Record<string, SharedConfig>` | No | Dependencies to share between containers | | ||
|
|
||
| #### SharedConfig | ||
|
|
||
| | Property | Type | Default | Description | | ||
| |----------|------|---------|-------------| | ||
| | `singleton` | `boolean` | `false` | Only allow a single version of this package | | ||
| | `strictVersion` | `boolean` | `false` | Throw error on version mismatch | | ||
| | `requiredVersion` | `string` | `'*'` | Required semver version range | | ||
| | `version` | `string` | auto | The version of the shared package | | ||
| | `eager` | `boolean` | `false` | Load shared module eagerly | | ||
|
|
||
| buildApp(); | ||
| ### `moduleFederationPlugin(config)` | ||
|
|
||
| Creates the esbuild plugin instance. | ||
|
|
||
| ```js | ||
| const { moduleFederationPlugin } = require('@module-federation/esbuild/plugin'); | ||
| ``` | ||
|
|
||
| // Example of federation.config.js | ||
| ## Examples | ||
|
|
||
| const { withFederation, shareAll } = require('@module-federation/esbuild/build'); | ||
| ### Host Application (Consumer) | ||
|
|
||
| ```js | ||
| // federation.config.js | ||
| const { withFederation } = require('@module-federation/esbuild/build'); | ||
|
|
||
| module.exports = withFederation({ | ||
| name: 'host', | ||
| remotes: { | ||
| mfe1: 'http://localhost:3001/remoteEntry.js', | ||
| }, | ||
| shared: { | ||
| react: { singleton: true, version: '^18.2.0' }, | ||
| 'react-dom': { singleton: true, version: '^18.2.0' }, | ||
| }, | ||
| }); | ||
| ``` | ||
|
|
||
| ```tsx | ||
| // App.tsx - Using remote modules | ||
| import RemoteComponent from 'mfe1/component'; | ||
|
|
||
| export function App() { | ||
| return ( | ||
| <div> | ||
| <h1>Host App</h1> | ||
| <RemoteComponent /> | ||
| </div> | ||
| ); | ||
| } | ||
| ``` | ||
|
|
||
| ### Remote Application (Provider) | ||
|
|
||
| ```js | ||
| // federation.config.js | ||
| const { withFederation } = require('@module-federation/esbuild/build'); | ||
|
|
||
| module.exports = withFederation({ | ||
| name: 'mfe1', | ||
| filename: 'remoteEntry.js', | ||
| exposes: { | ||
| './Component': './src/Component', | ||
| './component': './src/MyComponent', | ||
| }, | ||
| shared: { | ||
| react: { | ||
| singleton: true, | ||
| version: '^18.2.0', | ||
| }, | ||
| 'react-dom': { | ||
| singleton: true, | ||
| version: '^18.2.0', | ||
| }, | ||
| rxjs: { | ||
| singleton: true, | ||
| version: '^7.8.1', | ||
| }, | ||
| ...shareAll({ | ||
| singleton: true, | ||
| strictVersion: true, | ||
| requiredVersion: 'auto', | ||
| includeSecondaries: false, | ||
| }), | ||
| react: { singleton: true, version: '^18.2.0' }, | ||
| 'react-dom': { singleton: true, version: '^18.2.0' }, | ||
| }, | ||
| }); | ||
| ``` | ||
|
|
||
| The `moduleFederationPlugin` accepts a configuration object with the following properties: | ||
|
|
||
| - `name` (string): The name of the host application. | ||
| - `filename` (string, optional): The name of the remote entry file. Defaults to `'remoteEntry.js'`. | ||
| - `remotes` (object, optional): An object specifying the remote applications and their entry points. | ||
| - `exposes` (object, optional): An object specifying the modules to be exposed by the host application. | ||
| - `shared` (array, optional): An array of package names to be shared between the host and remote applications. | ||
| ### Both Host and Remote | ||
|
|
||
| ## Plugin Features | ||
| An application can be both a host and a remote simultaneously: | ||
|
|
||
| The `moduleFederationPlugin` includes the following features: | ||
| ```js | ||
| const { withFederation } = require('@module-federation/esbuild/build'); | ||
|
|
||
| - **Virtual Share Module**: Creates a virtual module for sharing dependencies between the host and remote applications. | ||
| - **Virtual Remote Module**: Creates a virtual module for importing exposed modules from remote applications. | ||
| - **CommonJS to ESM Transformation**: Transforms CommonJS modules to ESM format for compatibility with Module Federation. | ||
| - **Shared Dependencies Linking**: Links shared dependencies between the host and remote applications. | ||
| - **Manifest Generation**: Generates a manifest file containing information about the exposed modules and their exports. | ||
| module.exports = withFederation({ | ||
| name: 'shell', | ||
| filename: 'remoteEntry.js', | ||
| exposes: { | ||
| './Header': './src/Header', | ||
| }, | ||
| remotes: { | ||
| sidebar: 'http://localhost:3002/remoteEntry.js', | ||
| }, | ||
| shared: { | ||
| react: { singleton: true, version: '^18.2.0' }, | ||
| 'react-dom': { singleton: true, version: '^18.2.0' }, | ||
| }, | ||
| }); | ||
| ``` | ||
|
|
||
| ## API | ||
|
|
||
| ### `moduleFederationPlugin(config)` | ||
| ### Exports from `@module-federation/esbuild/plugin` | ||
|
|
||
| - `moduleFederationPlugin(config)` - Creates the esbuild plugin | ||
|
|
||
| Creates an esbuild plugin for Module Federation. | ||
| ### Exports from `@module-federation/esbuild/build` | ||
|
|
||
| - `withFederation(config)` - Normalizes federation configuration | ||
| - `share(shareObjects)` - Processes shared dependency configurations | ||
| - `shareAll(config)` - Shares all dependencies from package.json | ||
| - `findPackageJson(folder)` - Finds nearest package.json | ||
| - `lookupVersion(key, workspaceRoot)` - Looks up dependency version | ||
| - `setInferVersion(infer)` - Enable/disable version inference | ||
|
|
||
| ### Exports from `@module-federation/esbuild` | ||
|
|
||
| Re-exports everything from both `plugin` and `build` entry points. | ||
|
|
||
| ## Notes | ||
|
|
||
| ### Remote Module Named Exports | ||
|
|
||
| Since remote module exports are unknown at build time, only the default export is statically re-exported. For named exports from remote modules, use one of these patterns: | ||
|
|
||
| ```js | ||
| // Pattern 1: Default import (recommended for React components) | ||
| import RemoteComponent from 'remote/component'; | ||
|
|
||
| // Pattern 2: Destructure from default | ||
| import Remote from 'remote/utils'; | ||
| const { helper, formatter } = Remote; | ||
|
|
||
| // Pattern 3: Dynamic import | ||
| const { helper } = await import('remote/utils'); | ||
| ``` | ||
|
|
||
| - `config` (object): The Module Federation configuration. | ||
| - `name` (string): The name of the host application. | ||
| - `filename` (string, optional): The name of the remote entry file. Defaults to `'remoteEntry.js'`. | ||
| - `remotes` (object, optional): An object specifying the remote applications and their entry points. | ||
| - `exposes` (object, optional): An object specifying the modules to be exposed by the host application. | ||
| - `shared` (array, optional): An array of package names to be shared between the host and remote applications. | ||
| ### Shared Module Subpaths | ||
|
|
||
| Returns an esbuild plugin instance. | ||
| When you share a package like `react`, subpath imports like `react/jsx-runtime` are also handled through the share scope. The plugin automatically detects subpath imports and routes them appropriately. |
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,29 @@ | ||
| import { readFileSync } from 'fs'; | ||
|
|
||
| const { exclude: _, ...swcJestConfig } = JSON.parse( | ||
| readFileSync(`${__dirname}/.swcrc`, 'utf-8'), | ||
| ); | ||
|
|
||
| swcJestConfig.swcrc ??= false; | ||
|
|
||
| export default { | ||
| clearMocks: true, | ||
| cache: false, | ||
| testEnvironment: 'node', | ||
| coveragePathIgnorePatterns: ['__tests__', '/node_modules/', '/dist/'], | ||
| globals: { | ||
| __VERSION__: '"0.0.0-test"', | ||
| FEDERATION_DEBUG: '""', | ||
| }, | ||
| preset: 'ts-jest', | ||
| transformIgnorePatterns: ['/node_modules/', '/dist/'], | ||
| transform: { | ||
| '^.+\\.(t|j)sx?$': ['@swc/jest', swcJestConfig], | ||
| }, | ||
| rootDir: __dirname, | ||
| testMatch: [ | ||
| '<rootDir>/src/**/*.spec.[jt]s?(x)', | ||
| '<rootDir>/src/**/*.test.[jt]s?(x)', | ||
| ], | ||
| testPathIgnorePatterns: ['/node_modules/'], | ||
| }; |
Oops, something went wrong.
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
Uh oh!
There was an error while loading. Please reload this page.