Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
Show all changes
80 commits
Select commit Hold shift + click to select a range
c4d3c04
feat(mf): support layered container exposes
ScriptedAlchemy Jul 22, 2026
b3a3fed
Merge remote-tracking branch 'origin/codex/mf-share-scope-runtime' in…
ScriptedAlchemy Jul 27, 2026
c0f445d
Merge commit '2a348f967f' into codex/work-pr14912
ScriptedAlchemy Jul 27, 2026
f16f215
Merge commit 'e0a975ff10' into codex/work-pr14912
ScriptedAlchemy Jul 27, 2026
7ee5128
Merge commit '223f6e860f' into codex/work-pr14912
ScriptedAlchemy Jul 27, 2026
6c1af2c
Merge commit '3e4af0cb5f' into codex/work-pr14912
ScriptedAlchemy Jul 27, 2026
1c3eb05
perf(mf): use unstable sorts for deterministic strings
ScriptedAlchemy Jul 27, 2026
bce2884
perf(mf): patch shared exports without typed stats
ScriptedAlchemy Jul 27, 2026
53b1f7e
perf(mf): compact optional exposed layers
ScriptedAlchemy Jul 27, 2026
b21cf1d
Merge commit '9392c3bb75' into codex/work-pr14912
ScriptedAlchemy Jul 28, 2026
b1722dc
Merge commit 'ffd6b71541' into codex/work-pr14912
ScriptedAlchemy Jul 28, 2026
4f1eef5
Merge commit '72c9492ee4' into codex/work-pr14912
ScriptedAlchemy Jul 28, 2026
27771aa
Merge commit 'cbaca67ee4' into codex/work-pr14912
ScriptedAlchemy Jul 28, 2026
9c461fa
Merge commit 'fefd61d32e' into codex/work-pr14912
ScriptedAlchemy Jul 28, 2026
84a127a
Merge commit '2119bde04f' into codex/work-pr14912
ScriptedAlchemy Jul 29, 2026
2476c02
Merge commit '2c0d2b7210' into codex/work-pr14912
ScriptedAlchemy Jul 29, 2026
1a74f7d
fix(mf): preserve layered option compatibility
ScriptedAlchemy Jul 29, 2026
c20131b
Merge commit '982bd52e2c' into codex/work-pr14912
ScriptedAlchemy Jul 29, 2026
aa1d9ec
Merge commit '46a22cb771' into codex/work-pr14912
ScriptedAlchemy Jul 29, 2026
f592033
Merge commit 'd540607a38' into codex/work-pr14912
ScriptedAlchemy Jul 29, 2026
668cfee
fix(mf): preserve expose option compatibility
ScriptedAlchemy Jul 29, 2026
1326aca
fix(mf): preserve v1 option extensibility
ScriptedAlchemy Jul 29, 2026
bfa3a15
Merge commit '4aad05beae' into codex/work-pr14912
ScriptedAlchemy Jul 29, 2026
ef0f210
Merge commit '1656d1bec8' into codex/work-pr14912
ScriptedAlchemy Jul 29, 2026
3dee67e
fix(mf): preserve container option extensibility
ScriptedAlchemy Jul 29, 2026
a45c54d
fix(mf): retain exposed dependency constructor
ScriptedAlchemy Jul 29, 2026
11e4a2f
Merge remote-tracking branch 'origin/codex/mf-share-scope-runtime' in…
ScriptedAlchemy Jul 29, 2026
aecfa8e
fix(mf): export enhanced container options
ScriptedAlchemy Jul 29, 2026
04c9206
Merge remote-tracking branch 'origin/codex/mf-share-scope-runtime' in…
ScriptedAlchemy Jul 29, 2026
cabc390
Merge remote-tracking branch 'origin/codex/mf-share-scope-runtime' in…
ScriptedAlchemy Sep 7, 2026
c7a248b
Merge remote-tracking branch 'origin/codex/mf-share-scope-runtime' in…
ScriptedAlchemy Sep 8, 2026
bfe4fa5
Merge remote-tracking branch 'origin/codex/mf-share-scope-runtime' in…
ScriptedAlchemy Sep 8, 2026
ace9d2e
test(mf): check the legacy expose constructor signature without const…
ScriptedAlchemy Sep 8, 2026
e07cb23
Merge remote-tracking branch 'origin/codex/mf-share-scope-runtime' in…
ScriptedAlchemy Sep 8, 2026
22d1a17
Merge remote-tracking branch 'origin/codex/mf-share-scope-runtime' in…
ScriptedAlchemy Sep 8, 2026
789d856
Merge remote-tracking branch 'origin/codex/mf-share-scope-runtime' in…
ScriptedAlchemy Sep 8, 2026
b108343
fix(mf): carry expose layers inside the container identifier payload
ScriptedAlchemy Sep 8, 2026
7a95005
Merge remote-tracking branch 'origin/codex/mf-share-scope-runtime' in…
ScriptedAlchemy Sep 8, 2026
533028a
docs(mf): explain and exercise expose layers
ScriptedAlchemy Sep 8, 2026
089b02c
Merge refreshed shared runtime into layered exposes
ScriptedAlchemy Sep 8, 2026
e8b02d0
Merge branch 'codex/work-pr14911' into codex/work-pr14912
ScriptedAlchemy Sep 8, 2026
73c90ee
test(mf): cover layered exposes through config cases
ScriptedAlchemy Sep 8, 2026
5cee2d5
Merge branch 'codex/work-pr14911' into codex/work-pr14912
ScriptedAlchemy Sep 8, 2026
4b9a1e2
fix(mf): retain container hooks after test cleanup
ScriptedAlchemy Sep 8, 2026
d1d45c6
Merge branch 'codex/work-pr14911' into codex/work-pr14912
ScriptedAlchemy Sep 8, 2026
b3ca943
Merge branch 'codex/work-pr14911' into codex/work-pr14912
ScriptedAlchemy Sep 8, 2026
60e794b
refactor(mf): remove redundant expose layer truncation
ScriptedAlchemy Sep 9, 2026
0454b2f
chore: sync layered exposes with runtime base
ScriptedAlchemy Sep 9, 2026
f979f70
chore: sync layered exposes with runtime base
ScriptedAlchemy Sep 9, 2026
dc5faf4
chore: sync layered exposes with runtime base
ScriptedAlchemy Sep 9, 2026
960481d
chore: sync layered exposes with runtime review fixes
ScriptedAlchemy Sep 9, 2026
e30e940
fix(mf): export enhanced V1 federation options
ScriptedAlchemy Sep 8, 2026
4724c6b
Merge branch 'codex/mf-share-scope-runtime' into codex/mf-layered-con…
ScriptedAlchemy Sep 9, 2026
a00b374
Merge branch 'codex/mf-share-scope-runtime' into codex/mf-layered-con…
ScriptedAlchemy Sep 9, 2026
fca6e72
Merge branch 'codex/mf-share-scope-runtime' into codex/mf-layered-con…
ScriptedAlchemy Sep 10, 2026
1ae37ab
Merge branch 'codex/mf-share-scope-runtime' into codex/mf-layered-con…
ScriptedAlchemy Sep 10, 2026
e3dab88
Merge branch 'codex/mf-share-scope-runtime' into codex/mf-layered-con…
ScriptedAlchemy Sep 11, 2026
b55df4a
Merge branch 'stack-refresh/14911' into stack-refresh/14912
ScriptedAlchemy Sep 14, 2026
4605029
Merge branch 'stack-refresh2/14911' into stack-refresh2/14912
ScriptedAlchemy Sep 15, 2026
2e2ec31
Merge branch 'stack-refresh2/14911' into stack-refresh2/14912
ScriptedAlchemy Sep 15, 2026
12bd007
refactor(mf): carry the expose layer on ExposeOptions
ScriptedAlchemy Sep 16, 2026
5acd724
Merge branch 'stack-refresh2/14911' into stack-refresh2/14912
ScriptedAlchemy Sep 16, 2026
e4eb5ba
chore(mf): reject empty expose layer names and clean the layered fixt…
ScriptedAlchemy Sep 16, 2026
fc9224a
Merge branch 'stack-refresh2/14911' into stack-refresh2/14912
ScriptedAlchemy Sep 16, 2026
a47cc84
Merge branch 'stack-refresh2/14911' into stack-refresh2/14912
ScriptedAlchemy Sep 22, 2026
ef7c751
Merge branch 'stack-refresh2/14911' into stack-refresh2/14912
ScriptedAlchemy Sep 22, 2026
6fb8b6b
Merge ordered share scope runtime into layered container exposes
ScriptedAlchemy Sep 29, 2026
b0bf215
Merge corrected workspace lockfile into layered container exposes
ScriptedAlchemy Sep 29, 2026
3c0aed3
fix(mf): preserve unnamed consumers after base refresh
ScriptedAlchemy Sep 29, 2026
c9cb2be
fix(mf): enforce reusable expose map types and relocate Rust coverage
ScriptedAlchemy Sep 30, 2026
f65fa5e
Merge reviewed MF runtime fixes and format test dependency
ScriptedAlchemy Sep 30, 2026
046f3b2
fix(mf): preserve Rust container options source compatibility
ScriptedAlchemy Sep 30, 2026
baf20fb
Merge Rust sharing API compatibility into layered containers
ScriptedAlchemy Sep 30, 2026
1dbc84d
test(mf): retain enhanced expose serialization coverage
ScriptedAlchemy Sep 30, 2026
acf8939
Merge branch 'api-compat-runtime' into HEAD
ScriptedAlchemy Sep 30, 2026
d0d70ae
Merge branch 'api-compat-runtime' into HEAD
ScriptedAlchemy Sep 30, 2026
27c7890
Merge branch 'api-compat-runtime' into HEAD
ScriptedAlchemy Sep 30, 2026
1162106
chore: update layered exposes stack base
ScriptedAlchemy Oct 22, 2026
27998da
chore: propagate federation lockfile fix
ScriptedAlchemy Oct 22, 2026
728b7a9
chore: propagate combined preview build
ScriptedAlchemy Oct 22, 2026
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
1 change: 1 addition & 0 deletions crates/node_binding/napi-binding.d.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2302,6 +2302,7 @@ export interface RawExperiments {
export interface RawExposeOptions {
key: string
name?: string
layer?: string
import: Array<string>
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -58,6 +58,7 @@ impl From<RawContainerPluginOptions> for ContainerPluginOptions {
pub struct RawExposeOptions {
pub key: String,
pub name: Option<String>,
pub layer: Option<String>,
pub import: Vec<String>,
}

Expand All @@ -67,6 +68,7 @@ impl From<RawExposeOptions> for (String, ExposeOptions) {
value.key,
ExposeOptions {
name: value.name,
layer: value.layer,
import: value.import,
},
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -230,6 +230,7 @@ impl Module for ContainerEntryModule {
Box::new(ContainerExposedDependency::new(
name.clone(),
request.clone(),
options.layer.clone(),
)) as Box<dyn Dependency>
})
.collect(),
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
use rspack_cacheable::{cacheable, cacheable_dyn};
use rspack_core::{
AsContextDependency, AsDependencyCodeGeneration, Dependency, DependencyCategory, DependencyId,
DependencyType, FactorizeInfo, ModuleDependency, ResourceIdentifier,
DependencyType, FactorizeInfo, ModuleDependency, ModuleLayer, ResourceIdentifier,
};

#[cacheable]
Expand All @@ -10,18 +10,24 @@ pub struct ContainerExposedDependency {
id: DependencyId,
request: String,
pub exposed_name: String,
layer: Option<ModuleLayer>,
resource_identifier: ResourceIdentifier,
dependency_type: DependencyType,
factorize_info: FactorizeInfo,
}

impl ContainerExposedDependency {
pub fn new(exposed_name: String, request: String) -> Self {
let resource_identifier = format!("exposed dependency {exposed_name}={request}").into();
pub fn new(exposed_name: String, request: String, layer: Option<ModuleLayer>) -> Self {
Comment thread
ScriptedAlchemy marked this conversation as resolved.
Outdated
let resource_identifier = if let Some(layer) = &layer {
format!("exposed dependency {exposed_name}={request}|layer={layer}").into()
} else {
format!("exposed dependency {exposed_name}={request}").into()
Comment thread
ScriptedAlchemy marked this conversation as resolved.
Outdated
};
Self {
id: DependencyId::new(),
request,
exposed_name,
layer,
resource_identifier,
dependency_type: DependencyType::ContainerExposed,
factorize_info: Default::default(),
Expand All @@ -34,6 +40,7 @@ impl ContainerExposedDependency {
id: DependencyId::new(),
request,
exposed_name: String::new(),
layer: None,
resource_identifier,
dependency_type: DependencyType::ShareContainerFallback,
factorize_info: Default::default(),
Expand All @@ -55,6 +62,10 @@ impl Dependency for ContainerExposedDependency {
&self.dependency_type
}

fn get_layer(&self) -> Option<&ModuleLayer> {
self.layer.as_ref()
}

fn resource_identifier(&self) -> Option<&str> {
Some(&self.resource_identifier)
}
Expand Down
5 changes: 3 additions & 2 deletions crates/rspack_plugin_mf/src/container/container_plugin.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,8 +3,8 @@ use std::sync::Arc;
use rspack_core::{
ChunkUkey, Compilation, CompilationAdditionalTreeRuntimeRequirements, CompilationParams,
CompilationRuntimeRequirementInTree, CompilerCompilation, CompilerMake, DependencyType,
EntryOptions, EntryRuntime, Filename, LibraryOptions, Plugin, RuntimeGlobals, RuntimeModule,
SourceType,
EntryOptions, EntryRuntime, Filename, LibraryOptions, ModuleLayer, Plugin, RuntimeGlobals,
RuntimeModule, SourceType,
};
use rspack_error::Result;
use rspack_hook::{plugin, plugin_hook};
Expand Down Expand Up @@ -32,6 +32,7 @@ pub struct ContainerPluginOptions {
#[derive(Debug, Clone, Serialize)]
pub struct ExposeOptions {
pub name: Option<String>,
pub layer: Option<ModuleLayer>,
Comment thread
ScriptedAlchemy marked this conversation as resolved.
Outdated
pub import: Vec<String>,
}

Expand Down
50 changes: 37 additions & 13 deletions packages/rspack/src/container/ContainerPlugin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,31 +13,48 @@ import { parseOptions } from '../container/options';
import { normalizeShareScope, type ShareScope } from '../sharing/SharePlugin';
import { ShareRuntimePlugin } from '../sharing/ShareRuntimePlugin';

export type ContainerPluginOptions = {
exposes: Exposes;
type ContainerPluginBaseOptions<Enhanced extends boolean> = {
exposes: Exposes<Enhanced>;
filename?: FilenameTemplate;
library?: LibraryOptions;
name: string;
runtime?: EntryRuntime;
shareScope?: ShareScope;
enhanced?: boolean;
};
export type Exposes = (ExposesItem | ExposesObject)[] | ExposesObject;
export type ContainerPluginOptions<Enhanced extends boolean = boolean> = [
Enhanced,
] extends [true]
Comment thread
ScriptedAlchemy marked this conversation as resolved.
Outdated
? ContainerPluginBaseOptions<true> & { enhanced: true }
: [Enhanced] extends [false]
? ContainerPluginBaseOptions<false> & { enhanced?: false }
: | (ContainerPluginBaseOptions<false> & { enhanced?: false })
| (ContainerPluginBaseOptions<true> & { enhanced: true });
Comment thread
ScriptedAlchemy marked this conversation as resolved.
Outdated
export type Exposes<Enhanced extends boolean = boolean> =
(ExposesItem | ExposesObject<Enhanced>)[] | ExposesObject<Enhanced>;
Comment thread
ScriptedAlchemy marked this conversation as resolved.
Outdated
export type ExposesItem = string;
export type ExposesItems = ExposesItem[];
export type ExposesObject = {
[k: string]: ExposesConfig | ExposesItem | ExposesItems;
export type ExposesObject<Enhanced extends boolean = boolean> = {
[k: string]: ExposesConfig<Enhanced> | ExposesItem | ExposesItems;
};
export type ExposesConfig = {
type ExposesBaseConfig = {
import: ExposesItem | ExposesItems;
name?: string;
};
export type ExposesConfig<Enhanced extends boolean = boolean> = [
Comment thread
ScriptedAlchemy marked this conversation as resolved.
Outdated
Enhanced,
] extends [true]
? ExposesBaseConfig & { layer?: string }
Comment thread
ScriptedAlchemy marked this conversation as resolved.
Comment thread
ScriptedAlchemy marked this conversation as resolved.
: [Enhanced] extends [false]
? ExposesBaseConfig & { layer?: never }
: ExposesBaseConfig & { layer?: string };

export class ContainerPlugin extends RspackBuiltinPlugin {
export class ContainerPlugin<
Enhanced extends boolean = boolean,
> extends RspackBuiltinPlugin {
name = BuiltinPluginName.ContainerPlugin;
_options;

constructor(options: ContainerPluginOptions) {
constructor(options: ContainerPluginOptions<Enhanced>) {
super();
const enhanced = options.enhanced ?? false;
const shareScope = normalizeShareScope(
Expand All @@ -59,11 +76,18 @@ export class ContainerPlugin extends RspackBuiltinPlugin {
(item) => ({
import: Array.isArray(item) ? item : [item],
name: undefined,
layer: undefined,
}),
(item) => ({
import: Array.isArray(item.import) ? item.import : [item.import],
name: item.name || undefined,
}),
(item) => {
if (!enhanced && item.layer !== undefined) {
throw new Error('[ContainerPlugin] layer requires enhanced=true');
}
return {
import: Array.isArray(item.import) ? item.import : [item.import],
name: item.name || undefined,
layer: enhanced ? item.layer || undefined : undefined,
Comment thread
ScriptedAlchemy marked this conversation as resolved.
Outdated
};
},
),
enhanced,
};
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
module.exports = [/\[ContainerPlugin\] layer requires enhanced=true/];
Original file line number Diff line number Diff line change
@@ -0,0 +1,3 @@
it('should reject enhanced expose options in legacy mode', () => {
expect(true).toBe(true);
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,16 @@
const { ContainerPlugin } = require('@rspack/core').container;

module.exports = {
entry: './index.js',
plugins: [
new ContainerPlugin({
name: 'container',
exposes: {
'./entry': {
import: './index.js',
layer: 'server',
},
},
}),
],
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import React, { layeredComponentsReact } from 'react';
import ComponentA from 'containerA/ComponentA';
import ComponentALayers from 'containerA/ComponentALayers';
import LocalComponentALayers from './ComponentALayers';
import LocalComponentA from './ComponentA';

export default () => {
return `App rendered with [${React()}] ${layeredComponentsReact()}, ${LocalComponentALayers()}, ${LocalComponentA()}, [${ComponentA()}] and [${ComponentALayers()}]`;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import React, { layeredComponentsReact } from 'react';

export default () => {
return `ComponentA rendered with [${React()}]${layeredComponentsReact()}`;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
import React, { layeredComponentsReact } from 'react';

export default () => {
return `ComponentALayers ${React()} rendered with [${layeredComponentsReact()}]`;
};
export { React };
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
it('should load the component from container', () => {
return import('./App').then(({ default: App }) => {
const rendered = App();
expect(rendered).toBe(
'App rendered with [This is react 0.1.2] No Layer (1-layers-full), ComponentALayers This is react 0.1.2 rendered with [This is layered react (1-layers-full)], ComponentA rendered with [This is react 0.1.2]No Layer (1-layers-full), [ComponentA rendered with [This is react 0.1.2]No Layer (1-layers-full)] and [ComponentALayers This is react 0.1.2 rendered with [This is layered react (1-layers-full)]]',
);
});
});
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
// DO NOT EDIT
module.exports = function (source) {
return source.replace('No Layer', 'This is layered react');
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React from 'react';

// This file will be processed by the layered loader
export default function initializeLayeredReactVersion() {
// Set the layered React version
React.setVersion('1.2.3');
}

// Initialize version immediately
initializeLayeredReactVersion();

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Original file line number Diff line number Diff line change
@@ -0,0 +1,106 @@
const { ModuleFederationPlugin } = require('@rspack/core').container;
const path = require('path');

const common = {
name: 'layer_container',
exposes: {
'./ComponentA': {
import: './ComponentA',
},
'./ComponentALayers': {
import: './ComponentALayers',
},
Comment thread
ScriptedAlchemy marked this conversation as resolved.
},
shared: {
react: {
version: '18.0.0',
requiredVersion: '^18.0.0',
singleton: true,
},
'layered-react': {
request: 'react',
import: 'react',
shareKey: 'react',
version: '16.0.0',
requiredVersion: '^16.0.0',
singleton: true,
layer: 'layered-components',
issuerLayer: 'layered-components',
shareScope: 'layered-components',
},
},
};

const commonConfig = {
devtool: false,
experiments: {
layers: true,
},
entry: './index.js',
mode: 'development',
module: {
rules: [
{
test: /ComponentALayers\.js$/,
layer: 'layered-components',
},
{
test: /react\.js$/,
issuerLayer: 'layered-components',
layer: 'layered-components',
use: [
{
loader: path.resolve(__dirname, './layered-react-loader.js'),
},
],
},
],
},
};

module.exports = [
{
...commonConfig,
output: {
filename: '[name].js',
uniqueName: '1-layers-full',
},
plugins: [
new ModuleFederationPlugin({
library: { type: 'commonjs-module' },
filename: 'container.js',
remotes: {
containerA: {
external: './container.js',
},
},
...common,
}),
],
},
{
...commonConfig,
experiments: {
...commonConfig.experiments,
outputModule: true,
},
output: {
module: true,
filename: 'module/[name].mjs',
uniqueName: '1-layers-full-mjs',
},
plugins: [
new ModuleFederationPlugin({
library: { type: 'module' },
filename: 'module/container.mjs',
remotes: {
containerA: {
external: './container.mjs',
},
},
...common,
}),
],
target: 'node14',
},
];
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
module.exports = {
findBundle: function (i, options) {
return i === 0 ? './main.js' : './module/main.mjs';
},
};
10 changes: 10 additions & 0 deletions tests/rspack-test/serialCases/container-1-5/2-layers-full/App.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
import React, { layeredComponentsReact } from 'react';
import ComponentA from 'containerA/ComponentA';
import ComponentB from 'containerB/ComponentB';
import ComponentALayers from 'containerB/ComponentALayers';
import LocalComponentB from './ComponentB';
import LocalComponentALayers from './ComponentALayers';

export default () => {
return `App rendered with [${React()}] ${layeredComponentsReact()} and ${ComponentALayers()} and ${LocalComponentALayers()} and [${ComponentA()}] and [${ComponentB()}] and ${LocalComponentB()}`;
};
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import React, { layeredComponentsReact } from 'react';

export default function ComponentALayers() {
return `LocalComponentALayers ${React()} (Layered React: ${layeredComponentsReact()})`;
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
import React, { layeredComponentsReact } from 'react';

export default () => {
return `LocalComponentB rendered with [${React()}] ${layeredComponentsReact()}`;
};
Loading
Loading