diff --git a/README.md b/README.md index 73dd97edc..0fcdc32db 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@
- + Mako logo # Mako 🦈 @@ -10,7 +10,7 @@ [![](https://badgen.net/npm/license/umi)](https://www.npmjs.com/package/@umijs/mako) [![codecov](https://codecov.io/gh/umijs/mako/graph/badge.svg?token=ptCnNedFGf)](https://codecov.io/gh/umijs/mako) -Mako `['mɑːkoʊ]` is an **extremely fast**, **production-grade** web bundler based on **Rust**. +Mako `['mɑːkoʊ]` is an **extremely fast**, **production-grade** web bundler based on **Rust**. ✨ See more at [makojs.dev](https://makojs.dev).
@@ -56,6 +56,7 @@ This project is inspired by: - [oxc-resolver](https://github.com/oxc-project/oxc-resolver) by [@Boshen](https://github.com/Boshen) which powered the resolver of Mako. - [Oxc](https://github.com/oxc-project/oxc/) by [@Boshen](https://github.com/Boshen) from which we learned a lot about how to develop efficiently in Rust. - [biome](https://github.com/biomejs/biome) by [@ematipico](https://github.com/ematipico) from which we learned a lot about how to develop efficiently in Rust. +- [module-federation](https://github.com/module-federation/core) by [@ScriptedAlchemy](https://github.com/ScriptedAlchemy),which inspired a lot and powered the module federation feature of Mako. ## LICENSE diff --git a/crates/binding/src/js_plugin.rs b/crates/binding/src/js_plugin.rs index 221658554..9c3895a74 100644 --- a/crates/binding/src/js_plugin.rs +++ b/crates/binding/src/js_plugin.rs @@ -218,6 +218,7 @@ impl Plugin for JsPlugin { &self, content: &mut Content, path: &str, + _is_entry: bool, context: &Arc, ) -> Result> { if let Some(hook) = &self.hooks.transform_include { diff --git a/crates/binding/src/lib.rs b/crates/binding/src/lib.rs index ec5f5ae6e..8eadaf29a 100644 --- a/crates/binding/src/lib.rs +++ b/crates/binding/src/lib.rs @@ -157,9 +157,34 @@ pub struct BuildParams { rscClient?: false | { "logServerComponent": "error" | "ignore"; }; + moduleFederation?: { + name: string; + filename?: string; + exposes?: Record; + shared: Record; + remotes?: Record; + runtimePlugins?: string[]; + shareScope?: string; + shareStrategy?: "version-first" | "loaded-first"; + implementation: string; + }; experimental?: { webpackSyntaxValidate?: string[]; + requireContext?: bool; + ignoreNonLiteralRequire?: bool; + magicComment?: bool; + detectCircularDependence?: { ignore?: string[] }; rustPlugins?: Array<[string, any]>; + centralEnsure?: bool, + importsChecker?: bool, }; watch?: { ignoredPaths?: string[]; diff --git a/crates/mako/src/build.rs b/crates/mako/src/build.rs index 7c66dd0e7..d061d9aab 100644 --- a/crates/mako/src/build.rs +++ b/crates/mako/src/build.rs @@ -16,9 +16,9 @@ use crate::ast::file::{Content, File, JsContent}; use crate::ast::utils::get_module_system; use crate::compiler::{Compiler, Context}; use crate::generate::chunk_pot::util::hash_hashmap; -use crate::module::{Module, ModuleAst, ModuleId, ModuleInfo}; +use crate::module::{FedereationModuleType, Module, ModuleAst, ModuleId, ModuleInfo, ModuleSystem}; use crate::plugin::NextBuildParam; -use crate::resolve::ResolverResource; +use crate::resolve::{ConsumeSharedInfo, RemoteInfo, ResolverResource}; use crate::utils::thread_pool; #[derive(Debug, Error)] @@ -46,6 +46,15 @@ impl Compiler { rs.send(result).unwrap(); }); }; + + let build_consume_share_with_pool = |consume_share_info: ConsumeSharedInfo| { + let rs = rs.clone(); + let context = self.context.clone(); + thread_pool::spawn(move || { + let result = Self::build_consume_shared_module(consume_share_info, context.clone()); + rs.send(result).unwrap(); + }); + }; let mut count = 0; for file in files { count += 1; @@ -127,6 +136,14 @@ impl Compiler { ResolverResource::Ignored(_) => { Self::create_ignored_module(&path, self.context.clone()) } + ResolverResource::Remote(remote_into) => { + Self::create_remote_module(remote_into) + } + ResolverResource::Shared(consume_share_info) => { + count += 1; + build_consume_share_with_pool(consume_share_info.clone()); + Self::create_empty_module(&dep_module_id) + } }; // 拿到依赖之后需要直接添加 module 到 module_graph 里,不能等依赖 build 完再添加 @@ -134,6 +151,7 @@ impl Compiler { module_ids.insert(module.id.clone()); module_graph.add_module(module); } + module_graph.add_dependency(&module_id, &dep_module_id, dep.dependency); } if count == 0 { @@ -267,6 +285,12 @@ __mako_require__.loadScript('{}', (e) => e.type === 'load' ? resolve() : reject( result } } + pub fn build_consume_shared_module( + consume_share_info: ConsumeSharedInfo, + _context: Arc, + ) -> Result { + Ok(Self::create_consume_share_module(consume_share_info)) + } pub fn build_module( file: &File, @@ -279,6 +303,7 @@ __mako_require__.loadScript('{}', (e) => e.type === 'load' ? resolve() : reject( let content = context.plugin_driver.load_transform( &mut content, &file.path.to_string_lossy(), + file.is_entry, &context, )?; file.set_content(content); @@ -329,4 +354,32 @@ __mako_require__.loadScript('{}', (e) => e.type === 'load' ? resolve() : reject( let module = Module::new(module_id, is_entry, Some(info)); Ok(module) } + + pub(crate) fn create_remote_module(remote_info: RemoteInfo) -> Module { + Module { + is_entry: false, + id: remote_info.module_id.as_str().into(), + info: Some(ModuleInfo { + resolved_resource: Some(ResolverResource::Remote(remote_info.clone())), + federation: Some(FedereationModuleType::Remote), + ..Default::default() + }), + side_effects: true, + } + } + + pub(crate) fn create_consume_share_module(consume_share_info: ConsumeSharedInfo) -> Module { + Module { + is_entry: false, + id: consume_share_info.module_id.as_str().into(), + info: Some(ModuleInfo { + deps: consume_share_info.deps.clone(), + resolved_resource: Some(ResolverResource::Shared(consume_share_info.clone())), + federation: Some(FedereationModuleType::ConsumeShare), + module_system: ModuleSystem::Custom, + ..Default::default() + }), + side_effects: true, + } + } } diff --git a/crates/mako/src/build/analyze_deps.rs b/crates/mako/src/build/analyze_deps.rs index 1bb374aa5..2f7b5eb11 100644 --- a/crates/mako/src/build/analyze_deps.rs +++ b/crates/mako/src/build/analyze_deps.rs @@ -59,10 +59,14 @@ impl AnalyzeDeps { ); match result { Ok(resolver_resource) => { - resolved_deps.push(ResolvedDep { + let resolved_dep = ResolvedDep { resolver_resource, dependency: dep, - }); + }; + context + .plugin_driver + .after_resolve(&resolved_dep, &context)?; + resolved_deps.push(resolved_dep); } Err(_err) => { missing_deps.insert(dep.source.clone(), dep); diff --git a/crates/mako/src/compiler.rs b/crates/mako/src/compiler.rs index 960222c8e..187d9f3da 100644 --- a/crates/mako/src/compiler.rs +++ b/crates/mako/src/compiler.rs @@ -23,6 +23,7 @@ use crate::generate::optimize_chunk::OptimizeChunksInfo; use crate::module_graph::ModuleGraph; use crate::plugin::{Plugin, PluginDriver, PluginGenerateEndParams}; use crate::plugins; +use crate::plugins::module_federation::ModuleFederationPlugin; use crate::resolve::{get_resolvers, Resolvers}; use crate::share::helpers::SWC_HELPERS; use crate::stats::StatsInfo; @@ -314,7 +315,6 @@ impl Compiler { Arc::new(plugins::bundless_compiler::BundlessCompilerPlugin {}), ); } - if std::env::var("DEBUG_GRAPH").is_ok_and(|v| v == "true") { plugins.push(Arc::new(plugins::graphviz::Graphviz {})); } @@ -327,6 +327,10 @@ impl Compiler { plugins.push(Arc::new(plugins::central_ensure::CentralChunkEnsure {})); } + if let Some(mf_cfg) = config.module_federation.as_ref() { + plugins.push(Arc::new(ModuleFederationPlugin::new(mf_cfg.clone()))); + } + if let Some(minifish_config) = &config._minifish { let inject = if let Some(inject) = &minifish_config.inject { let mut map = HashMap::new(); @@ -423,7 +427,7 @@ impl Compiler { .entry .values() .map(|entry| { - let mut entry = entry.to_string_lossy().to_string(); + let mut entry = entry.import.to_string_lossy().to_string(); let is_browser = matches!( self.context.config.platform, crate::config::Platform::Browser @@ -492,6 +496,7 @@ impl Compiler { self.context .plugin_driver .generate_end(¶ms, &self.context)?; + self.context.plugin_driver.write_bundle(&self.context)?; Ok(()) } diff --git a/crates/mako/src/config.rs b/crates/mako/src/config.rs index 58f278eb5..822281c55 100644 --- a/crates/mako/src/config.rs +++ b/crates/mako/src/config.rs @@ -3,6 +3,7 @@ mod code_splitting; mod dev_server; mod devtool; mod duplicate_package_checker; +pub mod entry; mod experimental; mod external; mod generic_usize; @@ -12,6 +13,7 @@ mod macros; mod manifest; mod minifish; mod mode; +pub mod module_federation; mod module_id_strategy; mod optimization; mod output; @@ -28,9 +30,9 @@ mod tree_shaking; mod umd; mod watch; -use std::collections::{BTreeMap, HashMap}; +use std::collections::HashMap; use std::fmt; -use std::path::{Path, PathBuf}; +use std::path::Path; pub use analyze::AnalyzeConfig; use anyhow::{anyhow, Result}; @@ -42,6 +44,7 @@ pub use devtool::{deserialize_devtool, DevtoolConfig}; pub use duplicate_package_checker::{ deserialize_check_duplicate_package, DuplicatePackageCheckerConfig, }; +use entry::{Entry, EntryItem}; use experimental::ExperimentalConfig; pub use external::{ ExternalAdvanced, ExternalAdvancedSubpath, ExternalAdvancedSubpathConverter, @@ -54,6 +57,7 @@ pub use manifest::{deserialize_manifest, ManifestConfig}; use miette::{miette, ByteOffset, Diagnostic, NamedSource, SourceOffset, SourceSpan}; pub use minifish::{deserialize_minifish, MinifishConfig}; pub use mode::Mode; +use module_federation::ModuleFederationConfig; pub use module_id_strategy::ModuleIdStrategy; pub use optimization::{deserialize_optimization, OptimizationConfig}; use output::get_default_chunk_loading_global; @@ -135,7 +139,7 @@ pub enum CopyConfig { #[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] pub struct Config { - pub entry: BTreeMap, + pub entry: Entry, pub output: OutputConfig, pub resolve: ResolveConfig, #[serde(deserialize_with = "deserialize_manifest", default)] @@ -228,6 +232,7 @@ pub struct Config { default )] pub check_duplicate_package: Option, + pub module_federation: Option, // 是否开启 case sensitive 检查,只有mac平台才需要开启 #[serde(rename = "caseSensitiveCheck")] pub case_sensitive_check: bool, @@ -369,7 +374,13 @@ impl Config { for ext in JS_EXTENSIONS { let file_path = root.join(file_path).with_extension(ext); if file_path.exists() { - config.entry.insert("index".to_string(), file_path); + config.entry.insert( + "index".to_string(), + EntryItem { + filename: None, + import: file_path, + }, + ); break 'outer; } } @@ -382,28 +393,29 @@ impl Config { // normalize entry config.entry.iter_mut().try_for_each(|(k, v)| { #[allow(clippy::needless_borrows_for_generic_args)] - if let Ok(entry_path) = root.join(&v).canonicalize() + if let Ok(entry_path) = root.join(&v.import).canonicalize() && entry_path.is_file() { - *v = entry_path; + v.import = entry_path; } else { for ext in JS_EXTENSIONS { #[allow(clippy::needless_borrows_for_generic_args)] - if let Ok(entry_path) = root.join(&v).with_extension(ext).canonicalize() + if let Ok(entry_path) = + root.join(&v.import).with_extension(ext).canonicalize() && entry_path.is_file() { - *v = entry_path; + v.import = entry_path; return Ok(()); } if let Ok(entry_path) = root - .join(&v) + .join(&v.import) .join("index") .with_extension(ext) .canonicalize() && entry_path.is_file() { - *v = entry_path; + v.import = entry_path; return Ok(()); } } diff --git a/crates/mako/src/config/entry.rs b/crates/mako/src/config/entry.rs new file mode 100644 index 000000000..87ce2811d --- /dev/null +++ b/crates/mako/src/config/entry.rs @@ -0,0 +1,37 @@ +use std::collections::BTreeMap; +use std::path::PathBuf; + +use serde::{Deserialize, Serialize}; +use serde_json::Value; + +#[derive(Serialize, Debug)] +pub struct EntryItem { + #[serde(default)] + pub filename: Option, + pub import: PathBuf, +} + +pub type Entry = BTreeMap; + +impl<'de> Deserialize<'de> for EntryItem { + fn deserialize(deserializer: D) -> Result + where + D: serde::Deserializer<'de>, + { + let value: serde_json::Value = serde_json::Value::deserialize(deserializer)?; + match &value { + Value::String(s) => Ok(EntryItem { + filename: None, + import: s.into(), + }), + Value::Object(_) => { + Ok(serde_json::from_value::(value).map_err(serde::de::Error::custom)?) + } + _ => Err(serde::de::Error::custom(format!( + "invalid `{}` value: {}", + stringify!(deserialize_umd).replace("deserialize_", ""), + value + ))), + } + } +} diff --git a/crates/mako/src/config/module_federation.rs b/crates/mako/src/config/module_federation.rs new file mode 100644 index 000000000..314c3bb0a --- /dev/null +++ b/crates/mako/src/config/module_federation.rs @@ -0,0 +1,62 @@ +use std::collections::HashMap; + +use serde::{Deserialize, Serialize}; + +#[derive(Debug, Clone, Serialize, Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct ModuleFederationConfig { + pub name: String, + pub filename: Option, + pub exposes: Option, + pub shared: Option, + pub remotes: Option, + #[serde(default)] + pub runtime_plugins: Vec, + pub implementation: String, + #[serde(default)] + pub share_strategy: ShareStrategy, + #[serde(default = "default_share_scope")] + pub share_scope: String, + #[serde(default)] + pub manifest: bool, +} + +pub type ExposesConfig = HashMap; + +pub type SharedConfig = HashMap; + +#[derive(Debug, Clone, Serialize, Deserialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct SharedItemConfig { + #[serde(default)] + /// not supported now + pub eager: bool, + #[serde(default)] + pub singleton: bool, + #[serde(default)] + pub required_version: Option, + #[serde(default)] + pub strict_version: bool, + #[serde(default = "default_share_scope")] + pub shared_scope: String, +} + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub enum ShareStrategy { + #[serde(rename = "version-first")] + VersionFirst, + #[serde(rename = "loaded-first")] + LoadedFirst, +} + +impl Default for ShareStrategy { + fn default() -> Self { + Self::LoadedFirst + } +} + +pub type RemotesConfig = HashMap; + +fn default_share_scope() -> String { + "default".to_string() +} diff --git a/crates/mako/src/config/output.rs b/crates/mako/src/config/output.rs index 3042ab885..7099a6667 100644 --- a/crates/mako/src/config/output.rs +++ b/crates/mako/src/config/output.rs @@ -7,7 +7,7 @@ use swc_core::ecma::ast::EsVersion; use super::Umd; use crate::create_deserialize_fn; -use crate::utils::get_pkg_name; +use crate::utils::get_app_info; #[derive(Deserialize, Serialize, Debug)] #[serde(rename_all = "camelCase")] @@ -53,7 +53,7 @@ impl fmt::Display for CrossOriginLoading { pub fn get_default_chunk_loading_global(umd: Option, root: &Path) -> String { let unique_name = umd.map_or_else( - || get_pkg_name(root).unwrap_or("global".to_string()), + || get_app_info(root).0.unwrap_or("global".to_string()), |umd| umd.name.clone(), ); diff --git a/crates/mako/src/config/umd.rs b/crates/mako/src/config/umd.rs index 5c64dfa44..6ddbba0f4 100644 --- a/crates/mako/src/config/umd.rs +++ b/crates/mako/src/config/umd.rs @@ -12,13 +12,12 @@ where D: serde::Deserializer<'de>, { let value: serde_json::Value = serde_json::Value::deserialize(deserializer)?; - match value { - serde_json::Value::Object(obj) => Ok(Some( - serde_json::from_value::(serde_json::Value::Object(obj)) - .map_err(serde::de::Error::custom)?, + match &value { + serde_json::Value::Object(_) => Ok(Some( + serde_json::from_value::(value).map_err(serde::de::Error::custom)?, )), serde_json::Value::String(name) => Ok(Some(Umd { - name, + name: name.clone(), ..Default::default() })), serde_json::Value::Bool(false) => Ok(None), diff --git a/crates/mako/src/dev.rs b/crates/mako/src/dev.rs index 0dae20352..e88db069a 100644 --- a/crates/mako/src/dev.rs +++ b/crates/mako/src/dev.rs @@ -85,7 +85,6 @@ impl DevServer { let server = Server::bind(&addr).serve(make_svc); // TODO: print when mako is run standalone if std::env::var("MAKO_CLI").is_ok() { - println!(); if config_port != port { println!( "{}", @@ -214,7 +213,14 @@ impl DevServer { .body(hyper::Body::empty()) .unwrap(); let res = staticfile.serve(req).await; - res.map_err(anyhow::Error::from) + res.map_or_else( + |e| Err(anyhow::Error::from(e)), + |mut res| { + res.headers_mut() + .insert(ACCESS_CONTROL_ALLOW_ORIGIN, "*".parse().unwrap()); + Ok(res) + }, + ) } } } diff --git a/crates/mako/src/dev/update.rs b/crates/mako/src/dev/update.rs index cc041f742..0ecc1f7d2 100644 --- a/crates/mako/src/dev/update.rs +++ b/crates/mako/src/dev/update.rs @@ -283,7 +283,7 @@ impl Compiler { debug!("build by modify: {:?} start", entry); // first build let is_entry = { - let mut entries = self.context.config.entry.values(); + let mut entries = self.context.config.entry.values().map(|e| &e.import); entries.any(|e| e.eq(entry)) }; diff --git a/crates/mako/src/generate/chunk.rs b/crates/mako/src/generate/chunk.rs index 6bd52993d..e1817e8fb 100644 --- a/crates/mako/src/generate/chunk.rs +++ b/crates/mako/src/generate/chunk.rs @@ -137,6 +137,10 @@ impl Chunk { self.modules.contains(module_id) } + pub fn root_module(&self) -> Option<&ModuleId> { + self.modules.iter().last() + } + pub fn hash(&self, mg: &ModuleGraph) -> u64 { let mut sorted_module_ids = self.modules.iter().cloned().collect::>(); sorted_module_ids.sort_by_key(|m| m.id.clone()); diff --git a/crates/mako/src/generate/chunk_pot/ast_impl.rs b/crates/mako/src/generate/chunk_pot/ast_impl.rs index 793d041ad..7d775cdb0 100644 --- a/crates/mako/src/generate/chunk_pot/ast_impl.rs +++ b/crates/mako/src/generate/chunk_pot/ast_impl.rs @@ -218,6 +218,12 @@ pub(crate) fn render_entry_js_chunk( .into_bytes() }; + let entry_info = if let ChunkType::Entry(_, name, _) = &chunk.chunk_type { + context.config.entry.get(name) + } else { + None + }; + Ok(ChunkFile { raw_hash: hmr_hash, content, @@ -227,7 +233,12 @@ pub(crate) fn render_entry_js_chunk( chunk_id: pot.chunk_id.clone(), file_type: ChunkFileType::JS, chunk_name: pot.chunk_name.clone(), - file_name_template: context.config.output.filename.clone(), + file_name_template: entry_info.and_then(|e| { + e.filename + .as_ref() + .xor(context.config.output.filename.as_ref()) + .cloned() + }), }) } diff --git a/crates/mako/src/generate/chunk_pot/str_impl.rs b/crates/mako/src/generate/chunk_pot/str_impl.rs index beebc5a2f..430ae3218 100644 --- a/crates/mako/src/generate/chunk_pot/str_impl.rs +++ b/crates/mako/src/generate/chunk_pot/str_impl.rs @@ -95,6 +95,12 @@ pub(super) fn render_entry_js_chunk( let mut source_map_buf: Vec = vec![]; sourcemap::SourceMap::from(chunk_raw_sourcemap).to_writer(&mut source_map_buf)?; + let entry_info = if let ChunkType::Entry(_, name, _) = &chunk.chunk_type { + context.config.entry.get(name) + } else { + None + }; + Ok(ChunkFile { raw_hash: hmr_hash, content, @@ -103,8 +109,13 @@ pub(super) fn render_entry_js_chunk( file_name: pot.js_name.clone(), chunk_id: pot.chunk_id.clone(), file_type: ChunkFileType::JS, - file_name_template: None, chunk_name: pot.chunk_name.clone(), + file_name_template: entry_info.and_then(|e| { + e.filename + .as_ref() + .xor(context.config.output.filename.as_ref()) + .cloned() + }), }) } diff --git a/crates/mako/src/generate/chunk_pot/util.rs b/crates/mako/src/generate/chunk_pot/util.rs index 28420a066..4d027c9a8 100644 --- a/crates/mako/src/generate/chunk_pot/util.rs +++ b/crates/mako/src/generate/chunk_pot/util.rs @@ -25,7 +25,7 @@ use crate::config::Mode; use crate::generate::chunk_pot::ChunkPot; use crate::generate::runtime::AppRuntimeTemplate; use crate::module::{relative_to_root, Module, ModuleAst}; -use crate::utils::get_pkg_name; +use crate::utils::get_app_info; pub(crate) fn render_module_js( ast: &SwcModule, @@ -105,6 +105,15 @@ pub(crate) fn runtime_code(context: &Arc) -> Result { let chunk_graph = context.chunk_graph.read().unwrap(); let has_dynamic_chunks = chunk_graph.get_all_chunks().len() > 1; let has_hmr = context.args.watch; + let chunk_matcher = context.config.module_federation.as_ref().and_then(|mf| { + mf.remotes.as_ref().and_then(|remotes| { + if remotes.is_empty() { + None + } else { + Some(r#"/^mako\/container\/remote\//"#.to_string()) + } + }) + }); let app_runtime = AppRuntimeTemplate { has_dynamic_chunks, has_hmr, @@ -120,13 +129,14 @@ pub(crate) fn runtime_code(context: &Arc) -> Result { .cross_origin_loading .clone() .map(|s| s.to_string()), - pkg_name: get_pkg_name(&context.root), + pkg_name: get_app_info(&context.root).0, concatenate_enabled: context .config .optimization .as_ref() .map_or(false, |o| o.concatenate_modules.unwrap_or(false)), global_module_registry: context.config.output.global_module_registry, + chunk_matcher, }; let app_runtime = app_runtime.render_once()?; let app_runtime = app_runtime.replace( diff --git a/crates/mako/src/generate/generate_chunks.rs b/crates/mako/src/generate/generate_chunks.rs index 0e9646753..90499e1fd 100644 --- a/crates/mako/src/generate/generate_chunks.rs +++ b/crates/mako/src/generate/generate_chunks.rs @@ -83,9 +83,19 @@ type ChunksHashReplacer = HashMap; impl Compiler { pub fn generate_chunk_files(&self, hmr_hash: u64) -> Result> { - crate::mako_profile_function!(); + let module_graph = self.context.module_graph.read().unwrap(); let chunk_graph = self.context.chunk_graph.read().unwrap(); - let chunks = chunk_graph.get_chunks(); + + let chunks: Vec<&Chunk> = chunk_graph + .get_chunks() + .into_iter() + .filter(|c| { + !module_graph + .get_module(c.root_module().unwrap()) + .unwrap() + .is_remote() + }) + .collect(); let (entry_chunks, normal_chunks): (Vec<&Chunk>, Vec<&Chunk>) = chunks .into_iter() @@ -173,6 +183,15 @@ impl Compiler { let descendant_chunk = chunk_graph.chunk(descendant_chunk_id).unwrap(); // TODO: maybe we can split chunks to chunk pots before generate, because normal chunks will be // split here and fn generate_normal_chunk_files twice + // + if module_graph + .get_module(descendant_chunk.root_module().unwrap()) + .unwrap() + .is_remote() + { + return (acc_js, acc_css); + } + let chunk_pot = ChunkPot::from(descendant_chunk, &module_graph, &context); diff --git a/crates/mako/src/generate/group_chunk.rs b/crates/mako/src/generate/group_chunk.rs index c08a40fe3..3e84f525a 100644 --- a/crates/mako/src/generate/group_chunk.rs +++ b/crates/mako/src/generate/group_chunk.rs @@ -31,7 +31,7 @@ impl Compiler { for (key, value) in &self.context.config.entry { // hmr entry id has query '?hmr' - if parse_path(&value.to_string_lossy()).unwrap().0 + if parse_path(&value.import.to_string_lossy()).unwrap().0 == parse_path(&entry.id).unwrap().0 { entry_chunk_name = key; diff --git a/crates/mako/src/generate/optimize_chunk.rs b/crates/mako/src/generate/optimize_chunk.rs index f37897402..4eb5e1701 100644 --- a/crates/mako/src/generate/optimize_chunk.rs +++ b/crates/mako/src/generate/optimize_chunk.rs @@ -26,7 +26,11 @@ impl Compiler { pub fn optimize_chunk(&self) { crate::mako_profile_function!(); debug!("optimize chunk"); - if let Some(optimize_options) = self.get_optimize_chunk_options() { + if let Some(mut optimize_options) = self.get_optimize_chunk_options() { + self.context + .plugin_driver + .after_optimize_chunk_options(&mut optimize_options) + .unwrap(); debug!("optimize options: {:?}", optimize_options); // stage: prepare let mut optimize_chunks_infos = optimize_options @@ -123,7 +127,7 @@ impl Compiler { let async_chunk_root_modules = chunks .iter() .filter_map(|chunk| match chunk.chunk_type { - ChunkType::Async => chunk.modules.iter().last(), + ChunkType::Async => chunk.root_module(), _ => None, }) .collect::>(); diff --git a/crates/mako/src/generate/runtime.rs b/crates/mako/src/generate/runtime.rs index 81edd7ca3..84879c2f7 100644 --- a/crates/mako/src/generate/runtime.rs +++ b/crates/mako/src/generate/runtime.rs @@ -14,4 +14,5 @@ pub struct AppRuntimeTemplate { pub concatenate_enabled: bool, pub cross_origin_loading: Option, pub global_module_registry: bool, + pub chunk_matcher: Option, } diff --git a/crates/mako/src/generate/transform.rs b/crates/mako/src/generate/transform.rs index 8cc2dfeb2..4e927091e 100644 --- a/crates/mako/src/generate/transform.rs +++ b/crates/mako/src/generate/transform.rs @@ -100,6 +100,7 @@ pub fn transform_modules_in_thread( chunk_id: None, to_replace_source: chunk_name, resolved_module_id: id.clone(), + _is_federation_expose: false, } } ResolveType::DynamicImport(import_options) => { @@ -112,12 +113,14 @@ pub fn transform_modules_in_thread( chunk_id, to_replace_source: id.generate(&context), resolved_module_id: id.clone(), + _is_federation_expose: import_options._is_federation_expose, } } _ => ResolvedReplaceInfo { chunk_id: None, to_replace_source: id.generate(&context), resolved_module_id: id.clone(), + _is_federation_expose: false, }, }; @@ -189,6 +192,7 @@ fn insert_swc_helper_replace( chunk_id: None, to_replace_source: m_id.generate(context), resolved_module_id: m_id, + _is_federation_expose: false, }, ); }); diff --git a/crates/mako/src/lib.rs b/crates/mako/src/lib.rs index 3bebd29c7..4cf918a36 100644 --- a/crates/mako/src/lib.rs +++ b/crates/mako/src/lib.rs @@ -1,6 +1,7 @@ #![feature(box_patterns)] #![feature(hasher_prefixfree_extras)] #![feature(let_chains)] +#![feature(is_none_or)] pub mod ast; mod build; diff --git a/crates/mako/src/main.rs b/crates/mako/src/main.rs index cfc740591..ae366423d 100644 --- a/crates/mako/src/main.rs +++ b/crates/mako/src/main.rs @@ -1,5 +1,6 @@ #![feature(box_patterns)] #![feature(let_chains)] +#![feature(is_none_or)] extern crate swc_malloc; diff --git a/crates/mako/src/module.rs b/crates/mako/src/module.rs index 353774559..a2a85b976 100644 --- a/crates/mako/src/module.rs +++ b/crates/mako/src/module.rs @@ -137,6 +137,7 @@ impl From<&NamedExport> for NamedExportType { pub struct ImportOptions { pub chunk_name: Option, pub ignore: bool, + pub _is_federation_expose: bool, } impl ImportOptions { @@ -200,6 +201,7 @@ pub struct ModuleInfo { /// The transformed source map chain of this module pub source_map_chain: Vec>, pub module_system: ModuleSystem, + pub federation: Option, } impl Default for ModuleInfo { @@ -217,11 +219,12 @@ impl Default for ModuleInfo { resolved_resource: None, source_map_chain: vec![], is_ignored: false, + federation: None, } } } -fn md5_hash(source_str: &str, lens: usize) -> String { +pub fn md5_hash(source_str: &str, lens: usize) -> String { format!("{:x}", md5::compute(source_str)) .chars() .take(lens) @@ -390,6 +393,12 @@ pub enum ModuleType { PlaceHolder, } +#[derive(Clone, Debug)] +pub enum FedereationModuleType { + Remote, + ConsumeShare, +} + #[derive(Clone)] pub struct Module { pub id: ModuleId, @@ -426,6 +435,26 @@ impl Module { .map_or(false, |info| info.external.is_some()) } + pub fn is_remote(&self) -> bool { + if let Some(info) = self.info.as_ref() + && let Some(FedereationModuleType::Remote) = info.federation + { + true + } else { + false + } + } + + pub fn is_consume_share(&self) -> bool { + if let Some(info) = self.info.as_ref() + && let Some(FedereationModuleType::ConsumeShare) = info.federation + { + true + } else { + false + } + } + pub fn is_placeholder(&self) -> bool { self.get_module_type() == ModuleType::PlaceHolder } diff --git a/crates/mako/src/plugin.rs b/crates/mako/src/plugin.rs index 68add9b7d..7ae3aabdb 100644 --- a/crates/mako/src/plugin.rs +++ b/crates/mako/src/plugin.rs @@ -9,8 +9,9 @@ use swc_core::common::Mark; use swc_core::ecma::ast::Module; use crate::ast::file::{Content, File}; +use crate::build::analyze_deps::ResolvedDep; use crate::compiler::{Args, Compiler, Context}; -use crate::config::Config; +use crate::config::{CodeSplittingAdvancedOptions, Config}; use crate::generate::chunk_graph::ChunkGraph; use crate::generate::generate_chunks::ChunkFile; use crate::module::{Dependency, ModuleAst, ModuleId}; @@ -24,8 +25,9 @@ pub struct PluginLoadParam<'a> { } #[derive(Debug)] -pub struct PluginResolveIdParams { +pub struct PluginResolveIdParams<'a> { pub is_entry: bool, + pub dep: &'a Dependency, } pub struct PluginParseParam<'a> { @@ -66,6 +68,7 @@ pub trait Plugin: Any + Send + Sync { &self, _content: &mut Content, _path: &str, + _is_entry: bool, _context: &Arc, ) -> Result> { Ok(None) @@ -115,6 +118,10 @@ pub trait Plugin: Any + Send + Sync { Ok(()) } + fn after_resolve(&self, _resolved_dep: &ResolvedDep, _context: &Arc) -> Result<()> { + Ok(()) + } + fn after_build(&self, _context: &Arc, _compiler: &Compiler) -> Result<()> { Ok(()) } @@ -175,6 +182,13 @@ pub trait Plugin: Any + Send + Sync { Ok(()) } + fn after_optimize_chunk_options( + &self, + _optimize_chunk_options: &mut CodeSplittingAdvancedOptions, + ) -> Result<()> { + Ok(()) + } + fn optimize_chunk( &self, _chunk_graph: &mut ChunkGraph, @@ -293,6 +307,13 @@ impl PluginDriver { Ok(()) } + pub fn after_resolve(&self, resolved_dep: &ResolvedDep, context: &Arc) -> Result<()> { + for plugin in &self.plugins { + plugin.after_resolve(resolved_dep, context)?; + } + Ok(()) + } + pub fn resolve_id( &self, source: &str, @@ -410,6 +431,17 @@ impl PluginDriver { Ok(()) } + pub fn after_optimize_chunk_options( + &self, + optimize_chunk_options: &mut CodeSplittingAdvancedOptions, + ) -> Result<()> { + for p in &self.plugins { + p.after_optimize_chunk_options(optimize_chunk_options)?; + } + + Ok(()) + } + pub fn optimize_chunk( &self, chunk_graph: &mut ChunkGraph, @@ -440,10 +472,11 @@ impl PluginDriver { &self, content: &mut Content, path: &str, + _is_entry: bool, context: &Arc, ) -> Result { for plugin in &self.plugins { - if let Some(transformed) = plugin.load_transform(content, path, context)? { + if let Some(transformed) = plugin.load_transform(content, path, _is_entry, context)? { *content = transformed; } } diff --git a/crates/mako/src/plugins.rs b/crates/mako/src/plugins.rs index 36de47d69..3dc037846 100644 --- a/crates/mako/src/plugins.rs +++ b/crates/mako/src/plugins.rs @@ -15,6 +15,7 @@ pub mod imports_checker; pub mod invalid_webpack_syntax; pub mod manifest; pub mod minifish; +pub mod module_federation; pub mod progress; pub mod require_context; pub mod runtime; diff --git a/crates/mako/src/plugins/bundless_compiler.rs b/crates/mako/src/plugins/bundless_compiler.rs index 0ec58ebe9..0a69247d5 100644 --- a/crates/mako/src/plugins/bundless_compiler.rs +++ b/crates/mako/src/plugins/bundless_compiler.rs @@ -86,6 +86,7 @@ impl BundlessCompiler { chunk_id: None, to_replace_source: replacement, resolved_module_id: id.clone(), + _is_federation_expose: false, }, )) }) diff --git a/crates/mako/src/plugins/hmr_runtime/hmr_runtime.js b/crates/mako/src/plugins/hmr_runtime/hmr_runtime.js index 0227be6a5..ce4afc957 100644 --- a/crates/mako/src/plugins/hmr_runtime/hmr_runtime.js +++ b/crates/mako/src/plugins/hmr_runtime/hmr_runtime.js @@ -30,7 +30,26 @@ } return require(request); }; - Object.assign(fn, require); + + var createPropertyDescriptor = function (name) { + return { + configurable: true, + enumerable: true, + get: function () { + return require[name]; + }, + set: function (value) { + require[name] = value; + }, + }; + }; + + for (var name in require) { + if (Object.prototype.hasOwnProperty.call(require, name)) { + Object.defineProperty(fn, name, createPropertyDescriptor(name)); + } + } + return fn; }; const applyHotUpdate = (_chunkId, update) => { diff --git a/crates/mako/src/plugins/module_federation.rs b/crates/mako/src/plugins/module_federation.rs new file mode 100644 index 000000000..7fb941833 --- /dev/null +++ b/crates/mako/src/plugins/module_federation.rs @@ -0,0 +1,131 @@ +use std::collections::HashMap; +use std::path::Path; +use std::sync::{Arc, RwLock}; + +use anyhow::Result; +use constants::{FEDERATION_REMOTE_MODULE_PREFIX, FEDERATION_REMOTE_REFERENCE_PREFIX}; +use provide_shared::SharedDependency; + +use crate::ast::file::Content; +use crate::build::analyze_deps::ResolvedDep; +use crate::compiler::{Args, Context}; +use crate::config::module_federation::ModuleFederationConfig; +use crate::config::{CodeSplittingAdvancedOptions, Config}; +use crate::generate::chunk_graph::ChunkGraph; +use crate::module_graph::ModuleGraph; +use crate::plugin::{Plugin, PluginGenerateEndParams, PluginResolveIdParams}; +use crate::resolve::ResolverResource; + +mod constants; +mod consume_shared; +mod container; +mod container_reference; +mod manifest; +mod provide_for_consume; +mod provide_shared; +mod util; + +pub struct ModuleFederationPlugin { + pub config: ModuleFederationConfig, + shared_dependency_map: RwLock>, +} + +impl ModuleFederationPlugin { + pub fn new(config: ModuleFederationConfig) -> Self { + Self { + config, + shared_dependency_map: RwLock::new(HashMap::new()), + } + } +} + +impl Plugin for ModuleFederationPlugin { + fn name(&self) -> &str { + "module_federation" + } + + fn modify_config(&self, config: &mut Config, root: &Path, _args: &Args) -> Result<()> { + self.add_container_entry(config, root); + Ok(()) + } + + fn load_transform( + &self, + content: &mut Content, + _path: &str, + is_entry: bool, + context: &Arc, + ) -> Result> { + if !is_entry { + Ok(None) + } else { + // add container entry runtime dependency + match content { + Content::Js(js_content) => { + let entry_runtime_dep_path = self.prepare_container_entry_dep(&context.root); + js_content.content.insert_str( + 0, + format!(r#"import "{}";"#, entry_runtime_dep_path).as_str(), + ); + Ok(Some(content.clone())) + } + _ => Ok(None), + } + } + } + + fn runtime_plugins(&self, context: &Arc) -> Result> { + Ok(vec![ + self.init_federation_runtime_options(), + self.init_federation_runtime_remotes(context), + self.init_federation_runtime_consume(context), + self.init_federation_runtime_sharing(context), + self.export_federation_container(), + ]) + } + + fn resolve_id( + &self, + source: &str, + importer: &str, + params: &PluginResolveIdParams, + context: &Arc, + ) -> Result> { + let remote_module = self.resolve_remote(source); + if let Ok(Some(_)) = remote_module.as_ref() { + remote_module + } else { + self.resolve_to_consume_share(source, importer, params, context) + } + } + + fn generate_end(&self, params: &PluginGenerateEndParams, context: &Arc) -> Result<()> { + if self.config.manifest { + self.generate_federation_manifest(context, params)?; + } + Ok(()) + } + + fn after_resolve(&self, resolved_dep: &ResolvedDep, _context: &Arc) -> Result<()> { + self.collect_provide_shared(resolved_dep); + Ok(()) + } + + fn optimize_chunk( + &self, + chunk_graph: &mut ChunkGraph, + module_graph: &mut ModuleGraph, + _context: &Arc, + ) -> Result<()> { + self.connect_provide_shared_to_container(chunk_graph, module_graph); + Ok(()) + } + + fn after_optimize_chunk_options( + &self, + optimize_chunk_options: &mut CodeSplittingAdvancedOptions, + ) -> Result<()> { + self.patch_code_splitting(optimize_chunk_options); + Ok(()) + } +} diff --git a/crates/mako/src/plugins/module_federation/constants.rs b/crates/mako/src/plugins/module_federation/constants.rs new file mode 100644 index 000000000..ebe18c9b4 --- /dev/null +++ b/crates/mako/src/plugins/module_federation/constants.rs @@ -0,0 +1,9 @@ +pub(crate) const FEDERATION_GLOBAL: &str = "__mako_require__.federation"; + +pub(crate) const FEDERATION_REMOTE_MODULE_PREFIX: &str = "mako/container/remote/"; + +pub(crate) const FEDERATION_REMOTE_REFERENCE_PREFIX: &str = "mako/container/reference/"; + +pub(crate) const FEDERATION_SHARED_REFERENCE_PREFIX: &str = "mako/sharing/consume/"; + +pub(crate) const FEDERATION_EXPOSE_CHUNK_PREFIX: &str = "__mf_expose_"; diff --git a/crates/mako/src/plugins/module_federation/consume_shared.rs b/crates/mako/src/plugins/module_federation/consume_shared.rs new file mode 100644 index 000000000..41156e37e --- /dev/null +++ b/crates/mako/src/plugins/module_federation/consume_shared.rs @@ -0,0 +1,231 @@ +use std::collections::{HashMap, HashSet}; +use std::sync::Arc; + +use pathdiff::diff_paths; + +use super::constants::FEDERATION_SHARED_REFERENCE_PREFIX; +use super::ModuleFederationPlugin; +use crate::build::analyze_deps::{AnalyzeDepsResult, ResolvedDep}; +use crate::compiler::Context; +use crate::generate::chunk::ChunkType; +use crate::module::{md5_hash, Dependency, ResolveType}; +use crate::plugin::PluginResolveIdParams; +use crate::resolve::{do_resolve, ConsumeSharedInfo, ResolverResource, ResolverType}; + +impl ModuleFederationPlugin { + pub(super) fn init_federation_runtime_consume(&self, context: &Context) -> String { + let module_graph = context.module_graph.read().unwrap(); + let chunk_graph = context.chunk_graph.read().unwrap(); + let share_dependencies = self.shared_dependency_map.read().unwrap(); + + let mut initial_consumes = Vec::::new(); + + let consume_modules_chunk_map: HashMap> = chunk_graph + .get_all_chunks() + .into_iter() + .filter_map(|c| { + let modules = c + .modules + .iter() + .filter_map(|m| { + if let Some(module) = module_graph.get_module(m) + && module.is_consume_share() + { + if let ChunkType::Entry(_, _, _) = c.chunk_type { + initial_consumes.push(m.id.clone()); + } + + Some(m.id.clone()) + } else { + None + } + }) + .collect::>(); + if modules.is_empty() { + None + } else { + Some((c.id.id.clone(), modules)) + } + }) + .collect(); + + let consume_shared_module_ids = + consume_modules_chunk_map + .iter() + .fold(HashSet::<&String>::new(), |mut acc, cur| { + acc.extend(cur.1.iter()); + acc + }); + + let consume_shared_modules = consume_shared_module_ids + .iter() + .map(|id| module_graph.get_module(&id.as_str().into()).unwrap()) + .collect::>(); + + let module_to_handler_mapping_code = consume_shared_modules + .iter() + .map(|s| { + let resolved_resource = s.info.as_ref().unwrap().resolved_resource.as_ref().unwrap(); + let module_full_path = match resolved_resource { + ResolverResource::Shared(info) => + info.deps.resolved_deps[0].resolver_resource.get_resolved_path(), + _ => + panic!("{} is not a shared module", resolved_resource.get_resolved_path()) + }; + let module_relative_path = + diff_paths(&module_full_path, &context.root) + .unwrap() + .to_string_lossy() + .to_string(); + + let module_in_chunk = chunk_graph.get_chunk_for_module(&module_full_path.as_str().into()).unwrap(); + + let getter = match &module_in_chunk.chunk_type { + ChunkType::Entry(_, _, _) | ChunkType::Worker(_) => { + format!(r#"() => (() => requireModule("{module_relative_path}"))"# + ) + }, + ChunkType::Async + | ChunkType::Sync + => { + let dependency_chunks = chunk_graph.sync_dependencies_chunk(&module_in_chunk.id); + format!( + r#"() => (Promise.all([{}]).then(() => requireModule("{module_relative_path}")))"#, + [ + dependency_chunks, + vec![module_in_chunk.id.clone()] + ] + .concat().iter() + .map(|e| format!(r#"requireModule.ensure("{}")"#, e.id)) + .collect::>().join(",") + ) + }, + ChunkType::Runtime => panic!("mf shared dependency should not be bundled to runtime chunk") + }; + + let share_dependency = share_dependencies.get(&s.id.id).unwrap(); + format!( + r#""{shared_consume_id}": {{ + getter: {getter}, + shareInfo: {{ shareConfig: {share_config} }}, + shareKey: "{share_key}" + }}"#, + shared_consume_id = s.id.id, + share_config = serde_json::to_string(&share_dependency.shared_config).unwrap(), + share_key = share_dependency.share_key + + ) + }) + .collect::>() + .join(","); + + let initial_consumes_code = serde_json::to_string(&initial_consumes).unwrap(); + let install_initial_consumes_code = if initial_consumes.is_empty() { + "" + } else { + r#" + requireModule.federation.installInitialConsumes = () => (requireModule.federation.bundlerRuntime.installInitialConsumes({ + initialConsumes: initialConsumes, + installedModules: installedModules, + moduleToHandlerMapping: moduleToHandlerMapping, + webpackRequire: requireModule + }))"# + }; + let chunk_mapping_code = serde_json::to_string(&consume_modules_chunk_map).unwrap(); + format!( + r#" +/* mako/runtime/federation consumes */ +!(() => {{ + var installedModules = {{}}; + var moduleToHandlerMapping = {{{module_to_handler_mapping_code}}}; + var initialConsumes = {initial_consumes_code}; + {install_initial_consumes_code} + var chunkMapping = {chunk_mapping_code}; + requireModule.chunkEnsures.consumes = (chunkId, promises) => {{ + requireModule.federation.bundlerRuntime.consumes({{ + chunkMapping: chunkMapping, + installedModules: installedModules, + chunkId: chunkId, + moduleToHandlerMapping: moduleToHandlerMapping, + promises: promises, + webpackRequire: requireModule + }}); + }} +}})();"# + ) + } + + pub(super) fn resolve_to_consume_share( + &self, + source: &str, + importer: &str, + params: &PluginResolveIdParams, + context: &Arc, + ) -> Result, anyhow::Error> { + if let Some(shared) = self.config.shared.as_ref() + && let Some(shared_info) = shared.get(source) + { + let resolver = if params.dep.resolve_type == ResolveType::Require { + context.resolvers.get(&ResolverType::Cjs) + } else if params.dep.resolve_type == ResolveType::Css { + context.resolvers.get(&ResolverType::Css) + } else { + context.resolvers.get(&ResolverType::Esm) + } + .unwrap(); + let resolver_resource = + do_resolve(importer, source, resolver, Some(&context.config.externals))?; + if let ResolverResource::Resolved(_) = resolver_resource { + let config_joined_str = format!( + "{}|{}|{}|{}|{}|{}|{}", + shared_info.shared_scope, + source, + shared_info + .required_version + .as_ref() + .map_or("", |v| v.as_str()), + shared_info.strict_version, + resolver_resource.get_resolved_path(), + shared_info.singleton, + shared_info.eager + ); + let hash = md5_hash(&config_joined_str, 4); + return Ok(Some(ResolverResource::Shared(ConsumeSharedInfo { + name: source.to_string(), + version: resolver_resource.get_pkg_info().unwrap().version.unwrap(), + share_scope: shared_info.shared_scope.clone(), + eager: shared_info.eager, + singletion: shared_info.singleton, + required_version: shared_info.required_version.clone(), + strict_version: shared_info.strict_version, + module_id: format!( + "{}{}/{}/{}?{}", + FEDERATION_SHARED_REFERENCE_PREFIX, + shared_info.shared_scope, + source, + source, + hash + ), + deps: AnalyzeDepsResult { + resolved_deps: vec![ResolvedDep { + resolver_resource, + dependency: Dependency { + source: params.dep.source.clone(), + resolve_as: None, + resolve_type: if shared_info.eager { + ResolveType::Require + } else { + ResolveType::DynamicImport(Default::default()) + }, + order: params.dep.order, + span: params.dep.span, + }, + }], + ..Default::default() + }, + }))); + } + } + Ok(None) + } +} diff --git a/crates/mako/src/plugins/module_federation/container.rs b/crates/mako/src/plugins/module_federation/container.rs new file mode 100644 index 000000000..442f362b2 --- /dev/null +++ b/crates/mako/src/plugins/module_federation/container.rs @@ -0,0 +1,275 @@ +use std::collections::btree_map; +use std::fs; +use std::path::Path; + +use serde::Serialize; +use tracing::warn; + +use super::constants::{FEDERATION_EXPOSE_CHUNK_PREFIX, FEDERATION_GLOBAL}; +use super::util::parse_remote; +use super::ModuleFederationPlugin; +use crate::config::entry::EntryItem; +use crate::config::{AllowChunks, Config}; +use crate::module::md5_hash; +use crate::visitors::mako_require::MAKO_REQUIRE; + +impl ModuleFederationPlugin { + pub(super) fn add_container_entry(&self, config: &mut Config, root: &Path) { + // add container entry + if let Some(exposes) = self.config.exposes.as_ref() { + let container_entry_name = &self.config.name; + if !exposes.is_empty() { + match config.entry.entry(container_entry_name.clone()) { + btree_map::Entry::Occupied(_) => { + warn!( + "mf exposed name {} is conflicting with entry config.", + container_entry_name + ); + } + btree_map::Entry::Vacant(vacant_entry) => { + // TODO: refactor with virtual entry + let container_entry_code = self.get_container_entry_code(root); + let container_entry_path = root.join(format!( + "node_modules/.federation/.entry.container.{}.js", + container_entry_name + )); + let container_entry_parent_path = container_entry_path.parent().unwrap(); + if !fs::exists(container_entry_parent_path).unwrap() { + fs::create_dir_all(container_entry_parent_path).unwrap(); + } + fs::write(&container_entry_path, container_entry_code).unwrap(); + + vacant_entry.insert(EntryItem { + filename: self.config.filename.clone(), + import: container_entry_path, + }); + } + } + } + } + } + + pub(super) fn get_container_entry_code(&self, root: &Path) -> String { + let exposes_modules_code = self + .config + .exposes + .as_ref() + .unwrap() + .iter() + .map(|(name, module)| { + format!( + r#""{name}": () => import( + /* makoChunkName: "{FEDERATION_EXPOSE_CHUNK_PREFIX}{striped_name}" */ + /* federationExpose: true */ + "{module}" +),"#, + module = root.join(module).canonicalize().unwrap().to_string_lossy(), + striped_name = name.replace("./", "") + ) + }) + .collect::>() + .join("\n"); + + format!( + r#"var moduleMap = {{ + {exposes_modules_code} +}}; + +var get = (module, getScope) => {{ + {MAKO_REQUIRE}.R = getScope; + getScope = ( + Object.prototype.hasOwnProperty.call(moduleMap, module) + ? moduleMap[module]() + : Promise.resolve().then(() => {{ + throw new Error('Module "' + module + '" does not exist in container.'); + }}) + ); + {MAKO_REQUIRE}.R = undefined; + return getScope; +}}; + +var init = (shareScope, initScope, remoteEntryInitOptions) => {{ + return {FEDERATION_GLOBAL}.bundlerRuntime.initContainerEntry({{ + webpackRequire: {MAKO_REQUIRE}, + shareScope: shareScope, + initScope: initScope, + remoteEntryInitOptions: remoteEntryInitOptions, + shareScopeKey: "{share_scope}" + }}) +}}; + +export {{ get, init }}; +"#, + share_scope = self.config.share_scope + ) + } + + pub(super) fn prepare_container_entry_dep(&self, root: &Path) -> String { + let container_content = self.get_federation_init_code(); + + let content_hash = md5_hash(&container_content, 32); + + let dep_path = root.join(format!( + "node_modules/.federation/.entry.{}.js", + content_hash + )); + let dep_parent_path = dep_path.parent().unwrap(); + if !fs::exists(dep_parent_path).unwrap() { + fs::create_dir_all(dep_parent_path).unwrap(); + } + if !fs::exists(&dep_path).unwrap() { + fs::write(&dep_path, container_content).unwrap(); + } + + dep_path.to_string_lossy().to_string() + } + + pub(super) fn get_federation_init_code(&self) -> String { + let (plugins_imports, plugins_instantiations) = self.get_mf_runtime_plugins_content(); + + format!( + r#"import federation from "{federation_impl}"; +{plugins_imports} + +if(!{FEDERATION_GLOBAL}.runtime) {{ + var preFederation = {FEDERATION_GLOBAL}; + {FEDERATION_GLOBAL} = {{}}; + for(var key in federation) {{ + {FEDERATION_GLOBAL}[key] = federation[key]; + }} + for(var key in preFederation) {{ + {FEDERATION_GLOBAL}[key] = preFederation[key]; + }} +}} + +if(!{FEDERATION_GLOBAL}.instance) {{ + {plugins_instantiations} + {FEDERATION_GLOBAL}.instance = {FEDERATION_GLOBAL}.runtime.init({FEDERATION_GLOBAL}.initOptions); + if({FEDERATION_GLOBAL}.attachShareScopeMap) {{ + {FEDERATION_GLOBAL}.attachShareScopeMap({MAKO_REQUIRE}); + }} + if({FEDERATION_GLOBAL}.installInitialConsumes) {{ + {FEDERATION_GLOBAL}.installInitialConsumes(); + }} +}} +"#, + federation_impl = self.config.implementation, + ) + } + + pub(super) fn init_federation_runtime_options(&self) -> String { + let runtime_remotes = self.config.remotes.as_ref().map_or(Vec::new(), |remotes| { + remotes + .iter() + .map(|(alias, remote)| { + let (name, entry) = parse_remote(remote).unwrap(); + RuntimeRemoteItem { + name, + alias: alias.clone(), + entry, + share_scope: self.config.share_scope.clone(), + } + }) + .collect() + }); + let init_options: RuntimeInitOptions = RuntimeInitOptions { + name: self.config.name.clone(), + remotes: runtime_remotes, + share_strategy: serde_json::to_value(&self.config.share_strategy) + .unwrap() + .as_str() + .unwrap() + .to_string(), + }; + let init_options_code = serde_json::to_string(&init_options).unwrap(); + + let federation_runtime_code = format!( + r#" +/* mako/runtime/federation runtime */ +!(function() {{ + if(!requireModule.federation) {{ + requireModule.federation = {{ + initOptions: {init_options_code}, + chunkMatcher: () => true, + rootOutputDir: "", + initialConsumes: undefined, + bundlerRuntimeOptions: {{}} + }}; + }} +}})();"# + ); + federation_runtime_code + } + + pub(super) fn get_mf_runtime_plugins_content(&self) -> (String, String) { + let (imported_plugin_names, import_plugin_instantiations) = + self.config.runtime_plugins.iter().enumerate().fold( + (Vec::new(), Vec::new()), + |(mut names, mut stmts), (index, plugin)| { + names.push(format!("plugin_{index}")); + stmts.push(format!(r#"import plugin_{index} from "{plugin}";"#)); + (names, stmts) + }, + ); + + let plugins_imports = import_plugin_instantiations.join("\n"); + + let plugins_to_add = imported_plugin_names + .iter() + .map(|item| format!(r#"{item} ? (item.default || item)() : false"#)) + .collect::>() + .join(","); + + let plugins_instantiations = if imported_plugin_names.is_empty() { + "".to_string() + } else { + format!( + r#"var pluginsToAdd = [{plugins_to_add}].filter(Boolean); + {FEDERATION_GLOBAL}.initOptions.plugins = {FEDERATION_GLOBAL}.initOptions.plugins ? + {FEDERATION_GLOBAL}.initOptions.plugins.concat(pluginsToAdd) : pluginsToAdd; +"#, + ) + }; + + (plugins_imports, plugins_instantiations) + } + + pub(super) fn export_federation_container(&self) -> String { + if let Some(exposes) = self.config.exposes.as_ref() { + if !exposes.is_empty() { + format!( + r#"global["{}"] = requireModule(entryModuleId);"#, + self.config.name + ) + } else { + "".to_string() + } + } else { + "".to_string() + } + } + pub(crate) fn patch_code_splitting( + &self, + optimize_chunk_options: &mut crate::config::CodeSplittingAdvancedOptions, + ) { + optimize_chunk_options.groups.iter_mut().for_each(|group| { + group.allow_chunks = AllowChunks::Async; + }); + } +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct RuntimeRemoteItem { + name: String, + alias: String, + entry: String, + share_scope: String, +} + +#[derive(Serialize)] +struct RuntimeInitOptions { + name: String, + remotes: Vec, + share_strategy: String, +} diff --git a/crates/mako/src/plugins/module_federation/container_reference.rs b/crates/mako/src/plugins/module_federation/container_reference.rs new file mode 100644 index 000000000..841da7cd8 --- /dev/null +++ b/crates/mako/src/plugins/module_federation/container_reference.rs @@ -0,0 +1,127 @@ +use std::collections::HashMap; +use std::result::Result; +use std::sync::Arc; + +use serde::Serialize; + +use super::{ + ModuleFederationPlugin, FEDERATION_REMOTE_MODULE_PREFIX, FEDERATION_REMOTE_REFERENCE_PREFIX, +}; +use crate::compiler::Context; +use crate::resolve::{RemoteInfo, ResolverResource}; + +impl ModuleFederationPlugin { + pub(super) fn init_federation_runtime_remotes(&self, context: &Arc) -> String { + if self.config.remotes.as_ref().is_none_or(|r| r.is_empty()) { + return "".to_string(); + } + + let module_graph = context.module_graph.read().unwrap(); + let chunk_graph = context.chunk_graph.read().unwrap(); + let all_chunks = chunk_graph.get_all_chunks(); + + let mut chunk_mapping: HashMap<&str, Vec<&str>> = HashMap::new(); + let mut id_to_external_and_name_mapping: HashMap<&str, Vec<&str>> = HashMap::new(); + let mut id_to_remote_map: HashMap<&str, Vec> = HashMap::new(); + all_chunks.iter().for_each(|c| { + c.modules.iter().for_each(|m| { + if let Some(m) = module_graph.get_module(m) { + if m.is_remote() { + { + chunk_mapping + .entry(c.id.id.as_str()) + .or_default() + .push(m.id.id.as_str()); + } + + { + let remote_module = m + .info + .as_ref() + .unwrap() + .resolved_resource + .as_ref() + .unwrap() + .get_remote_info() + .unwrap(); + let remote_info = id_to_external_and_name_mapping + .entry(m.id.id.as_str()) + .or_default(); + + remote_info.push(&remote_module.share_scope); + remote_info.push(&remote_module.sub_path); + remote_info.push(&remote_module.external_reference_id); + + let external_info = + id_to_remote_map.entry(m.id.id.as_str()).or_default(); + + external_info.push(RemoteExternal { + name: remote_module.name.clone(), + external_type: remote_module.external_type.clone(), + external_module_id: remote_module.external_reference_id.clone(), + }); + } + } + } + }); + }); + + let chunk_mapping = serde_json::to_string(&chunk_mapping).unwrap(); + let id_to_external_and_name_mapping = + serde_json::to_string(&id_to_external_and_name_mapping).unwrap(); + let id_to_remote_map = serde_json::to_string(&id_to_remote_map).unwrap(); + + format!( + r#" +/* mako/runtime/federation remotes consume */ +!(function() {{ + var chunkMapping = {chunk_mapping}; + var idToExternalAndNameMapping = {id_to_external_and_name_mapping}; + var idToRemoteMap = {id_to_remote_map}; + requireModule.federation.bundlerRuntimeOptions.remotes = {{idToRemoteMap, chunkMapping, idToExternalAndNameMapping, webpackRequire: requireModule}}; + requireModule.chunkEnsures.remotes = (chunkId, promises) => {{ + requireModule.federation.bundlerRuntime.remotes({{ idToRemoteMap,chunkMapping, idToExternalAndNameMapping, chunkId, promises, webpackRequire: requireModule}}); + }} +}} +)()"#, + ) + } + + pub(super) fn resolve_remote( + &self, + source: &str, + ) -> Result, anyhow::Error> { + let source_parts = source + .split_once("/") + .map_or((source.to_string(), ".".to_string()), |(part_0, part_1)| { + (part_0.to_string(), part_1.to_string()) + }); + Ok(self.config.remotes.as_ref().map_or_else( + || None, + |remotes| { + remotes.get(&source_parts.0).map(|_remote| { + ResolverResource::Remote(RemoteInfo { + module_id: format!("{}{}", FEDERATION_REMOTE_MODULE_PREFIX, source), + external_reference_id: format!( + "{}{}", + FEDERATION_REMOTE_REFERENCE_PREFIX, source_parts.0 + ), + // FIXME: hard code now + external_type: "script".to_string(), + sub_path: format!("./{}", source_parts.1), + name: source_parts.0.to_string(), + share_scope: self.config.share_scope.clone(), + }) + }) + }, + )) + } +} + +#[derive(Serialize)] +#[serde(rename_all = "camelCase")] +struct RemoteExternal { + external_type: String, + name: String, + external_module_id: String, +} diff --git a/crates/mako/src/plugins/module_federation/manifest.rs b/crates/mako/src/plugins/module_federation/manifest.rs new file mode 100644 index 000000000..7c23ff9ef --- /dev/null +++ b/crates/mako/src/plugins/module_federation/manifest.rs @@ -0,0 +1,308 @@ +use std::fs; +use std::sync::Arc; + +use serde::Serialize; + +use super::constants::FEDERATION_EXPOSE_CHUNK_PREFIX; +use super::util::{parse_remote, serialize_none_to_false}; +use super::ModuleFederationPlugin; +use crate::compiler::Context; +use crate::generate::chunk_graph::ChunkGraph; +use crate::module::ModuleId; +use crate::plugin::PluginGenerateEndParams; +use crate::stats::StatsJsonMap; +use crate::utils::get_app_info; + +impl ModuleFederationPlugin { + pub(super) fn generate_federation_manifest( + &self, + context: &Arc, + params: &PluginGenerateEndParams, + ) -> Result<(), anyhow::Error> { + let app_info = get_app_info(&context.root); + let manifest = Manifest { + id: self.config.name.clone(), + name: self.config.name.clone(), + exposes: self.config.exposes.as_ref().map_or(Vec::new(), |exposes| { + exposes + .iter() + .map(|(path, module)| { + let name = path.replace("./", ""); + let remote_module_id: ModuleId = context + .root + .join(module) + .canonicalize() + .unwrap() + .to_string_lossy() + .to_string() + .into(); + // FIXME: this may be slow + let chunk_graph = context.chunk_graph.read().unwrap(); + let sync_chunks = chunk_graph + .graph + .node_weights() + .filter_map(|c| { + if c.id.id.starts_with(FEDERATION_EXPOSE_CHUNK_PREFIX) + && c.has_module(&remote_module_id) + { + Some(c.id.clone()) + } else { + None + } + }) + .collect::>(); + + let assets = extract_chunk_assets(sync_chunks, &chunk_graph, params); + ManifestExpose { + id: format!("{}:{}", self.config.name, name), + name, + path: path.clone(), + assets, + } + }) + .collect() + }), + shared: { + let chunk_graph = context.chunk_graph.read().unwrap(); + let provide_shared_map = self.shared_dependency_map.read().unwrap(); + provide_shared_map + .iter() + .filter_map(|(_, config)| { + let module_id: ModuleId = config.file_path.clone().into(); + let chunk_id = chunk_graph + .get_chunk_for_module(&module_id) + .as_ref()? + .id + .clone(); + let assets = extract_chunk_assets(vec![chunk_id], &chunk_graph, params); + Some(ManifestShared { + id: format!("{}:{}", self.config.name, config.share_key), + name: config.share_key.clone(), + require_version: config.shared_config.required_version.clone(), + version: config.version.clone(), + singleton: config.shared_config.singleton, + assets, + }) + }) + .collect() + }, + remotes: { + let module_graph = context.module_graph.read().unwrap(); + params + .stats + .chunk_modules + .iter() + .filter_map(|cm| { + if let Some(module) = module_graph.get_module(&cm.id.clone().into()) + && module.is_remote() + { + let data = cm.id.split('/').collect::>(); + Some(ManifestRemote { + entry: parse_remote( + self.config.remotes.as_ref().unwrap().get(data[3]).unwrap(), + ) + .unwrap() + .1, + module_name: data[4].to_string(), + alias: data[3].to_string(), + federation_container_name: data[3].to_string(), + }) + } else { + None + } + }) + .collect() + }, + meta_data: { + let chunk_graph = context.chunk_graph.read().unwrap(); + let mf_container_entry_root_module: Option = context + .config + .entry + .get(&self.config.name) + .map(|e| e.import.to_string_lossy().to_string().into()); + let mf_container_entry_chunk = mf_container_entry_root_module + .map(|m| chunk_graph.get_chunk_for_module(&m).unwrap()); + + ManifestMetaData { + name: self.config.name.clone(), + build_info: ManifestMetaBuildInfo { + build_name: app_info.0.unwrap_or("default".to_string()), + build_version: app_info.1.unwrap_or("".to_string()), + }, + global_name: self.config.name.clone(), + public_path: context.config.public_path.clone(), + // FIXME: hardcode now + r#type: "global".to_string(), + remote_entry: mf_container_entry_chunk.map(|c| ManifestMetaRemoteEntry { + name: extract_assets(&[c.id.clone()], ¶ms.stats).0[0].clone(), + path: "".to_string(), + r#type: "global".to_string(), + }), + ..Default::default() + } + }, + }; + fs::write( + context.root.join("./dist/mf-manifest.json"), + serde_json::to_string_pretty(&manifest)?, + ) + .unwrap(); + Ok(()) + } +} + +fn extract_chunk_assets( + sync_chunks: Vec, + chunk_graph: &ChunkGraph, + params: &PluginGenerateEndParams, +) -> ManifestAssets { + let sync_chunk_dependencies = sync_chunks.iter().fold(Vec::new(), |mut acc, cur| { + let sync_deps = chunk_graph.sync_dependencies_chunk(cur); + acc.splice(..0, sync_deps); + acc + }); + + let all_sync_chunks = [sync_chunk_dependencies, sync_chunks].concat(); + let all_async_chunks: Vec = all_sync_chunks.iter().fold(vec![], |mut acc, cur| { + acc.extend(chunk_graph.installable_descendants_chunk(cur)); + acc + }); + + let (sync_js_files, sync_css_files) = extract_assets(&all_sync_chunks, ¶ms.stats); + + let (async_js_files, async_css_files) = extract_assets(&all_async_chunks, ¶ms.stats); + + let async_js_files = async_js_files + .into_iter() + .filter(|f| !sync_js_files.contains(f)) + .collect(); + + let async_css_files = async_css_files + .into_iter() + .filter(|f| !sync_js_files.contains(f)) + .collect(); + + ManifestAssets { + js: ManifestAssetsItem { + sync: sync_js_files, + r#async: async_js_files, + }, + css: ManifestAssetsItem { + sync: sync_css_files, + r#async: async_css_files, + }, + } +} + +fn extract_assets( + all_exposes_sync_chunks: &[ModuleId], + stats: &StatsJsonMap, +) -> (Vec, Vec) { + all_exposes_sync_chunks.iter().fold( + (Vec::::new(), Vec::::new()), + |mut acc, cur| { + if let Some(c) = stats.chunks.iter().find(|c| c.id == cur.id) { + c.files.iter().for_each(|f| { + if f.ends_with(".js") { + acc.0.push(f.clone()); + } + if f.ends_with(".css") { + acc.1.push(f.clone()); + } + }); + } + acc + }, + ) +} + +#[derive(Serialize, Default)] +pub struct ManifestAssetsItem { + pub sync: Vec, + pub r#async: Vec, +} + +#[derive(Serialize, Default)] +pub struct ManifestAssets { + pub js: ManifestAssetsItem, + pub css: ManifestAssetsItem, +} + +#[derive(Serialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct ManifestExpose { + pub id: String, + pub name: String, + pub assets: ManifestAssets, + pub path: String, +} + +#[derive(Serialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct ManifestShared { + id: String, + name: String, + assets: ManifestAssets, + version: String, + #[serde(serialize_with = "serialize_none_to_false")] + require_version: Option, + singleton: bool, +} + +#[derive(Serialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct ManifestRemote { + pub entry: String, + pub alias: String, + pub module_name: String, + pub federation_container_name: String, +} + +#[derive(Serialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct ManifestMetaTypes { + path: String, + name: String, + zip: String, + api: String, +} + +#[derive(Serialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct ManifestMetaData { + #[serde(skip_serializing_if = "Option::is_none")] + pub remote_entry: Option, + pub global_name: String, + pub public_path: String, + pub r#type: String, + pub build_info: ManifestMetaBuildInfo, + pub name: String, + pub types: ManifestMetaTypes, + pub plugin_version: String, +} + +#[derive(Serialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct ManifestMetaBuildInfo { + pub build_version: String, + pub build_name: String, +} + +#[derive(Serialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct ManifestMetaRemoteEntry { + pub name: String, + pub path: String, + pub r#type: String, +} + +#[derive(Serialize, Default)] +#[serde(rename_all = "camelCase")] +pub struct Manifest { + pub id: String, + pub name: String, + pub meta_data: ManifestMetaData, + pub shared: Vec, + pub remotes: Vec, + pub exposes: Vec, +} diff --git a/crates/mako/src/plugins/module_federation/provide_for_consume.rs b/crates/mako/src/plugins/module_federation/provide_for_consume.rs new file mode 100644 index 000000000..2bb114517 --- /dev/null +++ b/crates/mako/src/plugins/module_federation/provide_for_consume.rs @@ -0,0 +1,43 @@ +use super::ModuleFederationPlugin; +use crate::generate::chunk::ChunkType; +use crate::generate::chunk_graph::ChunkGraph; +use crate::module_graph::ModuleGraph; + +impl ModuleFederationPlugin { + pub(super) fn connect_provide_shared_to_container( + &self, + chunk_graph: &mut ChunkGraph, + _module_graph: &mut ModuleGraph, + ) { + let entry_chunks = chunk_graph + .get_chunks() + .into_iter() + .filter_map(|c| { + if matches!(c.chunk_type, ChunkType::Entry(_, _, false)) { + Some(c.id.clone()) + } else { + None + } + }) + .collect::>(); + + let provide_shared_map = self.shared_dependency_map.read().unwrap(); + + let provide_shared_in_chunks = provide_shared_map + .iter() + .map(|m| { + chunk_graph + .get_chunk_for_module(&m.0.as_str().into()) + .unwrap() + .id + .clone() + }) + .collect::>(); + + entry_chunks.iter().for_each(|ec| { + provide_shared_in_chunks.iter().for_each(|c| { + chunk_graph.add_edge(ec, c); + }); + }); + } +} diff --git a/crates/mako/src/plugins/module_federation/provide_shared.rs b/crates/mako/src/plugins/module_federation/provide_shared.rs new file mode 100644 index 000000000..a6f0e59cf --- /dev/null +++ b/crates/mako/src/plugins/module_federation/provide_shared.rs @@ -0,0 +1,148 @@ +use pathdiff::diff_paths; +use serde::Serialize; + +use super::util::serialize_none_to_false; +use super::ModuleFederationPlugin; +use crate::build::analyze_deps::ResolvedDep; +use crate::compiler::Context; +use crate::generate::chunk::ChunkType; +use crate::module::ModuleId; + +impl ModuleFederationPlugin { + pub(super) fn init_federation_runtime_sharing(&self, context: &Context) -> String { + let provide_shared_map = self.shared_dependency_map.read().unwrap(); + let chunk_graph = context.chunk_graph.read().unwrap(); + + if provide_shared_map.is_empty() { + return "".to_string(); + } + + let provide_shared_map_code = format!( + r#"{{{}}}"#, + provide_shared_map + .iter() + .filter_map(|(_, share_item)| { + let module_id: ModuleId = share_item.file_path.as_str().into(); + let module_in_chunk = chunk_graph.get_chunk_for_module(&module_id)?; + + let share_item_code = format!(r#""{share_key}": [{infos}]"#, + share_key = share_item.share_key, + infos = { + let getter = { + let module_relative_path = + diff_paths(&share_item.file_path, &context.root) + .unwrap() + .to_string_lossy() + .to_string(); + + match &module_in_chunk.chunk_type { + ChunkType::Entry(_, _, _) | ChunkType::Worker(_) => { + format!( + r#"() => (() => requireModule("{module_relative_path}"))"# + ) + }, + ChunkType::Async + | ChunkType::Sync + => { + let dependency_chunks = chunk_graph.sync_dependencies_chunk(&module_in_chunk.id); + format!( + r#"() => (Promise.all([{}]).then(() => (() => requireModule("{module_relative_path}"))))"#, + [dependency_chunks, vec![module_in_chunk.id.clone()]] + .concat().iter().map(|e| format!(r#"requireModule.ensure("{}")"#, e.id)) + .collect::>().join(",") + ) + }, + // FIXME: + _ => panic!("mf shared dependency should not bundled to worker chunk, entries' shared chunk or runtime chunk") + } + }; + format!( + r#"{{ version: "{version}", get: {getter}, scope: {scope}, shareConfig: {share_config} }}"#, + version = share_item.version, + scope = serde_json::to_string(&share_item.scope).unwrap(), + share_config = serde_json::to_string(&share_item.shared_config).unwrap() + ) + }, + ); + Some(share_item_code)} + ) + .collect::>() + .join(",") + ); + + if let Some(shared) = self.config.shared.as_ref() + && !shared.is_empty() + { + format!( + r#" +/* mako/runtime/federation sharing */ +!(function() {{ + requireModule.federation.initOptions.shared = {provide_shared_map_code}; + requireModule.S = {{}}; + var initPromises = {{}}; + var initTokens = {{}}; + requireModule.I = function(name, initScope) {{ + return requireModule.federation.bundlerRuntime.I({{ + shareScopeName: name, + webpackRequire: requireModule, + initPromises: initPromises, + initTokens: initTokens, + initScope: initScope + }}); + }}; +}})(); +"#, + ) + } else { + "".to_string() + } + } + + pub(super) fn collect_provide_shared(&self, resolved_dep: &ResolvedDep) { + if let Some(shared) = self.config.shared.as_ref() + && let Some(pkg_info) = resolved_dep.resolver_resource.get_pkg_info() + && let Some(pkg_name) = pkg_info.name + && let Some(shared_info) = shared.get(&pkg_name) + && pkg_name == resolved_dep.dependency.source + { + let mut provide_shared_map = self.shared_dependency_map.write().unwrap(); + provide_shared_map + .entry(resolved_dep.resolver_resource.get_resolved_path()) + .or_insert(SharedDependency { + share_key: pkg_name.clone(), + version: pkg_info.version.clone().unwrap(), + scope: vec![shared_info.shared_scope.clone()], + file_path: pkg_info.file_path.clone(), + shared_config: SharedConfig { + eager: shared_info.eager, + strict_version: shared_info.strict_version, + singleton: shared_info.singleton, + required_version: shared_info.required_version.clone(), + // FIXME: hard code now + fixed_dependencies: false, + }, + }); + } + } +} + +#[derive(Debug)] +pub(super) struct SharedDependency { + pub(super) share_key: String, + pub(super) version: String, + pub(super) scope: Vec, + pub(super) shared_config: SharedConfig, + pub(super) file_path: String, +} + +#[derive(Serialize, Debug)] +#[serde(rename_all = "camelCase")] +pub(super) struct SharedConfig { + #[serde(default)] + pub(super) fixed_dependencies: bool, + pub(super) eager: bool, + pub(super) strict_version: bool, + pub(super) singleton: bool, + #[serde(serialize_with = "serialize_none_to_false")] + pub(super) required_version: Option, +} diff --git a/crates/mako/src/plugins/module_federation/util.rs b/crates/mako/src/plugins/module_federation/util.rs new file mode 100644 index 000000000..110296416 --- /dev/null +++ b/crates/mako/src/plugins/module_federation/util.rs @@ -0,0 +1,23 @@ +use anyhow::{anyhow, Result}; +use serde::{Serialize, Serializer}; + +pub(super) fn parse_remote(remote: &str) -> Result<(String, String)> { + let (left, right) = remote + .split_once('@') + .ok_or(anyhow!("invalid remote {}", remote))?; + if left.is_empty() || right.is_empty() { + Err(anyhow!("invalid remote {}", remote)) + } else { + Ok((left.to_string(), right.to_string())) + } +} + +pub(super) fn serialize_none_to_false( + t: &Option, + s: S, +) -> Result { + match t { + Some(t) => t.serialize(s), + None => s.serialize_bool(false), + } +} diff --git a/crates/mako/src/plugins/ssu.rs b/crates/mako/src/plugins/ssu.rs index 83a683c13..52bd99f35 100644 --- a/crates/mako/src/plugins/ssu.rs +++ b/crates/mako/src/plugins/ssu.rs @@ -166,7 +166,7 @@ impl Plugin for SUPlus { fn modify_config(&self, config: &mut Config, _root: &Path, _args: &Args) -> Result<()> { for p in config.entry.values_mut() { - *p = PathBuf::from(format!("{SSU_ENTRY_PREFIX}{}", p.to_string_lossy())); + p.import = PathBuf::from(format!("{SSU_ENTRY_PREFIX}{}", p.import.to_string_lossy())); } config.code_splitting = Some(CodeSplitting { @@ -258,8 +258,8 @@ let patch = require._su_patch(); console.log(patch); try{{ {} -}}catch(e){{ -//ignore the error +}}catch(e){{ +//ignore the error }} module.export = Promise.all( patch.map((d)=>__mako_require__.ensure(d)) diff --git a/crates/mako/src/plugins/tree_shaking/module.rs b/crates/mako/src/plugins/tree_shaking/module.rs index 4c449b9fb..b401f2012 100644 --- a/crates/mako/src/plugins/tree_shaking/module.rs +++ b/crates/mako/src/plugins/tree_shaking/module.rs @@ -297,7 +297,7 @@ impl TreeShakeModule { crate::module::ModuleAst::None => StatementGraph::empty(), }; - let used_exports = if module.is_entry { + let used_exports = if module.is_entry || module.is_consume_share() { UsedExports::All } else { UsedExports::Partial(Default::default()) diff --git a/crates/mako/src/plugins/tree_shaking/shake.rs b/crates/mako/src/plugins/tree_shaking/shake.rs index 57d23a545..52c4a0ba1 100644 --- a/crates/mako/src/plugins/tree_shaking/shake.rs +++ b/crates/mako/src/plugins/tree_shaking/shake.rs @@ -38,7 +38,9 @@ pub fn optimize_modules(module_graph: &mut ModuleGraph, context: &Arc) let module_type = module.get_module_type(); // skip non script modules and external modules - if module_type != ModuleType::Script || module.is_external() { + if (module_type != ModuleType::Script && !module.is_consume_share()) + || module.is_external() + { if module_type != ModuleType::Script && !module.is_external() { // mark all non script modules' script dependencies as side_effects for dep_id in module_graph.dependence_module_ids(module_id) { diff --git a/crates/mako/src/resolve.rs b/crates/mako/src/resolve.rs index 71ece577c..32b0d15d3 100644 --- a/crates/mako/src/resolve.rs +++ b/crates/mako/src/resolve.rs @@ -13,7 +13,9 @@ use tracing::debug; mod resolution; mod resource; pub use resolution::Resolution; -pub use resource::{ExternalResource, ResolvedResource, ResolverResource}; +pub use resource::{ + ConsumeSharedInfo, ExternalResource, RemoteInfo, ResolvedResource, ResolverResource, +}; use crate::ast::file::parse_path; use crate::compiler::Context; @@ -49,9 +51,6 @@ pub fn resolve( resolvers: &Resolvers, context: &Arc, ) -> Result { - crate::mako_profile_function!(); - crate::mako_profile_scope!("resolve", &dep.source); - // plugin first if let Some(resolved) = context.plugin_driver.resolve_id( &dep.source, @@ -59,7 +58,10 @@ pub fn resolve( // it's a compatibility feature for unplugin hooks // is_entry is always false for dependencies // since entry file does not need be be resolved - &PluginResolveIdParams { is_entry: false }, + &PluginResolveIdParams { + is_entry: false, + dep, + }, context, )? { return Ok(resolved); @@ -231,7 +233,7 @@ fn get_external_target_from_global_obj(global_obj_name: &str, external: &str) -> format!("{}{}", global_obj_name, external) } -fn do_resolve( +pub fn do_resolve( path: &str, source: &str, resolver: &Resolver, diff --git a/crates/mako/src/resolve/resource.rs b/crates/mako/src/resolve/resource.rs index 6e9219481..648c47628 100644 --- a/crates/mako/src/resolve/resource.rs +++ b/crates/mako/src/resolve/resource.rs @@ -1,7 +1,31 @@ use std::path::PathBuf; +use crate::build::analyze_deps::AnalyzeDepsResult; use crate::resolve::Resolution; +#[derive(Debug, Clone)] +pub struct RemoteInfo { + pub module_id: String, + pub external_reference_id: String, + pub external_type: String, + pub sub_path: String, + pub name: String, + pub share_scope: String, +} + +#[derive(Debug, Clone)] +pub struct ConsumeSharedInfo { + pub module_id: String, + pub name: String, + pub share_scope: String, + pub version: String, + pub eager: bool, + pub required_version: Option, + pub strict_version: bool, + pub singletion: bool, + pub deps: AnalyzeDepsResult, +} + #[derive(Debug, Clone)] pub struct ExternalResource { pub source: String, @@ -18,6 +42,8 @@ pub enum ResolverResource { Resolved(ResolvedResource), Ignored(PathBuf), Virtual(PathBuf), + Remote(RemoteInfo), + Shared(ConsumeSharedInfo), } impl ResolverResource { @@ -29,22 +55,56 @@ impl ResolverResource { } ResolverResource::Ignored(path) => path.to_string_lossy().to_string(), ResolverResource::Virtual(path) => path.to_string_lossy().to_string(), + ResolverResource::Remote(info) => info.module_id.to_string(), + ResolverResource::Shared(info) => info.module_id.clone(), } } pub fn get_external(&self) -> Option { match self { ResolverResource::External(ExternalResource { external, .. }) => Some(external.clone()), - ResolverResource::Resolved(_) => None, - ResolverResource::Ignored(_) => None, - ResolverResource::Virtual(_) => None, + _ => None, } } pub fn get_script(&self) -> Option { match self { ResolverResource::External(ExternalResource { script, .. }) => script.clone(), - ResolverResource::Resolved(_) => None, - ResolverResource::Ignored(_) => None, - ResolverResource::Virtual(_) => None, + _ => None, + } + } + + pub fn get_remote_info(&self) -> Option<&RemoteInfo> { + match &self { + ResolverResource::Remote(remote_info) => Some(remote_info), + _ => None, + } + } + + pub fn get_pkg_info(&self) -> Option { + match self { + ResolverResource::Resolved(ResolvedResource(resolution)) => Some(PkgInfo { + file_path: resolution.full_path().to_string_lossy().to_string(), + name: resolution.package_json().and_then(|p| { + p.raw_json() + .get("name") + .and_then(|v| v.as_str().map(|v| v.to_string())) + }), + version: resolution.package_json().and_then(|p| { + p.raw_json() + .get("version") + .and_then(|v| v.as_str().map(|v| v.to_string())) + }), + }), + ResolverResource::Shared(info) => { + info.deps.resolved_deps[0].resolver_resource.get_pkg_info() + } + _ => None, } } } + +#[derive(Debug)] +pub struct PkgInfo { + pub file_path: String, + pub name: Option, + pub version: Option, +} diff --git a/crates/mako/src/stats.rs b/crates/mako/src/stats.rs index a9fe89683..71cf8b2da 100644 --- a/crates/mako/src/stats.rs +++ b/crates/mako/src/stats.rs @@ -124,11 +124,9 @@ impl Compiler { ChunkType::Sync => chunk_graph .dependents_chunk(&chunk.id) .iter() - .filter_map(|chunk_id| { - chunk_graph.chunk(chunk_id).unwrap().modules.iter().last() - }) + .filter_map(|chunk_id| chunk_graph.chunk(chunk_id).unwrap().root_module()) .collect::>(), - _ => vec![chunk.modules.iter().last().unwrap()], + _ => vec![chunk.root_module().unwrap()], }; let mut origins_set = IndexMap::new(); for origin_chunk_module in origin_chunk_modules { @@ -506,9 +504,9 @@ pub struct StatsJsonMap { root_path: String, output_path: String, assets: Vec, - chunk_modules: Vec, + pub chunk_modules: Vec, modules: HashMap, - chunks: Vec, + pub chunks: Vec, entrypoints: HashMap, rsc_client_components: Vec, #[serde(rename = "rscCSSModules")] diff --git a/crates/mako/src/utils.rs b/crates/mako/src/utils.rs index a469dd3a8..0dc917b6b 100644 --- a/crates/mako/src/utils.rs +++ b/crates/mako/src/utils.rs @@ -53,18 +53,23 @@ pub fn process_req_url(public_path: &str, req_url: &str) -> Result { Ok(req_url.to_string()) } -pub(crate) fn get_pkg_name(root: &Path) -> Option { +pub(crate) fn get_app_info(root: &Path) -> (Option, Option) { let pkg_json_path = root.join("package.json"); if pkg_json_path.exists() { let pkg_json = std::fs::read_to_string(pkg_json_path).unwrap(); let pkg_json: serde_json::Value = serde_json::from_str(&pkg_json).unwrap(); - pkg_json - .get("name") - .map(|name| name.as_str().unwrap().to_string()) + ( + pkg_json + .get("name") + .map(|name| name.as_str().unwrap().to_string()), + pkg_json + .get("version") + .map(|name| name.as_str().unwrap().to_string()), + ) } else { - None + (None, None) } } diff --git a/crates/mako/src/visitors/dep_analyzer.rs b/crates/mako/src/visitors/dep_analyzer.rs index ca3f89295..c6c7efc2c 100644 --- a/crates/mako/src/visitors/dep_analyzer.rs +++ b/crates/mako/src/visitors/dep_analyzer.rs @@ -66,7 +66,19 @@ impl DepAnalyzer { }) }); - ImportOptions { chunk_name, ignore } + let _is_federation_expose = comments_texts.iter().any(|t| { + get_magic_federation_expose_regex() + .captures(t.trim()) + .map_or(false, |cap| { + cap.get(2).map_or(false, |m| m.as_str() == "true") + }) + }); + + ImportOptions { + chunk_name, + ignore, + _is_federation_expose, + } } } @@ -241,6 +253,10 @@ fn get_magic_comment_ignore_regex() -> Regex { create_cached_regex(r#"(makoIgnore|webpackIgnore):\s*(true|false)"#) } +fn get_magic_federation_expose_regex() -> Regex { + create_cached_regex(r#"(federationExpose):\s*(true|false)"#) +} + #[cfg(test)] mod tests { use swc_core::common::GLOBALS; diff --git a/crates/mako/src/visitors/dep_replacer.rs b/crates/mako/src/visitors/dep_replacer.rs index 47167f6e7..272783551 100644 --- a/crates/mako/src/visitors/dep_replacer.rs +++ b/crates/mako/src/visitors/dep_replacer.rs @@ -28,6 +28,7 @@ pub struct ResolvedReplaceInfo { pub chunk_id: Option, pub to_replace_source: String, pub resolved_module_id: ModuleId, + pub _is_federation_expose: bool, } #[derive(Debug, Clone)] @@ -351,6 +352,7 @@ try { chunk_id: None, to_replace_source: module_id.into(), resolved_module_id: "".into(), + _is_federation_expose: false, } } } diff --git a/crates/mako/src/visitors/dynamic_import.rs b/crates/mako/src/visitors/dynamic_import.rs index b8cd66ac9..aa3200607 100644 --- a/crates/mako/src/visitors/dynamic_import.rs +++ b/crates/mako/src/visitors/dynamic_import.rs @@ -135,8 +135,18 @@ impl<'a> VisitMut for DynamicImport<'a> { vec![self.interop.clone().as_arg(), lazy_require_call.as_arg()], ); + let dr_call_arg = if resolved_info._is_federation_expose { + dr_call + .as_call(call_expr.span, Vec::new()) + .into_lazy_fn(Vec::new()) + .into_lazy_fn(Vec::new()) + .as_arg() + } else { + dr_call.as_arg() + }; + member_expr!(@EXT, DUMMY_SP, load_promise.into(), then) - .as_call(call_expr.span, vec![dr_call.as_arg()]) + .as_call(call_expr.span, vec![dr_call_arg]) }; } } @@ -250,12 +260,14 @@ Promise.all([ "@swc/helpers/_/_interop_require_wildcard".to_string() => ResolvedReplaceInfo { chunk_id: None, to_replace_source: "hashed_helper".to_string(), - resolved_module_id:"dummy".into() + resolved_module_id:"dummy".into(), + _is_federation_expose: false, }, "foo".to_string() => ResolvedReplaceInfo { chunk_id: Some("foo".into()), to_replace_source: "foo".into(), - resolved_module_id: "foo".into() + resolved_module_id: "foo".into(), + _is_federation_expose: false, } }, missing: HashMap::new(), diff --git a/crates/mako/src/visitors/mako_require.rs b/crates/mako/src/visitors/mako_require.rs index 4929c9cde..701be9d39 100644 --- a/crates/mako/src/visitors/mako_require.rs +++ b/crates/mako/src/visitors/mako_require.rs @@ -9,7 +9,7 @@ use crate::ast::utils::is_ident_undefined; use crate::compiler::Context; use crate::config::Platform; -const MAKO_REQUIRE: &str = "__mako_require__"; +pub const MAKO_REQUIRE: &str = "__mako_require__"; pub struct MakoRequire { pub unresolved_mark: Mark, diff --git a/crates/mako/templates/app_runtime.stpl b/crates/mako/templates/app_runtime.stpl index 6824b75f1..69734d85c 100644 --- a/crates/mako/templates/app_runtime.stpl +++ b/crates/mako/templates/app_runtime.stpl @@ -64,11 +64,21 @@ function createRuntime(makoModules, entryModuleId, global) { get: all[name], }); }; + + // hasOwnProperty shorthand + requireModule.o = function (obj, prop) { return (Object.prototype.hasOwnProperty.call(obj, prop));}; + + // required modules + requireModule.m = makoModules; + + // modules registry + requireModule.c = modulesRegistry; + <% if concatenate_enabled { %> // Export Star util for concatenated modules requireModule.es = function(to, from) { Object.keys(from).forEach(function(k) { - if (k !== "default" && !Object.prototype.hasOwnProperty.call(to, k)) { + if (k !== "default" && !requireModule.o(to, k)) { Object.defineProperty(to, k, { enumerable: true, get: from[k] @@ -125,6 +135,11 @@ function createRuntime(makoModules, entryModuleId, global) { var data = installedChunks[chunkId]; if (data === 0) return; + <% if chunk_matcher.is_some() { %> + // skip federation remote chunk + if (<%= chunk_matcher.unwrap() %>.test(chunkId)) return + <% } %> + if (data) { // 0 1 2 // [resolve, reject, promise] diff --git a/e2e/fixtures/module-federation.consumer/.gitignore b/e2e/fixtures/module-federation.consumer/.gitignore new file mode 100644 index 000000000..de4d1f007 --- /dev/null +++ b/e2e/fixtures/module-federation.consumer/.gitignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/e2e/fixtures/module-federation.consumer/expect.js b/e2e/fixtures/module-federation.consumer/expect.js new file mode 100644 index 000000000..3cf2fe3b1 --- /dev/null +++ b/e2e/fixtures/module-federation.consumer/expect.js @@ -0,0 +1,22 @@ +const assert = require("assert"); +const { parseBuildResult } = require("../../../scripts/test-utils"); +const { files } = parseBuildResult(__dirname); + +const manifest = JSON.parse(files["mf-manifest.json"]); + +assert( + manifest.remotes[0].alias === 'producer' + && manifest.remotes[0].federationContainerName === 'producer' + && manifest.remotes[0].moduleName === 'App', + "should include mf remotes info" +) + +assert( + manifest.shared.map(s => s.name).sort().join(",") === "react,react-dom", + "should include mf shared dependencies" +) + +assert( + manifest.shared.every(s => s.assets.js.sync.length !== 0), + "should include mf shared assets" +) diff --git a/e2e/fixtures/module-federation.consumer/mako.config.json b/e2e/fixtures/module-federation.consumer/mako.config.json new file mode 100644 index 000000000..dcfe4ae67 --- /dev/null +++ b/e2e/fixtures/module-federation.consumer/mako.config.json @@ -0,0 +1,15 @@ +{ + "entry": { + "app1": "./src/index.ts" + }, + "minify": false, + "moduleFederation": { + "name": "consumer", + "remotes": { + "producer": "producer@http://localhost:3000/remoteEntry.js" + }, + "shared": { "react": { "eager": true }, "react-dom": { "eager": true } }, + "manifest": true, + "implementation": "../../../../../packages/mako/node_modules/@module-federation/webpack-bundler-runtime" + } +} diff --git a/e2e/fixtures/module-federation.consumer/package.json b/e2e/fixtures/module-federation.consumer/package.json new file mode 100644 index 000000000..723a9fb2d --- /dev/null +++ b/e2e/fixtures/module-federation.consumer/package.json @@ -0,0 +1,10 @@ +{ + "name": "mf-consumer", + "version": "0.0.1", + "dependencies": { + "react": "18.2.0", + "react-dom": "18.2.0", + "@types/react": "18.2.0", + "@types/react-dom": "18.2.0" + } +} diff --git a/e2e/fixtures/module-federation.consumer/public/index.html b/e2e/fixtures/module-federation.consumer/public/index.html new file mode 100644 index 000000000..7e0bd8d29 --- /dev/null +++ b/e2e/fixtures/module-federation.consumer/public/index.html @@ -0,0 +1,7 @@ + + + +
+ + + diff --git a/e2e/fixtures/module-federation.consumer/src/App.tsx b/e2e/fixtures/module-federation.consumer/src/App.tsx new file mode 100644 index 000000000..0d9833d2e --- /dev/null +++ b/e2e/fixtures/module-federation.consumer/src/App.tsx @@ -0,0 +1,26 @@ +import React from 'react'; + +// @ts-ignore +const RemoteComp = React.lazy(() => import('producer/App')); + +const App = () => { + return ( +
+
+

Consumer App

+
+ + + +
+ ); +}; + +export default App; diff --git a/e2e/fixtures/module-federation.consumer/src/bootstrap.tsx b/e2e/fixtures/module-federation.consumer/src/bootstrap.tsx new file mode 100644 index 000000000..b597a4423 --- /dev/null +++ b/e2e/fixtures/module-federation.consumer/src/bootstrap.tsx @@ -0,0 +1,5 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import App from './App'; + +ReactDOM.render(, document.getElementById('root')); diff --git a/e2e/fixtures/module-federation.consumer/src/index.ts b/e2e/fixtures/module-federation.consumer/src/index.ts new file mode 100644 index 000000000..e59d6a0ad --- /dev/null +++ b/e2e/fixtures/module-federation.consumer/src/index.ts @@ -0,0 +1 @@ +import './bootstrap'; diff --git a/e2e/fixtures/module-federation.producer/.gitignore b/e2e/fixtures/module-federation.producer/.gitignore new file mode 100644 index 000000000..de4d1f007 --- /dev/null +++ b/e2e/fixtures/module-federation.producer/.gitignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/e2e/fixtures/module-federation.producer/expect.js b/e2e/fixtures/module-federation.producer/expect.js new file mode 100644 index 000000000..ab5ff0183 --- /dev/null +++ b/e2e/fixtures/module-federation.producer/expect.js @@ -0,0 +1,30 @@ +const assert = require("assert"); +const { parseBuildResult } = require("../../../scripts/test-utils"); +const { files } = parseBuildResult(__dirname); + +const manifest = JSON.parse(files["mf-manifest.json"]); + +assert( + manifest.metaData.remoteEntry.name === 'remoteEntry.js', + "should generate mf contanier entry" +) + +assert( + manifest.exposes[0].name === 'App', + "should include mf exposes" +) + +assert( + manifest.exposes[0].assets.js.sync.length !== 0, + "should include mf exposes assets" +) + +assert( + manifest.shared.map(s => s.name).sort().join(",") === "react,react-dom", + "should include mf shared dependencies" +) + +assert( + manifest.shared.every(s => s.assets.js.sync.length !== 0), + "should include mf shared assets" +) diff --git a/e2e/fixtures/module-federation.producer/mako.config.json b/e2e/fixtures/module-federation.producer/mako.config.json new file mode 100644 index 000000000..e966ee81d --- /dev/null +++ b/e2e/fixtures/module-federation.producer/mako.config.json @@ -0,0 +1,16 @@ +{ + "entry": { + "app2": "./src/index.ts" + }, + "publicPath": "auto", + "moduleFederation": { + "name": "producer", + "filename": "remoteEntry.js", + "exposes": { + "./App": "./src/App.tsx" + }, + "shared": { "react": {}, "react-dom": {} }, + "manifest": true, + "implementation": "../../../../../packages/mako/node_modules/@module-federation/webpack-bundler-runtime" + } +} diff --git a/e2e/fixtures/module-federation.producer/package.json b/e2e/fixtures/module-federation.producer/package.json new file mode 100644 index 000000000..c803f552e --- /dev/null +++ b/e2e/fixtures/module-federation.producer/package.json @@ -0,0 +1,10 @@ +{ + "name": "mf-producer", + "version": "0.0.1", + "dependencies": { + "react": "18.2.0", + "react-dom": "18.2.0", + "@types/react": "18.2.0", + "@types/react-dom": "18.2.0" + } +} diff --git a/e2e/fixtures/module-federation.producer/public/index.html b/e2e/fixtures/module-federation.producer/public/index.html new file mode 100644 index 000000000..5de48ef2f --- /dev/null +++ b/e2e/fixtures/module-federation.producer/public/index.html @@ -0,0 +1,7 @@ + + + +
+ + + diff --git a/e2e/fixtures/module-federation.producer/src/App.tsx b/e2e/fixtures/module-federation.producer/src/App.tsx new file mode 100644 index 000000000..2a1875c7f --- /dev/null +++ b/e2e/fixtures/module-federation.producer/src/App.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +const App = () => { + return ( +
+

Widget App2

+
+ ); +}; + +export default App; diff --git a/e2e/fixtures/module-federation.producer/src/bootstrap.tsx b/e2e/fixtures/module-federation.producer/src/bootstrap.tsx new file mode 100644 index 000000000..ae31e4134 --- /dev/null +++ b/e2e/fixtures/module-federation.producer/src/bootstrap.tsx @@ -0,0 +1,6 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; + +import App from './App'; + +ReactDOM.render(, document.getElementById('root')); diff --git a/e2e/fixtures/module-federation.producer/src/index.ts b/e2e/fixtures/module-federation.producer/src/index.ts new file mode 100644 index 000000000..b93c7a026 --- /dev/null +++ b/e2e/fixtures/module-federation.producer/src/index.ts @@ -0,0 +1 @@ +import('./bootstrap'); diff --git a/examples/module-federation/host/.gitignore b/examples/module-federation/host/.gitignore new file mode 100644 index 000000000..de4d1f007 --- /dev/null +++ b/examples/module-federation/host/.gitignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/examples/module-federation/host/mako.config.json b/examples/module-federation/host/mako.config.json new file mode 100644 index 000000000..bb73b47fb --- /dev/null +++ b/examples/module-federation/host/mako.config.json @@ -0,0 +1,33 @@ +{ + "entry": { + "app1": "./src/index.ts" + }, + "minify": false, + "moduleFederation": { + "name": "mfHost", + "remotes": { + "widget": "mfWidget@http://localhost:3000/remoteEntry.js" + }, + "shared": { "react": { "eager": true }, "react-dom": { "eager": true } }, + "implementation": "../../../../../packages/mako/node_modules/@module-federation/webpack-bundler-runtime" + }, + "experimental": { + "centralEnsure": false + }, + "define": { + "process.env.SOCKET_SERVER": "\"http://localhost:3001\"" + }, + "codeSplitting": { + "strategy": "advanced", + "options": { + "groups": [ + { + "name": "vendor", + "allowChunks": "all", + "minSize": 1, + "test": "node_modules" + } + ] + } + } +} diff --git a/examples/module-federation/host/package.json b/examples/module-federation/host/package.json new file mode 100644 index 000000000..1f3c721bc --- /dev/null +++ b/examples/module-federation/host/package.json @@ -0,0 +1,10 @@ +{ + "name": "mf-host", + "version": "0.0.1", + "dependencies": { + "react": "18.2.0", + "react-dom": "18.2.0", + "@types/react": "18.2.0", + "@types/react-dom": "18.2.0" + } +} diff --git a/examples/module-federation/host/public/index.html b/examples/module-federation/host/public/index.html new file mode 100644 index 000000000..7e0bd8d29 --- /dev/null +++ b/examples/module-federation/host/public/index.html @@ -0,0 +1,7 @@ + + + +
+ + + diff --git a/examples/module-federation/host/src/App.tsx b/examples/module-federation/host/src/App.tsx new file mode 100644 index 000000000..1cc8f71ef --- /dev/null +++ b/examples/module-federation/host/src/App.tsx @@ -0,0 +1,31 @@ +import React from 'react'; +// import Widget1 from 'widget/App1'; +// import Widget2 from 'widget/App2'; + +const Widget1 = React.lazy(() => import('widget/App1')); +const Widget2 = React.lazy(() => import('widget/App2')); + +const App = () => { + return ( +
+
+

