diff --git a/crates/mako/src/compiler.rs b/crates/mako/src/compiler.rs index 18fab8c70..fc124a87e 100644 --- a/crates/mako/src/compiler.rs +++ b/crates/mako/src/compiler.rs @@ -298,6 +298,10 @@ impl Compiler { plugins.push(Arc::new(plugins::ssu::SUPlus::new())); } + if args.watch && config.experimental.central_ensure { + plugins.push(Arc::new(plugins::central_ensure::CentralChunkEnsure {})); + } + if let Some(minifish_config) = &config._minifish { let inject = if let Some(inject) = &minifish_config.inject { let mut map = HashMap::new(); diff --git a/crates/mako/src/config/experimental.rs b/crates/mako/src/config/experimental.rs index 7b65f40fa..1268e3678 100644 --- a/crates/mako/src/config/experimental.rs +++ b/crates/mako/src/config/experimental.rs @@ -10,6 +10,7 @@ pub struct ExperimentalConfig { pub magic_comment: bool, #[serde(deserialize_with = "deserialize_detect_loop")] pub detect_circular_dependence: Option, + pub central_ensure: bool, } #[derive(Deserialize, Serialize, Debug)] diff --git a/crates/mako/src/config/mako.config.default.json b/crates/mako/src/config/mako.config.default.json index d7c50c566..eaec1e1a1 100644 --- a/crates/mako/src/config/mako.config.default.json +++ b/crates/mako/src/config/mako.config.default.json @@ -71,7 +71,8 @@ "detectCircularDependence": { "ignores": ["node_modules"], "graphviz": false - } + }, + "centralEnsure": true }, "useDefineForClassFields": true, "emitDecoratorMetadata": false, diff --git a/crates/mako/src/generate/chunk_graph.rs b/crates/mako/src/generate/chunk_graph.rs index 208a66d63..33e505dce 100644 --- a/crates/mako/src/generate/chunk_graph.rs +++ b/crates/mako/src/generate/chunk_graph.rs @@ -63,6 +63,12 @@ impl ChunkGraph { self.graph.node_weights().find(|c| c.has_module(module_id)) } + pub fn get_async_chunk_for_module(&self, module_id: &ModuleId) -> Option<&Chunk> { + self.graph + .node_weights() + .find(|c| c.has_module(module_id) && matches!(c.chunk_type, ChunkType::Async)) + } + // pub fn get_chunk_by_id(&self, id: &String) -> Option<&Chunk> { // self.graph.node_weights().find(|c| c.id.id.eq(id)) // } diff --git a/crates/mako/src/generate/hmr.rs b/crates/mako/src/generate/hmr.rs index 43f917587..f531eac05 100644 --- a/crates/mako/src/generate/hmr.rs +++ b/crates/mako/src/generate/hmr.rs @@ -22,10 +22,18 @@ impl Compiler { let module_graph = &self.context.module_graph.read().unwrap(); let (js_stmts, _) = modules_to_js_stmts(module_ids, module_graph, &self.context).unwrap(); let content = include_str!("../runtime/runtime_hmr.js").to_string(); - let content = content.replace("__CHUNK_ID__", &chunk.id.id).replace( - "__runtime_code__", - &format!("runtime._h='{}';", current_hash), - ); + + let runtime_code_snippets = [ + format!("runtime._h='{}';", current_hash), + self.context + .plugin_driver + .hmr_runtime_update_code(&self.context)?, + ]; + + let content = content + .replace("__CHUNK_ID__", &chunk.id.id) + .replace("__runtime_code__", &runtime_code_snippets.join("\n")); + let mut js_ast = JsAst::build(filename, content.as_str(), self.context.clone()) /* safe */ .unwrap(); diff --git a/crates/mako/src/generate/transform.rs b/crates/mako/src/generate/transform.rs index 972740311..93eab1617 100644 --- a/crates/mako/src/generate/transform.rs +++ b/crates/mako/src/generate/transform.rs @@ -27,7 +27,7 @@ use crate::utils::thread_pool; use crate::visitors::async_module::{mark_async, AsyncModule}; use crate::visitors::common_js::common_js; use crate::visitors::css_imports::CSSImports; -use crate::visitors::dep_replacer::{DepReplacer, DependenciesToReplace}; +use crate::visitors::dep_replacer::{DepReplacer, DependenciesToReplace, ResolvedReplaceInfo}; use crate::visitors::dynamic_import::DynamicImport; use crate::visitors::mako_require::MakoRequire; use crate::visitors::meta_url_replacer::MetaUrlReplacer; @@ -84,38 +84,53 @@ pub fn transform_modules_in_thread( thread_pool::spawn(move || { let module_graph = context.module_graph.read().unwrap(); let deps = module_graph.get_dependencies(&module_id); - let mut resolved_deps: HashMap = deps - .into_iter() - .map(|(id, dep)| { - ( - dep.source.clone(), - ( - match &dep.resolve_type { - ResolveType::Worker(import_options) => { - let chunk_id = match import_options.get_chunk_name() { - Some(chunk_name) => { - generate_module_id(chunk_name, &context) - } - None => id.generate(&context), - }; - let chunk_graph = context.chunk_graph.read().unwrap(); - chunk_graph.chunk(&chunk_id.into()).unwrap().filename() - } - ResolveType::DynamicImport(import_options) => { - match import_options.get_chunk_name() { - Some(chunk_name) => { - generate_module_id(chunk_name, &context) - } - None => id.generate(&context), - } - } - _ => id.generate(&context), - }, - id.id.clone(), - ), - ) - }) - .collect(); + let mut resolved_deps: HashMap = Default::default(); + + deps.into_iter().for_each(|(id, dep)| { + let replace_info = match &dep.resolve_type { + ResolveType::Worker(import_options) => { + let chunk_id = match import_options.get_chunk_name() { + Some(chunk_name) => generate_module_id(chunk_name, &context), + None => id.generate(&context), + }; + let chunk_graph = context.chunk_graph.read().unwrap(); + let chunk_name = chunk_graph.chunk(&chunk_id.into()).unwrap().filename(); + + ResolvedReplaceInfo { + chunk_id: None, + to_replace_source: chunk_name, + resolved_module_id: id.clone(), + } + } + ResolveType::DynamicImport(import_options) => { + let chunk_id = Some(match import_options.get_chunk_name() { + Some(chunk_name) => generate_module_id(chunk_name, &context), + None => id.generate(&context), + }); + + ResolvedReplaceInfo { + chunk_id, + to_replace_source: id.generate(&context), + resolved_module_id: id.clone(), + } + } + _ => ResolvedReplaceInfo { + chunk_id: None, + to_replace_source: id.generate(&context), + resolved_module_id: id.clone(), + }, + }; + + resolved_deps + .entry(dep.source.clone()) + .and_modify(|info: &mut ResolvedReplaceInfo| { + match (&replace_info.chunk_id, &info.chunk_id) { + (None, _) => {} + (Some(id), _) => info.chunk_id = Some(id.clone()), + } + }) + .or_insert(replace_info); + }); insert_swc_helper_replace(&mut resolved_deps, &context); let module = module_graph.get_module(&module_id).unwrap(); let info = module.info.as_ref().unwrap(); @@ -162,10 +177,20 @@ pub fn transform_modules_in_thread( Ok(()) } -fn insert_swc_helper_replace(map: &mut HashMap, context: &Arc) { +fn insert_swc_helper_replace( + map: &mut HashMap, + context: &Arc, +) { SWC_HELPERS.into_iter().for_each(|h| { let m_id: ModuleId = h.to_string().into(); - map.insert(m_id.id.clone(), (m_id.generate(context), h.to_string())); + map.insert( + m_id.id.clone(), + ResolvedReplaceInfo { + chunk_id: None, + to_replace_source: m_id.generate(context), + resolved_module_id: m_id, + }, + ); }); } diff --git a/crates/mako/src/plugin.rs b/crates/mako/src/plugin.rs index 6e794a0db..fe02b88fe 100644 --- a/crates/mako/src/plugin.rs +++ b/crates/mako/src/plugin.rs @@ -159,6 +159,10 @@ pub trait Plugin: Any + Send + Sync { Ok(Vec::new()) } + fn hmr_runtime_updates(&self, _context: &Arc) -> Result> { + Ok(Vec::new()) + } + fn optimize_module_graph( &self, _module_graph: &mut ModuleGraph, @@ -373,6 +377,14 @@ impl PluginDriver { Ok(plugins.join("\n")) } + pub fn hmr_runtime_update_code(&self, context: &Arc) -> Result { + let mut plugins = Vec::new(); + for plugin in &self.plugins { + plugins.extend(plugin.hmr_runtime_updates(context)?); + } + Ok(plugins.join("\n")) + } + pub fn optimize_module_graph( &self, module_graph: &mut ModuleGraph, diff --git a/crates/mako/src/plugins.rs b/crates/mako/src/plugins.rs index add2cc46e..b2c545d58 100644 --- a/crates/mako/src/plugins.rs +++ b/crates/mako/src/plugins.rs @@ -1,5 +1,6 @@ pub mod async_runtime; pub mod bundless_compiler; +pub mod central_ensure; pub mod context_module; pub mod copy; pub mod detect_circular_dependence; diff --git a/crates/mako/src/plugins/bundless_compiler.rs b/crates/mako/src/plugins/bundless_compiler.rs index 68c10be36..e93aa3412 100644 --- a/crates/mako/src/plugins/bundless_compiler.rs +++ b/crates/mako/src/plugins/bundless_compiler.rs @@ -24,7 +24,7 @@ use crate::compiler::{Args, Context}; use crate::config::Config; use crate::module::{ModuleAst, ModuleId}; use crate::plugin::{Plugin, PluginTransformJsParam}; -use crate::visitors::dep_replacer::{DepReplacer, DependenciesToReplace}; +use crate::visitors::dep_replacer::{DepReplacer, DependenciesToReplace, ResolvedReplaceInfo}; use crate::visitors::dynamic_import::DynamicImport; pub struct BundlessCompiler { @@ -80,11 +80,18 @@ impl BundlessCompiler { } }; - Ok((dep.source.clone(), (replacement.clone(), replacement))) + Ok(( + dep.source.clone(), + ResolvedReplaceInfo { + chunk_id: None, + to_replace_source: replacement, + resolved_module_id: id.clone(), + }, + )) }) .collect::>>(); - let resolved_deps: HashMap = + let resolved_deps: HashMap = resolved_deps?.into_iter().collect(); drop(module_graph); diff --git a/crates/mako/src/plugins/central_ensure.rs b/crates/mako/src/plugins/central_ensure.rs new file mode 100644 index 000000000..2f9d8ed49 --- /dev/null +++ b/crates/mako/src/plugins/central_ensure.rs @@ -0,0 +1,90 @@ +use std::collections::BTreeMap; +use std::sync::Arc; + +use anyhow::anyhow; + +use crate::compiler::Context; +use crate::module::generate_module_id; +use crate::plugin::Plugin; + +pub struct CentralChunkEnsure {} + +pub fn module_ensure_map(context: &Arc) -> anyhow::Result>> { + let mg = context + .module_graph + .read() + .map_err(|e| anyhow!("Read_Module_Graph_error:\n{:?}", e))?; + let cg = context + .chunk_graph + .read() + .map_err(|e| anyhow!("Read_Chunk_Graph_error:\n{:?}", e))?; + + let mut chunk_async_map: BTreeMap> = Default::default(); + + mg.modules().iter().for_each(|module| { + let be_dynamic_imported = mg + .get_dependents(&module.id) + .iter() + .any(|(_, dep)| dep.resolve_type.is_dynamic_esm()); + + if be_dynamic_imported { + cg.get_async_chunk_for_module(&module.id) + .iter() + .for_each(|chunk| { + let deps_chunks = cg + .installable_descendants_chunk(&chunk.id) + .iter() + .map(|chunk_id| chunk_id.generate(context)) + .collect::>(); + + chunk_async_map.insert(generate_module_id(&module.id.id, context), deps_chunks); + }); + } + }); + + Ok(chunk_async_map) +} + +impl Plugin for CentralChunkEnsure { + fn name(&self) -> &str { + "dev_ensure2" + } + fn runtime_plugins(&self, context: &Arc) -> anyhow::Result> { + let chunk_async_map = module_ensure_map(context)?; + + // TODO: compress the map to reduce duplicated chunk ids + let ensure_map = serde_json::to_string(&chunk_async_map)?; + + let runtime = format!( + r#" +(function(){{ + let map = {ensure_map}; + requireModule.updateEnsure2Map = function(newMapping) {{ + map = newMapping; + }} + requireModule.ensure2 = function(chunkId){{ + let toEnsure = map[chunkId]; + if (toEnsure) {{ + return Promise.all(toEnsure.map(function(c){{ return requireModule.ensure(c); }})) + }}else{{ + return Promise.resolve(); + }} + }}; +}})(); +"# + ); + + Ok(vec![runtime]) + } + + fn hmr_runtime_updates(&self, _context: &Arc) -> anyhow::Result> { + let map = module_ensure_map(_context)?; + + let update_mapping = format!( + "runtime.updateEnsure2Map({});", + serde_json::to_string(&map)? + ); + + Ok(vec![update_mapping]) + } +} diff --git a/crates/mako/src/visitors/dep_replacer.rs b/crates/mako/src/visitors/dep_replacer.rs index 3e44434e1..47167f6e7 100644 --- a/crates/mako/src/visitors/dep_replacer.rs +++ b/crates/mako/src/visitors/dep_replacer.rs @@ -23,14 +23,18 @@ pub struct DepReplacer<'a> { pub unresolved_mark: Mark, } -type ResolvedModuleId = String; -type ResolvedModulePath = String; +#[derive(Debug, Clone)] +pub struct ResolvedReplaceInfo { + pub chunk_id: Option, + pub to_replace_source: String, + pub resolved_module_id: ModuleId, +} #[derive(Debug, Clone)] pub struct DependenciesToReplace { // resolved stores the "source" maps to (generate_id, raw_module_id) // e.g. "react" => ("hashed_id", "/abs/to/react/index.js") - pub resolved: HashMap, + pub resolved: HashMap, pub missing: HashMap, } @@ -124,8 +128,9 @@ impl VisitMut for DepReplacer<'_> { // css // TODO: add testcases for this let is_replaceable_css = - if let Some((_, raw_id)) = self.to_replace.resolved.get(&source_string) { - let (path, _search, query, _) = parse_path(raw_id).unwrap(); + if let Some(replace_info) = self.to_replace.resolved.get(&source_string) { + let (path, _search, query, _) = + parse_path(&replace_info.resolved_module_id.id).unwrap(); // when inline_css is enabled // css is parsed as js modules self.context.config.inline_css.is_none() @@ -194,7 +199,7 @@ impl VisitMut for DepReplacer<'_> { impl DepReplacer<'_> { fn replace_source(&mut self, source: &mut Str) { if let Some(replacement) = self.to_replace.resolved.get(&source.value.to_string()) { - let module_id = replacement.0.clone(); + let module_id = replacement.to_replace_source.clone(); let span = source.span; *source = Str::from(module_id); source.span = span; @@ -247,7 +252,7 @@ mod tests { use swc_core::common::GLOBALS; use swc_core::ecma::visit::VisitMutWith; - use super::{DepReplacer, DependenciesToReplace, ResolvedModuleId, ResolvedModulePath}; + use super::{DepReplacer, DependenciesToReplace, ResolvedReplaceInfo}; use crate::ast::tests::TestUtils; use crate::module::{Dependency, ImportType, ModuleId, ResolveType}; @@ -339,17 +344,14 @@ try { ); } - fn build_resolved( - key: &str, - module_id: &str, - ) -> HashMap { + fn build_resolved(key: &str, module_id: &str) -> HashMap { hashmap! { key.to_string() => - ( - - module_id.to_string(), - "".to_string() - ) + ResolvedReplaceInfo { + chunk_id: None, + to_replace_source: module_id.into(), + resolved_module_id: "".into(), + } } } @@ -367,7 +369,7 @@ try { fn run( js_code: &str, - resolved: HashMap, + resolved: HashMap, missing: HashMap, ) -> String { let mut test_utils = TestUtils::gen_js_ast(js_code); diff --git a/crates/mako/src/visitors/dynamic_import.rs b/crates/mako/src/visitors/dynamic_import.rs index 28d30f92b..b8cd66ac9 100644 --- a/crates/mako/src/visitors/dynamic_import.rs +++ b/crates/mako/src/visitors/dynamic_import.rs @@ -9,12 +9,11 @@ use swc_core::ecma::utils::{ }; use swc_core::ecma::visit::{VisitMut, VisitMutWith}; -use super::dep_replacer::miss_throw_stmt; +use super::dep_replacer::{miss_throw_stmt, ResolvedReplaceInfo}; use crate::ast::utils::{is_dynamic_import, promise_all, require_ensure}; use crate::ast::DUMMY_CTXT; use crate::compiler::Context; use crate::generate::chunk::ChunkId; -use crate::module::generate_module_id; use crate::visitors::dep_replacer::DependenciesToReplace; pub struct DynamicImport<'a> { @@ -48,14 +47,16 @@ impl<'a> VisitMut for DynamicImport<'a> { .position(|module_item| !module_item.directive_continue()) .unwrap(); - let (id, _) = self + let interop_replace = self .dep_to_replace .resolved .get("@swc/helpers/_/_interop_require_wildcard") .unwrap(); - let require_interop = quote_ident!("__mako_require__") - .as_call(DUMMY_SP, vec![quote_str!(id.clone()).as_arg()]); + let require_interop = quote_ident!("__mako_require__").as_call( + DUMMY_SP, + vec![quote_str!(interop_replace.to_replace_source.clone()).as_arg()], + ); let stmt: Stmt = Expr::Member(MemberExpr { span: DUMMY_SP, @@ -106,69 +107,26 @@ impl<'a> VisitMut for DynamicImport<'a> { // it must be resolved, so unwrap is safe here. .unwrap(); - let chunk_ids = { - let chunk_id: ChunkId = resolved_info.0.as_str().into(); - let chunk_graph = &self.context.chunk_graph.read().unwrap(); - let chunk = chunk_graph.chunk(&chunk_id); - let chunk_ids = match chunk { - Some(chunk) => { - [ - chunk_graph.sync_dependencies_chunk(&chunk.id), - vec![chunk.id.clone()], - ] - .concat() - .iter() - .filter_map(|chunk_id| { - // skip empty chunk because it will not be generated - if chunk_graph - .chunk(chunk_id) - .is_some_and(|c| !c.modules.is_empty()) - { - Some(chunk_id.id.clone()) - } else { - None - } - }) - .collect::>() - } - // None means the original chunk has been optimized to entry chunk - None => vec![], - }; - chunk_ids - }; - self.changed = true; - // build new expr - // e.g. - // Promise.all([ require.ensure("id") ]).then(require.bind(require, "id")) - // Promise.all([ require.ensure("d1"), require.ensure("id)]).then(require.bind(require, "id")) + + let generated_module_id = resolved_info.to_replace_source.clone(); *expr = { - let to_ensure_elems = chunk_ids - .iter() - .map(|c_id| { - Some(ExprOrSpread { - spread: None, - expr: Box::new(require_ensure(c_id.clone())), - }) - }) - .collect::>(); - let load_promise = promise_all(ExprOrSpread { - spread: None, - expr: ArrayLit { - span: DUMMY_SP, - elems: to_ensure_elems, - } - .into(), - }); + // let load_promise = self.make_load_promise(&chunk_ids); - let require_module = generate_module_id(&resolved_info.1, &self.context); + let load_promise = if self.context.args.watch + && self.context.config.experimental.central_ensure + { + self.central_ensure(&generated_module_id) + } else { + self.inline_ensure(resolved_info, &self.context) + }; let lazy_require_call = member_expr!(DUMMY_CTXT, DUMMY_SP, __mako_require__.bind).as_call( DUMMY_SP, vec![ quote_ident!("__mako_require__").as_arg(), - quote_str!(require_module.as_str()).as_arg(), + quote_str!(generated_module_id).as_arg(), ], ); let dr_call = member_expr!(DUMMY_CTXT, DUMMY_SP, __mako_require__.dr) @@ -187,6 +145,69 @@ impl<'a> VisitMut for DynamicImport<'a> { } } +impl DynamicImport<'_> { + // require.ensure2("id").then(require.bind(require,"id")) + fn central_ensure(&self, module_id: &str) -> Expr { + member_expr!(DUMMY_CTXT, DUMMY_SP, __mako_require__.ensure2) + .as_call(DUMMY_SP, vec![quote_str!(module_id).as_arg()]) + } + + // build the Promise.all([...]) part + // Promise.all([ require.ensure("id") ]).then(require.bind(require, "id")) + // Promise.all([ require.ensure("d1"), require.ensure("id)]).then(require.bind(require, "id")) + fn inline_ensure(&self, replace_info: &ResolvedReplaceInfo, context: &Arc) -> Expr { + let chunk_graph = context.chunk_graph.read().unwrap(); + + let init_chunk_id: ChunkId = replace_info.chunk_id.as_ref().unwrap().clone().into(); + let chunk_ids = { + let chunk = chunk_graph.chunk(&init_chunk_id); + let chunk_ids = match chunk { + Some(chunk) => { + [ + chunk_graph.sync_dependencies_chunk(&chunk.id), + vec![chunk.id.clone()], + ] + .concat() + .iter() + .filter_map(|chunk_id| { + // skip empty chunk because it will not be generated + if chunk_graph + .chunk(chunk_id) + .is_some_and(|c| !c.modules.is_empty()) + { + Some(chunk_id.id.clone()) + } else { + None + } + }) + .collect::>() + } + // None means the original chunk has been optimized to entry chunk + None => vec![], + }; + chunk_ids + }; + + let to_ensure_elems = chunk_ids + .iter() + .map(|c_id| { + Some(ExprOrSpread { + spread: None, + expr: Box::new(require_ensure(c_id.clone())), + }) + }) + .collect::>(); + promise_all(ExprOrSpread { + spread: None, + expr: ArrayLit { + span: DUMMY_SP, + elems: to_ensure_elems, + } + .into(), + }) + } +} + #[cfg(test)] mod tests { use std::collections::HashMap; @@ -197,7 +218,7 @@ mod tests { use super::DynamicImport; use crate::ast::tests::TestUtils; use crate::generate::chunk::{Chunk, ChunkType}; - use crate::visitors::dep_replacer::DependenciesToReplace; + use crate::visitors::dep_replacer::{DependenciesToReplace, ResolvedReplaceInfo}; // TODO: add nested chunk test #[test] @@ -226,9 +247,16 @@ Promise.all([ let dep_to_replace = DependenciesToReplace { resolved: maplit::hashmap! { - "@swc/helpers/_/_interop_require_wildcard".to_string() => - ("hashed_helper".to_string(), "dummy".into()), - "foo".to_string() => ("foo".to_string(), "foo".to_string()) + "@swc/helpers/_/_interop_require_wildcard".to_string() => ResolvedReplaceInfo { + chunk_id: None, + to_replace_source: "hashed_helper".to_string(), + resolved_module_id:"dummy".into() + }, + "foo".to_string() => ResolvedReplaceInfo { + chunk_id: Some("foo".into()), + to_replace_source: "foo".into(), + resolved_module_id: "foo".into() + } }, missing: HashMap::new(), };