diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c941f67ba..05bec3386 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -115,6 +115,8 @@ jobs: - name: LS run: ls -l ./packages/mako - name: Test E2E + env: + RUST_BACKTRACE: full run: pnpm ${{ matrix.script }} lint: diff --git a/crates/mako/src/module_graph.rs b/crates/mako/src/module_graph.rs index 550f2d983..cdff39630 100644 --- a/crates/mako/src/module_graph.rs +++ b/crates/mako/src/module_graph.rs @@ -9,7 +9,7 @@ use petgraph::visit::IntoEdgeReferences; use petgraph::Direction; use tracing::{debug, warn}; -use crate::module::{Dependencies, Dependency, Module, ModuleId, ResolveType}; +use crate::module::{Dependencies, Dependency, Module, ModuleId}; #[derive(Debug)] pub struct ModuleGraph { @@ -304,29 +304,14 @@ impl ModuleGraph { targets } - pub fn remove_dependency_module_by_source_and_resolve_type( - &mut self, - module_id: &ModuleId, - source: &String, - resolve_type: ResolveType, - ) { + pub fn rewrite_dependency(&mut self, module_id: &ModuleId, deps: Vec<(ModuleId, Dependency)>) { let mut edges = self.get_edges(module_id, Direction::Outgoing); - while let Some((edge_index, _node_index)) = edges.next(&self.graph) { - let dependencies = self.graph.edge_weight_mut(edge_index).unwrap(); - - if let Some(to_del_dep) = dependencies - .iter() - .position(|dep| *source == dep.source && dep.resolve_type.same_enum(&resolve_type)) - { - dependencies.take(&dependencies.iter().nth(to_del_dep).unwrap().clone()); - - if dependencies.is_empty() { - self.graph.remove_edge(edge_index); - } - return; - } + self.graph.remove_edge(edge_index); } + deps.into_iter().for_each(|(m, d)| { + self.add_dependency(module_id, &m, d); + }); } pub fn get_dependency_module_by_source( diff --git a/crates/mako/src/plugins/tree_shaking.rs b/crates/mako/src/plugins/tree_shaking.rs index ef7a64b1a..86c289e83 100644 --- a/crates/mako/src/plugins/tree_shaking.rs +++ b/crates/mako/src/plugins/tree_shaking.rs @@ -9,6 +9,7 @@ use crate::compiler::Context; use crate::module_graph::ModuleGraph; use crate::plugin::{Plugin, PluginTransformJsParam}; +mod collect_explicit_prop; mod module; mod module_side_effects_flag; mod remove_useless_stmts; diff --git a/crates/mako/src/plugins/tree_shaking/collect_explicit_prop.rs b/crates/mako/src/plugins/tree_shaking/collect_explicit_prop.rs new file mode 100644 index 000000000..9f1ca985a --- /dev/null +++ b/crates/mako/src/plugins/tree_shaking/collect_explicit_prop.rs @@ -0,0 +1,222 @@ +use std::collections::{HashMap, HashSet}; + +use swc_core::ecma::ast::{ComputedPropName, Id, Ident, Lit, MemberExpr, MemberProp}; +use swc_core::ecma::visit::{Visit, VisitWith}; + +#[derive(Debug)] +pub struct IdExplicitPropAccessCollector { + to_detected: HashSet, + accessed_by_explicit_prop_count: HashMap, + ident_accessed_count: HashMap, + accessed_by: HashMap>, +} + +impl IdExplicitPropAccessCollector { + pub(crate) fn new(ids: HashSet) -> Self { + Self { + to_detected: ids, + accessed_by_explicit_prop_count: Default::default(), + ident_accessed_count: Default::default(), + accessed_by: Default::default(), + } + } + pub(crate) fn explicit_accessed_props(mut self) -> HashMap> { + self.to_detected + .iter() + .filter_map(|id| { + let member_prop_accessed = self.accessed_by_explicit_prop_count.get(id); + let ident_accessed = self.ident_accessed_count.get(id); + + match (member_prop_accessed, ident_accessed) { + // all ident are accessed explicitly, so there is member expr there is a name + // ident, and at last plus the extra ident in import decl, that's 1 comes from. + (Some(m), Some(i)) if (i - m) == 1 => { + let mut accessed_by = Vec::from_iter(self.accessed_by.remove(id).unwrap()); + accessed_by.sort(); + + let str_key = format!("{}#{}", id.0, id.1.as_u32()); + + Some((str_key, accessed_by)) + } + // Some un-explicitly access e.g: obj[foo] + _ => None, + } + }) + .collect() + } + + fn increase_explicit_prop_accessed_count(&mut self, id: Id) { + self.accessed_by_explicit_prop_count + .entry(id.clone()) + .and_modify(|c| { + *c += 1; + }) + .or_insert(1); + } + + fn insert_member_accessed_by(&mut self, id: Id, prop: &str) { + self.increase_explicit_prop_accessed_count(id.clone()); + self.accessed_by + .entry(id) + .and_modify(|accessed| { + accessed.insert(prop.to_string()); + }) + .or_insert(HashSet::from([prop.to_string()])); + } +} + +impl Visit for IdExplicitPropAccessCollector { + fn visit_ident(&mut self, n: &Ident) { + let id = n.to_id(); + + if self.to_detected.contains(&id) { + self.ident_accessed_count + .entry(id) + .and_modify(|c| { + *c += 1; + }) + .or_insert(1); + } + } + + fn visit_member_expr(&mut self, n: &MemberExpr) { + if let Some(obj_ident) = n.obj.as_ident() { + let id = obj_ident.to_id(); + + if self.to_detected.contains(&id) { + match &n.prop { + MemberProp::Ident(prop_ident) => { + self.insert_member_accessed_by(id, prop_ident.sym.as_ref()); + } + MemberProp::PrivateName(_) => {} + MemberProp::Computed(ComputedPropName { expr, .. }) => { + if let Some(lit) = expr.as_lit() + && let Lit::Str(str) = lit + { + let visited_by = str.value.to_string(); + self.insert_member_accessed_by(id, &visited_by) + } + } + } + } + } + + n.visit_children_with(self); + } +} + +#[cfg(test)] +mod tests { + use maplit::hashset; + + use super::*; + use crate::ast::tests::TestUtils; + + #[test] + fn test_no_prop() { + let fields = extract_explicit_fields( + r#" + import * as foo from "./foo.js"; + console.log(foo) + "#, + ); + + assert_eq!(fields, None); + } + #[test] + fn test_no_access() { + let fields = extract_explicit_fields( + r#" + import * as foo from "./foo.js"; + "#, + ); + + assert_eq!(fields, None); + } + + #[test] + fn test_computed_prop() { + let fields = extract_explicit_fields( + r#" + import * as foo from "./foo.js"; + foo['f' + 'o' + 'o'] + "#, + ); + + assert_eq!(fields, None); + } + + #[test] + fn test_simple_explicit_prop() { + let fields = extract_explicit_fields( + r#" + import * as foo from "./foo.js"; + foo.x; + foo.y; + "#, + ); + + assert_eq!(fields.unwrap(), vec!["x".to_string(), "y".to_string()]); + } + + #[test] + fn test_nest_prop_explicit_prop() { + let fields = extract_explicit_fields( + r#" + import * as foo from "./foo.js"; + foo.x.z[foo.y] + "#, + ); + + assert_eq!(fields.unwrap(), vec!["x".to_string(), "y".to_string()]); + } + + #[test] + fn test_string_literal_prop_explicit() { + let fields = extract_explicit_fields( + r#" + import * as foo from "./foo.js"; + foo['x'] + "#, + ); + + assert_eq!(fields.unwrap(), vec!["x".to_string()]); + } + + #[test] + fn test_num_literal_prop_not_explicit() { + let fields = extract_explicit_fields( + r#" + import * as foo from "./foo.js"; + foo[1] + "#, + ); + + assert_eq!(fields, None); + } + + fn extract_explicit_fields(code: &str) -> Option> { + let tu = TestUtils::gen_js_ast(code); + + let id = namespace_id(&tu); + let str = format!("{}#{}", id.0, id.1.as_u32()); + + let mut v = IdExplicitPropAccessCollector::new(hashset! { id }); + tu.ast.js().ast.visit_with(&mut v); + + v.explicit_accessed_props().remove(&str) + } + + fn namespace_id(tu: &TestUtils) -> Id { + tu.ast.js().ast.body[0] + .as_module_decl() + .unwrap() + .as_import() + .unwrap() + .specifiers[0] + .as_namespace() + .unwrap() + .local + .to_id() + } +} diff --git a/crates/mako/src/plugins/tree_shaking/remove_useless_stmts.rs b/crates/mako/src/plugins/tree_shaking/remove_useless_stmts.rs index ac7da5850..dde449438 100644 --- a/crates/mako/src/plugins/tree_shaking/remove_useless_stmts.rs +++ b/crates/mako/src/plugins/tree_shaking/remove_useless_stmts.rs @@ -1,9 +1,16 @@ +use std::collections::HashSet; + +use swc_core::common::util::take::Take; +use swc_core::common::SyntaxContext; use swc_core::ecma::ast::{ - Decl, ExportDecl, ExportSpecifier, ImportDecl, ImportSpecifier, Module as SwcModule, - ModuleExportName, + Decl, ExportDecl, ExportSpecifier, Id, ImportDecl, ImportSpecifier, Module as SwcModule, + Module, ModuleExportName, }; +use swc_core::ecma::transforms::compat::es2015::destructuring; +use swc_core::ecma::transforms::compat::es2018::object_rest_spread; use swc_core::ecma::visit::{VisitMut, VisitMutWith, VisitWith}; +use super::collect_explicit_prop::IdExplicitPropAccessCollector; use crate::plugins::tree_shaking::module::TreeShakeModule; use crate::plugins::tree_shaking::statement_graph::analyze_imports_and_exports::{ analyze_imports_and_exports, StatementInfo, @@ -105,10 +112,10 @@ pub fn remove_useless_stmts( // remove from the end to the start stmts_to_remove.reverse(); - for stmt in stmts_to_remove { swc_module.body.remove(stmt); } + optimize_import_namespace(&mut used_import_infos, swc_module); (used_import_infos, used_export_from_infos) } @@ -231,6 +238,72 @@ impl VisitMut for UselessExportStmtRemover { } } +fn optimize_import_namespace(import_infos: &mut [ImportInfo], module: &mut Module) { + let namespaces = import_infos + .iter() + .filter_map(|import_info| { + let ns = import_info + .specifiers + .iter() + .filter_map(|sp| match sp { + ImportSpecifierInfo::Namespace(ns) => Some(ns.clone()), + _ => None, + }) + .collect::>(); + if ns.is_empty() { + None + } else { + Some(ns) + } + }) + .flatten() + .collect::>(); + + let ids = namespaces + .iter() + .map(|ns| { + let (sym, ctxt) = ns.rsplit_once('#').unwrap(); + (sym.into(), SyntaxContext::from_u32(ctxt.parse().unwrap())) + }) + .collect::>(); + + if !ids.is_empty() { + let mut v = IdExplicitPropAccessCollector::new(ids); + let mut shadow = module.clone(); + + shadow.visit_mut_with(&mut object_rest_spread(Default::default())); + shadow.visit_mut_with(&mut destructuring(Default::default())); + shadow.visit_with(&mut v); + + let explicit_prop_accessed_ids = v.explicit_accessed_props(); + + import_infos.iter_mut().for_each(|ii| { + ii.specifiers = ii + .specifiers + .take() + .into_iter() + .flat_map(|specifier_info| { + if let ImportSpecifierInfo::Namespace(ref ns) = specifier_info { + if let Some(visited_fields) = explicit_prop_accessed_ids.get(ns) { + return visited_fields + .iter() + .map(|v| { + let imported_name = format!("{v}#0"); + ImportSpecifierInfo::Named { + imported: Some(imported_name.clone()), + local: imported_name, + } + }) + .collect::>(); + } + } + vec![specifier_info] + }) + .collect::>(); + }) + } +} + #[cfg(test)] mod tests { use super::*; diff --git a/crates/mako/src/plugins/tree_shaking/shake.rs b/crates/mako/src/plugins/tree_shaking/shake.rs index 05b8916b1..4f236f4ae 100644 --- a/crates/mako/src/plugins/tree_shaking/shake.rs +++ b/crates/mako/src/plugins/tree_shaking/shake.rs @@ -11,6 +11,7 @@ use anyhow::Result; use rayon::prelude::*; use swc_core::common::util::take::Take; use swc_core::common::GLOBALS; +use swc_core::ecma::transforms::base::helpers::{Helpers, HELPERS}; use self::skip_module::skip_module_optimize; use crate::compiler::Context; @@ -134,7 +135,7 @@ pub fn optimize_modules(module_graph: &mut ModuleGraph, context: &Arc) let mut current_index: usize = 0; let len = tree_shake_modules_ids.len(); - { + GLOBALS.set(&context.meta.script.globals, || { mako_profile_scope!("tree-shake"); while current_index < len { @@ -142,15 +143,16 @@ pub fn optimize_modules(module_graph: &mut ModuleGraph, context: &Arc) "tree-shake-module", &tree_shake_modules_ids[current_index].id ); - - current_index = shake_module( - module_graph, - &tree_shake_modules_ids, - &tree_shake_modules_map, - current_index, - ); + HELPERS.set(&Helpers::new(true), || { + current_index = shake_module( + module_graph, + &tree_shake_modules_ids, + &tree_shake_modules_map, + current_index, + ); + }); } - } + }); { mako_profile_scope!("update ast"); diff --git a/crates/mako/src/plugins/tree_shaking/shake/skip_module.rs b/crates/mako/src/plugins/tree_shaking/shake/skip_module.rs index 9d73138a8..94f2d76ac 100644 --- a/crates/mako/src/plugins/tree_shaking/shake/skip_module.rs +++ b/crates/mako/src/plugins/tree_shaking/shake/skip_module.rs @@ -12,6 +12,7 @@ use swc_core::ecma::utils::{quote_ident, quote_str}; use swc_core::quote; use crate::ast::DUMMY_CTXT; +use crate::build::analyze_deps; use crate::compiler::Context; use crate::module::{Dependency, ImportType, ModuleId, NamedExportType, ResolveType}; use crate::module_graph::ModuleGraph; @@ -166,7 +167,7 @@ pub(super) fn skip_module_optimize( module_graph: &mut ModuleGraph, tree_shake_modules_ids: &[ModuleId], tree_shake_modules_map: &HashMap>, - _context: &Arc, + context: &Arc, ) -> Result<()> { mako_profile_function!(); @@ -182,10 +183,11 @@ pub(super) fn skip_module_optimize( to_replace: &(StatementId, Vec, String), module_id: &ModuleId, module_graph: &mut ModuleGraph, + context: &Arc, ) { let stmt_id = to_replace.0; let replaces = &to_replace.1; - let source = &to_replace.2; + let _source = &to_replace.2; let module = module_graph.get_module_mut(module_id).unwrap(); @@ -195,13 +197,10 @@ pub(super) fn skip_module_optimize( let mut to_insert = vec![]; let mut to_insert_deps = vec![]; let mut to_delete = false; - let mut resolve_type: Option = None; match &mut stmt { ModuleItem::ModuleDecl(module_decl) => match module_decl { ModuleDecl::Import(import_decl) => { - resolve_type = Some(ResolveType::Import(ImportType::empty())); - for replace in replaces { let mut matched_index = None; let mut matched_ident = None; @@ -253,8 +252,6 @@ pub(super) fn skip_module_optimize( ModuleDecl::ExportDecl(_) => {} ModuleDecl::ExportNamed(export_named) => { if export_named.src.is_some() { - resolve_type = Some(ResolveType::ExportNamed(NamedExportType::empty())); - for replace in replaces { let mut matched_index = None; let mut matched_ident = None; @@ -331,16 +328,16 @@ pub(super) fn skip_module_optimize( swc_module.body.splice(stmt_id..stmt_id, to_insert); } - if to_delete { - module_graph.remove_dependency_module_by_source_and_resolve_type( - module_id, - source, - resolve_type.unwrap(), - ); - } - for dep in to_insert_deps { - module_graph.add_dependency(module_id, &dep.source.clone().into(), dep); - } + let info = &module.info.as_mut().unwrap(); + let mut deps_vec: Vec<(ModuleId, Dependency)> = vec![]; + let deps = analyze_deps::AnalyzeDeps::analyze_deps(&info.ast, &info.file, context.clone()); + deps.unwrap().resolved_deps.into_iter().for_each(|r| { + deps_vec.push(( + ModuleId::new(r.resolver_resource.get_resolved_path()), + r.dependency, + )); + }); + module_graph.rewrite_dependency(module_id, deps_vec); } while current_index < len { @@ -497,7 +494,7 @@ pub(super) fn skip_module_optimize( // stmt_id is reversed order for to_replace in replaces.iter() { // println!("{} apply with {:?}", module_id.id, to_replace.1); - apply_replace(to_replace, module_id, module_graph); + apply_replace(to_replace, module_id, module_graph, context); } let mut tsm = tree_shake_modules_map.get(module_id).unwrap().borrow_mut(); diff --git a/e2e/fixtures/tree-shaking.import_namespace/expect.js b/e2e/fixtures/tree-shaking.import_namespace/expect.js new file mode 100644 index 000000000..ed279bea0 --- /dev/null +++ b/e2e/fixtures/tree-shaking.import_namespace/expect.js @@ -0,0 +1,14 @@ +const assert = require("assert"); +const { + parseBuildResult, + injectSimpleJest, + moduleReg, +} = require("../../../scripts/test-utils"); +const { files } = parseBuildResult(__dirname); + +injectSimpleJest(); +const content = files["index.js"]; + +expect(content).toContain("shouldKeep1"); +expect(content).toContain("shouldKeep2"); +expect(content).not.toContain("shouldNotKeep"); diff --git a/e2e/fixtures/tree-shaking.import_namespace/mako.config.json b/e2e/fixtures/tree-shaking.import_namespace/mako.config.json new file mode 100644 index 000000000..5c422f400 --- /dev/null +++ b/e2e/fixtures/tree-shaking.import_namespace/mako.config.json @@ -0,0 +1,5 @@ +{ + "optimization": { + "concatenateModules": false + } +} diff --git a/e2e/fixtures/tree-shaking.import_namespace/src/index.tsx b/e2e/fixtures/tree-shaking.import_namespace/src/index.tsx new file mode 100644 index 000000000..cfba35f0a --- /dev/null +++ b/e2e/fixtures/tree-shaking.import_namespace/src/index.tsx @@ -0,0 +1,21 @@ +import * as mod from "./mod"; + +const { shouldKeep2 } = mod + +console.log(mod.shouldKeep1(42)); +console.log(shouldKeep2(42)) + +// Guardian don't remove +let array= [1,2,3,] +console.log(array) +let [first, ...rest] = array +console.log(first,rest) +let copiedArray = [...array] +console.log(copiedArray) + +let object = {a: 1,b: 2,c: 3} +console.log(object) +let {a, ...restObject} = object +console.log(a,restObject) +let copiedObject = {...object} +console.log(copiedObject) diff --git a/e2e/fixtures/tree-shaking.import_namespace/src/mod.js b/e2e/fixtures/tree-shaking.import_namespace/src/mod.js new file mode 100644 index 000000000..5882ccb73 --- /dev/null +++ b/e2e/fixtures/tree-shaking.import_namespace/src/mod.js @@ -0,0 +1,11 @@ +export function shouldKeep1(x) { + return x * x; +} + +export function shouldKeep2(x) { + return x + x; +} + +export function shouldNotKeep(x) { + return x * x * x; +} diff --git a/packages/mako/binding.d.ts b/packages/mako/src/binding.d.ts similarity index 100% rename from packages/mako/binding.d.ts rename to packages/mako/src/binding.d.ts diff --git a/packages/mako/binding.js b/packages/mako/src/binding.js similarity index 100% rename from packages/mako/binding.js rename to packages/mako/src/binding.js