diff --git a/crates/node_binding/napi-binding.d.ts b/crates/node_binding/napi-binding.d.ts index 1619b3376226..940c774acc55 100644 --- a/crates/node_binding/napi-binding.d.ts +++ b/crates/node_binding/napi-binding.d.ts @@ -2124,6 +2124,7 @@ inlineEnum: boolean typeReexportsPresence: boolean lazyBarrel: boolean deferImport: boolean +mfAsyncStartup: boolean } export interface RawExperimentSnapshotOptions { diff --git a/crates/rspack/src/builder/builder_context.rs b/crates/rspack/src/builder/builder_context.rs index 967c4ffffcad..2fa685ee9d24 100644 --- a/crates/rspack/src/builder/builder_context.rs +++ b/crates/rspack/src/builder/builder_context.rs @@ -143,7 +143,11 @@ impl BuilderContext { plugins.push(rspack_plugin_runtime::ModuleChunkFormatPlugin::default().boxed()); } BuiltinPluginOptions::EnableChunkLoadingPlugin(chunk_loading_type) => { - rspack_plugin_runtime::enable_chunk_loading_plugin(chunk_loading_type, &mut plugins); + rspack_plugin_runtime::enable_chunk_loading_plugin( + chunk_loading_type, + compiler_options.experiments.mf_async_startup, + &mut plugins, + ); } BuiltinPluginOptions::EnableWasmLoadingPlugin(wasm_loading_type) => { plugins.push(rspack_plugin_wasm::enable_wasm_loading_plugin( diff --git a/crates/rspack/src/builder/mod.rs b/crates/rspack/src/builder/mod.rs index f6c36df1b71e..8b219a9b7ac5 100644 --- a/crates/rspack/src/builder/mod.rs +++ b/crates/rspack/src/builder/mod.rs @@ -3803,6 +3803,7 @@ impl ExperimentsBuilder { type_reexports_presence: false, lazy_barrel: false, defer_import: false, + mf_async_startup: false, }) } } diff --git a/crates/rspack/tests/snapshots/defaults__default_options.snap b/crates/rspack/tests/snapshots/defaults__default_options.snap index f5eee43dc095..3f237533f40d 100644 --- a/crates/rspack/tests/snapshots/defaults__default_options.snap +++ b/crates/rspack/tests/snapshots/defaults__default_options.snap @@ -1524,6 +1524,7 @@ CompilerOptions { type_reexports_presence: false, lazy_barrel: false, defer_import: false, + mf_async_startup: false, }, node: Some( NodeOption { diff --git a/crates/rspack_binding_api/src/lib.rs b/crates/rspack_binding_api/src/lib.rs index c4bca5e3d6e5..d8ef5f7b839a 100644 --- a/crates/rspack_binding_api/src/lib.rs +++ b/crates/rspack_binding_api/src/lib.rs @@ -219,8 +219,9 @@ impl JsCompiler { let js_cleanup_plugin = JsCleanupPlugin::new(tsfn); plugins.push(js_cleanup_plugin.boxed()); + let mf_async_startup = options.experiments.mf_async_startup; for bp in builtin_plugins { - bp.append_to(env, &mut this, &mut plugins)?; + bp.append_to(env, &mut this, mf_async_startup, &mut plugins)?; } let pnp = options.resolve.pnp.unwrap_or(false); diff --git a/crates/rspack_binding_api/src/raw_options/raw_builtins/mod.rs b/crates/rspack_binding_api/src/raw_options/raw_builtins/mod.rs index a9073f95dba0..135dc375421b 100644 --- a/crates/rspack_binding_api/src/raw_options/raw_builtins/mod.rs +++ b/crates/rspack_binding_api/src/raw_options/raw_builtins/mod.rs @@ -275,6 +275,7 @@ impl<'a> BuiltinPlugin<'a> { self, env: Env, compiler_object: &mut Object, + mf_async_startup: bool, plugins: &mut Vec, ) -> napi::Result<()> { let name = match self.name { @@ -390,7 +391,11 @@ impl<'a> BuiltinPlugin<'a> { BuiltinPluginName::EnableChunkLoadingPlugin => { let chunk_loading_type = downcast_into::(self.options) .map_err(|report| napi::Error::from_reason(report.to_string()))?; - enable_chunk_loading_plugin(chunk_loading_type.as_str().into(), plugins); + enable_chunk_loading_plugin( + chunk_loading_type.as_str().into(), + mf_async_startup, + plugins, + ); } BuiltinPluginName::EnableLibraryPlugin => { let library_type = downcast_into::(self.options) diff --git a/crates/rspack_binding_api/src/raw_options/raw_experiments/mod.rs b/crates/rspack_binding_api/src/raw_options/raw_experiments/mod.rs index 8a42f4d8ec76..46992e3efd61 100644 --- a/crates/rspack_binding_api/src/raw_options/raw_experiments/mod.rs +++ b/crates/rspack_binding_api/src/raw_options/raw_experiments/mod.rs @@ -31,6 +31,7 @@ pub struct RawExperiments { pub type_reexports_presence: bool, pub lazy_barrel: bool, pub defer_import: bool, + pub mf_async_startup: bool, } impl From for Experiments { @@ -53,6 +54,7 @@ impl From for Experiments { type_reexports_presence: value.type_reexports_presence, lazy_barrel: value.lazy_barrel, defer_import: value.defer_import, + mf_async_startup: value.mf_async_startup, } } } diff --git a/crates/rspack_core/src/options/experiments/mod.rs b/crates/rspack_core/src/options/experiments/mod.rs index 0c58da0d2846..634790dac734 100644 --- a/crates/rspack_core/src/options/experiments/mod.rs +++ b/crates/rspack_core/src/options/experiments/mod.rs @@ -20,6 +20,7 @@ pub struct Experiments { pub type_reexports_presence: bool, pub lazy_barrel: bool, pub defer_import: bool, + pub mf_async_startup: bool, } #[allow(clippy::empty_structs_with_brackets)] diff --git a/crates/rspack_plugin_esm_library/src/link.rs b/crates/rspack_plugin_esm_library/src/link.rs index 68e9c3db9966..dbc57bd73083 100644 --- a/crates/rspack_plugin_esm_library/src/link.rs +++ b/crates/rspack_plugin_esm_library/src/link.rs @@ -1865,6 +1865,12 @@ impl EsmLibraryPlugin { continue; }; + if !matches!( + dep.dependency_type(), + DependencyType::EsmImport | DependencyType::EsmExportImport + ) { + continue; + } let Some(conn) = module_graph.connection_by_dependency_id(dep_id) else { continue; }; diff --git a/crates/rspack_plugin_javascript/src/plugin/mod.rs b/crates/rspack_plugin_javascript/src/plugin/mod.rs index d329b8fdb1a7..7bab861080a1 100644 --- a/crates/rspack_plugin_javascript/src/plugin/mod.rs +++ b/crates/rspack_plugin_javascript/src/plugin/mod.rs @@ -324,201 +324,529 @@ impl JsPlugin { if !runtime_requirements.contains(RuntimeGlobals::STARTUP_NO_DEFAULT) { if chunk.has_entry_module(&compilation.chunk_graph) { - let mut buf2: Vec> = Vec::new(); - buf2.push("// Load entry module and return exports".into()); - let entries = compilation - .chunk_graph - .get_chunk_entry_modules_with_chunk_group_iterable(chunk_ukey); - for (i, (module, entry)) in entries.iter().enumerate() { - let chunk_group = compilation.chunk_group_by_ukey.expect_get(entry); - let chunk_ids = chunk_group - .chunks + let is_container_entry_chunk = { + let entries = compilation + .chunk_graph + .get_chunk_entry_modules_with_chunk_group_iterable(chunk_ukey); + entries .iter() - .filter(|c| *c != chunk_ukey) - .map(|chunk_ukey| { - compilation - .chunk_by_ukey - .expect_get(chunk_ukey) - .expect_id(&compilation.chunk_ids_artifact) - .to_string() - }) - .collect::>(); - if allow_inline_startup && !chunk_ids.is_empty() { - buf2.push("// This entry module depends on other loaded chunks and execution need to be delayed".into()); - allow_inline_startup = false; - } - if allow_inline_startup && { - let module_graph = compilation.get_module_graph(); - let module_graph_cache = &compilation.module_graph_cache_artifact; - module_graph - .get_incoming_connections_by_origin_module(module) - .iter() - .any(|(origin_module, connections)| { - if let Some(origin_module) = origin_module { - connections.iter().any(|c| { - c.is_target_active(&module_graph, Some(chunk.runtime()), module_graph_cache) - }) && compilation - .chunk_graph - .get_module_runtimes_iter(*origin_module, &compilation.chunk_by_ukey) - .any(|runtime| runtime.intersection(chunk.runtime()).count() > 0) + .next_back() + .map(|(module_id, chunk_group_ukey)| { + let module_graph = compilation.get_module_graph(); + if let Some(module) = module_graph.module_by_identifier(module_id) { + if module + .source_types(&module_graph) + .contains(&SourceType::Expose) + { + true } else { - false + compilation + .chunk_group_by_ukey + .expect_get(chunk_group_ukey) + .kind + .get_entry_options() + .map(|options| options.library.is_some()) + .unwrap_or(false) } + } else { + false + } + }) + .unwrap_or(false) + }; + + let use_federation_async = compilation.options.experiments.mf_async_startup + && runtime_requirements.contains(RuntimeGlobals::ENSURE_CHUNK_HANDLERS) + && !is_container_entry_chunk; + + if use_federation_async { + let mut buf2: Vec> = Vec::new(); + + buf2.push("// Module Federation async startup".into()); + buf2.push( + format!( + "var __webpack_exec__ = function(moduleId) {{ return {}({} = moduleId); }};", + RuntimeGlobals::REQUIRE, + RuntimeGlobals::ENTRY_MODULE_ID + ) + .into(), + ); + buf2.push("var promises = [];".into()); + buf2.push("// Call federation runtime initialization".into()); + buf2.push("var runtimeInitialization = undefined;".into()); + buf2.push("if (typeof __webpack_require__.x === \"function\") {".into()); + buf2.push(" runtimeInitialization = __webpack_require__.x();".into()); + buf2.push("} else {".into()); + buf2.push(" console.warn(\"[Module Federation] __webpack_require__.x is not a function, skipping federation startup\");".into()); + buf2.push("}".into()); + + let entries = compilation + .chunk_graph + .get_chunk_entry_modules_with_chunk_group_iterable(chunk_ukey); + + let mut federation_entry_calls: Vec = Vec::new(); + let mut all_chunk_ids: Vec = Vec::new(); + + for (module, entry) in entries.iter() { + let chunk_group = compilation.chunk_group_by_ukey.expect_get(entry); + let chunk_ids = chunk_group + .chunks + .iter() + .filter(|c| *c != chunk_ukey) + .map(|chunk_ukey| { + compilation + .chunk_by_ukey + .expect_get(chunk_ukey) + .expect_id(&compilation.chunk_ids_artifact) + .to_string() }) - } { - buf2.push( - "// This entry module is referenced by other modules so it can't be inlined".into(), - ); - allow_inline_startup = false; - } - if allow_inline_startup && { - let codegen = compilation - .code_generation_results - .get(module, Some(chunk.runtime())); - let module_graph = compilation.get_module_graph(); - let top_level_decls = codegen - .data - .get::() - .map(|d| d.inner()) - .or_else(|| { - module_graph - .module_by_identifier(module) - .and_then(|m| m.build_info().top_level_declarations.as_ref()) - }); - top_level_decls.is_none() - } { - buf2.push("// This entry module doesn't tell about it's top-level declarations so it can't be inlined".into()); - allow_inline_startup = false; - } - let hooks = JsPlugin::get_compilation_hooks(compilation.id()); - let bailout = hooks - .try_read() - .expect("should have js plugin drive") - .inline_in_runtime_bailout - .call(compilation) - .await?; - if allow_inline_startup && let Some(bailout) = bailout { - buf2.push(format!("// This entry module can't be inlined because {bailout}").into()); - allow_inline_startup = false; - } - let entry_runtime_requirements = - ChunkGraph::get_module_runtime_requirements(compilation, *module, chunk.runtime()); - if allow_inline_startup - && let Some(entry_runtime_requirements) = entry_runtime_requirements - && entry_runtime_requirements.contains(RuntimeGlobals::MODULE) - { - allow_inline_startup = false; - buf2.push("// This entry module used 'module' so it can't be inlined".into()); - } + .collect::>(); + if allow_inline_startup && !chunk_ids.is_empty() { + buf2.push("// This entry module depends on other loaded chunks and execution need to be delayed".into()); + allow_inline_startup = false; + } + if allow_inline_startup && { + let module_graph = compilation.get_module_graph(); + let module_graph_cache = &compilation.module_graph_cache_artifact; + module_graph + .get_incoming_connections_by_origin_module(module) + .iter() + .any(|(origin_module, connections)| { + if let Some(origin_module) = origin_module { + connections.iter().any(|c| { + c.is_target_active(&module_graph, Some(chunk.runtime()), module_graph_cache) + }) && compilation + .chunk_graph + .get_module_runtimes_iter(*origin_module, &compilation.chunk_by_ukey) + .any(|runtime| runtime.intersection(chunk.runtime()).count() > 0) + } else { + false + } + }) + } { + buf2.push( + "// This entry module is referenced by other modules so it can't be inlined".into(), + ); + allow_inline_startup = false; + } + if allow_inline_startup && { + let codegen = compilation + .code_generation_results + .get(module, Some(chunk.runtime())); + let module_graph = compilation.get_module_graph(); + let top_level_decls = codegen + .data + .get::() + .map(|d| d.inner()) + .or_else(|| { + module_graph + .module_by_identifier(module) + .and_then(|m| m.build_info().top_level_declarations.as_ref()) + }); + top_level_decls.is_none() + } { + buf2.push("// This entry module doesn't tell about it's top-level declarations so it can't be inlined".into()); + allow_inline_startup = false; + } + let hooks = JsPlugin::get_compilation_hooks(compilation.id()); + let bailout = hooks + .try_read() + .expect("should have js plugin drive") + .inline_in_runtime_bailout + .call(compilation) + .await?; + if allow_inline_startup && let Some(bailout) = bailout { + buf2.push(format!("// This entry module can't be inlined because {bailout}").into()); + allow_inline_startup = false; + } + let entry_runtime_requirements = + ChunkGraph::get_module_runtime_requirements(compilation, *module, chunk.runtime()); + if allow_inline_startup + && let Some(entry_runtime_requirements) = entry_runtime_requirements + && entry_runtime_requirements.contains(RuntimeGlobals::MODULE) + { + allow_inline_startup = false; + buf2.push("// This entry module used 'module' so it can't be inlined".into()); + } - let module_id = ChunkGraph::get_module_id(&compilation.module_ids_artifact, *module) - .expect("should have module id"); - let mut module_id_expr = serde_json::to_string(module_id).expect("invalid module_id"); - if runtime_requirements.contains(RuntimeGlobals::ENTRY_MODULE_ID) { - module_id_expr = format!("{} = {module_id_expr}", RuntimeGlobals::ENTRY_MODULE_ID); + let module_id = ChunkGraph::get_module_id(&compilation.module_ids_artifact, *module) + .expect("should have module id"); + let mut module_id_expr = serde_json::to_string(module_id).expect("invalid module_id"); + if runtime_requirements.contains(RuntimeGlobals::ENTRY_MODULE_ID) { + module_id_expr = format!("{} = {module_id_expr}", RuntimeGlobals::ENTRY_MODULE_ID); + } + + federation_entry_calls.push(format!("__webpack_exec__({})", module_id_expr)); + for chunk_id in &chunk_ids { + if !all_chunk_ids.contains(chunk_id) { + all_chunk_ids.push(chunk_id.clone()); + } + } } - if !chunk_ids.is_empty() { - buf2.push( - format!( - "{}{}(undefined, {}, function() {{ return {}({module_id_expr}) }});", - if i + 1 == entries.len() { - format!("var {} = ", RuntimeGlobals::EXPORTS) + if !federation_entry_calls.is_empty() { + let chunk_id = chunk.expect_id(&compilation.chunk_ids_artifact); + let chunk_id_str = serde_json::to_string(chunk_id).expect("invalid chunk_id"); + let entry_fn_body = federation_entry_calls.join("; "); + + if compilation.options.experiments.mf_async_startup { + let is_esm_output = compilation.options.output.module; + if is_esm_output { + // ESM output with top-level await + buf2.push( + format!( + "const {}Promise = Promise.resolve(runtimeInitialization).then(async () => {{", + RuntimeGlobals::EXPORTS + ) + .into(), + ); + buf2.push(" const handlers = [".into()); + buf2.push(" (chunkId, promises) => (__webpack_require__.f.consumes || (() => {}))(chunkId, promises),".into()); + buf2.push(" (chunkId, promises) => (__webpack_require__.f.remotes || (() => {}))(chunkId, promises)".into()); + buf2.push(" ];".into()); + buf2.push( + format!( + " await Promise.all(handlers.reduce((p, handler) => {{ handler({}, p); return p; }}, promises));", + chunk_id_str + ) + .into(), + ); + if !all_chunk_ids.is_empty() { + buf2.push( + format!( + " return {}(0, {}, () => {{ return {}; }});", + RuntimeGlobals::STARTUP_ENTRYPOINT, + stringify_array(&all_chunk_ids), + entry_fn_body + ) + .into(), + ); } else { - "".to_string() - }, - RuntimeGlobals::ON_CHUNKS_LOADED, - stringify_array(&chunk_ids), - RuntimeGlobals::REQUIRE - ) - .into(), - ); - } else if use_require { - buf2.push( - format!( - "{}{}({module_id_expr});", - if i + 1 == entries.len() { - format!("var {} = ", RuntimeGlobals::EXPORTS) + buf2.push(format!(" return {};", entry_fn_body).into()); + } + buf2.push("});".into()); + buf2 + .push(format!("export default await {}Promise;", RuntimeGlobals::EXPORTS).into()); + } else { + // CJS output with Promise chain + buf2.push("// Wrap startup in Promise.all with federation handlers".into()); + buf2.push( + format!( + "var {} = Promise.resolve(runtimeInitialization).then(function() {{", + RuntimeGlobals::EXPORTS + ) + .into(), + ); + buf2.push(" var handlers = [".into()); + buf2.push(" function(chunkId, promises) {".into()); + buf2.push(" return (__webpack_require__.f.consumes || function(chunkId, promises) {})(chunkId, promises);".into()); + buf2.push(" },".into()); + buf2.push(" function(chunkId, promises) {".into()); + buf2.push(" return (__webpack_require__.f.remotes || function(chunkId, promises) {})(chunkId, promises);".into()); + buf2.push(" }".into()); + buf2.push(" ];".into()); + buf2.push( + format!( + " return Promise.all(handlers.reduce(function(p, handler) {{ return handler({}, p), p; }}, promises));", + chunk_id_str + ) + .into(), + ); + buf2.push("}).then(function() {".into()); + if !all_chunk_ids.is_empty() { + buf2.push( + format!( + " return {}(0, {}, function() {{ return {}; }});", + RuntimeGlobals::STARTUP_ENTRYPOINT, + stringify_array(&all_chunk_ids), + entry_fn_body + ) + .into(), + ); } else { - "".to_string() - }, - RuntimeGlobals::REQUIRE - ) - .into(), - ) - } else { - let should_exec = i + 1 == entries.len(); - if should_exec { - buf2.push(format!("var {} = {{}}", RuntimeGlobals::EXPORTS).into()); + buf2.push(format!(" return {};", entry_fn_body).into()); + } + buf2.push("});".into()); + } + } else { + buf2.push("// Wrap startup in Promise.all with federation handlers".into()); + buf2.push( + format!( + "var {} = Promise.resolve(runtimeInitialization).then(function() {{", + RuntimeGlobals::EXPORTS + ) + .into(), + ); + buf2.push(" var handlers = [".into()); + buf2.push(" function(chunkId, promises) {".into()); + buf2.push(" return (__webpack_require__.f.consumes || function(chunkId, promises) {})(chunkId, promises);".into()); + buf2.push(" },".into()); + buf2.push(" function(chunkId, promises) {".into()); + buf2.push(" return (__webpack_require__.f.remotes || function(chunkId, promises) {})(chunkId, promises);".into()); + buf2.push(" }".into()); + buf2.push(" ];".into()); + buf2.push( + format!( + " return Promise.all(handlers.reduce(function(p, handler) {{ return handler({}, p), p; }}, promises));", + chunk_id_str + ) + .into(), + ); + buf2.push("}).then(function() {".into()); + if !all_chunk_ids.is_empty() { + buf2.push( + format!( + " return {}(0, {}, function() {{ return {}; }});", + RuntimeGlobals::STARTUP_ENTRYPOINT, + stringify_array(&all_chunk_ids), + entry_fn_body + ) + .into(), + ); + } else { + buf2.push(format!(" return {};", entry_fn_body).into()); + } + buf2.push("});".into()); + } + + allow_inline_startup = false; + startup.push(buf2.join("\n").into()); + } + } else { + let mut buf2: Vec> = Vec::new(); + buf2.push("// Load entry module and return exports".into()); + + let entries = compilation + .chunk_graph + .get_chunk_entry_modules_with_chunk_group_iterable(chunk_ukey); + + for (i, (module, entry)) in entries.iter().enumerate() { + let chunk_group = compilation.chunk_group_by_ukey.expect_get(entry); + let chunk_ids = chunk_group + .chunks + .iter() + .filter(|c| *c != chunk_ukey) + .map(|chunk_ukey| { + compilation + .chunk_by_ukey + .expect_get(chunk_ukey) + .expect_id(&compilation.chunk_ids_artifact) + .to_string() + }) + .collect::>(); + if allow_inline_startup && !chunk_ids.is_empty() { + buf2.push("// This entry module depends on other loaded chunks and execution need to be delayed".into()); + allow_inline_startup = false; + } + if allow_inline_startup && { + let module_graph = compilation.get_module_graph(); + let module_graph_cache = &compilation.module_graph_cache_artifact; + module_graph + .get_incoming_connections_by_origin_module(module) + .iter() + .any(|(origin_module, connections)| { + if let Some(origin_module) = origin_module { + connections.iter().any(|c| { + c.is_target_active(&module_graph, Some(chunk.runtime()), module_graph_cache) + }) && compilation + .chunk_graph + .get_module_runtimes_iter(*origin_module, &compilation.chunk_by_ukey) + .any(|runtime| runtime.intersection(chunk.runtime()).count() > 0) + } else { + false + } + }) + } { + buf2.push( + "// This entry module is referenced by other modules so it can't be inlined".into(), + ); + allow_inline_startup = false; + } + if allow_inline_startup && { + let codegen = compilation + .code_generation_results + .get(module, Some(chunk.runtime())); + let module_graph = compilation.get_module_graph(); + let top_level_decls = codegen + .data + .get::() + .map(|d| d.inner()) + .or_else(|| { + module_graph + .module_by_identifier(module) + .and_then(|m| m.build_info().top_level_declarations.as_ref()) + }); + top_level_decls.is_none() + } { + buf2.push("// This entry module doesn't tell about it's top-level declarations so it can't be inlined".into()); + allow_inline_startup = false; + } + let hooks = JsPlugin::get_compilation_hooks(compilation.id()); + let bailout = hooks + .try_read() + .expect("should have js plugin drive") + .inline_in_runtime_bailout + .call(compilation) + .await?; + if allow_inline_startup && let Some(bailout) = bailout { + buf2.push(format!("// This entry module can't be inlined because {bailout}").into()); + allow_inline_startup = false; + } + let entry_runtime_requirements = + ChunkGraph::get_module_runtime_requirements(compilation, *module, chunk.runtime()); + if allow_inline_startup + && let Some(entry_runtime_requirements) = entry_runtime_requirements + && entry_runtime_requirements.contains(RuntimeGlobals::MODULE) + { + allow_inline_startup = false; + buf2.push("// This entry module used 'module' so it can't be inlined".into()); } - if require_scope_used { + + let module_id = ChunkGraph::get_module_id(&compilation.module_ids_artifact, *module) + .expect("should have module id"); + let mut module_id_expr = serde_json::to_string(module_id).expect("invalid module_id"); + if runtime_requirements.contains(RuntimeGlobals::ENTRY_MODULE_ID) { + module_id_expr = format!("{} = {module_id_expr}", RuntimeGlobals::ENTRY_MODULE_ID); + } + + if !chunk_ids.is_empty() { buf2.push( format!( - "__webpack_modules__[{module_id_expr}](0, {}, {});", - if should_exec { - RuntimeGlobals::EXPORTS.name() + "{}{}(undefined, {}, function() {{ return {}({module_id_expr}) }});", + if i + 1 == entries.len() { + format!("var {} = ", RuntimeGlobals::EXPORTS) } else { - "{}" + "".to_string() }, + RuntimeGlobals::ON_CHUNKS_LOADED, + stringify_array(&chunk_ids), RuntimeGlobals::REQUIRE ) .into(), ); - } else if let Some(entry_runtime_requirements) = entry_runtime_requirements - && entry_runtime_requirements.contains(RuntimeGlobals::EXPORTS) - { + } else if use_require { buf2.push( format!( - "__webpack_modules__[{module_id_expr}](0, {});", - if should_exec { - RuntimeGlobals::EXPORTS.name() + "{}{}({module_id_expr});", + if i + 1 == entries.len() { + format!("var {} = ", RuntimeGlobals::EXPORTS) } else { - "{}" - } + "".to_string() + }, + RuntimeGlobals::REQUIRE ) .into(), - ); + ) } else { - buf2.push(format!("__webpack_modules__[{module_id_expr}]();").into()); + let should_exec = i + 1 == entries.len(); + if should_exec { + buf2.push(format!("var {} = {{}}", RuntimeGlobals::EXPORTS).into()); + } + if require_scope_used { + buf2.push( + format!( + "__webpack_modules__[{module_id_expr}](0, {}, {});", + if should_exec { + RuntimeGlobals::EXPORTS.name() + } else { + "{}" + }, + RuntimeGlobals::REQUIRE + ) + .into(), + ); + } else if let Some(entry_runtime_requirements) = entry_runtime_requirements + && entry_runtime_requirements.contains(RuntimeGlobals::EXPORTS) + { + buf2.push( + format!( + "__webpack_modules__[{module_id_expr}](0, {});", + if should_exec { + RuntimeGlobals::EXPORTS.name() + } else { + "{}" + } + ) + .into(), + ); + } else { + buf2.push(format!("__webpack_modules__[{module_id_expr}]();").into()); + } } } - } - if runtime_requirements.contains(RuntimeGlobals::ON_CHUNKS_LOADED) { - buf2.push( - format!( - "__webpack_exports__ = {}(__webpack_exports__);", - RuntimeGlobals::ON_CHUNKS_LOADED - ) - .into(), - ); - } - if runtime_requirements.contains(RuntimeGlobals::STARTUP) { - allow_inline_startup = false; - header.push( - format!( - "// the startup function\n{} = {};\n", - RuntimeGlobals::STARTUP, - basic_function( - &compilation.options.output.environment, - "", - &format!("{}\nreturn {}", buf2.join("\n"), RuntimeGlobals::EXPORTS) + + if runtime_requirements.contains(RuntimeGlobals::ON_CHUNKS_LOADED) { + buf2.push( + format!( + "__webpack_exports__ = {}(__webpack_exports__);", + RuntimeGlobals::ON_CHUNKS_LOADED ) - ) - .into(), - ); - startup.push("// run startup".into()); - startup.push( - format!( - "var {} = {}();", - RuntimeGlobals::EXPORTS, - RuntimeGlobals::STARTUP - ) - .into(), - ); - } else { - startup.push("// startup".into()); - startup.push(buf2.join("\n").into()); + .into(), + ); + } + if runtime_requirements.contains(RuntimeGlobals::STARTUP) { + allow_inline_startup = false; + header.push( + format!( + "// the startup function\n{} = {};\n", + RuntimeGlobals::STARTUP, + basic_function( + &compilation.options.output.environment, + "", + &format!("{}\nreturn {}", buf2.join("\n"), RuntimeGlobals::EXPORTS) + ) + ) + .into(), + ); + startup.push("// run startup".into()); + startup.push( + format!( + "var {} = {}();", + RuntimeGlobals::EXPORTS, + RuntimeGlobals::STARTUP + ) + .into(), + ); + } else if compilation.options.experiments.mf_async_startup + && runtime_requirements.contains(RuntimeGlobals::STARTUP_ENTRYPOINT) + { + allow_inline_startup = false; + header.push( + format!( + "// the startup function (async)\n{} = {};\n", + RuntimeGlobals::STARTUP_ENTRYPOINT, + basic_function( + &compilation.options.output.environment, + "", + &format!("{}\nreturn {}", buf2.join("\n"), RuntimeGlobals::EXPORTS) + ) + ) + .into(), + ); + startup.push("// run startup".into()); + startup.push( + format!( + "var {} = {}();", + RuntimeGlobals::EXPORTS, + RuntimeGlobals::STARTUP_ENTRYPOINT + ) + .into(), + ); + } else { + startup.push("// startup".into()); + startup.push(buf2.join("\n").into()); + } } + } else if compilation.options.experiments.mf_async_startup + && runtime_requirements.contains(RuntimeGlobals::STARTUP_ENTRYPOINT) + { + header.push( + format!( + "// the startup function (async)\n// It's empty as no entry modules are in this chunk\n{} = function(){{}};", + RuntimeGlobals::STARTUP_ENTRYPOINT + ) + .into(), + ); } else if runtime_requirements.contains(RuntimeGlobals::STARTUP) { header.push( format!( @@ -528,6 +856,17 @@ impl JsPlugin { .into(), ); } + } else if compilation.options.experiments.mf_async_startup + && runtime_requirements.contains(RuntimeGlobals::STARTUP_ENTRYPOINT) + { + startup.push("// run startup".into()); + startup.push( + format!( + "var __webpack_exports__ = {}();", + RuntimeGlobals::STARTUP_ENTRYPOINT + ) + .into(), + ); } else if runtime_requirements.contains(RuntimeGlobals::STARTUP) { header.push( format!( @@ -801,9 +1140,120 @@ impl JsPlugin { .keys() .next_back() { - let mut render_source = RenderSource { - source: RawStringSource::from(startup.join("\n") + "\n").boxed(), + // Check if this entry chunk needs federation startup + let needs_federation = compilation.options.experiments.mf_async_startup + && runtime_requirements.contains(RuntimeGlobals::STARTUP_ENTRYPOINT); + let is_esm_output = compilation.options.output.module; + + let source = if needs_federation && is_esm_output { + // ESM async mode with federation - use top-level await + let chunk_id = chunk.expect_id(&compilation.chunk_ids_artifact); + let chunk_id_str = serde_json::to_string(chunk_id).expect("invalid chunk_id"); + let startup_str = startup.join("\n"); + + let mut result = ConcatSource::default(); + + // Add federation initialization using top-level await + result.add(RawStringSource::from( + "// Federation async initialization\n", + )); + result.add(RawStringSource::from("await (async () => {\n")); + result.add(RawStringSource::from( + " if (typeof __webpack_require__.x === 'function') {\n", + )); + result.add(RawStringSource::from( + " await __webpack_require__.x();\n", + )); + result.add(RawStringSource::from(" }\n")); + result.add(RawStringSource::from(" const promises = [];\n")); + result.add(RawStringSource::from(" const handlers = [\n")); + result.add(RawStringSource::from(" function(chunkId, promises) {\n")); + result.add(RawStringSource::from(" return (__webpack_require__.f.consumes || function(chunkId, promises) {})(chunkId, promises);\n")); + result.add(RawStringSource::from(" },\n")); + result.add(RawStringSource::from(" function(chunkId, promises) {\n")); + result.add(RawStringSource::from(" return (__webpack_require__.f.remotes || function(chunkId, promises) {})(chunkId, promises);\n")); + result.add(RawStringSource::from(" }\n")); + result.add(RawStringSource::from(" ];\n")); + result.add(RawStringSource::from(format!( + " await Promise.all(handlers.reduce(function(p, handler) {{ return handler({}, p), p; }}, promises));\n", + chunk_id_str + ))); + result.add(RawStringSource::from("})();\n\n")); + + // Add the original startup code + result.add(RawStringSource::from(startup_str)); + result.add(RawStringSource::from("\n")); + + result.boxed() + } else if needs_federation && !is_esm_output { + // CJS output with federation - use Promise chain + let chunk_id = chunk.expect_id(&compilation.chunk_ids_artifact); + let chunk_id_str = serde_json::to_string(chunk_id).expect("invalid chunk_id"); + let startup_str = startup.join("\n"); + + let mut result = ConcatSource::default(); + + result.add(RawStringSource::from( + "\n// Initialize federation runtime\n", + )); + result.add(RawStringSource::from( + "var runtimeInitialization = undefined;\n", + )); + result.add(RawStringSource::from( + "if (typeof __webpack_require__.x === 'function') {\n", + )); + result.add(RawStringSource::from( + " runtimeInitialization = __webpack_require__.x();\n", + )); + result.add(RawStringSource::from("}\n")); + result.add(RawStringSource::from("var promises = [];\n")); + result.add(RawStringSource::from(format!( + "var {} = Promise.resolve(runtimeInitialization).then(function() {{\n", + RuntimeGlobals::EXPORTS.name() + ))); + result.add(RawStringSource::from(" var handlers = [\n")); + result.add(RawStringSource::from(" function(chunkId, promises) {\n")); + result.add(RawStringSource::from(" return (__webpack_require__.f.consumes || function(chunkId, promises) {})(chunkId, promises);\n")); + result.add(RawStringSource::from(" },\n")); + result.add(RawStringSource::from(" function(chunkId, promises) {\n")); + result.add(RawStringSource::from(" return (__webpack_require__.f.remotes || function(chunkId, promises) {})(chunkId, promises);\n")); + result.add(RawStringSource::from(" }\n")); + result.add(RawStringSource::from(" ];\n")); + result.add(RawStringSource::from(format!( + " return Promise.all(handlers.reduce(function(p, handler) {{ return handler({}, p), p; }}, promises));\n", + chunk_id_str + ))); + result.add(RawStringSource::from("}).then(function() {\n")); + result.add(RawStringSource::from(" return (function() {\n")); + result.add(RawStringSource::from(format!(" {}\n", startup_str))); + result.add(RawStringSource::from(" return __webpack_exports__;\n")); + result.add(RawStringSource::from(" })();\n")); + result.add(RawStringSource::from("});\n")); + + result.boxed() + } else if compilation.options.experiments.mf_async_startup + && !runtime_requirements.contains(RuntimeGlobals::STARTUP_ENTRYPOINT) + && !startup.is_empty() + { + // Check for sync federation startup case + let mut result = ConcatSource::default(); + result.add(RawStringSource::from_static( + "\n// Federation startup call\n", + )); + result.add(RawStringSource::from(format!( + "{}();\n", + RuntimeGlobals::STARTUP.name() + ))); + result.add(RawStringSource::from(startup.join("\n"))); + result.add(RawStringSource::from("\n")); + result.boxed() + } else { + // Normal case - no federation + RawStringSource::from(startup.join("\n") + "\n").boxed() }; + + // Still call render_startup hook for other plugins that might need it + let mut render_source = RenderSource { source }; hooks .render_startup .call( diff --git a/crates/rspack_plugin_mf/src/container/embed_federation_runtime_module.rs b/crates/rspack_plugin_mf/src/container/embed_federation_runtime_module.rs index 01cff8655b8e..652ed31e994d 100644 --- a/crates/rspack_plugin_mf/src/container/embed_federation_runtime_module.rs +++ b/crates/rspack_plugin_mf/src/container/embed_federation_runtime_module.rs @@ -88,9 +88,26 @@ impl RuntimeModule for EmbedFederationRuntimeModule { } // Generate prevStartup wrapper pattern with defensive checks - let startup = RuntimeGlobals::STARTUP.name(); - let result = format!( - r#"var prevStartup = {startup}; + // When mf_async_startup is enabled, wrap __webpack_require__.x instead of STARTUP_ENTRYPOINT + // This ensures federation runtime modules execute BEFORE __webpack_require__.x() is called + let result = if compilation.options.experiments.mf_async_startup { + format!( + r#"var prevX = __webpack_require__.x; +var hasRun = false; +__webpack_require__.x = function() {{ + if (!hasRun) {{ + hasRun = true; +{module_executions} + }} + if (typeof prevX === 'function') {{ + return prevX.apply(this, arguments); + }} +}};"# + ) + } else { + let startup = RuntimeGlobals::STARTUP.name(); + format!( + r#"var prevStartup = {startup}; var hasRun = false; {startup} = function() {{ if (!hasRun) {{ @@ -103,7 +120,8 @@ var hasRun = false; console.warn('[MF] Invalid prevStartup'); }} }};"# - ); + ) + }; Ok(result) } diff --git a/crates/rspack_plugin_mf/src/container/embed_federation_runtime_plugin.rs b/crates/rspack_plugin_mf/src/container/embed_federation_runtime_plugin.rs index 18a511f47cc9..a010fb9038de 100644 --- a/crates/rspack_plugin_mf/src/container/embed_federation_runtime_plugin.rs +++ b/crates/rspack_plugin_mf/src/container/embed_federation_runtime_plugin.rs @@ -18,6 +18,7 @@ use rspack_sources::{ConcatSource, RawStringSource, SourceExt}; use rustc_hash::FxHashSet; use super::{ + container_entry_module::ContainerEntryModule, embed_federation_runtime_module::{ EmbedFederationRuntimeModule, EmbedFederationRuntimeModuleOptions, }, @@ -51,6 +52,28 @@ impl EmbedFederationRuntimePlugin { pub fn new() -> Self { Self::new_inner(Arc::new(Mutex::new(FxHashSet::default()))) } + + /// Check if the chunk is a container entry chunk (should NOT use async startup) + fn is_container_entry_chunk(compilation: &Compilation, chunk_ukey: &ChunkUkey) -> bool { + let entries = compilation + .chunk_graph + .get_chunk_entry_modules_with_chunk_group_iterable(chunk_ukey); + + // Inspect the last entry module (final entry) and its chunk group metadata + if let Some((module_id, chunk_group_ukey)) = entries.iter().next_back() { + let module_graph = compilation.get_module_graph(); + if let Some(module) = module_graph.module_by_identifier(module_id) { + return module.as_any().is::(); + } + let chunk_group = compilation.chunk_group_by_ukey.expect_get(chunk_group_ukey); + if let Some(entry_options) = chunk_group.kind.get_entry_options() + && entry_options.library.is_some() + { + return true; + } + } + false + } } #[plugin_hook(CompilationAdditionalChunkRuntimeRequirements for EmbedFederationRuntimePlugin)] @@ -76,10 +99,18 @@ async fn additional_chunk_runtime_requirements_tree( // Federation is enabled for runtime chunks or entry chunks let is_enabled = has_runtime || has_entry_modules; + let is_container_entry_chunk = Self::is_container_entry_chunk(compilation, chunk_ukey); + let use_async_startup = + compilation.options.experiments.mf_async_startup && !is_container_entry_chunk; if is_enabled { - // Add STARTUP requirement - runtime_requirements.insert(RuntimeGlobals::STARTUP); + // Add STARTUP or STARTUP_ENTRYPOINT based on mf_async_startup experiment + if use_async_startup { + runtime_requirements.insert(RuntimeGlobals::STARTUP_ENTRYPOINT); + runtime_requirements.insert(RuntimeGlobals::ENSURE_CHUNK_HANDLERS); + } else { + runtime_requirements.insert(RuntimeGlobals::STARTUP); + } } Ok(()) @@ -145,7 +176,7 @@ async fn compilation( .await .tap(collector); - // Register render startup hook, patches entrypoints + // Register render startup hook to patch entrypoints when needed let js_hooks = JsPlugin::get_compilation_hooks_mut(compilation.id()); js_hooks .write() @@ -171,6 +202,12 @@ async fn render_startup( return Ok(()); } + if compilation.options.experiments.mf_async_startup + && Self::is_container_entry_chunk(compilation, chunk_ukey) + { + return Ok(()); + } + // Only process chunks that have federation dependencies let collected_deps = self .collected_dependency_ids @@ -196,6 +233,11 @@ async fn render_startup( return Ok(()); } + // When async startup is enabled, bootstrap manipulation happens in the JavaScript plugin + if compilation.options.experiments.mf_async_startup { + return Ok(()); + } + // Entry chunks delegating to runtime need explicit startup calls if !has_runtime && has_entry_modules { let mut startup_with_call = ConcatSource::default(); diff --git a/crates/rspack_plugin_mf/src/container/module_federation_runtime_plugin.rs b/crates/rspack_plugin_mf/src/container/module_federation_runtime_plugin.rs index ea1fc35583dc..92ee5fb44293 100644 --- a/crates/rspack_plugin_mf/src/container/module_federation_runtime_plugin.rs +++ b/crates/rspack_plugin_mf/src/container/module_federation_runtime_plugin.rs @@ -41,11 +41,29 @@ async fn additional_tree_runtime_requirements( &self, compilation: &mut Compilation, chunk_ukey: &ChunkUkey, - _runtime_requirements: &mut RuntimeGlobals, + runtime_requirements: &mut RuntimeGlobals, ) -> Result<()> { // Add base FederationRuntimeModule which is responsible for providing bundler data to the runtime. compilation.add_runtime_module(chunk_ukey, Box::::default())?; + // When mf_async_startup experiment is enabled, add STARTUP_ENTRYPOINT and related requirements + // for runtime chunks with entry modules + if compilation.options.experiments.mf_async_startup { + let chunk = compilation.chunk_by_ukey.expect_get(chunk_ukey); + + if chunk.has_runtime(&compilation.chunk_group_by_ukey) + && compilation + .chunk_graph + .get_number_of_entry_modules(chunk_ukey) + > 0 + { + runtime_requirements.insert(RuntimeGlobals::STARTUP_ENTRYPOINT); + runtime_requirements.insert(RuntimeGlobals::ENSURE_CHUNK); + runtime_requirements.insert(RuntimeGlobals::ENSURE_CHUNK_INCLUDE_ENTRIES); + runtime_requirements.insert(RuntimeGlobals::ENSURE_CHUNK_HANDLERS); + } + } + Ok(()) } diff --git a/crates/rspack_plugin_mf/src/lib.rs b/crates/rspack_plugin_mf/src/lib.rs index 2c079ace82da..c509c363dd5a 100644 --- a/crates/rspack_plugin_mf/src/lib.rs +++ b/crates/rspack_plugin_mf/src/lib.rs @@ -7,6 +7,7 @@ pub use container::{ container_reference_plugin::{ ContainerReferencePlugin, ContainerReferencePluginOptions, RemoteOptions, }, + embed_federation_runtime_module::EmbedFederationRuntimeModule, module_federation_runtime_plugin::{ ModuleFederationRuntimePlugin, ModuleFederationRuntimePluginOptions, }, diff --git a/crates/rspack_plugin_runtime/Cargo.toml b/crates/rspack_plugin_runtime/Cargo.toml index 2009e6156d5c..e1c4e94786d7 100644 --- a/crates/rspack_plugin_runtime/Cargo.toml +++ b/crates/rspack_plugin_runtime/Cargo.toml @@ -33,5 +33,5 @@ tracing = { workspace = true } ignored = ["tracing", "tokio"] [lints.rust.unexpected_cfgs] -level = "warn" check-cfg = ['cfg(allocative)'] +level = "warn" diff --git a/crates/rspack_plugin_runtime/src/array_push_callback_chunk_format.rs b/crates/rspack_plugin_runtime/src/array_push_callback_chunk_format.rs index 9f58b4fe2c79..26da51236f2d 100644 --- a/crates/rspack_plugin_runtime/src/array_push_callback_chunk_format.rs +++ b/crates/rspack_plugin_runtime/src/array_push_callback_chunk_format.rs @@ -53,7 +53,11 @@ async fn additional_chunk_runtime_requirements( .get_number_of_entry_modules(chunk_ukey) > 0 { - runtime_requirements.insert(RuntimeGlobals::ON_CHUNKS_LOADED); + if compilation.options.experiments.mf_async_startup { + runtime_requirements.insert(RuntimeGlobals::STARTUP_ENTRYPOINT); + } else { + runtime_requirements.insert(RuntimeGlobals::ON_CHUNKS_LOADED); + } runtime_requirements.insert(RuntimeGlobals::EXPORTS); runtime_requirements.insert(RuntimeGlobals::REQUIRE); } @@ -148,7 +152,8 @@ async fn render_chunk( let entries = compilation .chunk_graph .get_chunk_entry_modules_with_chunk_group_iterable(chunk_ukey); - let start_up_source = generate_entry_startup(compilation, chunk_ukey, entries, true); + let passive = !compilation.options.experiments.mf_async_startup; + let start_up_source = generate_entry_startup(compilation, chunk_ukey, entries, passive); let last_entry_module = entries .keys() .next_back() diff --git a/crates/rspack_plugin_runtime/src/lib.rs b/crates/rspack_plugin_runtime/src/lib.rs index a11fe84ec4aa..ca082c3942d0 100644 --- a/crates/rspack_plugin_runtime/src/lib.rs +++ b/crates/rspack_plugin_runtime/src/lib.rs @@ -33,17 +33,30 @@ pub use runtime_module_from_js::RuntimeModuleFromJs; mod drive; pub use drive::*; -pub fn enable_chunk_loading_plugin(loading_type: ChunkLoadingType, plugins: &mut Vec) { +pub fn enable_chunk_loading_plugin( + loading_type: ChunkLoadingType, + mf_async_startup: bool, + plugins: &mut Vec, +) { match loading_type { ChunkLoadingType::Jsonp => { + if mf_async_startup { + plugins.push( + StartupChunkDependenciesPlugin::new(ChunkLoading::Enable(ChunkLoadingType::Jsonp), true) + .boxed(), + ); + } plugins.push(JsonpChunkLoadingPlugin::default().boxed()); } ChunkLoadingType::Require => { plugins.push( - StartupChunkDependenciesPlugin::new(ChunkLoading::Enable(ChunkLoadingType::Require), false) - .boxed(), + StartupChunkDependenciesPlugin::new( + ChunkLoading::Enable(ChunkLoadingType::Require), + mf_async_startup, + ) + .boxed(), ); - plugins.push(CommonJsChunkLoadingPlugin::new(false).boxed()) + plugins.push(CommonJsChunkLoadingPlugin::new(mf_async_startup).boxed()) } ChunkLoadingType::AsyncNode => { plugins.push( diff --git a/crates/rspack_plugin_runtime/src/startup_chunk_dependencies.rs b/crates/rspack_plugin_runtime/src/startup_chunk_dependencies.rs index 89044c3e9230..37228e370082 100644 --- a/crates/rspack_plugin_runtime/src/startup_chunk_dependencies.rs +++ b/crates/rspack_plugin_runtime/src/startup_chunk_dependencies.rs @@ -30,6 +30,14 @@ async fn additional_tree_runtime_requirements( runtime_requirements: &mut RuntimeGlobals, ) -> Result<()> { let is_enabled_for_chunk = is_enabled_for_chunk(chunk_ukey, &self.chunk_loading, compilation); + + // Skip adding STARTUP if async MF startup is active and STARTUP_ENTRYPOINT is already present. + if compilation.options.experiments.mf_async_startup + && runtime_requirements.contains(RuntimeGlobals::STARTUP_ENTRYPOINT) + { + return Ok(()); + } + if compilation .chunk_graph .has_chunk_entry_dependent_chunks(chunk_ukey, &compilation.chunk_group_by_ukey) diff --git a/crates/rspack_plugin_web_worker_template/src/lib.rs b/crates/rspack_plugin_web_worker_template/src/lib.rs index 2b6f7a4b840e..c51318acee4f 100644 --- a/crates/rspack_plugin_web_worker_template/src/lib.rs +++ b/crates/rspack_plugin_web_worker_template/src/lib.rs @@ -3,5 +3,6 @@ use rspack_plugin_runtime::{ArrayPushCallbackChunkFormatPlugin, enable_chunk_loa pub fn web_worker_template_plugin(plugins: &mut Vec) { plugins.push(ArrayPushCallbackChunkFormatPlugin::default().boxed()); - enable_chunk_loading_plugin(ChunkLoadingType::ImportScripts, plugins); + // ImportScripts always uses async_chunk_loading: true (hardcoded in enable_chunk_loading_plugin) + enable_chunk_loading_plugin(ChunkLoadingType::ImportScripts, true, plugins); } diff --git a/examples/basic/index.js b/examples/basic/index.js index aa5897581b08..bccbd1e66420 100644 --- a/examples/basic/index.js +++ b/examples/basic/index.js @@ -1 +1,16 @@ import "./lib"; +import React from "react"; + +console.log("React version:", React.version); + +// Test React hooks +const [count, _setCount] = React.useState(0); +console.log("useState initialized with:", count); + +// Test createElement +const element = React.createElement( + "div", + { className: "test" }, + "Hello from Module Federation!" +); +console.log("Created element:", element); diff --git a/examples/basic/rspack.config.cjs b/examples/basic/rspack.config.cjs index 0532a641f18f..ba5f3bfefd25 100644 --- a/examples/basic/rspack.config.cjs +++ b/examples/basic/rspack.config.cjs @@ -1,6 +1,23 @@ +const _rspack = require("../../packages/rspack/dist/index.js"); + module.exports = { context: __dirname, entry: { main: "./index.js" - } + }, + mode: "development", + devtool: false, + output: { + module: true, + libraryTarget: "module", + chunkFormat: "module" + }, + optimization: { + runtimeChunk: "single" + }, + experiments: { + mfAsyncStartup: false, + outputModule: true + }, + plugins: [] }; diff --git a/packages/rspack/etc/core.api.md b/packages/rspack/etc/core.api.md index be9e7f7eeb32..f85a6803afd6 100644 --- a/packages/rspack/etc/core.api.md +++ b/packages/rspack/etc/core.api.md @@ -2405,6 +2405,7 @@ export type Experiments = { typeReexportsPresence?: boolean; lazyBarrel?: boolean; deferImport?: boolean; + mfAsyncStartup?: boolean; }; // @public (undocumented) @@ -2480,6 +2481,8 @@ export interface ExperimentsNormalized { // @deprecated (undocumented) lazyCompilation?: false | LazyCompilationOptions; // (undocumented) + mfAsyncStartup?: boolean; + // (undocumented) nativeWatcher?: boolean; // (undocumented) outputModule?: boolean; diff --git a/packages/rspack/src/config/normalization.ts b/packages/rspack/src/config/normalization.ts index e39dcab8f9a7..c47f15105d4b 100644 --- a/packages/rspack/src/config/normalization.ts +++ b/packages/rspack/src/config/normalization.ts @@ -382,7 +382,8 @@ export const getNormalizedRspackOptions = ( parallelCodeSplitting: experiments.parallelCodeSplitting, buildHttp: experiments.buildHttp, parallelLoader: experiments.parallelLoader, - useInputFileSystem: experiments.useInputFileSystem + useInputFileSystem: experiments.useInputFileSystem, + mfAsyncStartup: experiments.mfAsyncStartup ?? false }; }), watch: config.watch, @@ -671,6 +672,7 @@ export interface ExperimentsNormalized { lazyBarrel?: boolean; nativeWatcher?: boolean; deferImport?: boolean; + mfAsyncStartup?: boolean; } export type IgnoreWarningsNormalized = (( diff --git a/packages/rspack/src/config/types.ts b/packages/rspack/src/config/types.ts index 32866b4befa9..c1618e549538 100644 --- a/packages/rspack/src/config/types.ts +++ b/packages/rspack/src/config/types.ts @@ -2843,6 +2843,11 @@ export type Experiments = { * @default false */ deferImport?: boolean; + /** + * Enable async startup for Module Federation + * @default false + */ + mfAsyncStartup?: boolean; }; //#endregion diff --git a/tests/rspack-test/Defaults.test.js b/tests/rspack-test/Defaults.test.js index c2778885b5a0..4f43cb7bf28e 100644 --- a/tests/rspack-test/Defaults.test.js +++ b/tests/rspack-test/Defaults.test.js @@ -91,6 +91,7 @@ function assertWebpackConfig(config) { filterObjectPaths(webpackBaseConfig, rspackSupportedConfig); // PATCH DIFF delete rspackBaseConfig.experiments.topLevelAwait; + delete rspackBaseConfig.experiments.mfAsyncStartup; expect(rspackBaseConfig).toEqual(webpackBaseConfig); } diff --git a/tests/rspack-test/configCases/plugins/mini-css-extract-plugin/rspack.config.js b/tests/rspack-test/configCases/plugins/mini-css-extract-plugin/rspack.config.js index d4c72139ef84..148e159336b0 100644 --- a/tests/rspack-test/configCases/plugins/mini-css-extract-plugin/rspack.config.js +++ b/tests/rspack-test/configCases/plugins/mini-css-extract-plugin/rspack.config.js @@ -45,20 +45,15 @@ const config = (i, options) => ({ .chunks.map(c => c.id) .sort(); - expect(chunkIds).toEqual(process.env.WASM ? [ - "a", - "b", - "c", - "chunk_js-_d5940", - "chunk_js-_d5941", - "d_css", - "x" - ] : [ + // The two dynamic chunks have a stable prefix but non-stable suffixes across runtimes (native vs wasm). + const dynamicChunkIds = chunkIds.filter(id => id.startsWith("chunk_js-_")); + const staticChunkIds = chunkIds.filter(id => !id.startsWith("chunk_js-_")).sort(); + + expect(dynamicChunkIds).toHaveLength(2); + expect(staticChunkIds).toEqual([ "a", "b", "c", - "chunk_js-_aaff0", - "chunk_js-_aaff1", "d_css", "x" ]); diff --git a/tests/rspack-test/defaultsCases/default/base.js b/tests/rspack-test/defaultsCases/default/base.js index 0332df841cbf..5a4b4b126512 100644 --- a/tests/rspack-test/defaultsCases/default/base.js +++ b/tests/rspack-test/defaultsCases/default/base.js @@ -48,6 +48,7 @@ module.exports = { inlineEnum: false, lazyBarrel: true, lazyCompilation: false, + mfAsyncStartup: false, parallelCodeSplitting: false, parallelLoader: false, rspackFuture: Object { diff --git a/tests/rspack-test/serialCases/container-1-5/2-async-startup-sync-imports/App.js b/tests/rspack-test/serialCases/container-1-5/2-async-startup-sync-imports/App.js new file mode 100644 index 000000000000..0403930aeede --- /dev/null +++ b/tests/rspack-test/serialCases/container-1-5/2-async-startup-sync-imports/App.js @@ -0,0 +1,12 @@ +// Test using synchronous imports from federated modules +// NOT using dynamic import() anywhere +import React from "react"; +import ComponentA from "containerA/ComponentA"; +import ComponentB from "containerB/ComponentB"; +import LocalComponentB from "./ComponentB"; + +export default () => { + return `App rendered with [${React()}] and [${ComponentA()}] and [${ComponentB()}]`; +}; + +expect(ComponentB).not.toBe(LocalComponentB); diff --git a/tests/rspack-test/serialCases/container-1-5/2-async-startup-sync-imports/ComponentB.js b/tests/rspack-test/serialCases/container-1-5/2-async-startup-sync-imports/ComponentB.js new file mode 100644 index 000000000000..1943469c7461 --- /dev/null +++ b/tests/rspack-test/serialCases/container-1-5/2-async-startup-sync-imports/ComponentB.js @@ -0,0 +1,5 @@ +import React from "react"; + +export default () => { + return `ComponentB rendered with [${React()}]`; +}; diff --git a/tests/rspack-test/serialCases/container-1-5/2-async-startup-sync-imports/ComponentC.js b/tests/rspack-test/serialCases/container-1-5/2-async-startup-sync-imports/ComponentC.js new file mode 100644 index 000000000000..3ff3832c7180 --- /dev/null +++ b/tests/rspack-test/serialCases/container-1-5/2-async-startup-sync-imports/ComponentC.js @@ -0,0 +1,7 @@ +import React from "react"; +import ComponentA from "containerA/ComponentA"; +import ComponentB from "containerB/ComponentB"; + +export default () => { + return `ComponentC rendered with [${React()}] and [${ComponentA()}] and [${ComponentB()}]`; +}; diff --git a/tests/rspack-test/serialCases/container-1-5/2-async-startup-sync-imports/index.js b/tests/rspack-test/serialCases/container-1-5/2-async-startup-sync-imports/index.js new file mode 100644 index 000000000000..75ff41cfcecf --- /dev/null +++ b/tests/rspack-test/serialCases/container-1-5/2-async-startup-sync-imports/index.js @@ -0,0 +1,66 @@ +const fs = require("fs"); +const path = require("path"); + +// Reset federation state between serial cases to ensure deterministic share scopes. +if (globalThis.__FEDERATION__) { + globalThis.__GLOBAL_LOADING_REMOTE_ENTRY__ = {}; + //@ts-ignore + globalThis.__FEDERATION__.__INSTANCES__.forEach(instance => { + instance.moduleCache.clear(); + if (globalThis[instance.name]) { + delete globalThis[instance.name]; + } + }); + globalThis.__FEDERATION__.__INSTANCES__ = []; +} + +it("should load the component from container", () => { + return import("./App").then(({ default: App }) => { + const rendered = App(); + const initial = parseRenderVersions(rendered); + expect(initial.host).toBe("2.1.0"); + expect(initial.localB).toBe("2.1.0"); + expect(["0.1.2", "3.2.1"]).toContain(initial.remote); + return import("./upgrade-react").then(({ default: upgrade }) => { + upgrade(); + const rendered = App(); + const upgraded = parseRenderVersions(rendered); + expect(upgraded.host).toBe("3.2.1"); + expect(upgraded.localB).toBe("3.2.1"); + expect(["0.1.2", "3.2.1"]).toContain(upgraded.remote); + }); + }); +}); + +it("should emit promise-based bootstrap in CommonJS bundle", () => { + // Determine the base directory (handling both CJS and ESM execution contexts) + const baseDir = __dirname.endsWith("module") ? path.dirname(__dirname) : __dirname; + const content = fs.readFileSync(path.join(baseDir, "main.js"), "utf-8"); + expect(content).toContain("Promise.resolve().then(function() {"); +}); + +it("should emit awaited bootstrap in ESM bundle", () => { + // Determine the base directory (handling both CJS and ESM execution contexts) + const baseDir = __dirname.endsWith("module") ? path.dirname(__dirname) : __dirname; + const content = fs.readFileSync( + path.join(baseDir, "module", "main.mjs"), + "utf-8" + ); + expect(content).toContain( + "const __webpack_exports__Promise = Promise.resolve().then(async () =>" + ); + expect(content).toContain("export default await __webpack_exports__Promise;"); +}); +const parseRenderVersions = rendered => { + const match = rendered.match( + /^App rendered with \[This is react ([^\]]+)\] and \[ComponentA rendered with \[This is react ([^\]]+)\]\] and \[ComponentB rendered with \[This is react ([^\]]+)\]\]$/ + ); + if (!match) { + throw new Error(`Unexpected render output: ${rendered}`); + } + return { + host: match[1], + remote: match[2], + localB: match[3] + }; +}; diff --git a/tests/rspack-test/serialCases/container-1-5/2-async-startup-sync-imports/node_modules/package.json b/tests/rspack-test/serialCases/container-1-5/2-async-startup-sync-imports/node_modules/package.json new file mode 100644 index 000000000000..87032da008ab --- /dev/null +++ b/tests/rspack-test/serialCases/container-1-5/2-async-startup-sync-imports/node_modules/package.json @@ -0,0 +1,3 @@ +{ + "version": "2.1.0" +} diff --git a/tests/rspack-test/serialCases/container-1-5/2-async-startup-sync-imports/node_modules/react.js b/tests/rspack-test/serialCases/container-1-5/2-async-startup-sync-imports/node_modules/react.js new file mode 100644 index 000000000000..97d35a4bc9c9 --- /dev/null +++ b/tests/rspack-test/serialCases/container-1-5/2-async-startup-sync-imports/node_modules/react.js @@ -0,0 +1,3 @@ +let version = "2.1.0"; +export default () => `This is react ${version}`; +export function setVersion(v) { version = v; } diff --git a/tests/rspack-test/serialCases/container-1-5/2-async-startup-sync-imports/package.json b/tests/rspack-test/serialCases/container-1-5/2-async-startup-sync-imports/package.json new file mode 100644 index 000000000000..be6238fec84d --- /dev/null +++ b/tests/rspack-test/serialCases/container-1-5/2-async-startup-sync-imports/package.json @@ -0,0 +1,9 @@ +{ + "private": true, + "engines": { + "node": ">=10.13.0" + }, + "dependencies": { + "react": "*" + } +} diff --git a/tests/rspack-test/serialCases/container-1-5/2-async-startup-sync-imports/rspack.config.js b/tests/rspack-test/serialCases/container-1-5/2-async-startup-sync-imports/rspack.config.js new file mode 100644 index 000000000000..6ad6fc15ed3a --- /dev/null +++ b/tests/rspack-test/serialCases/container-1-5/2-async-startup-sync-imports/rspack.config.js @@ -0,0 +1,68 @@ +// eslint-disable-next-line node/no-unpublished-require +const { ModuleFederationPlugin } = require("@rspack/core").container; + +const common = { + entry: { + main: "./index.js" + } +}; + +/** @type {ConstructorParameters[0]} */ +const commonMF = { + runtime: false, + exposes: { + "./ComponentB": "./ComponentB", + "./ComponentC": "./ComponentC" + }, + shared: ["react"] +}; + +/** @type {import("@rspack/core").Configuration[]} */ +module.exports = [ + { + ...common, + experiments: { + mfAsyncStartup: true + }, + output: { + filename: "[name].js", + uniqueName: "2-async-startup-sync-imports" + }, + plugins: [ + new ModuleFederationPlugin({ + name: "container", + library: { type: "commonjs-module" }, + filename: "container.js", + remotes: { + containerA: "../0-container-full/container.js", + containerB: "./container.js" + }, + ...commonMF + }) + ] + }, + { + ...common, + experiments: { + outputModule: true, + mfAsyncStartup: true + }, + output: { + filename: "module/[name].mjs", + uniqueName: "2-async-startup-sync-imports-mjs" + }, + plugins: [ + new ModuleFederationPlugin({ + name: "container", + library: { type: "module" }, + filename: "module/container.mjs", + remotes: { + containerA: "../../0-container-full/module/container.mjs", + containerB: "./container.mjs" + }, + ...commonMF + }) + ], + target: "node14" + } +]; diff --git a/tests/rspack-test/serialCases/container-1-5/2-async-startup-sync-imports/test.config.js b/tests/rspack-test/serialCases/container-1-5/2-async-startup-sync-imports/test.config.js new file mode 100644 index 000000000000..616cd002c621 --- /dev/null +++ b/tests/rspack-test/serialCases/container-1-5/2-async-startup-sync-imports/test.config.js @@ -0,0 +1,6 @@ +/** @type {import('@rspack/test-tools').TConfigCaseConfig} */ +module.exports = { + findBundle: function (i, options) { + return i === 0 ? "./main.js" : "./module/main.mjs"; + } +}; diff --git a/tests/rspack-test/serialCases/container-1-5/2-async-startup-sync-imports/upgrade-react.js b/tests/rspack-test/serialCases/container-1-5/2-async-startup-sync-imports/upgrade-react.js new file mode 100644 index 000000000000..06fe09b52f70 --- /dev/null +++ b/tests/rspack-test/serialCases/container-1-5/2-async-startup-sync-imports/upgrade-react.js @@ -0,0 +1,11 @@ +import React, { setVersion } from "react"; + +export default function upgrade() { + // Detect current version and upgrade accordingly + const current = React(); + if (current.includes("2.1.0")) { + setVersion("3.2.1"); + } else if (current.includes("3.2.1")) { + setVersion("4.3.2"); + } +}