diff --git a/crates/mako/src/ast/utils.rs b/crates/mako/src/ast/utils.rs index 2ef8ba3be..496e8f73e 100644 --- a/crates/mako/src/ast/utils.rs +++ b/crates/mako/src/ast/utils.rs @@ -4,6 +4,8 @@ use swc_core::ecma::ast::{ MetaPropExpr, MetaPropKind, Module, ModuleItem, }; +use crate::module::{ModuleAst, ModuleSystem}; + pub fn is_remote_or_data(url: &str) -> bool { let lower_url = url.to_lowercase(); // ref: @@ -148,3 +150,22 @@ pub fn require_ensure(source: String) -> Expr { }], ) } + +pub fn get_module_system(ast: &ModuleAst) -> ModuleSystem { + match ast { + ModuleAst::Script(module) => { + let is_esm = module + .ast + .body + .iter() + .any(|s| matches!(s, ModuleItem::ModuleDecl(_))); + if is_esm { + ModuleSystem::ESModule + } else { + ModuleSystem::CommonJS + } + } + crate::module::ModuleAst::Css(_) => ModuleSystem::Custom, + crate::module::ModuleAst::None => ModuleSystem::Custom, + } +} diff --git a/crates/mako/src/build.rs b/crates/mako/src/build.rs index b1af48f53..7c66dd0e7 100644 --- a/crates/mako/src/build.rs +++ b/crates/mako/src/build.rs @@ -13,6 +13,7 @@ use colored::Colorize; use thiserror::Error; 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}; @@ -183,6 +184,7 @@ __mako_require__.loadScript('{}', (e) => e.type === 'load' ? resolve() : reject( let raw = file.get_content_raw(); let info = ModuleInfo { file, + module_system: get_module_system(&ast), ast, external: Some(external_name), is_async, @@ -207,6 +209,7 @@ __mako_require__.loadScript('{}', (e) => e.type === 'load' ? resolve() : reject( let raw = file.get_content_raw(); let info = ModuleInfo { file, + module_system: get_module_system(&ast), ast, raw, ..Default::default() @@ -232,6 +235,7 @@ __mako_require__.loadScript('{}', (e) => e.type === 'load' ? resolve() : reject( ModuleInfo { file, + module_system: get_module_system(&ast), ast, is_ignored: true, ..Default::default() @@ -312,6 +316,7 @@ __mako_require__.loadScript('{}', (e) => e.type === 'load' ? resolve() : reject( let info = ModuleInfo { file, deps, + module_system: get_module_system(&ast), ast, resolved_resource: parent_resource, source_map_chain, diff --git a/crates/mako/src/compiler.rs b/crates/mako/src/compiler.rs index fc124a87e..b4be7457b 100644 --- a/crates/mako/src/compiler.rs +++ b/crates/mako/src/compiler.rs @@ -15,7 +15,7 @@ use tracing::debug; use crate::ast::comments::Comments; use crate::ast::file::win_path; -use crate::config::{Config, ModuleIdStrategy, OutputMode}; +use crate::config::{Config, Mode, ModuleIdStrategy, OutputMode}; use crate::generate::chunk_graph::ChunkGraph; use crate::generate::optimize_chunk::OptimizeChunksInfo; use crate::module_graph::ModuleGraph; @@ -258,6 +258,10 @@ impl Compiler { let mut config = config; + if config.mode == Mode::Production && config.experimental.imports_checker { + plugins.push(Arc::new(plugins::imports_checker::ImportsChecker {})); + } + if let Some(progress) = &config.progress { plugins.push(Arc::new(plugins::progress::ProgressPlugin::new( plugins::progress::ProgressPluginOptions { diff --git a/crates/mako/src/config/experimental.rs b/crates/mako/src/config/experimental.rs index 1c86ec1cc..f0987cfdd 100644 --- a/crates/mako/src/config/experimental.rs +++ b/crates/mako/src/config/experimental.rs @@ -13,6 +13,7 @@ pub struct ExperimentalConfig { #[serde(deserialize_with = "deserialize_detect_loop")] pub detect_circular_dependence: Option, pub central_ensure: bool, + pub imports_checker: 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 be971fdbb..dc0d0ef45 100644 --- a/crates/mako/src/config/mako.config.default.json +++ b/crates/mako/src/config/mako.config.default.json @@ -73,7 +73,8 @@ "ignores": ["node_modules"], "graphviz": false }, - "centralEnsure": true + "centralEnsure": true, + "importsChecker": false }, "useDefineForClassFields": true, "emitDecoratorMetadata": false, diff --git a/crates/mako/src/module.rs b/crates/mako/src/module.rs index 3122ea00f..e23aa9b2c 100644 --- a/crates/mako/src/module.rs +++ b/crates/mako/src/module.rs @@ -33,6 +33,13 @@ pub struct Dependency { pub span: Option, } +#[derive(Debug, Clone, PartialEq, Eq)] +pub enum ModuleSystem { + CommonJS, + ESModule, + Custom, +} + bitflags! { #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Default)] pub struct ResolveTypeFlags: u16 { @@ -192,11 +199,13 @@ pub struct ModuleInfo { pub resolved_resource: Option, /// The transformed source map chain of this module pub source_map_chain: Vec>, + pub module_system: ModuleSystem, } impl Default for ModuleInfo { fn default() -> Self { Self { + module_system: ModuleSystem::CommonJS, ast: ModuleAst::None, file: Default::default(), deps: Default::default(), diff --git a/crates/mako/src/plugins.rs b/crates/mako/src/plugins.rs index b2c545d58..53dc5f267 100644 --- a/crates/mako/src/plugins.rs +++ b/crates/mako/src/plugins.rs @@ -10,6 +10,7 @@ pub mod graphviz; pub mod hmr_runtime; pub mod ignore; pub mod import; +pub mod imports_checker; pub mod invalid_webpack_syntax; pub mod manifest; pub mod minifish; diff --git a/crates/mako/src/plugins/imports_checker.rs b/crates/mako/src/plugins/imports_checker.rs new file mode 100644 index 000000000..9db76193a --- /dev/null +++ b/crates/mako/src/plugins/imports_checker.rs @@ -0,0 +1,122 @@ +mod collect_exports; +mod collect_imports; + +use std::collections::{HashMap, HashSet}; +use std::sync::{Arc, RwLockReadGuard}; + +use anyhow::Result; +use collect_exports::CollectExports; +use collect_imports::CollectImports; +use swc_core::ecma::visit::VisitWith; +use tracing::error; + +use crate::compiler::{Compiler, Context}; +use crate::module::{ModuleId, ModuleSystem}; +use crate::module_graph::ModuleGraph; +use crate::plugin::Plugin; + +pub struct ImportsChecker {} + +fn pick_no_export_specifiers_with_imports_info( + module_id: &ModuleId, + module_graph: &RwLockReadGuard, + specifiers: &mut HashSet, +) { + if !specifiers.is_empty() { + let dep_module = module_graph.get_module(module_id).unwrap(); + if let Some(info) = &dep_module.info { + match info.module_system { + ModuleSystem::ESModule => { + let mut exports_star_sources: Vec = vec![]; + let ast = &info.ast.as_script().unwrap().ast; + ast.visit_with(&mut CollectExports { + specifiers, + exports_star_sources: &mut exports_star_sources, + }); + exports_star_sources.into_iter().for_each(|source| { + if let Some(id) = + module_graph.get_dependency_module_by_source(module_id, &source) + { + pick_no_export_specifiers_with_imports_info( + id, + module_graph, + specifiers, + ); + } + }) + } + ModuleSystem::CommonJS | ModuleSystem::Custom => { + specifiers.clear(); + } + } + } + } +} +impl Plugin for ImportsChecker { + fn name(&self) -> &str { + "imports_checker" + } + fn after_build(&self, context: &Arc, _compiler: &Compiler) -> Result<()> { + let mut modules_imports_map: HashMap<&ModuleId, HashMap>> = + HashMap::new(); + + let module_graph = context.module_graph.read().unwrap(); + let modules = module_graph.modules(); + + for m in modules { + if let Some(info) = &m.info { + if !info.file.is_under_node_modules + && matches!(info.module_system, ModuleSystem::ESModule) + { + // 收集 imports + let ast = &info.ast.as_script().unwrap().ast; + let mut import_specifiers: HashMap> = HashMap::new(); + + ast.visit_with(&mut CollectImports { + imports_specifiers_with_source: &mut import_specifiers, + }); + modules_imports_map.insert(&m.id, import_specifiers); + } + } + } + // 收集 exports + modules_imports_map + .iter_mut() + .for_each(|(module_id, import_specifiers)| { + import_specifiers + .iter_mut() + .for_each(|(source, specifiers)| { + if let Some(dep_module_id) = + module_graph.get_dependency_module_by_source(module_id, source) + { + pick_no_export_specifiers_with_imports_info( + dep_module_id, + &module_graph, + specifiers, + ); + } + }) + }); + let mut should_panic = false; + modules_imports_map + .into_iter() + .for_each(|(module_id, import_specifiers)| { + import_specifiers + .into_iter() + .filter(|(_, specifiers)| !specifiers.is_empty()) + .for_each(|(source, specifiers)| { + should_panic = true; + specifiers.iter().for_each(|specifier| { + error!( + "'{}' is undefined: import from '{}' in '{}'", + specifier, source, module_id.id + ); + }) + }); + }); + if should_panic { + panic!("dependency check error!"); + }; + Ok(()) + } +} diff --git a/crates/mako/src/plugins/imports_checker/collect_exports.rs b/crates/mako/src/plugins/imports_checker/collect_exports.rs new file mode 100644 index 000000000..86af24258 --- /dev/null +++ b/crates/mako/src/plugins/imports_checker/collect_exports.rs @@ -0,0 +1,68 @@ +use std::collections::HashSet; + +use swc_core::ecma::ast::*; +use swc_core::ecma::visit::Visit; + +pub struct CollectExports<'a> { + pub specifiers: &'a mut HashSet, + pub exports_star_sources: &'a mut Vec, +} + +impl<'a> Visit for CollectExports<'a> { + fn visit_module_decl(&mut self, node: &ModuleDecl) { + match &node { + // export const a = 1 + ModuleDecl::ExportDecl(ExportDecl { decl, .. }) => match decl { + Decl::Fn(FnDecl { ident, .. }) => { + self.specifiers.remove(&ident.sym.to_string()); + } + Decl::Class(ClassDecl { ident, .. }) => { + self.specifiers.remove(&ident.sym.to_string()); + } + Decl::Var(box VarDecl { decls, .. }) => decls.iter().for_each(|decl| { + if let Pat::Ident(ident) = &decl.name { + self.specifiers.remove(&ident.sym.to_string()); + } + }), + _ => {} + }, + // export default function + ModuleDecl::ExportDefaultDecl(_) => { + self.specifiers.remove(&"default".to_string()); + } + // export default 1 + ModuleDecl::ExportDefaultExpr(_) => { + self.specifiers.remove(&"default".to_string()); + } + // export * from 'b' + ModuleDecl::ExportAll(all) => { + let source = all.src.value.to_string(); + self.exports_star_sources.push(source); + } + // export {a, b} || export {default as c} from 'd' || export a from 'b' + ModuleDecl::ExportNamed(named) => { + named + .specifiers + .iter() + .for_each(|specifier| match &specifier { + ExportSpecifier::Named(named) => { + if let Some(ModuleExportName::Ident(ident)) = &named.exported { + self.specifiers.remove(&ident.sym.to_string()); + } else if let ModuleExportName::Ident(ident) = &named.orig { + self.specifiers.remove(&ident.sym.to_string()); + } + } + ExportSpecifier::Namespace(name_spacing) => { + if let ModuleExportName::Ident(ident) = &name_spacing.name { + self.specifiers.remove(&ident.sym.to_string()); + } + } + ExportSpecifier::Default(default) => { + self.specifiers.remove(&default.exported.sym.to_string()); + } + }) + } + _ => {} + } + } +} diff --git a/crates/mako/src/plugins/imports_checker/collect_imports.rs b/crates/mako/src/plugins/imports_checker/collect_imports.rs new file mode 100644 index 000000000..146c0840a --- /dev/null +++ b/crates/mako/src/plugins/imports_checker/collect_imports.rs @@ -0,0 +1,67 @@ +use std::collections::{HashMap, HashSet}; + +use swc_core::ecma::ast::*; +use swc_core::ecma::visit::Visit; + +pub struct CollectImports<'a> { + pub imports_specifiers_with_source: &'a mut HashMap>, +} + +impl<'a> Visit for CollectImports<'a> { + fn visit_import_decl(&mut self, node: &ImportDecl) { + let source = node.src.value.to_string(); + if self.imports_specifiers_with_source.get(&source).is_none() { + self.imports_specifiers_with_source + .insert(source.clone(), HashSet::new()); + } + + node.specifiers + .iter() + .for_each(|specifier| match specifier { + ImportSpecifier::Named(named) => { + if let Some(ModuleExportName::Ident(ident)) = &named.imported { + self.imports_specifiers_with_source + .get_mut(&source) + .unwrap() + .insert(ident.sym.to_string()); + } else { + self.imports_specifiers_with_source + .get_mut(&source) + .unwrap() + .insert(named.local.sym.to_string()); + } + } + ImportSpecifier::Default(_) => { + self.imports_specifiers_with_source + .get_mut(&source) + .unwrap() + .insert("default".into()); + } + _ => {} + }) + } + + fn visit_named_export(&mut self, node: &NamedExport) { + if let Some(src) = &node.src { + if self + .imports_specifiers_with_source + .get(src.value.as_str()) + .is_none() + { + self.imports_specifiers_with_source + .insert(src.value.to_string(), HashSet::new()); + } + + node.specifiers.iter().for_each(|specifier| { + if let ExportSpecifier::Named(named) = specifier { + if let ModuleExportName::Ident(ident) = &named.orig { + self.imports_specifiers_with_source + .get_mut(src.value.as_str()) + .unwrap() + .insert(ident.sym.to_string()); + } + } + }) + }; + } +} diff --git a/crates/mako/src/plugins/tree_shaking/module.rs b/crates/mako/src/plugins/tree_shaking/module.rs index 50608d790..4c449b9fb 100644 --- a/crates/mako/src/plugins/tree_shaking/module.rs +++ b/crates/mako/src/plugins/tree_shaking/module.rs @@ -4,7 +4,7 @@ use std::fmt::Display; use swc_core::common::SyntaxContext; use swc_core::ecma::ast::{Module as SwcModule, ModuleItem}; -use crate::module::{Module, ModuleId}; +use crate::module::{Module, ModuleId, ModuleSystem}; use crate::plugins::tree_shaking::statement_graph::{ ExportInfo, ExportInfoMatch, ExportSource, ExportSpecifierInfo, ImportInfo, StatementGraph, StatementId, @@ -34,13 +34,6 @@ impl Display for UsedIdent { } } -#[derive(Debug, PartialEq, Eq)] -pub enum ModuleSystem { - CommonJS, - ESModule, - Custom, -} - #[derive(Debug, Clone)] pub enum UsedExports { All, @@ -285,7 +278,7 @@ impl TreeShakeModule { let mut unresolved_ctxt = SyntaxContext::empty(); // 1. generate statement graph - let mut module_system = ModuleSystem::CommonJS; + let module_system = module_info.module_system.clone(); let stmt_graph = match &module_info.ast { crate::module::ModuleAst::Script(module) => { let is_esm = module @@ -294,21 +287,14 @@ impl TreeShakeModule { .iter() .any(|s| matches!(s, ModuleItem::ModuleDecl(_))); if is_esm { - module_system = ModuleSystem::ESModule; unresolved_ctxt = unresolved_ctxt.apply_mark(module.unresolved_mark); StatementGraph::new(&module.ast, unresolved_ctxt) } else { StatementGraph::empty() } } - crate::module::ModuleAst::Css(_) => { - module_system = ModuleSystem::Custom; - StatementGraph::empty() - } - crate::module::ModuleAst::None => { - module_system = ModuleSystem::Custom; - StatementGraph::empty() - } + crate::module::ModuleAst::Css(_) => StatementGraph::empty(), + crate::module::ModuleAst::None => StatementGraph::empty(), }; let used_exports = if module.is_entry { diff --git a/crates/mako/src/plugins/tree_shaking/shake.rs b/crates/mako/src/plugins/tree_shaking/shake.rs index ecefe1886..57d23a545 100644 --- a/crates/mako/src/plugins/tree_shaking/shake.rs +++ b/crates/mako/src/plugins/tree_shaking/shake.rs @@ -15,9 +15,9 @@ use swc_core::ecma::transforms::base::helpers::{Helpers, HELPERS}; use self::skip_module::skip_module_optimize; use crate::compiler::Context; -use crate::module::{ModuleAst, ModuleId, ModuleType, ResolveType}; +use crate::module::{ModuleAst, ModuleId, ModuleSystem, ModuleType, ResolveType}; use crate::module_graph::ModuleGraph; -use crate::plugins::tree_shaking::module::{AllExports, ModuleSystem, TreeShakeModule}; +use crate::plugins::tree_shaking::module::{AllExports, TreeShakeModule}; use crate::plugins::tree_shaking::shake::module_concatenate::optimize_module_graph; use crate::plugins::tree_shaking::statement_graph::{ExportInfo, ExportSpecifierInfo, ImportInfo}; use crate::plugins::tree_shaking::{module, remove_useless_stmts, statement_graph}; diff --git a/crates/mako/src/plugins/tree_shaking/shake/find_export_source.rs b/crates/mako/src/plugins/tree_shaking/shake/find_export_source.rs index 66913341f..030a92e10 100644 --- a/crates/mako/src/plugins/tree_shaking/shake/find_export_source.rs +++ b/crates/mako/src/plugins/tree_shaking/shake/find_export_source.rs @@ -189,6 +189,7 @@ mod tests { use super::TreeShakeModule; use crate::ast::file::{Content, File, JsContent}; use crate::ast::js_ast::JsAst; + use crate::ast::utils::get_module_system; use crate::compiler::Context; use crate::module::{Module, ModuleAst, ModuleInfo}; use crate::plugins::tree_shaking::shake::skip_module::ReExportSource; @@ -465,13 +466,13 @@ mod tests { }), context.clone(), ); - let ast = JsAst::new(&file, context.clone()).unwrap(); - + let ast = ModuleAst::Script(JsAst::new(&file, context.clone()).unwrap()); let mako_module = Module { id: "test.js".into(), is_entry: false, info: Some(ModuleInfo { - ast: ModuleAst::Script(ast), + module_system: get_module_system(&ast), + ast, file, ..Default::default() }), diff --git a/crates/mako/src/plugins/tree_shaking/shake/module_concatenate.rs b/crates/mako/src/plugins/tree_shaking/shake/module_concatenate.rs index d8c67a059..610ecfb56 100644 --- a/crates/mako/src/plugins/tree_shaking/shake/module_concatenate.rs +++ b/crates/mako/src/plugins/tree_shaking/shake/module_concatenate.rs @@ -21,9 +21,9 @@ use self::concatenate_context::EsmDependantFlags; use self::utils::uniq_module_prefix; use crate::ast::js_ast::JsAst; use crate::compiler::Context; -use crate::module::{Dependency, ImportType, ModuleId, ResolveType}; +use crate::module::{Dependency, ImportType, ModuleId, ModuleSystem, ResolveType}; use crate::module_graph::ModuleGraph; -use crate::plugins::tree_shaking::module::{AllExports, ModuleSystem, TreeShakeModule}; +use crate::plugins::tree_shaking::module::{AllExports, TreeShakeModule}; use crate::plugins::tree_shaking::shake::module_concatenate::concatenate_context::{ ConcatenateContext, RuntimeFlags, }; diff --git a/packages/mako/src/binding.d.ts b/packages/mako/src/binding.d.ts index b99643f94..fa889880d 100644 --- a/packages/mako/src/binding.d.ts +++ b/packages/mako/src/binding.d.ts @@ -5,6 +5,7 @@ export interface JsHooks { name?: string; + enforce?: string; load?: ( filePath: string, ) => Promise<{ content: string; type: 'css' | 'js' } | void> | void; @@ -50,13 +51,24 @@ export interface JsHooks { endTime: number; }; }) => void; + writeBundle?: () => Promise; + watchChanges?: ( + id: string, + change: { event: 'create' | 'delete' | 'update' }, + ) => Promise | void; onGenerateFile?: (path: string, content: Buffer) => Promise; buildStart?: () => Promise; + buildEnd?: () => Promise; resolveId?: ( source: string, importer: string, { isEntry: bool }, ) => Promise<{ id: string }>; + transform?: ( + content: { content: string; type: 'css' | 'js' }, + path: string, + ) => Promise<{ content: string; type: 'css' | 'js' } | void> | void; + transformInclude?: (filePath: string) => Promise | bool; } export interface WriteFile { path: string; @@ -66,6 +78,9 @@ export interface LoadResult { content: string; type: string; } +export interface WatchChangesParams { + event: string; +} export interface ResolveIdResult { id: string; external: boolean | null; @@ -73,6 +88,10 @@ export interface ResolveIdResult { export interface ResolveIdParams { isEntry: boolean; } +export interface TransformResult { + content: string; + type: string; +} export interface BuildParams { root: string; config: { @@ -146,6 +165,7 @@ export interface BuildParams { providers?: Record; publicPath?: string; inlineLimit?: number; + inlineExcludesExtensions?: string[]; targets?: Record; platform?: 'node' | 'browser'; hmr?: false | {};