Host App

+
+ + + + + + +
+ ); +}; + +export default App; diff --git a/examples/module-federation/host/src/bootstrap.tsx b/examples/module-federation/host/src/bootstrap.tsx new file mode 100644 index 000000000..b597a4423 --- /dev/null +++ b/examples/module-federation/host/src/bootstrap.tsx @@ -0,0 +1,5 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; +import App from './App'; + +ReactDOM.render(, document.getElementById('root')); diff --git a/examples/module-federation/host/src/index.ts b/examples/module-federation/host/src/index.ts new file mode 100644 index 000000000..e59d6a0ad --- /dev/null +++ b/examples/module-federation/host/src/index.ts @@ -0,0 +1 @@ +import './bootstrap'; diff --git a/examples/module-federation/widget/.gitignore b/examples/module-federation/widget/.gitignore new file mode 100644 index 000000000..de4d1f007 --- /dev/null +++ b/examples/module-federation/widget/.gitignore @@ -0,0 +1,2 @@ +dist +node_modules diff --git a/examples/module-federation/widget/mako.config.json b/examples/module-federation/widget/mako.config.json new file mode 100644 index 000000000..bf67e0f18 --- /dev/null +++ b/examples/module-federation/widget/mako.config.json @@ -0,0 +1,36 @@ +{ + "entry": { + "app2": "./src/index.ts" + }, + "minify": false, + "publicPath": "auto", + "moduleFederation": { + "name": "mfWidget", + "filename": "remoteEntry.js", + "exposes": { + "./App1": "./src/App1.tsx", + "./App2": "./src/App2.tsx" + }, + "shared": { "react": {}, "react-dom": {} }, + "runtimePlugins": [], + "implementation": "../../../../../packages/mako/node_modules/@module-federation/webpack-bundler-runtime" + }, + "experimental": { + "centralEnsure": false + }, + "define": { + "process.env.SOCKET_SERVER": "\"http://localhost:3000\"" + }, + "codeSplitting": { + "strategy": "advanced", + "options": { + "groups": [ + { + "name": "vendor", + "allowChunks": "async", + "test": "node_modules" + } + ] + } + } +} diff --git a/examples/module-federation/widget/package.json b/examples/module-federation/widget/package.json new file mode 100644 index 000000000..2c7c3154d --- /dev/null +++ b/examples/module-federation/widget/package.json @@ -0,0 +1,10 @@ +{ + "name": "mf-widget", + "version": "0.0.1", + "dependencies": { + "react": "18.2.0", + "react-dom": "18.2.0", + "@types/react": "18.2.0", + "@types/react-dom": "18.2.0" + } +} diff --git a/examples/module-federation/widget/public/index.html b/examples/module-federation/widget/public/index.html new file mode 100644 index 000000000..5de48ef2f --- /dev/null +++ b/examples/module-federation/widget/public/index.html @@ -0,0 +1,7 @@ + + + +
+ + + diff --git a/examples/module-federation/widget/src/App1.tsx b/examples/module-federation/widget/src/App1.tsx new file mode 100644 index 000000000..66fb59110 --- /dev/null +++ b/examples/module-federation/widget/src/App1.tsx @@ -0,0 +1,25 @@ +import React, { Suspense } from 'react'; + +const Lazy = React.lazy(() => import('./lazy')); + +const App = () => { + return ( +
+

Widget App1

+

+ + + +

+
+ ); +}; + +export default App; diff --git a/examples/module-federation/widget/src/App2.tsx b/examples/module-federation/widget/src/App2.tsx new file mode 100644 index 000000000..2a1875c7f --- /dev/null +++ b/examples/module-federation/widget/src/App2.tsx @@ -0,0 +1,18 @@ +import React from 'react'; + +const App = () => { + return ( +
+

Widget App2

+
+ ); +}; + +export default App; diff --git a/examples/module-federation/widget/src/bootstrap.tsx b/examples/module-federation/widget/src/bootstrap.tsx new file mode 100644 index 000000000..39c8c02eb --- /dev/null +++ b/examples/module-federation/widget/src/bootstrap.tsx @@ -0,0 +1,6 @@ +import React from 'react'; +import ReactDOM from 'react-dom'; + +import App from './App1'; + +ReactDOM.render(, document.getElementById('root')); diff --git a/examples/module-federation/widget/src/index.ts b/examples/module-federation/widget/src/index.ts new file mode 100644 index 000000000..b93c7a026 --- /dev/null +++ b/examples/module-federation/widget/src/index.ts @@ -0,0 +1 @@ +import('./bootstrap'); diff --git a/examples/module-federation/widget/src/lazy.less b/examples/module-federation/widget/src/lazy.less new file mode 100644 index 000000000..7e30e7dbb --- /dev/null +++ b/examples/module-federation/widget/src/lazy.less @@ -0,0 +1,3 @@ +.lazy { + color: green; +} diff --git a/examples/module-federation/widget/src/lazy.tsx b/examples/module-federation/widget/src/lazy.tsx new file mode 100644 index 000000000..e0edcac47 --- /dev/null +++ b/examples/module-federation/widget/src/lazy.tsx @@ -0,0 +1,4 @@ +import React from 'react'; +import './lazy.less'; + +export default () =>
Lazy
; diff --git a/packages/mako/binding.d.ts b/packages/mako/binding.d.ts index 7dd433296..755640c86 100644 --- a/packages/mako/binding.d.ts +++ b/packages/mako/binding.d.ts @@ -258,9 +258,34 @@ export interface BuildParams { | { logServerComponent: 'error' | 'ignore'; }; + moduleFederation?: { + name: string; + // filename?: string; + exposes?: Record; + shared: Record< + string, + { + singleton?: bool; + strictVersion?: bool; + requiredVersion?: string; + /* eager?: bool; */ /* shareScope?: string; */ + } + >; + remotes?: Record; + runtimePlugins?: string[]; + shareScope?: string; + shareStrategy?: 'version-first' | 'loaded-first'; + implementation: string; + }; experimental?: { webpackSyntaxValidate?: string[]; + requireContext?: bool; + ignoreNonLiteralRequire?: bool; + magicComment?: bool; + detectCircularDependence?: { ignore?: string[] }; rustPlugins?: Array<[string, any]>; + centralEnsure?: bool; + importsChecker?: bool; }; watch?: { ignoredPaths?: string[]; diff --git a/packages/mako/package.json b/packages/mako/package.json index aaf57b95d..01d4fa07d 100644 --- a/packages/mako/package.json +++ b/packages/mako/package.json @@ -36,7 +36,8 @@ "react-refresh": "^0.14.0", "resolve": "^1.22.8", "semver": "^7.6.2", - "yargs-parser": "^21.1.1" + "yargs-parser": "^21.1.1", + "@module-federation/webpack-bundler-runtime": "^0.8.0" }, "devDependencies": { "@napi-rs/cli": "^2.18.0", diff --git a/packages/mako/src/index.ts b/packages/mako/src/index.ts index f70502f81..feb410a62 100644 --- a/packages/mako/src/index.ts +++ b/packages/mako/src/index.ts @@ -59,7 +59,7 @@ export async function build(params: BuildParams) { await rustPluginResolver(rustPlugins); } - let makoConfig: any = {}; + let makoConfig: binding.BuildParams['config'] = {}; let makoConfigPath = path.join(params.root, 'mako.config.json'); if (fs.existsSync(makoConfigPath)) { try { @@ -129,9 +129,9 @@ export async function build(params: BuildParams) { }, }); - if (makoConfig?.sass || params.config?.sass) { + if ((makoConfig as any)?.sass || params.config?.sass) { const sassOpts = { - ...(makoConfig?.sass || {}), + ...((makoConfig as any)?.sass || {}), ...(params.config?.sass || {}), }; let sass = sassLoader(null, sassOpts); @@ -151,6 +151,31 @@ export async function build(params: BuildParams) { }); } + if (makoConfig?.moduleFederation || params.config?.moduleFederation) { + // @ts-ignore + const moduleFederation = { + ...(makoConfig.moduleFederation || {}), + ...(params.config.moduleFederation || {}), + }; + if (!moduleFederation.implementation) { + // @ts-ignore + moduleFederation.implementation = require.resolve( + '@module-federation/webpack-bundler-runtime', + ); + } + + if (moduleFederation?.shared) { + if (Array.isArray(moduleFederation.shared)) { + moduleFederation.shared = moduleFederation.shared.reduce( + (acc, cur) => ({ ...acc, [cur]: {} }), + {}, + ); + } + } + // @ts-ignore + params.config.moduleFederation = moduleFederation; + } + // support dump mako config if (process.env.DUMP_MAKO_CONFIG) { const configFile = path.join(params.root, 'mako.config.json'); @@ -187,7 +212,7 @@ export async function build(params: BuildParams) { return plugin; } }); - makoConfig.plugins?.forEach((plugin: any) => { + (makoConfig as any).plugins?.forEach((plugin: any) => { if (typeof plugin === 'string') { let fn = require( resolve.sync(plugin, { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5d45e57e8..ffc945928 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -191,6 +191,36 @@ importers: specifier: 18.2.0 version: 18.2.0(react@18.2.0) + e2e/fixtures/module-federation.consumer: + dependencies: + '@types/react': + specifier: 18.2.0 + version: 18.2.0 + '@types/react-dom': + specifier: 18.2.0 + version: 18.2.0 + react: + specifier: 18.2.0 + version: 18.2.0 + react-dom: + specifier: 18.2.0 + version: 18.2.0(react@18.2.0) + + e2e/fixtures/module-federation.producer: + dependencies: + '@types/react': + specifier: 18.2.0 + version: 18.2.0 + '@types/react-dom': + specifier: 18.2.0 + version: 18.2.0 + react: + specifier: 18.2.0 + version: 18.2.0 + react-dom: + specifier: 18.2.0 + version: 18.2.0(react@18.2.0) + examples/config-externals: {} examples/dead-simple: {} @@ -509,6 +539,9 @@ importers: packages/mako: dependencies: + '@module-federation/webpack-bundler-runtime': + specifier: ^0.8.0 + version: 0.8.0 '@swc/helpers': specifier: 0.5.1 version: 0.5.1 @@ -652,7 +685,7 @@ packages: '@emotion/hash': 0.8.0 '@emotion/unitless': 0.7.5 classnames: 2.3.2 - csstype: 3.1.2 + csstype: 3.1.3 rc-util: 5.37.0(react-dom@18.2.0)(react@18.2.0) react: 18.2.0 react-dom: 18.2.0(react@18.2.0) @@ -5006,6 +5039,30 @@ packages: resolution: {integrity: sha512-9b8mPpKrfeGRuhFH5iO1iwCLeIIsV6+H1sRfxbkoGXIyQE2BTsPd9zqSqQJ+pv5sJ/hT5M1zvOFL02MnEezFug==} dev: true + /@module-federation/error-codes@0.8.0: + resolution: {integrity: sha512-xU0eUA0xTUx93Li/eYCZ+kuGP9qt+8fEaOu+i6U9jJP1RYONvPifibfLNo4SQszQTzqfGViyrx1O3uiA7XUYQQ==} + dev: false + + /@module-federation/runtime@0.8.0: + resolution: {integrity: sha512-UfDsJYAFOyJoErpmjf1sy8d2WXHGitFsSQrIiDzNpDHv4SzHgjuhWQeAuJKlQq2zdE/F4IPhkHgTatQigRKZCA==} + dependencies: + '@module-federation/error-codes': 0.8.0 + '@module-federation/sdk': 0.8.0 + dev: false + + /@module-federation/sdk@0.8.0: + resolution: {integrity: sha512-V2cNGO//sWCyHTaQ0iTcoslolqVgdBIBOkZVLyk9AkZ4B3CO49pe/TmIIaVs9jVg3GO+ZmmazBFKRkqdn2PdRg==} + dependencies: + isomorphic-rslog: 0.0.6 + dev: false + + /@module-federation/webpack-bundler-runtime@0.8.0: + resolution: {integrity: sha512-NwkQbgUgh0ubwmPR/36YNuOaITkWPTYnJyYqi9vgxHBDp4tURnRI2b1ocG2Gw8c9sEW88ImWYMeF1qT58hQ32w==} + dependencies: + '@module-federation/runtime': 0.8.0 + '@module-federation/sdk': 0.8.0 + dev: false + /@monaco-editor/loader@1.3.3(monaco-editor@0.38.0): resolution: {integrity: sha512-6KKF4CTzcJiS8BJwtxtfyYt9shBiEv32ateQ9T4UVogwn4HM/uPo9iJd2Dmbkpz8CM6Y0PDUpjnZzCwC+eYo2Q==} peerDependencies: @@ -5862,7 +5919,6 @@ packages: /@types/prop-types@15.7.5: resolution: {integrity: sha512-JCB8C6SnDoQf0cNycqd/35A7MjcnK+ZTqE7judS6o7utxUCg6imJg3QK2qzHKszlTjcj2cn+NwMB2i96ubpj7w==} - dev: true /@types/ps-tree@1.1.2: resolution: {integrity: sha512-ZREFYlpUmPQJ0esjxoG1fMvB2HNaD3z+mjqdSosZvd3RalncI9NEur73P8ZJz4YQdL64CmV1w0RuqoRUlhQRBw==} @@ -5876,6 +5932,12 @@ packages: resolution: {integrity: sha512-hKormJbkJqzQGhziax5PItDUTMAM9uE2XXQmM37dyd4hVM+5aVl7oVxMVUiVQn2oCQFN/LKCZdvSM0pFRqbSmQ==} dev: true + /@types/react-dom@18.2.0: + resolution: {integrity: sha512-8yQrvS6sMpSwIovhPOwfyNf2Wz6v/B62LFSVYQ85+Rq3tLsBIG7rP5geMxaijTUxSkrO6RzN/IRuIAADYQsleA==} + dependencies: + '@types/react': 18.2.7 + dev: false + /@types/react-dom@18.2.4: resolution: {integrity: sha512-G2mHoTMTL4yoydITgOGwWdWMVd8sNgyEP85xVmMKAPUBwQWm9wBPQUmvbeF4V3WBY1P7mmL4BkjQ0SqUpf1snw==} dependencies: @@ -5906,20 +5968,26 @@ packages: '@types/react': 18.2.7 dev: true + /@types/react@18.2.0: + resolution: {integrity: sha512-0FLj93y5USLHdnhIhABk83rm8XEGA7kH3cr+YUlvxoUGp1xNt/DINUMvqPxLyOQMzLmZe8i4RTHbvb8MC7NmrA==} + dependencies: + '@types/prop-types': 15.7.5 + '@types/scheduler': 0.16.3 + csstype: 3.1.3 + dev: false + /@types/react@18.2.7: resolution: {integrity: sha512-ojrXpSH2XFCmHm7Jy3q44nXDyN54+EYKP2lBhJ2bqfyPj6cIUW/FZW/Csdia34NQgq7KYcAlHi5184m4X88+yw==} dependencies: '@types/prop-types': 15.7.5 '@types/scheduler': 0.16.3 csstype: 3.1.3 - dev: true /@types/resolve@1.20.6: resolution: {integrity: sha512-A4STmOXPhMUtHH+S6ymgE2GiBSMqf4oTvcQZMcHzokuTLVYzXTB8ttjcgxOVaAp2lGwEdzZ0J+cRbbeevQj1UQ==} /@types/scheduler@0.16.3: resolution: {integrity: sha512-5cJ8CB4yAx7BH1oMvdU0Jh9lrEXyPkar6F9G/ERswkCuvP4KQZfZkSjcMbAICCpQTN4OuZn8tz0HiKv9TGZgrQ==} - dev: true /@types/semver@7.5.8: resolution: {integrity: sha512-I8EUhyrgfLrcTkzV3TSsGyl1tSuPrEDzr0yd5m90UgNxQkyDXULk3b6MlQqTCpZpNtWe1K0hzclnZkTcLBe2UQ==} @@ -9052,7 +9120,6 @@ packages: /csstype@3.1.3: resolution: {integrity: sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw==} - dev: true /current-script-polyfill@1.0.0: resolution: {integrity: sha512-qv8s+G47V6Hq+g2kRE5th+ASzzrL7b6l+tap1DHKK25ZQJv3yIFhH96XaQ7NGL+zRW3t/RDbweJf/dJDe5Z5KA==} @@ -11803,6 +11870,11 @@ packages: whatwg-fetch: 3.6.2 dev: true + /isomorphic-rslog@0.0.6: + resolution: {integrity: sha512-HM0q6XqQ93psDlqvuViNs/Ea3hAyGDkIdVAHlrEocjjAwGrs1fZ+EdQjS9eUPacnYB7Y8SoDdSY3H8p3ce205A==} + engines: {node: '>=14.17.6'} + dev: false + /isomorphic-unfetch@4.0.2: resolution: {integrity: sha512-1Yd+CF/7al18/N2BDbsLBcp6RO3tucSW+jcLq24dqdX5MNbCNTw1z4BsGsp4zNmjr/Izm2cs/cEqZPp4kvWSCA==} dependencies: diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index 58b2f7b75..b5d16bf03 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -3,7 +3,7 @@ packages: - "client" - "crates/binding" - "packages/*" - - "crates/mako/test/compile/auto-code-splitting" + - "e2e/fixtures/module-federation.*" - "e2e/fixtures.umi/react-16" - "e2e/fixtures.umi/config.less.plugins" - "e2e/fixtures.umi/stable-hash"