Skip to content

Commit

Permalink
feat: ensure esm imports exists when mode is production (#1709)
Browse files Browse the repository at this point in the history
* feat: ensure esm imports exists when mode is production
  • Loading branch information
Jinbao1001 authored Dec 5, 2024
1 parent a89b63d commit ea532ba
Show file tree
Hide file tree
Showing 15 changed files with 333 additions and 27 deletions.
21 changes: 21 additions & 0 deletions crates/mako/src/ast/utils.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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:
Expand Down Expand Up @@ -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,
}
}
5 changes: 5 additions & 0 deletions crates/mako/src/build.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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};
Expand Down Expand Up @@ -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,
Expand All @@ -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()
Expand All @@ -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()
Expand Down Expand Up @@ -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,
Expand Down
6 changes: 5 additions & 1 deletion crates/mako/src/compiler.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down Expand Up @@ -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 {
Expand Down
1 change: 1 addition & 0 deletions crates/mako/src/config/experimental.rs
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ pub struct ExperimentalConfig {
#[serde(deserialize_with = "deserialize_detect_loop")]
pub detect_circular_dependence: Option<DetectCircularDependence>,
pub central_ensure: bool,
pub imports_checker: bool,
}

#[derive(Deserialize, Serialize, Debug)]
Expand Down
3 changes: 2 additions & 1 deletion crates/mako/src/config/mako.config.default.json
Original file line number Diff line number Diff line change
Expand Up @@ -73,7 +73,8 @@
"ignores": ["node_modules"],
"graphviz": false
},
"centralEnsure": true
"centralEnsure": true,
"importsChecker": false
},
"useDefineForClassFields": true,
"emitDecoratorMetadata": false,
Expand Down
9 changes: 9 additions & 0 deletions crates/mako/src/module.rs
Original file line number Diff line number Diff line change
Expand Up @@ -33,6 +33,13 @@ pub struct Dependency {
pub span: Option<Span>,
}

#[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 {
Expand Down Expand Up @@ -192,11 +199,13 @@ pub struct ModuleInfo {
pub resolved_resource: Option<ResolverResource>,
/// The transformed source map chain of this module
pub source_map_chain: Vec<Vec<u8>>,
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(),
Expand Down
1 change: 1 addition & 0 deletions crates/mako/src/plugins.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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;
Expand Down
122 changes: 122 additions & 0 deletions crates/mako/src/plugins/imports_checker.rs
Original file line number Diff line number Diff line change
@@ -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<ModuleGraph>,
specifiers: &mut HashSet<String>,
) {
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<String> = 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<Context>, _compiler: &Compiler) -> Result<()> {
let mut modules_imports_map: HashMap<&ModuleId, HashMap<String, HashSet<String>>> =
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<String, HashSet<String>> = 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(())
}
}
68 changes: 68 additions & 0 deletions crates/mako/src/plugins/imports_checker/collect_exports.rs
Original file line number Diff line number Diff line change
@@ -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<String>,
pub exports_star_sources: &'a mut Vec<String>,
}

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());
}
})
}
_ => {}
}
}
}
Loading

0 comments on commit ea532ba

Please sign in to comment.