From 6d42d7b2fb5f071fdeb0cc3f8ae029681b626110 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Thu, 10 Jun 2021 00:00:52 +0200 Subject: [PATCH 01/13] Prevent `*:*` from getExportedSymbols --- packages/core/core/src/BundleGraph.js | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/packages/core/core/src/BundleGraph.js b/packages/core/core/src/BundleGraph.js index f9def5692ac..4747a43cd6a 100644 --- a/packages/core/core/src/BundleGraph.js +++ b/packages/core/core/src/BundleGraph.js @@ -1481,7 +1481,9 @@ export default class BundleGraph { if (!resolved) continue; let exported = this.getExportedSymbols(resolved, boundary) .filter(s => s.exportSymbol !== 'default') - .map(s => ({...s, exportAs: s.exportSymbol})); + .map(s => + s.exportSymbol !== '*' ? {...s, exportAs: s.exportSymbol} : s, + ); symbols.push(...exported); } } From 08a9f2d89699ba6c1d210b11757a1b27b71ecf22 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Wed, 3 Mar 2021 23:31:34 +0100 Subject: [PATCH 02/13] Add deferring test --- .../test/integration/side-effects-false/a.js | 3 +++ .../node_modules/bar/bar.js | 5 +++++ .../node_modules/bar/foo.js | 3 +++ .../node_modules/bar/index.js | 2 ++ .../node_modules/bar/package.json | 4 ++++ .../core/integration-tests/test/javascript.js | 20 +++++++++++++++++++ .../integration-tests/test/scope-hoisting.js | 8 ++++---- 7 files changed, 41 insertions(+), 4 deletions(-) create mode 100644 packages/core/integration-tests/test/integration/side-effects-false/a.js create mode 100644 packages/core/integration-tests/test/integration/side-effects-false/node_modules/bar/bar.js create mode 100644 packages/core/integration-tests/test/integration/side-effects-false/node_modules/bar/foo.js create mode 100644 packages/core/integration-tests/test/integration/side-effects-false/node_modules/bar/index.js create mode 100644 packages/core/integration-tests/test/integration/side-effects-false/node_modules/bar/package.json diff --git a/packages/core/integration-tests/test/integration/side-effects-false/a.js b/packages/core/integration-tests/test/integration/side-effects-false/a.js new file mode 100644 index 00000000000..fe948bb713e --- /dev/null +++ b/packages/core/integration-tests/test/integration/side-effects-false/a.js @@ -0,0 +1,3 @@ +import {foo} from 'bar'; + +export default foo(2); diff --git a/packages/core/integration-tests/test/integration/side-effects-false/node_modules/bar/bar.js b/packages/core/integration-tests/test/integration/side-effects-false/node_modules/bar/bar.js new file mode 100644 index 00000000000..4ce39dd3c76 --- /dev/null +++ b/packages/core/integration-tests/test/integration/side-effects-false/node_modules/bar/bar.js @@ -0,0 +1,5 @@ +sideEffect(); + +export default function bar() { + return "returned from bar"; +} diff --git a/packages/core/integration-tests/test/integration/side-effects-false/node_modules/bar/foo.js b/packages/core/integration-tests/test/integration/side-effects-false/node_modules/bar/foo.js new file mode 100644 index 00000000000..36b0b344468 --- /dev/null +++ b/packages/core/integration-tests/test/integration/side-effects-false/node_modules/bar/foo.js @@ -0,0 +1,3 @@ +export default function foo(a) { + return a * a; +} diff --git a/packages/core/integration-tests/test/integration/side-effects-false/node_modules/bar/index.js b/packages/core/integration-tests/test/integration/side-effects-false/node_modules/bar/index.js new file mode 100644 index 00000000000..cf61931c936 --- /dev/null +++ b/packages/core/integration-tests/test/integration/side-effects-false/node_modules/bar/index.js @@ -0,0 +1,2 @@ +export {default as foo} from './foo'; +export {default as bar} from './bar'; diff --git a/packages/core/integration-tests/test/integration/side-effects-false/node_modules/bar/package.json b/packages/core/integration-tests/test/integration/side-effects-false/node_modules/bar/package.json new file mode 100644 index 00000000000..1ea9bea7dfa --- /dev/null +++ b/packages/core/integration-tests/test/integration/side-effects-false/node_modules/bar/package.json @@ -0,0 +1,4 @@ +{ + "name": "bar", + "sideEffects": false +} diff --git a/packages/core/integration-tests/test/javascript.js b/packages/core/integration-tests/test/javascript.js index 5ed13c7b3e1..e861c7e70dc 100644 --- a/packages/core/integration-tests/test/javascript.js +++ b/packages/core/integration-tests/test/javascript.js @@ -5735,4 +5735,24 @@ describe('javascript', function () { assert.deepEqual(calls, ['common', 'deep']); }); + + it('supports deferring unused dependencies with sideEffects: false', async function() { + let b = await bundle( + path.join(__dirname, '/integration/side-effects-false/a.js'), + ); + + let content = await outputFS.readFile(b.getBundles()[0].filePath, 'utf8'); + + assert(!content.includes('returned from bar')); + + let called = false; + let output = await run(b, { + sideEffect: () => { + called = true; + }, + }); + + assert(!called, 'side effect called'); + assert.deepEqual(output.default, 4); + }); }); diff --git a/packages/core/integration-tests/test/scope-hoisting.js b/packages/core/integration-tests/test/scope-hoisting.js index b871856e0f1..30da9917c8d 100644 --- a/packages/core/integration-tests/test/scope-hoisting.js +++ b/packages/core/integration-tests/test/scope-hoisting.js @@ -2626,7 +2626,7 @@ describe('scope hoisting', function () { assert.deepEqual(res.output, 2); }); - it('supports deferring an unused ES6 re-exports (namespace used)', async function () { + it('supports deferring unused ES6 re-exports (namespace used)', async function () { let b = await bundle( path.join( __dirname, @@ -2661,7 +2661,7 @@ describe('scope hoisting', function () { assert.deepEqual(await run(b), 123); }); - it('supports deferring an unused ES6 re-exports (reexport named used)', async function () { + it('supports deferring unused ES6 re-exports (reexport named used)', async function () { let b = await bundle( path.join( __dirname, @@ -2683,7 +2683,7 @@ describe('scope hoisting', function () { assert.deepEqual(output, 'Message 2'); }); - it('supports deferring an unused ES6 re-exports (namespace rename used)', async function () { + it('supports deferring unused ES6 re-exports (namespace rename used)', async function () { let b = await bundle( path.join( __dirname, @@ -2705,7 +2705,7 @@ describe('scope hoisting', function () { assert.deepEqual(output, {default: 'Message 3'}); }); - it('supports deferring an unused ES6 re-exports (direct export used)', async function () { + it('supports deferring unused ES6 re-exports (direct export used)', async function () { let b = await bundle( path.join( __dirname, From 347f86136fa4ca909afd064c51d107ebf1b6a10b Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Thu, 10 Jun 2021 00:04:40 +0200 Subject: [PATCH 03/13] Package deferred deps without scope-hoisting --- packages/packagers/js/src/DevPackager.js | 4 +++- packages/packagers/js/src/dev-prelude.js | 6 ++++-- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/packages/packagers/js/src/DevPackager.js b/packages/packagers/js/src/DevPackager.js index 9151e459ffc..1d5857220ba 100644 --- a/packages/packagers/js/src/DevPackager.js +++ b/packages/packagers/js/src/DevPackager.js @@ -98,7 +98,9 @@ export class DevPackager { let dependencies = this.bundleGraph.getDependencies(asset); for (let dep of dependencies) { let resolved = this.bundleGraph.getResolvedAsset(dep, this.bundle); - if (resolved) { + if (this.bundleGraph.isDependencySkipped(dep)) { + deps[getSpecifier(dep)] = false; + } else if (resolved) { deps[getSpecifier(dep)] = this.bundleGraph.getAssetPublicId(resolved); } diff --git a/packages/packagers/js/src/dev-prelude.js b/packages/packagers/js/src/dev-prelude.js index a8c68b36189..5636da413bd 100644 --- a/packages/packagers/js/src/dev-prelude.js +++ b/packages/packagers/js/src/dev-prelude.js @@ -80,11 +80,13 @@ return cache[name].exports; function localRequire(x) { - return newRequire(localRequire.resolve(x)); + var res = localRequire.resolve(x); + return res === false ? {} : newRequire(res); } function resolve(x) { - return modules[name][1][x] || x; + var id = modules[name][1][x]; + return id != null ? id : x; } } From 5071d37a822c2efec22b702877716f7460edcf93 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Fri, 12 Mar 2021 00:45:52 +0100 Subject: [PATCH 04/13] Add test to correctly deopt when using require --- .../side-effects-false/import-require.js | 4 ++++ .../side-effects-false/{a.js => import.js} | 0 .../core/integration-tests/test/javascript.js | 20 +++++++++++++++---- 3 files changed, 20 insertions(+), 4 deletions(-) create mode 100644 packages/core/integration-tests/test/integration/side-effects-false/import-require.js rename packages/core/integration-tests/test/integration/side-effects-false/{a.js => import.js} (100%) diff --git a/packages/core/integration-tests/test/integration/side-effects-false/import-require.js b/packages/core/integration-tests/test/integration/side-effects-false/import-require.js new file mode 100644 index 00000000000..0bf82ad6b76 --- /dev/null +++ b/packages/core/integration-tests/test/integration/side-effects-false/import-require.js @@ -0,0 +1,4 @@ +import {foo} from 'bar'; +const { bar } = require("bar"); + +export default foo(2) + bar(); diff --git a/packages/core/integration-tests/test/integration/side-effects-false/a.js b/packages/core/integration-tests/test/integration/side-effects-false/import.js similarity index 100% rename from packages/core/integration-tests/test/integration/side-effects-false/a.js rename to packages/core/integration-tests/test/integration/side-effects-false/import.js diff --git a/packages/core/integration-tests/test/javascript.js b/packages/core/integration-tests/test/javascript.js index e861c7e70dc..361d406a781 100644 --- a/packages/core/integration-tests/test/javascript.js +++ b/packages/core/integration-tests/test/javascript.js @@ -5736,9 +5736,9 @@ describe('javascript', function () { assert.deepEqual(calls, ['common', 'deep']); }); - it('supports deferring unused dependencies with sideEffects: false', async function() { + it('supports deferring unused ESM imports with sideEffects: false', async function() { let b = await bundle( - path.join(__dirname, '/integration/side-effects-false/a.js'), + path.join(__dirname, '/integration/side-effects-false/import.js'), ); let content = await outputFS.readFile(b.getBundles()[0].filePath, 'utf8'); @@ -5747,12 +5747,24 @@ describe('javascript', function () { let called = false; let output = await run(b, { - sideEffect: () => { + sideEffect() { called = true; }, }); assert(!called, 'side effect called'); - assert.deepEqual(output.default, 4); + assert.strictEqual(output.default, 4); + }); + + it('supports ESM imports and requires with sideEffects: false', async function() { + let b = await bundle( + path.join(__dirname, '/integration/side-effects-false/import-require.js'), + ); + + let output = await run(b, { + sideEffect() {}, + }); + + assert.strictEqual(output.default, '4returned from bar'); }); }); From ff38f137e864cdaf2e243e6e9ff697a31e95ceef Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Sat, 30 Oct 2021 17:09:34 +0200 Subject: [PATCH 05/13] Refactor collect into separate file --- packages/transformers/js/core/src/fs.rs | 6 +- packages/transformers/js/core/src/hoist.rs | 857 +----------------- .../transformers/js/core/src/hoist_collect.rs | 843 +++++++++++++++++ packages/transformers/js/core/src/lib.rs | 21 +- packages/transformers/js/core/src/utils.rs | 9 + 5 files changed, 884 insertions(+), 852 deletions(-) create mode 100644 packages/transformers/js/core/src/hoist_collect.rs diff --git a/packages/transformers/js/core/src/fs.rs b/packages/transformers/js/core/src/fs.rs index b0c33849edc..b92b8442c21 100644 --- a/packages/transformers/js/core/src/fs.rs +++ b/packages/transformers/js/core/src/fs.rs @@ -1,5 +1,5 @@ use crate::dependency_collector::{DependencyDescriptor, DependencyKind}; -use crate::hoist::{Collect, Import}; +use crate::hoist_collect::{HoistCollect, Import}; use crate::utils::SourceLocation; use data_encoding::{BASE64, HEXLOWER}; use std::collections::HashSet; @@ -26,7 +26,7 @@ pub fn inline_fs<'a>( ) -> impl Fold + 'a { InlineFS { filename: Path::new(filename).to_path_buf(), - collect: Collect::new( + collect: HoistCollect::new( source_map, decls, Mark::fresh(Mark::root()), @@ -41,7 +41,7 @@ pub fn inline_fs<'a>( struct InlineFS<'a> { filename: PathBuf, - collect: Collect, + collect: HoistCollect, global_mark: Mark, project_root: &'a str, deps: &'a mut Vec, diff --git a/packages/transformers/js/core/src/hoist.rs b/packages/transformers/js/core/src/hoist.rs index d2a4abed5b1..66b82aff186 100644 --- a/packages/transformers/js/core/src/hoist.rs +++ b/packages/transformers/js/core/src/hoist.rs @@ -5,20 +5,15 @@ use std::hash::Hasher; use swc_atoms::JsWord; use swc_common::{sync::Lrc, Mark, Span, SyntaxContext, DUMMY_SP}; use swc_ecmascript::ast::*; -use swc_ecmascript::visit::{Fold, FoldWith, Node, Visit, VisitWith}; +use swc_ecmascript::visit::{Fold, FoldWith, VisitWith}; +use crate::hoist_collect::{HoistCollect, Import, ImportKind}; +use crate::id; use crate::utils::{ - match_import, match_member_expr, match_require, Bailout, BailoutReason, CodeHighlight, - Diagnostic, DiagnosticSeverity, SourceLocation, + match_import, match_member_expr, match_require, CodeHighlight, Diagnostic, DiagnosticSeverity, + IdentId, SourceLocation, }; -type IdentId = (JsWord, SyntaxContext); -macro_rules! id { - ($ident: expr) => { - ($ident.sym.clone(), $ident.span.ctxt) - }; -} - macro_rules! hash { ($str:expr) => {{ let mut hasher = DefaultHasher::new(); @@ -36,7 +31,7 @@ pub fn hoist( global_mark: Mark, trace_bailouts: bool, ) -> Result<(Module, HoistResult, Vec), Vec> { - let mut collect = Collect::new(source_map, decls, ignore_mark, global_mark, trace_bailouts); + let mut collect = HoistCollect::new(source_map, decls, ignore_mark, global_mark, trace_bailouts); module.visit_with(&Invalid { span: DUMMY_SP } as _, &mut collect); let mut hoist = Hoist::new(module_id, &collect); @@ -72,7 +67,7 @@ struct ImportedSymbol { struct Hoist<'a> { module_id: &'a str, - collect: &'a Collect, + collect: &'a HoistCollect, module_items: Vec, export_decls: HashSet, hoisted_imports: Vec, @@ -100,7 +95,7 @@ pub struct HoistResult { } impl<'a> Hoist<'a> { - fn new(module_id: &'a str, collect: &'a Collect) -> Self { + fn new(module_id: &'a str, collect: &'a HoistCollect) -> Self { Hoist { module_id, collect, @@ -1112,838 +1107,6 @@ impl<'a> Hoist<'a> { } } -macro_rules! collect_visit_fn { - ($name:ident, $type:ident) => { - fn $name(&mut self, node: &$type, _parent: &dyn Node) { - let in_module_this = self.in_module_this; - let in_function = self.in_function; - self.in_module_this = false; - self.in_function = true; - node.visit_children_with(self); - self.in_module_this = in_module_this; - self.in_function = in_function; - } - }; -} - -#[derive(PartialEq, Clone, Copy)] -pub enum ImportKind { - Require, - Import, - DynamicImport, -} - -pub struct Import { - pub source: JsWord, - pub specifier: JsWord, - pub kind: ImportKind, - pub loc: SourceLocation, -} - -pub struct Collect { - pub source_map: Lrc, - pub decls: HashSet, - ignore_mark: Mark, - global_ctxt: SyntaxContext, - static_cjs_exports: bool, - has_cjs_exports: bool, - is_esm: bool, - should_wrap: bool, - pub imports: HashMap, - exports: HashMap, - non_static_access: HashMap>, - non_const_bindings: HashMap>, - non_static_requires: HashSet, - wrapped_requires: HashSet, - in_module_this: bool, - in_top_level: bool, - in_export_decl: bool, - in_function: bool, - in_assign: bool, - bailouts: Option>, -} - -impl Collect { - pub fn new( - source_map: Lrc, - decls: HashSet, - ignore_mark: Mark, - global_mark: Mark, - trace_bailouts: bool, - ) -> Self { - Collect { - source_map, - decls, - ignore_mark, - global_ctxt: SyntaxContext::empty().apply_mark(global_mark), - static_cjs_exports: true, - has_cjs_exports: false, - is_esm: false, - should_wrap: false, - imports: HashMap::new(), - exports: HashMap::new(), - non_static_access: HashMap::new(), - non_const_bindings: HashMap::new(), - non_static_requires: HashSet::new(), - wrapped_requires: HashSet::new(), - in_module_this: true, - in_top_level: true, - in_export_decl: false, - in_function: false, - in_assign: false, - bailouts: if trace_bailouts { Some(vec![]) } else { None }, - } - } -} - -impl Visit for Collect { - fn visit_module(&mut self, node: &Module, _parent: &dyn Node) { - self.in_module_this = true; - self.in_top_level = true; - self.in_function = false; - node.visit_children_with(self); - self.in_module_this = false; - - if let Some(bailouts) = &mut self.bailouts { - for key in self.imports.keys() { - if let Some(spans) = self.non_static_access.get(key) { - for span in spans { - bailouts.push(Bailout { - loc: SourceLocation::from(&self.source_map, *span), - reason: BailoutReason::NonStaticAccess, - }) - } - } - } - - bailouts.sort_by(|a, b| a.loc.partial_cmp(&b.loc).unwrap()); - } - } - - collect_visit_fn!(visit_function, Function); - collect_visit_fn!(visit_class, Class); - collect_visit_fn!(visit_getter_prop, GetterProp); - collect_visit_fn!(visit_setter_prop, SetterProp); - - fn visit_arrow_expr(&mut self, node: &ArrowExpr, _parent: &dyn Node) { - let in_function = self.in_function; - self.in_function = true; - node.visit_children_with(self); - self.in_function = in_function; - } - - fn visit_module_item(&mut self, node: &ModuleItem, _parent: &dyn Node) { - match node { - ModuleItem::ModuleDecl(_decl) => { - self.is_esm = true; - } - ModuleItem::Stmt(stmt) => { - match stmt { - Stmt::Decl(decl) => { - if let Decl::Var(_var) = decl { - decl.visit_children_with(self); - return; - } - } - Stmt::Expr(expr) => { - // Top-level require(). Do not traverse further so it is not marked as wrapped. - if let Some(_source) = self.match_require(&*expr.expr) { - return; - } - - // TODO: optimize `require('foo').bar` / `require('foo').bar()` as well - } - _ => {} - } - } - } - - self.in_top_level = false; - node.visit_children_with(self); - self.in_top_level = true; - } - - fn visit_import_decl(&mut self, node: &ImportDecl, _parent: &dyn Node) { - for specifier in &node.specifiers { - match specifier { - ImportSpecifier::Named(named) => { - let imported = match &named.imported { - Some(imported) => imported.sym.clone(), - None => named.local.sym.clone(), - }; - self.imports.insert( - id!(named.local), - Import { - source: node.src.value.clone(), - specifier: imported, - kind: ImportKind::Import, - loc: SourceLocation::from(&self.source_map, named.span), - }, - ); - } - ImportSpecifier::Default(default) => { - self.imports.insert( - id!(default.local), - Import { - source: node.src.value.clone(), - specifier: js_word!("default"), - kind: ImportKind::Import, - loc: SourceLocation::from(&self.source_map, default.span), - }, - ); - } - ImportSpecifier::Namespace(namespace) => { - self.imports.insert( - id!(namespace.local), - Import { - source: node.src.value.clone(), - specifier: "*".into(), - kind: ImportKind::Import, - loc: SourceLocation::from(&self.source_map, namespace.span), - }, - ); - } - } - } - } - - fn visit_named_export(&mut self, node: &NamedExport, _parent: &dyn Node) { - if node.src.is_some() { - return; - } - - for specifier in &node.specifiers { - match specifier { - ExportSpecifier::Named(named) => { - let exported = match &named.exported { - Some(exported) => exported.sym.clone(), - None => named.orig.sym.clone(), - }; - self.exports.entry(id!(named.orig)).or_insert(exported); - } - ExportSpecifier::Default(default) => { - self - .exports - .entry(id!(default.exported)) - .or_insert(js_word!("default")); - } - ExportSpecifier::Namespace(namespace) => { - self - .exports - .entry(id!(namespace.name)) - .or_insert_with(|| "*".into()); - } - } - } - } - - fn visit_export_decl(&mut self, node: &ExportDecl, _parent: &dyn Node) { - match &node.decl { - Decl::Class(class) => { - self - .exports - .insert(id!(class.ident), class.ident.sym.clone()); - } - Decl::Fn(func) => { - self.exports.insert(id!(func.ident), func.ident.sym.clone()); - } - Decl::Var(var) => { - for decl in &var.decls { - self.in_export_decl = true; - decl.name.visit_with(decl, self); - self.in_export_decl = false; - - decl.init.visit_with(decl, self); - } - } - _ => {} - } - - node.visit_children_with(self); - } - - fn visit_export_default_decl(&mut self, node: &ExportDefaultDecl, _parent: &dyn Node) { - match &node.decl { - DefaultDecl::Class(class) => { - if let Some(ident) = &class.ident { - self.exports.insert(id!(ident), "default".into()); - } - } - DefaultDecl::Fn(func) => { - if let Some(ident) = &func.ident { - self.exports.insert(id!(ident), "default".into()); - } - } - _ => { - unreachable!("unsupported export default declaration"); - } - }; - - node.visit_children_with(self); - } - - fn visit_return_stmt(&mut self, node: &ReturnStmt, _parent: &dyn Node) { - if !self.in_function { - self.should_wrap = true; - self.add_bailout(node.span, BailoutReason::TopLevelReturn); - } - - node.visit_children_with(self) - } - - fn visit_binding_ident(&mut self, node: &BindingIdent, _parent: &dyn Node) { - if self.in_export_decl { - self.exports.insert(id!(node.id), node.id.sym.clone()); - } - - if self.in_assign && node.id.span.ctxt() == self.global_ctxt { - self - .non_const_bindings - .entry(id!(node.id)) - .or_default() - .push(node.id.span); - } - } - - fn visit_assign_pat_prop(&mut self, node: &AssignPatProp, _parent: &dyn Node) { - if self.in_export_decl { - self.exports.insert(id!(node.key), node.key.sym.clone()); - } - - if self.in_assign && node.key.span.ctxt() == self.global_ctxt { - self - .non_const_bindings - .entry(id!(node.key)) - .or_default() - .push(node.key.span); - } - } - - fn visit_member_expr(&mut self, node: &MemberExpr, _parent: &dyn Node) { - // if module.exports, ensure only assignment or static member expression - // if exports, ensure only static member expression - // if require, could be static access (handle in fold) - - if match_member_expr(node, vec!["module", "exports"], &self.decls) { - self.static_cjs_exports = false; - self.has_cjs_exports = true; - return; - } - - if match_member_expr(node, vec!["module", "hot"], &self.decls) { - return; - } - - if match_member_expr(node, vec!["module", "require"], &self.decls) { - return; - } - - let is_static = match &*node.prop { - Expr::Ident(_) => !node.computed, - Expr::Lit(Lit::Str(_)) => true, - _ => false, - }; - - if let ExprOrSuper::Expr(expr) = &node.obj { - match &**expr { - Expr::Member(member) => { - if match_member_expr(member, vec!["module", "exports"], &self.decls) { - self.has_cjs_exports = true; - if !is_static { - self.static_cjs_exports = false; - self.add_bailout(node.span, BailoutReason::NonStaticExports); - } - } - return; - } - Expr::Ident(ident) => { - let exports: JsWord = "exports".into(); - if ident.sym == exports && !self.decls.contains(&id!(ident)) { - self.has_cjs_exports = true; - if !is_static { - self.static_cjs_exports = false; - self.add_bailout(node.span, BailoutReason::NonStaticExports); - } - } - - if ident.sym == js_word!("module") && !self.decls.contains(&id!(ident)) { - self.has_cjs_exports = true; - self.static_cjs_exports = false; - self.should_wrap = true; - self.add_bailout(node.span, BailoutReason::FreeModule); - } - - // `import` isn't really an identifier... - if !is_static && ident.sym != js_word!("import") { - self - .non_static_access - .entry(id!(ident)) - .or_default() - .push(node.span); - } - return; - } - Expr::This(_this) => { - if self.in_module_this { - self.has_cjs_exports = true; - if !is_static { - self.static_cjs_exports = false; - self.add_bailout(node.span, BailoutReason::NonStaticExports); - } - } - return; - } - _ => {} - } - } - - node.visit_children_with(self); - } - - fn visit_unary_expr(&mut self, node: &UnaryExpr, _parent: &dyn Node) { - if node.op == UnaryOp::TypeOf { - match &*node.arg { - Expr::Ident(ident) - if ident.sym == js_word!("module") && !self.decls.contains(&id!(ident)) => - { - // Do nothing to avoid the ident visitor from marking the module as non-static. - } - _ => node.visit_children_with(self), - } - } else { - node.visit_children_with(self); - } - } - - fn visit_expr(&mut self, node: &Expr, _parent: &dyn Node) { - // If we reached this visitor, this is a non-top-level require that isn't in a variable - // declaration. We need to wrap the referenced module to preserve side effect ordering. - if let Some(source) = self.match_require(node) { - self.wrapped_requires.insert(source); - let span = match node { - Expr::Call(c) => c.span, - _ => unreachable!(), - }; - self.add_bailout(span, BailoutReason::NonTopLevelRequire); - } - - if let Some(source) = match_import(node, self.ignore_mark) { - self.non_static_requires.insert(source.clone()); - self.wrapped_requires.insert(source); - let span = match node { - Expr::Call(c) => c.span, - _ => unreachable!(), - }; - self.add_bailout(span, BailoutReason::NonStaticDynamicImport); - } - - match node { - Expr::Ident(ident) => { - // Bail if `module` or `exports` are accessed non-statically. - let is_module = ident.sym == js_word!("module"); - let exports: JsWord = "exports".into(); - let is_exports = ident.sym == exports; - if (is_module || is_exports) && !self.decls.contains(&id!(ident)) { - self.has_cjs_exports = true; - self.static_cjs_exports = false; - if is_module { - self.should_wrap = true; - self.add_bailout(ident.span, BailoutReason::FreeModule); - } else { - self.add_bailout(ident.span, BailoutReason::FreeExports); - } - } - - // `import` isn't really an identifier... - if ident.sym != js_word!("import") { - self - .non_static_access - .entry(id!(ident)) - .or_default() - .push(ident.span); - } - } - _ => { - node.visit_children_with(self); - } - } - } - - fn visit_this_expr(&mut self, node: &ThisExpr, _parent: &dyn Node) { - if self.in_module_this { - self.has_cjs_exports = true; - self.static_cjs_exports = false; - self.add_bailout(node.span, BailoutReason::FreeExports); - } - } - - fn visit_assign_expr(&mut self, node: &AssignExpr, _parent: &dyn Node) { - // if rhs is a require, record static accesses - // if lhs is `exports`, mark as CJS exports re-assigned - // if lhs is `module.exports` - // if lhs is `module.exports.XXX` or `exports.XXX`, record static export - - self.in_assign = true; - node.left.visit_with(node, self); - self.in_assign = false; - node.right.visit_with(node, self); - - if let PatOrExpr::Pat(pat) = &node.left { - if has_binding_identifier(pat, &"exports".into(), &self.decls) { - // Must wrap for cases like - // ``` - // function logExports() { - // console.log(exports); - // } - // exports.test = 2; - // logExports(); - // exports = {test: 4}; - // logExports(); - // ``` - self.static_cjs_exports = false; - self.has_cjs_exports = true; - self.should_wrap = true; - self.add_bailout(node.span, BailoutReason::ExportsReassignment); - } else if has_binding_identifier(pat, &"module".into(), &self.decls) { - // Same for `module`. If it is reassigned we can't correctly statically analyze. - self.static_cjs_exports = false; - self.has_cjs_exports = true; - self.should_wrap = true; - self.add_bailout(node.span, BailoutReason::ModuleReassignment); - } - } - } - - fn visit_var_declarator(&mut self, node: &VarDeclarator, _parent: &dyn Node) { - // if init is a require call, record static accesses - if let Some(init) = &node.init { - if let Some(source) = self.match_require(init) { - self.add_pat_imports(&node.name, &source, ImportKind::Require); - return; - } - - match &**init { - Expr::Member(member) => { - if let ExprOrSuper::Expr(expr) = &member.obj { - if let Some(source) = self.match_require(&*expr) { - // Convert member expression on require to a destructuring assignment. - // const yx = require('y').x; -> const {x: yx} = require('x'); - let key = match &*member.prop { - Expr::Ident(ident) => { - if !member.computed { - PropName::Ident(ident.clone()) - } else { - PropName::Computed(ComputedPropName { - span: DUMMY_SP, - expr: Box::new(*expr.clone()), - }) - } - } - Expr::Lit(Lit::Str(str_)) => PropName::Str(str_.clone()), - _ => PropName::Computed(ComputedPropName { - span: DUMMY_SP, - expr: Box::new(*expr.clone()), - }), - }; - - self.add_pat_imports( - &Pat::Object(ObjectPat { - optional: false, - span: DUMMY_SP, - type_ann: None, - props: vec![ObjectPatProp::KeyValue(KeyValuePatProp { - key, - value: Box::new(node.name.clone()), - })], - }), - &source, - ImportKind::Require, - ); - return; - } - } - } - Expr::Await(await_exp) => { - // let x = await import('foo'); - // let {x} = await import('foo'); - if let Some(source) = match_import(&*await_exp.arg, self.ignore_mark) { - self.add_pat_imports(&node.name, &source, ImportKind::DynamicImport); - return; - } - } - _ => {} - } - } - - // This is visited via visit_module_item with is_top_level == true, it needs to be - // set to false for called visitors (and restored again). - let in_top_level = self.in_top_level; - self.in_top_level = false; - node.visit_children_with(self); - self.in_top_level = in_top_level; - } - - fn visit_call_expr(&mut self, node: &CallExpr, _parent: &dyn Node) { - if let ExprOrSuper::Expr(expr) = &node.callee { - match &**expr { - Expr::Ident(ident) => { - if ident.sym == js_word!("eval") && !self.decls.contains(&id!(ident)) { - self.should_wrap = true; - self.add_bailout(node.span, BailoutReason::Eval); - } - } - Expr::Member(member) => { - // import('foo').then(foo => ...); - if let ExprOrSuper::Expr(obj) = &member.obj { - if let Some(source) = match_import(&*obj, self.ignore_mark) { - let then: JsWord = "then".into(); - let is_then = match &*member.prop { - Expr::Ident(ident) => !member.computed && ident.sym == then, - Expr::Lit(Lit::Str(str)) => str.value == then, - _ => false, - }; - - if is_then { - if let Some(ExprOrSpread { expr, .. }) = node.args.get(0) { - let param = match &**expr { - Expr::Fn(func) => func.function.params.get(0).map(|param| ¶m.pat), - Expr::Arrow(arrow) => arrow.params.get(0), - _ => None, - }; - - if let Some(param) = param { - self.add_pat_imports(param, &source, ImportKind::DynamicImport); - } else { - self.non_static_requires.insert(source.clone()); - self.wrapped_requires.insert(source); - self.add_bailout(node.span, BailoutReason::NonStaticDynamicImport); - } - - expr.visit_with(node, self); - return; - } - } - } - } - } - _ => {} - } - } - - node.visit_children_with(self); - } -} - -impl Collect { - pub fn match_require(&self, node: &Expr) -> Option { - match_require(node, &self.decls, self.ignore_mark) - } - - fn add_pat_imports(&mut self, node: &Pat, src: &JsWord, kind: ImportKind) { - if !self.in_top_level { - self.wrapped_requires.insert(src.clone()); - if kind != ImportKind::DynamicImport { - self.non_static_requires.insert(src.clone()); - let span = match node { - Pat::Ident(id) => id.id.span, - Pat::Array(arr) => arr.span, - Pat::Object(obj) => obj.span, - Pat::Rest(rest) => rest.span, - Pat::Assign(assign) => assign.span, - Pat::Invalid(i) => i.span, - Pat::Expr(_) => DUMMY_SP, - }; - self.add_bailout(span, BailoutReason::NonTopLevelRequire); - } - } - - match node { - Pat::Ident(ident) => { - // let x = require('y'); - // Need to track member accesses of `x`. - self.imports.insert( - id!(ident.id), - Import { - source: src.clone(), - specifier: "*".into(), - kind, - loc: SourceLocation::from(&self.source_map, ident.id.span), - }, - ); - } - Pat::Object(object) => { - for prop in &object.props { - match prop { - ObjectPatProp::KeyValue(kv) => { - let imported = match &kv.key { - PropName::Ident(ident) => ident.sym.clone(), - PropName::Str(str) => str.value.clone(), - _ => { - // Non-static. E.g. computed property. - self.non_static_requires.insert(src.clone()); - self.add_bailout(object.span, BailoutReason::NonStaticDestructuring); - continue; - } - }; - - match &*kv.value { - Pat::Ident(ident) => { - // let {x: y} = require('y'); - // Need to track `x` as a used symbol. - self.imports.insert( - id!(ident.id), - Import { - source: src.clone(), - specifier: imported, - kind, - loc: SourceLocation::from(&self.source_map, ident.id.span), - }, - ); - - // Mark as non-constant. CJS exports can be mutated by other modules, - // so it's not safe to reference them directly. - self - .non_const_bindings - .entry(id!(ident.id)) - .or_default() - .push(ident.id.span); - } - _ => { - // Non-static. - self.non_static_requires.insert(src.clone()); - self.add_bailout(object.span, BailoutReason::NonStaticDestructuring); - } - } - } - ObjectPatProp::Assign(assign) => { - // let {x} = require('y'); - // let {x = 2} = require('y'); - // Need to track `x` as a used symbol. - self.imports.insert( - id!(assign.key), - Import { - source: src.clone(), - specifier: assign.key.sym.clone(), - kind, - loc: SourceLocation::from(&self.source_map, assign.key.span), - }, - ); - self - .non_const_bindings - .entry(id!(assign.key)) - .or_default() - .push(assign.key.span); - } - ObjectPatProp::Rest(_rest) => { - // let {x, ...y} = require('y'); - // Non-static. We don't know what keys are used. - self.non_static_requires.insert(src.clone()); - self.add_bailout(object.span, BailoutReason::NonStaticDestructuring); - } - } - } - } - _ => { - // Non-static. - self.non_static_requires.insert(src.clone()); - let span = match node { - Pat::Ident(id) => id.id.span, - Pat::Array(arr) => arr.span, - Pat::Object(obj) => obj.span, - Pat::Rest(rest) => rest.span, - Pat::Assign(assign) => assign.span, - Pat::Invalid(i) => i.span, - Pat::Expr(_) => DUMMY_SP, - }; - self.add_bailout(span, BailoutReason::NonStaticDestructuring); - } - } - } - - fn get_non_const_binding_idents(&self, node: &Pat, idents: &mut Vec) { - match node { - Pat::Ident(ident) => { - if self.non_const_bindings.contains_key(&id!(ident.id)) { - idents.push(ident.id.clone()); - } - } - Pat::Object(object) => { - for prop in &object.props { - match prop { - ObjectPatProp::KeyValue(kv) => { - self.get_non_const_binding_idents(&*kv.value, idents); - } - ObjectPatProp::Assign(assign) => { - if self.non_const_bindings.contains_key(&id!(assign.key)) { - idents.push(assign.key.clone()); - } - } - ObjectPatProp::Rest(rest) => { - self.get_non_const_binding_idents(&*rest.arg, idents); - } - } - } - } - Pat::Array(array) => { - for el in array.elems.iter().flatten() { - self.get_non_const_binding_idents(el, idents); - } - } - _ => {} - } - } - - fn add_bailout(&mut self, span: Span, reason: BailoutReason) { - if let Some(bailouts) = &mut self.bailouts { - bailouts.push(Bailout { - loc: SourceLocation::from(&self.source_map, span), - reason, - }) - } - } -} - -fn has_binding_identifier(node: &Pat, sym: &JsWord, decls: &HashSet) -> bool { - match node { - Pat::Ident(ident) => { - if ident.id.sym == *sym && !decls.contains(&id!(ident.id)) { - return true; - } - } - Pat::Object(object) => { - for prop in &object.props { - match prop { - ObjectPatProp::KeyValue(kv) => { - if has_binding_identifier(&*kv.value, sym, decls) { - return true; - } - } - ObjectPatProp::Assign(assign) => { - if assign.key.sym == *sym && !decls.contains(&id!(assign.key)) { - return true; - } - } - ObjectPatProp::Rest(rest) => { - if has_binding_identifier(&*rest.arg, sym, decls) { - return true; - } - } - } - } - } - Pat::Array(array) => { - for el in array.elems.iter().flatten() { - if has_binding_identifier(el, sym, decls) { - return true; - } - } - } - _ => {} - } - - false -} - #[cfg(test)] mod tests { use super::*; @@ -1958,7 +1121,7 @@ mod tests { extern crate indoc; use self::indoc::indoc; - fn parse(code: &str) -> (Collect, String, HoistResult) { + fn parse(code: &str) -> (HoistCollect, String, HoistResult) { let source_map = Lrc::new(SourceMap::default()); let source_file = source_map.new_source_file(FileName::Anon, code.into()); @@ -1982,7 +1145,7 @@ mod tests { let global_mark = Mark::fresh(Mark::root()); let module = module.fold_with(&mut resolver_with_mark(global_mark)); - let mut collect = Collect::new( + let mut collect = HoistCollect::new( source_map.clone(), collect_decls(&module), Mark::fresh(Mark::root()), diff --git a/packages/transformers/js/core/src/hoist_collect.rs b/packages/transformers/js/core/src/hoist_collect.rs new file mode 100644 index 00000000000..8082c9f7cbb --- /dev/null +++ b/packages/transformers/js/core/src/hoist_collect.rs @@ -0,0 +1,843 @@ +use std::collections::{HashMap, HashSet}; +use swc_atoms::JsWord; +use swc_common::{sync::Lrc, Mark, Span, SyntaxContext, DUMMY_SP}; +use swc_ecmascript::ast::*; +use swc_ecmascript::visit::{Node, Visit, VisitWith}; + +use crate::id; +use crate::utils::{ + match_import, match_member_expr, match_require, Bailout, BailoutReason, IdentId, SourceLocation, +}; + +#[derive(Debug, PartialEq, Clone, Copy)] +pub enum ImportKind { + Require, + Import, + DynamicImport, +} + +#[derive(Debug)] +pub struct Import { + pub source: JsWord, + pub specifier: JsWord, + pub kind: ImportKind, + pub loc: SourceLocation, +} + +pub struct HoistCollect { + pub source_map: Lrc, + pub decls: HashSet, + pub ignore_mark: Mark, + pub global_ctxt: SyntaxContext, + pub static_cjs_exports: bool, + pub has_cjs_exports: bool, + pub is_esm: bool, + pub should_wrap: bool, + pub imports: HashMap, + pub exports: HashMap, + pub non_static_access: HashMap>, + pub non_const_bindings: HashMap>, + pub non_static_requires: HashSet, + pub wrapped_requires: HashSet, + pub bailouts: Option>, + in_module_this: bool, + in_top_level: bool, + in_export_decl: bool, + in_function: bool, + in_assign: bool, +} + +impl HoistCollect { + pub fn new( + source_map: Lrc, + decls: HashSet, + ignore_mark: Mark, + global_mark: Mark, + trace_bailouts: bool, + ) -> Self { + HoistCollect { + source_map, + decls, + ignore_mark, + global_ctxt: SyntaxContext::empty().apply_mark(global_mark), + static_cjs_exports: true, + has_cjs_exports: false, + is_esm: false, + should_wrap: false, + imports: HashMap::new(), + exports: HashMap::new(), + non_static_access: HashMap::new(), + non_const_bindings: HashMap::new(), + non_static_requires: HashSet::new(), + wrapped_requires: HashSet::new(), + in_module_this: true, + in_top_level: true, + in_export_decl: false, + in_function: false, + in_assign: false, + bailouts: if trace_bailouts { Some(vec![]) } else { None }, + } + } +} + +macro_rules! collect_visit_fn { + ($name:ident, $type:ident) => { + fn $name(&mut self, node: &$type, _parent: &dyn Node) { + let in_module_this = self.in_module_this; + let in_function = self.in_function; + self.in_module_this = false; + self.in_function = true; + node.visit_children_with(self); + self.in_module_this = in_module_this; + self.in_function = in_function; + } + }; +} + +impl Visit for HoistCollect { + fn visit_module(&mut self, node: &Module, _parent: &dyn Node) { + self.in_module_this = true; + self.in_top_level = true; + self.in_function = false; + node.visit_children_with(self); + self.in_module_this = false; + + if let Some(bailouts) = &mut self.bailouts { + for key in self.imports.keys() { + if let Some(spans) = self.non_static_access.get(key) { + for span in spans { + bailouts.push(Bailout { + loc: SourceLocation::from(&self.source_map, *span), + reason: BailoutReason::NonStaticAccess, + }) + } + } + } + + bailouts.sort_by(|a, b| a.loc.partial_cmp(&b.loc).unwrap()); + } + } + + collect_visit_fn!(visit_function, Function); + collect_visit_fn!(visit_class, Class); + collect_visit_fn!(visit_getter_prop, GetterProp); + collect_visit_fn!(visit_setter_prop, SetterProp); + + fn visit_arrow_expr(&mut self, node: &ArrowExpr, _parent: &dyn Node) { + let in_function = self.in_function; + self.in_function = true; + node.visit_children_with(self); + self.in_function = in_function; + } + + fn visit_module_item(&mut self, node: &ModuleItem, _parent: &dyn Node) { + match node { + ModuleItem::ModuleDecl(_decl) => { + self.is_esm = true; + } + ModuleItem::Stmt(stmt) => { + match stmt { + Stmt::Decl(decl) => { + if let Decl::Var(_var) = decl { + decl.visit_children_with(self); + return; + } + } + Stmt::Expr(expr) => { + // Top-level require(). Do not traverse further so it is not marked as wrapped. + if let Some(_source) = self.match_require(&*expr.expr) { + return; + } + + // TODO: optimize `require('foo').bar` / `require('foo').bar()` as well + } + _ => {} + } + } + } + + self.in_top_level = false; + node.visit_children_with(self); + self.in_top_level = true; + } + + fn visit_import_decl(&mut self, node: &ImportDecl, _parent: &dyn Node) { + for specifier in &node.specifiers { + match specifier { + ImportSpecifier::Named(named) => { + let imported = match &named.imported { + Some(imported) => imported.sym.clone(), + None => named.local.sym.clone(), + }; + self.imports.insert( + id!(named.local), + Import { + source: node.src.value.clone(), + specifier: imported, + kind: ImportKind::Import, + loc: SourceLocation::from(&self.source_map, named.span), + }, + ); + } + ImportSpecifier::Default(default) => { + self.imports.insert( + id!(default.local), + Import { + source: node.src.value.clone(), + specifier: js_word!("default"), + kind: ImportKind::Import, + loc: SourceLocation::from(&self.source_map, default.span), + }, + ); + } + ImportSpecifier::Namespace(namespace) => { + self.imports.insert( + id!(namespace.local), + Import { + source: node.src.value.clone(), + specifier: "*".into(), + kind: ImportKind::Import, + loc: SourceLocation::from(&self.source_map, namespace.span), + }, + ); + } + } + } + } + + fn visit_named_export(&mut self, node: &NamedExport, _parent: &dyn Node) { + if node.src.is_some() { + return; + } + + for specifier in &node.specifiers { + match specifier { + ExportSpecifier::Named(named) => { + let exported = match &named.exported { + Some(exported) => exported.sym.clone(), + None => named.orig.sym.clone(), + }; + self.exports.entry(id!(named.orig)).or_insert(exported); + } + ExportSpecifier::Default(default) => { + self + .exports + .entry(id!(default.exported)) + .or_insert(js_word!("default")); + } + ExportSpecifier::Namespace(namespace) => { + self + .exports + .entry(id!(namespace.name)) + .or_insert_with(|| "*".into()); + } + } + } + } + + fn visit_export_decl(&mut self, node: &ExportDecl, _parent: &dyn Node) { + match &node.decl { + Decl::Class(class) => { + self + .exports + .insert(id!(class.ident), class.ident.sym.clone()); + } + Decl::Fn(func) => { + self.exports.insert(id!(func.ident), func.ident.sym.clone()); + } + Decl::Var(var) => { + for decl in &var.decls { + self.in_export_decl = true; + decl.name.visit_with(decl, self); + self.in_export_decl = false; + + decl.init.visit_with(decl, self); + } + } + _ => {} + } + + node.visit_children_with(self); + } + + fn visit_export_default_decl(&mut self, node: &ExportDefaultDecl, _parent: &dyn Node) { + match &node.decl { + DefaultDecl::Class(class) => { + if let Some(ident) = &class.ident { + self.exports.insert(id!(ident), "default".into()); + } + } + DefaultDecl::Fn(func) => { + if let Some(ident) = &func.ident { + self.exports.insert(id!(ident), "default".into()); + } + } + _ => { + unreachable!("unsupported export default declaration"); + } + }; + + node.visit_children_with(self); + } + + fn visit_return_stmt(&mut self, node: &ReturnStmt, _parent: &dyn Node) { + if !self.in_function { + self.should_wrap = true; + self.add_bailout(node.span, BailoutReason::TopLevelReturn); + } + + node.visit_children_with(self) + } + + fn visit_binding_ident(&mut self, node: &BindingIdent, _parent: &dyn Node) { + if self.in_export_decl { + self.exports.insert(id!(node.id), node.id.sym.clone()); + } + + if self.in_assign && node.id.span.ctxt() == self.global_ctxt { + self + .non_const_bindings + .entry(id!(node.id)) + .or_default() + .push(node.id.span); + } + } + + fn visit_assign_pat_prop(&mut self, node: &AssignPatProp, _parent: &dyn Node) { + if self.in_export_decl { + self.exports.insert(id!(node.key), node.key.sym.clone()); + } + + if self.in_assign && node.key.span.ctxt() == self.global_ctxt { + self + .non_const_bindings + .entry(id!(node.key)) + .or_default() + .push(node.key.span); + } + } + + fn visit_member_expr(&mut self, node: &MemberExpr, _parent: &dyn Node) { + // if module.exports, ensure only assignment or static member expression + // if exports, ensure only static member expression + // if require, could be static access (handle in fold) + + if match_member_expr(node, vec!["module", "exports"], &self.decls) { + self.static_cjs_exports = false; + self.has_cjs_exports = true; + return; + } + + if match_member_expr(node, vec!["module", "hot"], &self.decls) { + return; + } + + if match_member_expr(node, vec!["module", "require"], &self.decls) { + return; + } + + let is_static = match &*node.prop { + Expr::Ident(_) => !node.computed, + Expr::Lit(Lit::Str(_)) => true, + _ => false, + }; + + if let ExprOrSuper::Expr(expr) = &node.obj { + match &**expr { + Expr::Member(member) => { + if match_member_expr(member, vec!["module", "exports"], &self.decls) { + self.has_cjs_exports = true; + if !is_static { + self.static_cjs_exports = false; + self.add_bailout(node.span, BailoutReason::NonStaticExports); + } + } + return; + } + Expr::Ident(ident) => { + let exports: JsWord = "exports".into(); + if ident.sym == exports && !self.decls.contains(&id!(ident)) { + self.has_cjs_exports = true; + if !is_static { + self.static_cjs_exports = false; + self.add_bailout(node.span, BailoutReason::NonStaticExports); + } + } + + if ident.sym == js_word!("module") && !self.decls.contains(&id!(ident)) { + self.has_cjs_exports = true; + self.static_cjs_exports = false; + self.should_wrap = true; + self.add_bailout(node.span, BailoutReason::FreeModule); + } + + // `import` isn't really an identifier... + if !is_static && ident.sym != js_word!("import") { + self + .non_static_access + .entry(id!(ident)) + .or_default() + .push(node.span); + } + return; + } + Expr::This(_this) => { + if self.in_module_this { + self.has_cjs_exports = true; + if !is_static { + self.static_cjs_exports = false; + self.add_bailout(node.span, BailoutReason::NonStaticExports); + } + } + return; + } + _ => {} + } + } + + node.visit_children_with(self); + } + + fn visit_unary_expr(&mut self, node: &UnaryExpr, _parent: &dyn Node) { + if node.op == UnaryOp::TypeOf { + match &*node.arg { + Expr::Ident(ident) + if ident.sym == js_word!("module") && !self.decls.contains(&id!(ident)) => + { + // Do nothing to avoid the ident visitor from marking the module as non-static. + } + _ => node.visit_children_with(self), + } + } else { + node.visit_children_with(self); + } + } + + fn visit_expr(&mut self, node: &Expr, _parent: &dyn Node) { + // If we reached this visitor, this is a non-top-level require that isn't in a variable + // declaration. We need to wrap the referenced module to preserve side effect ordering. + if let Some(source) = self.match_require(node) { + self.wrapped_requires.insert(source); + let span = match node { + Expr::Call(c) => c.span, + _ => unreachable!(), + }; + self.add_bailout(span, BailoutReason::NonTopLevelRequire); + } + + if let Some(source) = match_import(node, self.ignore_mark) { + self.non_static_requires.insert(source.clone()); + self.wrapped_requires.insert(source); + let span = match node { + Expr::Call(c) => c.span, + _ => unreachable!(), + }; + self.add_bailout(span, BailoutReason::NonStaticDynamicImport); + } + + match node { + Expr::Ident(ident) => { + // Bail if `module` or `exports` are accessed non-statically. + let is_module = ident.sym == js_word!("module"); + let exports: JsWord = "exports".into(); + let is_exports = ident.sym == exports; + if (is_module || is_exports) && !self.decls.contains(&id!(ident)) { + self.has_cjs_exports = true; + self.static_cjs_exports = false; + if is_module { + self.should_wrap = true; + self.add_bailout(ident.span, BailoutReason::FreeModule); + } else { + self.add_bailout(ident.span, BailoutReason::FreeExports); + } + } + + // `import` isn't really an identifier... + if ident.sym != js_word!("import") { + self + .non_static_access + .entry(id!(ident)) + .or_default() + .push(ident.span); + } + } + _ => { + node.visit_children_with(self); + } + } + } + + fn visit_this_expr(&mut self, node: &ThisExpr, _parent: &dyn Node) { + if self.in_module_this { + self.has_cjs_exports = true; + self.static_cjs_exports = false; + self.add_bailout(node.span, BailoutReason::FreeExports); + } + } + + fn visit_assign_expr(&mut self, node: &AssignExpr, _parent: &dyn Node) { + // if rhs is a require, record static accesses + // if lhs is `exports`, mark as CJS exports re-assigned + // if lhs is `module.exports` + // if lhs is `module.exports.XXX` or `exports.XXX`, record static export + + self.in_assign = true; + node.left.visit_with(node, self); + self.in_assign = false; + node.right.visit_with(node, self); + + if let PatOrExpr::Pat(pat) = &node.left { + if has_binding_identifier(pat, &"exports".into(), &self.decls) { + // Must wrap for cases like + // ``` + // function logExports() { + // console.log(exports); + // } + // exports.test = 2; + // logExports(); + // exports = {test: 4}; + // logExports(); + // ``` + self.static_cjs_exports = false; + self.has_cjs_exports = true; + self.should_wrap = true; + self.add_bailout(node.span, BailoutReason::ExportsReassignment); + } else if has_binding_identifier(pat, &"module".into(), &self.decls) { + // Same for `module`. If it is reassigned we can't correctly statically analyze. + self.static_cjs_exports = false; + self.has_cjs_exports = true; + self.should_wrap = true; + self.add_bailout(node.span, BailoutReason::ModuleReassignment); + } + } + } + + fn visit_var_declarator(&mut self, node: &VarDeclarator, _parent: &dyn Node) { + // if init is a require call, record static accesses + if let Some(init) = &node.init { + if let Some(source) = self.match_require(init) { + self.add_pat_imports(&node.name, &source, ImportKind::Require); + return; + } + + match &**init { + Expr::Member(member) => { + if let ExprOrSuper::Expr(expr) = &member.obj { + if let Some(source) = self.match_require(&*expr) { + // Convert member expression on require to a destructuring assignment. + // const yx = require('y').x; -> const {x: yx} = require('x'); + let key = match &*member.prop { + Expr::Ident(ident) => { + if !member.computed { + PropName::Ident(ident.clone()) + } else { + PropName::Computed(ComputedPropName { + span: DUMMY_SP, + expr: Box::new(*expr.clone()), + }) + } + } + Expr::Lit(Lit::Str(str_)) => PropName::Str(str_.clone()), + _ => PropName::Computed(ComputedPropName { + span: DUMMY_SP, + expr: Box::new(*expr.clone()), + }), + }; + + self.add_pat_imports( + &Pat::Object(ObjectPat { + optional: false, + span: DUMMY_SP, + type_ann: None, + props: vec![ObjectPatProp::KeyValue(KeyValuePatProp { + key, + value: Box::new(node.name.clone()), + })], + }), + &source, + ImportKind::Require, + ); + return; + } + } + } + Expr::Await(await_exp) => { + // let x = await import('foo'); + // let {x} = await import('foo'); + if let Some(source) = match_import(&*await_exp.arg, self.ignore_mark) { + self.add_pat_imports(&node.name, &source, ImportKind::DynamicImport); + return; + } + } + _ => {} + } + } + + // This is visited via visit_module_item with is_top_level == true, it needs to be + // set to false for called visitors (and restored again). + let in_top_level = self.in_top_level; + self.in_top_level = false; + node.visit_children_with(self); + self.in_top_level = in_top_level; + } + + fn visit_call_expr(&mut self, node: &CallExpr, _parent: &dyn Node) { + if let ExprOrSuper::Expr(expr) = &node.callee { + match &**expr { + Expr::Ident(ident) => { + if ident.sym == js_word!("eval") && !self.decls.contains(&id!(ident)) { + self.should_wrap = true; + self.add_bailout(node.span, BailoutReason::Eval); + } + } + Expr::Member(member) => { + // import('foo').then(foo => ...); + if let ExprOrSuper::Expr(obj) = &member.obj { + if let Some(source) = match_import(&*obj, self.ignore_mark) { + let then: JsWord = "then".into(); + let is_then = match &*member.prop { + Expr::Ident(ident) => !member.computed && ident.sym == then, + Expr::Lit(Lit::Str(str)) => str.value == then, + _ => false, + }; + + if is_then { + if let Some(ExprOrSpread { expr, .. }) = node.args.get(0) { + let param = match &**expr { + Expr::Fn(func) => func.function.params.get(0).map(|param| ¶m.pat), + Expr::Arrow(arrow) => arrow.params.get(0), + _ => None, + }; + + if let Some(param) = param { + self.add_pat_imports(param, &source, ImportKind::DynamicImport); + } else { + self.non_static_requires.insert(source.clone()); + self.wrapped_requires.insert(source); + self.add_bailout(node.span, BailoutReason::NonStaticDynamicImport); + } + + expr.visit_with(node, self); + return; + } + } + } + } + } + _ => {} + } + } + + node.visit_children_with(self); + } +} + +impl HoistCollect { + pub fn match_require(&self, node: &Expr) -> Option { + match_require(node, &self.decls, self.ignore_mark) + } + + fn add_pat_imports(&mut self, node: &Pat, src: &JsWord, kind: ImportKind) { + if !self.in_top_level { + self.wrapped_requires.insert(src.clone()); + if kind != ImportKind::DynamicImport { + self.non_static_requires.insert(src.clone()); + let span = match node { + Pat::Ident(id) => id.id.span, + Pat::Array(arr) => arr.span, + Pat::Object(obj) => obj.span, + Pat::Rest(rest) => rest.span, + Pat::Assign(assign) => assign.span, + Pat::Invalid(i) => i.span, + Pat::Expr(_) => DUMMY_SP, + }; + self.add_bailout(span, BailoutReason::NonTopLevelRequire); + } + } + + match node { + Pat::Ident(ident) => { + // let x = require('y'); + // Need to track member accesses of `x`. + self.imports.insert( + id!(ident.id), + Import { + source: src.clone(), + specifier: "*".into(), + kind, + loc: SourceLocation::from(&self.source_map, ident.id.span), + }, + ); + } + Pat::Object(object) => { + for prop in &object.props { + match prop { + ObjectPatProp::KeyValue(kv) => { + let imported = match &kv.key { + PropName::Ident(ident) => ident.sym.clone(), + PropName::Str(str) => str.value.clone(), + _ => { + // Non-static. E.g. computed property. + self.non_static_requires.insert(src.clone()); + self.add_bailout(object.span, BailoutReason::NonStaticDestructuring); + continue; + } + }; + + match &*kv.value { + Pat::Ident(ident) => { + // let {x: y} = require('y'); + // Need to track `x` as a used symbol. + self.imports.insert( + id!(ident.id), + Import { + source: src.clone(), + specifier: imported, + kind, + loc: SourceLocation::from(&self.source_map, ident.id.span), + }, + ); + + // Mark as non-constant. CJS exports can be mutated by other modules, + // so it's not safe to reference them directly. + self + .non_const_bindings + .entry(id!(ident.id)) + .or_default() + .push(ident.id.span); + } + _ => { + // Non-static. + self.non_static_requires.insert(src.clone()); + self.add_bailout(object.span, BailoutReason::NonStaticDestructuring); + } + } + } + ObjectPatProp::Assign(assign) => { + // let {x} = require('y'); + // let {x = 2} = require('y'); + // Need to track `x` as a used symbol. + self.imports.insert( + id!(assign.key), + Import { + source: src.clone(), + specifier: assign.key.sym.clone(), + kind, + loc: SourceLocation::from(&self.source_map, assign.key.span), + }, + ); + self + .non_const_bindings + .entry(id!(assign.key)) + .or_default() + .push(assign.key.span); + } + ObjectPatProp::Rest(_rest) => { + // let {x, ...y} = require('y'); + // Non-static. We don't know what keys are used. + self.non_static_requires.insert(src.clone()); + self.add_bailout(object.span, BailoutReason::NonStaticDestructuring); + } + } + } + } + _ => { + // Non-static. + self.non_static_requires.insert(src.clone()); + let span = match node { + Pat::Ident(id) => id.id.span, + Pat::Array(arr) => arr.span, + Pat::Object(obj) => obj.span, + Pat::Rest(rest) => rest.span, + Pat::Assign(assign) => assign.span, + Pat::Invalid(i) => i.span, + Pat::Expr(_) => DUMMY_SP, + }; + self.add_bailout(span, BailoutReason::NonStaticDestructuring); + } + } + } + + pub fn get_non_const_binding_idents(&self, node: &Pat, idents: &mut Vec) { + match node { + Pat::Ident(ident) => { + if self.non_const_bindings.contains_key(&id!(ident.id)) { + idents.push(ident.id.clone()); + } + } + Pat::Object(object) => { + for prop in &object.props { + match prop { + ObjectPatProp::KeyValue(kv) => { + self.get_non_const_binding_idents(&*kv.value, idents); + } + ObjectPatProp::Assign(assign) => { + if self.non_const_bindings.contains_key(&id!(assign.key)) { + idents.push(assign.key.clone()); + } + } + ObjectPatProp::Rest(rest) => { + self.get_non_const_binding_idents(&*rest.arg, idents); + } + } + } + } + Pat::Array(array) => { + for el in array.elems.iter().flatten() { + self.get_non_const_binding_idents(el, idents); + } + } + _ => {} + } + } + + fn add_bailout(&mut self, span: Span, reason: BailoutReason) { + if let Some(bailouts) = &mut self.bailouts { + bailouts.push(Bailout { + loc: SourceLocation::from(&self.source_map, span), + reason, + }) + } + } +} + +fn has_binding_identifier(node: &Pat, sym: &JsWord, decls: &HashSet) -> bool { + match node { + Pat::Ident(ident) => { + if ident.id.sym == *sym && !decls.contains(&id!(ident.id)) { + return true; + } + } + Pat::Object(object) => { + for prop in &object.props { + match prop { + ObjectPatProp::KeyValue(kv) => { + if has_binding_identifier(&*kv.value, sym, decls) { + return true; + } + } + ObjectPatProp::Assign(assign) => { + if assign.key.sym == *sym && !decls.contains(&id!(assign.key)) { + return true; + } + } + ObjectPatProp::Rest(rest) => { + if has_binding_identifier(&*rest.arg, sym, decls) { + return true; + } + } + } + } + } + Pat::Array(array) => { + for el in array.elems.iter().flatten() { + if has_binding_identifier(el, sym, decls) { + return true; + } + } + } + _ => {} + } + + false +} diff --git a/packages/transformers/js/core/src/lib.rs b/packages/transformers/js/core/src/lib.rs index 09a0a472650..8e831127f26 100644 --- a/packages/transformers/js/core/src/lib.rs +++ b/packages/transformers/js/core/src/lib.rs @@ -18,6 +18,7 @@ mod env_replacer; mod fs; mod global_replacer; mod hoist; +mod hoist_collect; mod modules; mod utils; @@ -29,9 +30,10 @@ use path_slash::PathExt; use serde::{Deserialize, Serialize}; use swc_common::comments::SingleThreadedComments; use swc_common::errors::{DiagnosticBuilder, Emitter, Handler}; +use swc_common::DUMMY_SP; use swc_common::{chain, sync::Lrc, FileName, Globals, Mark, SourceMap}; use swc_ecma_preset_env::{preset_env, Mode::Entry, Targets, Version, Versions}; -use swc_ecmascript::ast::Module; +use swc_ecmascript::ast::{Invalid, Module}; use swc_ecmascript::codegen::text_writer::JsWriter; use swc_ecmascript::parser::lexer::Lexer; use swc_ecmascript::parser::{EsConfig, PResult, Parser, StringInput, Syntax, TsConfig}; @@ -41,7 +43,7 @@ use swc_ecmascript::transforms::{ optimization::simplify::dead_branch_remover, optimization::simplify::expr_simplifier, pass::Optional, proposals::decorators, react, typescript, }; -use swc_ecmascript::visit::FoldWith; +use swc_ecmascript::visit::{FoldWith, VisitWith}; use decl_collector::*; use dependency_collector::*; @@ -49,6 +51,7 @@ use env_replacer::*; use fs::inline_fs; use global_replacer::GlobalReplacer; use hoist::hoist; +use hoist_collect::HoistCollect; use modules::esm2cjs; use utils::{CodeHighlight, Diagnostic, DiagnosticSeverity, SourceLocation, SourceType}; @@ -414,6 +417,20 @@ pub fn transform(config: Config) -> Result { } } } else { + let mut symbols_collect = HoistCollect::new( + source_map.clone(), + decls, + Mark::fresh(Mark::root()), + global_mark, + false, + ); + module.visit_with(&Invalid { span: DUMMY_SP } as _, &mut symbols_collect); + + println!( + "{} {:?} {:?}\n\n", + config.filename, symbols_collect.imports, symbols_collect.exports + ); + let (module, needs_helpers) = esm2cjs(module, versions); result.needs_esm_helpers = needs_helpers; module diff --git a/packages/transformers/js/core/src/utils.rs b/packages/transformers/js/core/src/utils.rs index c2ec6dac2ae..83e2758c9ce 100644 --- a/packages/transformers/js/core/src/utils.rs +++ b/packages/transformers/js/core/src/utils.rs @@ -327,3 +327,12 @@ macro_rules! fold_member_expr_skip_prop { } }; } + +#[macro_export] +macro_rules! id { + ($ident: expr) => { + ($ident.sym.clone(), $ident.span.ctxt) + }; +} + +pub type IdentId = (JsWord, SyntaxContext); From 3450db414fe31d1a1eac2457a0457a5c91b65f66 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Sat, 30 Oct 2021 18:02:29 +0200 Subject: [PATCH 06/13] Store full export information in HoistCollect --- packages/transformers/js/core/src/hoist.rs | 13 +- .../transformers/js/core/src/hoist_collect.rs | 192 +++++++++++++++--- packages/transformers/js/core/src/lib.rs | 19 +- 3 files changed, 187 insertions(+), 37 deletions(-) diff --git a/packages/transformers/js/core/src/hoist.rs b/packages/transformers/js/core/src/hoist.rs index 66b82aff186..028d47da0eb 100644 --- a/packages/transformers/js/core/src/hoist.rs +++ b/packages/transformers/js/core/src/hoist.rs @@ -7,7 +7,7 @@ use swc_common::{sync::Lrc, Mark, Span, SyntaxContext, DUMMY_SP}; use swc_ecmascript::ast::*; use swc_ecmascript::visit::{Fold, FoldWith, VisitWith}; -use crate::hoist_collect::{HoistCollect, Import, ImportKind}; +use crate::hoist_collect::{Export, HoistCollect, Import, ImportKind}; use crate::id; use crate::utils::{ match_import, match_member_expr, match_require, CodeHighlight, Diagnostic, DiagnosticSeverity, @@ -272,7 +272,10 @@ impl<'a> Fold for Hoist<'a> { id.0 } else { self - .get_export_ident(DUMMY_SP, self.collect.exports.get(&id).unwrap()) + .get_export_ident( + DUMMY_SP, + &self.collect.exports.get(&id).unwrap().specifier, + ) .sym }; self.exported_symbols.push(ExportedSymbol { @@ -818,7 +821,11 @@ impl<'a> Fold for Hoist<'a> { } } - if let Some(exported) = self.collect.exports.get(&id!(node)) { + if let Some(Export { + specifier: exported, + .. + }) = self.collect.exports.get(&id!(node)) + { // If wrapped, mark the original symbol as exported. // Otherwise replace with an export identifier. if self.collect.should_wrap { diff --git a/packages/transformers/js/core/src/hoist_collect.rs b/packages/transformers/js/core/src/hoist_collect.rs index 8082c9f7cbb..018becf0891 100644 --- a/packages/transformers/js/core/src/hoist_collect.rs +++ b/packages/transformers/js/core/src/hoist_collect.rs @@ -1,3 +1,4 @@ +use serde::Serialize; use std::collections::{HashMap, HashSet}; use swc_atoms::JsWord; use swc_common::{sync::Lrc, Mark, Span, SyntaxContext, DUMMY_SP}; @@ -9,7 +10,7 @@ use crate::utils::{ match_import, match_member_expr, match_require, Bailout, BailoutReason, IdentId, SourceLocation, }; -#[derive(Debug, PartialEq, Clone, Copy)] +#[derive(Debug, PartialEq, Clone, Copy, Serialize)] pub enum ImportKind { Require, Import, @@ -24,6 +25,13 @@ pub struct Import { pub loc: SourceLocation, } +#[derive(Debug)] +pub struct Export { + pub source: Option, + pub specifier: JsWord, + pub loc: SourceLocation, +} + pub struct HoistCollect { pub source_map: Lrc, pub decls: HashSet, @@ -34,7 +42,8 @@ pub struct HoistCollect { pub is_esm: bool, pub should_wrap: bool, pub imports: HashMap, - pub exports: HashMap, + pub exports: HashMap, + pub exports_all: HashMap, pub non_static_access: HashMap>, pub non_const_bindings: HashMap>, pub non_static_requires: HashSet, @@ -47,6 +56,36 @@ pub struct HoistCollect { in_assign: bool, } +#[derive(Debug, Serialize)] +struct ImportedSymbol { + source: JsWord, + local: JsWord, + imported: JsWord, + loc: SourceLocation, + kind: ImportKind, +} + +#[derive(Debug, Serialize)] +struct ExportedSymbol { + source: Option, + local: JsWord, + exported: JsWord, + loc: SourceLocation, +} + +#[derive(Debug, Serialize)] +struct ExportedAll { + source: JsWord, + loc: SourceLocation, +} + +#[derive(Serialize, Debug)] +pub struct HoistCollectResult { + imports: Vec, + exports: Vec, + exports_all: Vec, +} + impl HoistCollect { pub fn new( source_map: Lrc, @@ -66,6 +105,7 @@ impl HoistCollect { should_wrap: false, imports: HashMap::new(), exports: HashMap::new(), + exports_all: HashMap::new(), non_static_access: HashMap::new(), non_const_bindings: HashMap::new(), non_static_requires: HashSet::new(), @@ -80,6 +120,58 @@ impl HoistCollect { } } +impl From for HoistCollectResult { + fn from(collect: HoistCollect) -> HoistCollectResult { + HoistCollectResult { + imports: collect + .imports + .into_iter() + .map( + |( + local, + Import { + source, + specifier, + loc, + kind, + }, + )| ImportedSymbol { + source, + local: local.0, + imported: specifier, + loc, + kind, + }, + ) + .collect(), + exports: collect + .exports + .into_iter() + .map( + |( + local, + Export { + source, + specifier, + loc, + }, + )| ExportedSymbol { + source, + local: local.0, + exported: specifier, + loc, + }, + ) + .collect(), + exports_all: collect + .exports_all + .into_iter() + .map(|(source, loc)| ExportedAll { source, loc }) + .collect(), + } + } +} + macro_rules! collect_visit_fn { ($name:ident, $type:ident) => { fn $name(&mut self, node: &$type, _parent: &dyn Node) { @@ -206,30 +298,33 @@ impl Visit for HoistCollect { } fn visit_named_export(&mut self, node: &NamedExport, _parent: &dyn Node) { - if node.src.is_some() { - return; - } - for specifier in &node.specifiers { + let source = node.src.as_ref().map(|s| s.value.clone()); match specifier { ExportSpecifier::Named(named) => { let exported = match &named.exported { - Some(exported) => exported.sym.clone(), - None => named.orig.sym.clone(), + Some(exported) => exported.clone(), + None => named.orig.clone(), }; - self.exports.entry(id!(named.orig)).or_insert(exported); + self.exports.entry(id!(named.orig)).or_insert(Export { + specifier: exported.sym, + loc: SourceLocation::from(&self.source_map, exported.span), + source, + }); } ExportSpecifier::Default(default) => { - self - .exports - .entry(id!(default.exported)) - .or_insert(js_word!("default")); + self.exports.entry(id!(default.exported)).or_insert(Export { + specifier: js_word!("default"), + loc: SourceLocation::from(&self.source_map, default.exported.span), + source, + }); } ExportSpecifier::Namespace(namespace) => { - self - .exports - .entry(id!(namespace.name)) - .or_insert_with(|| "*".into()); + self.exports.entry(id!(namespace.name)).or_insert(Export { + specifier: "*".into(), + loc: SourceLocation::from(&self.source_map, namespace.span), + source, + }); } } } @@ -238,12 +333,24 @@ impl Visit for HoistCollect { fn visit_export_decl(&mut self, node: &ExportDecl, _parent: &dyn Node) { match &node.decl { Decl::Class(class) => { - self - .exports - .insert(id!(class.ident), class.ident.sym.clone()); + self.exports.insert( + id!(class.ident), + Export { + specifier: class.ident.sym.clone(), + loc: SourceLocation::from(&self.source_map, class.ident.span), + source: None, + }, + ); } Decl::Fn(func) => { - self.exports.insert(id!(func.ident), func.ident.sym.clone()); + self.exports.insert( + id!(func.ident), + Export { + specifier: func.ident.sym.clone(), + loc: SourceLocation::from(&self.source_map, func.ident.span), + source: None, + }, + ); } Decl::Var(var) => { for decl in &var.decls { @@ -264,12 +371,26 @@ impl Visit for HoistCollect { match &node.decl { DefaultDecl::Class(class) => { if let Some(ident) = &class.ident { - self.exports.insert(id!(ident), "default".into()); + self.exports.insert( + id!(ident), + Export { + specifier: "default".into(), + loc: SourceLocation::from(&self.source_map, node.span), + source: None, + }, + ); } } DefaultDecl::Fn(func) => { if let Some(ident) = &func.ident { - self.exports.insert(id!(ident), "default".into()); + self.exports.insert( + id!(ident), + Export { + specifier: "default".into(), + loc: SourceLocation::from(&self.source_map, node.span), + source: None, + }, + ); } } _ => { @@ -280,6 +401,13 @@ impl Visit for HoistCollect { node.visit_children_with(self); } + fn visit_export_all(&mut self, node: &ExportAll, _parent: &dyn Node) { + self.exports_all.insert( + node.src.value.clone(), + SourceLocation::from(&self.source_map, node.span), + ); + } + fn visit_return_stmt(&mut self, node: &ReturnStmt, _parent: &dyn Node) { if !self.in_function { self.should_wrap = true; @@ -291,7 +419,14 @@ impl Visit for HoistCollect { fn visit_binding_ident(&mut self, node: &BindingIdent, _parent: &dyn Node) { if self.in_export_decl { - self.exports.insert(id!(node.id), node.id.sym.clone()); + self.exports.insert( + id!(node.id), + Export { + specifier: node.id.sym.clone(), + loc: SourceLocation::from(&self.source_map, node.id.span), + source: None, + }, + ); } if self.in_assign && node.id.span.ctxt() == self.global_ctxt { @@ -305,7 +440,14 @@ impl Visit for HoistCollect { fn visit_assign_pat_prop(&mut self, node: &AssignPatProp, _parent: &dyn Node) { if self.in_export_decl { - self.exports.insert(id!(node.key), node.key.sym.clone()); + self.exports.insert( + id!(node.key), + Export { + specifier: node.key.sym.clone(), + loc: SourceLocation::from(&self.source_map, node.key.span), + source: None, + }, + ); } if self.in_assign && node.key.span.ctxt() == self.global_ctxt { diff --git a/packages/transformers/js/core/src/lib.rs b/packages/transformers/js/core/src/lib.rs index 8e831127f26..0410325cce2 100644 --- a/packages/transformers/js/core/src/lib.rs +++ b/packages/transformers/js/core/src/lib.rs @@ -50,8 +50,8 @@ use dependency_collector::*; use env_replacer::*; use fs::inline_fs; use global_replacer::GlobalReplacer; -use hoist::hoist; -use hoist_collect::HoistCollect; +use hoist::{hoist, HoistResult}; +use hoist_collect::{HoistCollect, HoistCollectResult}; use modules::esm2cjs; use utils::{CodeHighlight, Diagnostic, DiagnosticSeverity, SourceLocation, SourceType}; @@ -89,14 +89,15 @@ pub struct Config { trace_bailouts: bool, } -#[derive(Serialize, Debug, Deserialize, Default)] +#[derive(Serialize, Debug, Default)] pub struct TransformResult { #[serde(with = "serde_bytes")] code: Vec, map: Option, shebang: Option, dependencies: Vec, - hoist_result: Option, + hoist_result: Option, + symbol_result: Option, diagnostics: Option>, needs_esm_helpers: bool, used_env: HashSet, @@ -422,14 +423,14 @@ pub fn transform(config: Config) -> Result { decls, Mark::fresh(Mark::root()), global_mark, - false, + config.trace_bailouts, ); module.visit_with(&Invalid { span: DUMMY_SP } as _, &mut symbols_collect); - println!( - "{} {:?} {:?}\n\n", - config.filename, symbols_collect.imports, symbols_collect.exports - ); + if let Some(bailouts) = &symbols_collect.bailouts { + diagnostics.extend(bailouts.iter().map(|bailout| bailout.to_diagnostic())); + } + result.symbol_result = Some(symbols_collect.into()); let (module, needs_helpers) = esm2cjs(module, versions); result.needs_esm_helpers = needs_helpers; From 45b4502884a62cfeae059eb14ee171e2dfb7acea Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Sat, 30 Oct 2021 19:34:57 +0200 Subject: [PATCH 07/13] Register symbols --- packages/transformers/js/src/JSTransformer.js | 64 ++++++++++++++++--- 1 file changed, 54 insertions(+), 10 deletions(-) diff --git a/packages/transformers/js/src/JSTransformer.js b/packages/transformers/js/src/JSTransformer.js index cd16019b0e2..a5f42f130d5 100644 --- a/packages/transformers/js/src/JSTransformer.js +++ b/packages/transformers/js/src/JSTransformer.js @@ -367,6 +367,7 @@ export default (new Transformer({ map, shebang, hoist_result, + symbol_result, needs_esm_helpers, diagnostics, used_env, @@ -768,17 +769,60 @@ export default (new Transformer({ asset.meta.hasCJSExports = hoist_result.has_cjs_exports; asset.meta.staticExports = hoist_result.static_cjs_exports; asset.meta.shouldWrap = hoist_result.should_wrap; - } else if (needs_esm_helpers) { - asset.addDependency({ - specifier: '@parcel/transformer-js/src/esmodule-helpers.js', - specifierType: 'esm', - resolveFrom: __filename, - env: { - includeNodeModules: { - '@parcel/transformer-js': true, + } else { + if (symbol_result) { + let deps = new Map( + asset + .getDependencies() + .map(dep => [dep.meta.placeholder ?? dep.specifier, dep]), + ); + for (let dep of deps.values()) { + dep.symbols.ensure(); + } + asset.symbols.ensure(); + + for (let {exported, local, loc, source} of symbol_result.exports) { + let dep = source ? deps.get(source) : undefined; + asset.symbols.set( + exported, + `${dep?.id ?? ''}$${local}`, + convertLoc(loc), + ); + if (dep != null) { + dep.symbols.set( + local, + `${dep?.id ?? ''}$${local}`, + convertLoc(loc), + true, + ); + } + } + + for (let {source, local, imported, loc} of symbol_result.imports) { + let dep = deps.get(source); + if (!dep) continue; + dep.symbols.set(imported, local, convertLoc(loc)); + } + + for (let {source, loc} of symbol_result.exports_all) { + let dep = deps.get(source); + if (!dep) continue; + dep.symbols.set('*', '*', convertLoc(loc), true); + } + } + + if (needs_esm_helpers) { + asset.addDependency({ + specifier: '@parcel/transformer-js/src/esmodule-helpers.js', + specifierType: 'esm', + resolveFrom: __filename, + env: { + includeNodeModules: { + '@parcel/transformer-js': true, + }, }, - }, - }); + }); + } } asset.type = 'js'; From b133c89c33939017743c6ebc29c4333339534243 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Sat, 30 Oct 2021 23:41:44 +0200 Subject: [PATCH 08/13] Reverse collect.exports mapping --- packages/transformers/js/core/src/hoist.rs | 10 +- .../transformers/js/core/src/hoist_collect.rs | 102 +++++++++++++----- 2 files changed, 78 insertions(+), 34 deletions(-) diff --git a/packages/transformers/js/core/src/hoist.rs b/packages/transformers/js/core/src/hoist.rs index 028d47da0eb..93484e86ecc 100644 --- a/packages/transformers/js/core/src/hoist.rs +++ b/packages/transformers/js/core/src/hoist.rs @@ -7,7 +7,7 @@ use swc_common::{sync::Lrc, Mark, Span, SyntaxContext, DUMMY_SP}; use swc_ecmascript::ast::*; use swc_ecmascript::visit::{Fold, FoldWith, VisitWith}; -use crate::hoist_collect::{Export, HoistCollect, Import, ImportKind}; +use crate::hoist_collect::{HoistCollect, Import, ImportKind}; use crate::id; use crate::utils::{ match_import, match_member_expr, match_require, CodeHighlight, Diagnostic, DiagnosticSeverity, @@ -274,7 +274,7 @@ impl<'a> Fold for Hoist<'a> { self .get_export_ident( DUMMY_SP, - &self.collect.exports.get(&id).unwrap().specifier, + self.collect.exports_locals.get(&id.0).unwrap(), ) .sym }; @@ -821,11 +821,7 @@ impl<'a> Fold for Hoist<'a> { } } - if let Some(Export { - specifier: exported, - .. - }) = self.collect.exports.get(&id!(node)) - { + if let Some(exported) = self.collect.exports_locals.get(&node.sym) { // If wrapped, mark the original symbol as exported. // Otherwise replace with an export identifier. if self.collect.should_wrap { diff --git a/packages/transformers/js/core/src/hoist_collect.rs b/packages/transformers/js/core/src/hoist_collect.rs index 018becf0891..d1524510b32 100644 --- a/packages/transformers/js/core/src/hoist_collect.rs +++ b/packages/transformers/js/core/src/hoist_collect.rs @@ -41,8 +41,12 @@ pub struct HoistCollect { pub has_cjs_exports: bool, pub is_esm: bool, pub should_wrap: bool, + // local name -> descriptor pub imports: HashMap, - pub exports: HashMap, + // exported name -> descriptor + pub exports: HashMap, + // local name -> exported name + pub exports_locals: HashMap, pub exports_all: HashMap, pub non_static_access: HashMap>, pub non_const_bindings: HashMap>, @@ -105,6 +109,7 @@ impl HoistCollect { should_wrap: false, imports: HashMap::new(), exports: HashMap::new(), + exports_locals: HashMap::new(), exports_all: HashMap::new(), non_static_access: HashMap::new(), non_const_bindings: HashMap::new(), @@ -149,7 +154,7 @@ impl From for HoistCollectResult { .into_iter() .map( |( - local, + exported, Export { source, specifier, @@ -157,8 +162,8 @@ impl From for HoistCollectResult { }, )| ExportedSymbol { source, - local: local.0, - exported: specifier, + local: specifier, + exported, loc, }, ) @@ -306,25 +311,44 @@ impl Visit for HoistCollect { Some(exported) => exported.clone(), None => named.orig.clone(), }; - self.exports.entry(id!(named.orig)).or_insert(Export { - specifier: exported.sym, - loc: SourceLocation::from(&self.source_map, exported.span), - source, - }); + self.exports.insert( + exported.sym.clone(), + Export { + specifier: named.orig.sym.clone(), + loc: SourceLocation::from(&self.source_map, exported.span), + source, + }, + ); + self + .exports_locals + .entry(named.orig.sym.clone()) + .or_insert_with(|| exported.sym.clone()); } ExportSpecifier::Default(default) => { - self.exports.entry(id!(default.exported)).or_insert(Export { - specifier: js_word!("default"), - loc: SourceLocation::from(&self.source_map, default.exported.span), - source, - }); + self.exports.insert( + js_word!("default"), + Export { + specifier: default.exported.sym.clone(), + loc: SourceLocation::from(&self.source_map, default.exported.span), + source, + }, + ); + self + .exports_locals + .entry(default.exported.sym.clone()) + .or_insert_with(|| js_word!("default")); } ExportSpecifier::Namespace(namespace) => { - self.exports.entry(id!(namespace.name)).or_insert(Export { - specifier: "*".into(), - loc: SourceLocation::from(&self.source_map, namespace.span), - source, - }); + self.exports.insert( + namespace.name.sym.clone(), + Export { + specifier: "*".into(), + loc: SourceLocation::from(&self.source_map, namespace.span), + source, + }, + ); + // Populating exports_locals with * doesn't make any sense at all + // and hoist doesn't use this anyway. } } } @@ -334,23 +358,31 @@ impl Visit for HoistCollect { match &node.decl { Decl::Class(class) => { self.exports.insert( - id!(class.ident), + class.ident.sym.clone(), Export { specifier: class.ident.sym.clone(), loc: SourceLocation::from(&self.source_map, class.ident.span), source: None, }, ); + self + .exports_locals + .entry(class.ident.sym.clone()) + .or_insert_with(|| class.ident.sym.clone()); } Decl::Fn(func) => { self.exports.insert( - id!(func.ident), + func.ident.sym.clone(), Export { specifier: func.ident.sym.clone(), loc: SourceLocation::from(&self.source_map, func.ident.span), source: None, }, ); + self + .exports_locals + .entry(func.ident.sym.clone()) + .or_insert_with(|| func.ident.sym.clone()); } Decl::Var(var) => { for decl in &var.decls { @@ -372,25 +404,33 @@ impl Visit for HoistCollect { DefaultDecl::Class(class) => { if let Some(ident) = &class.ident { self.exports.insert( - id!(ident), + "default".into(), Export { - specifier: "default".into(), + specifier: ident.sym.clone(), loc: SourceLocation::from(&self.source_map, node.span), source: None, }, ); + self + .exports_locals + .entry(ident.sym.clone()) + .or_insert_with(|| "default".into()); } } DefaultDecl::Fn(func) => { if let Some(ident) = &func.ident { self.exports.insert( - id!(ident), + "default".into(), Export { - specifier: "default".into(), + specifier: ident.sym.clone(), loc: SourceLocation::from(&self.source_map, node.span), source: None, }, ); + self + .exports_locals + .entry(ident.sym.clone()) + .or_insert_with(|| "default".into()); } } _ => { @@ -420,13 +460,17 @@ impl Visit for HoistCollect { fn visit_binding_ident(&mut self, node: &BindingIdent, _parent: &dyn Node) { if self.in_export_decl { self.exports.insert( - id!(node.id), + node.id.sym.clone(), Export { specifier: node.id.sym.clone(), loc: SourceLocation::from(&self.source_map, node.id.span), source: None, }, ); + self + .exports_locals + .entry(node.id.sym.clone()) + .or_insert_with(|| node.id.sym.clone()); } if self.in_assign && node.id.span.ctxt() == self.global_ctxt { @@ -441,13 +485,17 @@ impl Visit for HoistCollect { fn visit_assign_pat_prop(&mut self, node: &AssignPatProp, _parent: &dyn Node) { if self.in_export_decl { self.exports.insert( - id!(node.key), + node.key.sym.clone(), Export { specifier: node.key.sym.clone(), loc: SourceLocation::from(&self.source_map, node.key.span), source: None, }, ); + self + .exports_locals + .entry(node.key.sym.clone()) + .or_insert_with(|| node.key.sym.clone()); } if self.in_assign && node.key.span.ctxt() == self.global_ctxt { From 8ed98194a5d8a5adafaba87131be2bf2327765a9 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Sun, 31 Oct 2021 11:39:44 +0100 Subject: [PATCH 09/13] Add tests --- .../core/integration-tests/test/javascript.js | 1078 ++++++++++++++++- .../integration-tests/test/scope-hoisting.js | 864 +------------ packages/core/test-utils/src/utils.js | 2 +- 3 files changed, 1092 insertions(+), 852 deletions(-) diff --git a/packages/core/integration-tests/test/javascript.js b/packages/core/integration-tests/test/javascript.js index 361d406a781..45fa45996c6 100644 --- a/packages/core/integration-tests/test/javascript.js +++ b/packages/core/integration-tests/test/javascript.js @@ -2,8 +2,12 @@ import assert from 'assert'; import path from 'path'; import url from 'url'; import { + assertDependencyWasExcluded, bundle, bundler, + findAsset, + findDependency, + getNextBuild, run, runBundle, runBundles, @@ -18,6 +22,7 @@ import { import {makeDeferredWithPromise, normalizePath} from '@parcel/utils'; import vm from 'vm'; import Logger from '@parcel/logger'; +import nullthrows from 'nullthrows'; describe('javascript', function () { beforeEach(async () => { @@ -5736,7 +5741,7 @@ describe('javascript', function () { assert.deepEqual(calls, ['common', 'deep']); }); - it('supports deferring unused ESM imports with sideEffects: false', async function() { + it('supports deferring unused ESM imports with sideEffects: false', async function () { let b = await bundle( path.join(__dirname, '/integration/side-effects-false/import.js'), ); @@ -5756,7 +5761,7 @@ describe('javascript', function () { assert.strictEqual(output.default, 4); }); - it('supports ESM imports and requires with sideEffects: false', async function() { + it('supports ESM imports and requires with sideEffects: false', async function () { let b = await bundle( path.join(__dirname, '/integration/side-effects-false/import-require.js'), ); @@ -5767,4 +5772,1073 @@ describe('javascript', function () { assert.strictEqual(output.default, '4returned from bar'); }); + + for (let shouldScopeHoist of [false, true]) { + let options = { + defaultTargetOptions: { + shouldScopeHoist, + }, + }; + let usesSymbolPropagation = shouldScopeHoist; + describe(`sideEffects: false with${ + shouldScopeHoist ? '' : 'out' + } scope-hoisting`, function () { + if (usesSymbolPropagation) { + it('supports excluding unused CSS imports', async function () { + let b = await bundle( + path.join( + __dirname, + '/integration/scope-hoisting/es6/side-effects-css/index.html', + ), + options, + ); + + assertBundles(b, [ + { + name: 'index.html', + assets: ['index.html'], + }, + { + type: 'js', + assets: ['index.js', 'a.js', 'b1.js'], + }, + { + type: 'css', + assets: ['b1.css'], + }, + ]); + + let calls = []; + let res = await run( + b, + { + output: null, + sideEffect: caller => { + calls.push(caller); + }, + }, + {require: false}, + ); + assert.deepEqual(calls, ['b1']); + assert.deepEqual(res.output, 2); + + let css = await outputFS.readFile( + b.getBundles().find(bundle => bundle.type === 'css').filePath, + 'utf8', + ); + assert(!css.includes('.b2')); + }); + + it("doesn't create new bundles for dynamic imports in excluded assets", async function () { + let b = await bundle( + path.join( + __dirname, + '/integration/scope-hoisting/es6/side-effects-no-new-bundle/index.html', + ), + options, + ); + + assertBundles(b, [ + { + name: 'index.html', + assets: ['index.html'], + }, + { + type: 'js', + assets: ['index.js', 'a.js', 'b1.js'], + }, + ]); + + let calls = []; + let res = await run( + b, + { + output: null, + sideEffect: caller => { + calls.push(caller); + }, + }, + {require: false}, + ); + assert.deepEqual(calls, ['b1']); + assert.deepEqual(res.output, 2); + }); + } + + it('supports deferring unused ES6 re-exports (namespace used)', async function () { + let b = await bundle( + path.join( + __dirname, + '/integration/scope-hoisting/es6/side-effects-re-exports/a.js', + ), + options, + ); + + assertDependencyWasExcluded(b, 'index.js', './message2.js'); + if (usesSymbolPropagation) { + // TODO this only excluded, but should be deferred. + assert(!findAsset(b, 'message3.js')); + } + + let calls = []; + let res = await run( + b, + { + sideEffect: caller => { + calls.push(caller); + }, + }, + {require: false}, + ); + + assert.deepEqual( + calls, + shouldScopeHoist ? ['message1'] : ['message1', 'message3', 'index'], + ); + assert.deepEqual(res.output, 'Message 1'); + }); + + it('supports deferring an unused ES6 re-export (wildcard, empty, unused)', async function () { + let b = await bundle( + path.join( + __dirname, + '/integration/scope-hoisting/es6/side-effects-re-exports-all-empty/a.js', + ), + options, + ); + + if (usesSymbolPropagation) { + assertDependencyWasExcluded(b, 'index.js', './empty.js'); + } + + assert.deepEqual((await run(b, null, {require: false})).output, 123); + }); + + it('supports deferring unused ES6 re-exports (reexport named used)', async function () { + let b = await bundle( + path.join( + __dirname, + '/integration/scope-hoisting/es6/side-effects-re-exports/b.js', + ), + options, + ); + + if (usesSymbolPropagation) { + assert(!findAsset(b, 'message1.js')); + assert(!findAsset(b, 'message3.js')); + } + + let calls = []; + let res = await run( + b, + { + sideEffect: caller => { + calls.push(caller); + }, + }, + {require: false}, + ); + + assert.deepEqual( + calls, + shouldScopeHoist + ? ['message2'] + : ['message1', 'message2', 'message3', 'index'], + ); + assert.deepEqual(res.output, 'Message 2'); + }); + + it('supports deferring unused ES6 re-exports (namespace rename used)', async function () { + let b = await bundle( + path.join( + __dirname, + '/integration/scope-hoisting/es6/side-effects-re-exports/c.js', + ), + options, + ); + + if (usesSymbolPropagation) { + assert(!findAsset(b, 'message1.js')); + } + assertDependencyWasExcluded(b, 'index.js', './message2.js'); + + let calls = []; + let res = await run( + b, + { + sideEffect: caller => { + calls.push(caller); + }, + }, + {require: false}, + ); + + assert.deepEqual( + calls, + shouldScopeHoist ? ['message3'] : ['message1', 'message3', 'index'], + ); + assert.deepEqual(res.output, {default: 'Message 3'}); + }); + + it('supports deferring unused ES6 re-exports (direct export used)', async function () { + let b = await bundle( + path.join( + __dirname, + '/integration/scope-hoisting/es6/side-effects-re-exports/d.js', + ), + options, + ); + + assertDependencyWasExcluded(b, 'index.js', './message2.js'); + if (usesSymbolPropagation) { + assert(!findAsset(b, 'message1.js')); + assert(!findAsset(b, 'message3.js')); + } + + let calls = []; + let res = await run( + b, + { + sideEffect: caller => { + calls.push(caller); + }, + }, + {require: false}, + ); + + assert.deepEqual( + calls, + shouldScopeHoist ? ['index'] : ['message1', 'message3', 'index'], + ); + assert.deepEqual(res.output, 'Message 4'); + }); + + it('supports chained ES6 re-exports', async function () { + let b = await bundle( + path.join( + __dirname, + '/integration/scope-hoisting/es6/side-effects-re-exports-chained/index.js', + ), + options, + ); + + if (usesSymbolPropagation) { + assert(!findAsset(b, 'bar.js')); + } + + let calls = []; + let res = await run( + b, + { + sideEffect: caller => { + calls.push(caller); + }, + }, + {require: false}, + ); + + assert.deepEqual( + calls, + shouldScopeHoist + ? ['key', 'foo', 'index'] + : ['key', 'foo', 'bar', 'types', 'index'], + ); + assert.deepEqual(res.output, ['key', 'foo']); + }); + + it('should not optimize away an unused ES6 re-export and an used import', async function () { + let b = await bundle( + path.join( + __dirname, + '/integration/scope-hoisting/es6/side-effects-re-exports-import/a.js', + ), + options, + ); + + let res = await run(b, null, {require: false}); + assert.deepEqual(res.output, 123); + }); + + it('should not optimize away an unused ES6 re-export and an used import (different symbols)', async function () { + let b = await bundle( + path.join( + __dirname, + '/integration/scope-hoisting/es6/side-effects-re-exports-import-different/a.js', + ), + options, + ); + + let res = await run(b, null, {require: false}); + assert.deepEqual(res.output, 123); + }); + + it('correctly handles ES6 re-exports in library mode entries', async function () { + let b = await bundle( + path.join( + __dirname, + '/integration/scope-hoisting/es6/side-effects-re-exports-library/a.js', + ), + options, + ); + + let contents = await outputFS.readFile( + path.join( + __dirname, + '/integration/scope-hoisting/es6/side-effects-re-exports-library/build.js', + ), + 'utf8', + ); + assert(!contents.includes('console.log')); + + let res = await run(b); + assert.deepEqual(res, {c1: 'foo'}); + }); + + if (shouldScopeHoist) { + it('correctly updates deferred assets that are reexported', async function () { + let testDir = path.join( + __dirname, + '/integration/scope-hoisting/es6/side-effects-update-deferred-reexported', + ); + + let b = bundler(path.join(testDir, 'index.js'), { + inputFS: overlayFS, + outputFS: overlayFS, + ...options, + }); + + let subscription = await b.watch(); + + let bundleEvent = await getNextBuild(b); + assert(bundleEvent.type === 'buildSuccess'); + let output = await run(bundleEvent.bundleGraph); + assert.deepEqual(output, '12345hello'); + + await overlayFS.mkdirp(path.join(testDir, 'node_modules', 'foo')); + await overlayFS.copyFile( + path.join(testDir, 'node_modules', 'foo', 'foo_updated.js'), + path.join(testDir, 'node_modules', 'foo', 'foo.js'), + ); + + bundleEvent = await getNextBuild(b); + assert(bundleEvent.type === 'buildSuccess'); + output = await run(bundleEvent.bundleGraph); + assert.deepEqual(output, '1234556789'); + + await subscription.unsubscribe(); + }); + + it('correctly updates deferred assets that are reexported and imported directly', async function () { + let testDir = path.join( + __dirname, + '/integration/scope-hoisting/es6/side-effects-update-deferred-direct', + ); + + let b = bundler(path.join(testDir, 'index.js'), { + inputFS: overlayFS, + outputFS: overlayFS, + ...options, + }); + + let subscription = await b.watch(); + + let bundleEvent = await getNextBuild(b); + assert(bundleEvent.type === 'buildSuccess'); + let output = await run(bundleEvent.bundleGraph); + assert.deepEqual(output, '12345hello'); + + await overlayFS.mkdirp(path.join(testDir, 'node_modules', 'foo')); + await overlayFS.copyFile( + path.join(testDir, 'node_modules', 'foo', 'foo_updated.js'), + path.join(testDir, 'node_modules', 'foo', 'foo.js'), + ); + + bundleEvent = await getNextBuild(b); + assert(bundleEvent.type === 'buildSuccess'); + output = await run(bundleEvent.bundleGraph); + assert.deepEqual(output, '1234556789'); + + await subscription.unsubscribe(); + }); + + it('removes deferred reexports when imported from multiple asssets', async function () { + let b = await bundle( + path.join( + __dirname, + '/integration/scope-hoisting/es6/side-effects-re-exports-multiple-dynamic/a.js', + ), + options, + ); + + let contents = await outputFS.readFile( + b.getBundles()[0].filePath, + 'utf8', + ); + + assert(!contents.includes('$import$')); + assert(contents.includes('= 1234;')); + assert(!contents.includes('= 5678;')); + + let output = await run(b); + assert.deepEqual(output, [1234, {default: 1234}]); + }); + } + + it('keeps side effects by default', async function () { + let b = await bundle( + path.join( + __dirname, + '/integration/scope-hoisting/es6/side-effects/a.js', + ), + options, + ); + + let called = false; + let res = await run( + b, + { + sideEffect: () => { + called = true; + }, + }, + {require: false}, + ); + + assert(called, 'side effect not called'); + assert.deepEqual(res.output, 4); + }); + + it('supports the package.json sideEffects: false flag', async function () { + let b = await bundle( + path.join( + __dirname, + '/integration/scope-hoisting/es6/side-effects-false/a.js', + ), + options, + ); + + let called = false; + let res = await run( + b, + { + sideEffect: () => { + called = true; + }, + }, + {require: false}, + ); + + assert(!called, 'side effect called'); + assert.deepEqual(res.output, 4); + }); + + it('supports removing a deferred dependency', async function () { + let testDir = path.join( + __dirname, + '/integration/scope-hoisting/es6/side-effects-false', + ); + + let b = bundler(path.join(testDir, 'a.js'), { + inputFS: overlayFS, + outputFS: overlayFS, + ...options, + }); + + let subscription = await b.watch(); + + try { + let bundleEvent = await getNextBuild(b); + assert.strictEqual(bundleEvent.type, 'buildSuccess'); + let called = false; + let res = await run( + bundleEvent.bundleGraph, + { + sideEffect: () => { + called = true; + }, + }, + {require: false}, + ); + assert(!called, 'side effect called'); + assert.deepEqual(res.output, 4); + assertDependencyWasExcluded( + bundleEvent.bundleGraph, + 'index.js', + './bar', + ); + + await overlayFS.mkdirp(path.join(testDir, 'node_modules/bar')); + await overlayFS.copyFile( + path.join(testDir, 'node_modules/bar/index.1.js'), + path.join(testDir, 'node_modules/bar/index.js'), + ); + + bundleEvent = await getNextBuild(b); + assert.strictEqual(bundleEvent.type, 'buildSuccess'); + called = false; + res = await run( + bundleEvent.bundleGraph, + { + sideEffect: () => { + called = true; + }, + }, + {require: false}, + ); + assert(!called, 'side effect called'); + assert.deepEqual(res.output, 4); + } finally { + await subscription.unsubscribe(); + } + }); + + it('supports wildcards', async function () { + let b = await bundle( + path.join( + __dirname, + '/integration/scope-hoisting/es6/side-effects-false-wildcards/a.js', + ), + options, + ); + let called = false; + let res = await run( + b, + { + sideEffect: () => { + called = true; + }, + }, + {require: false}, + ); + + if (usesSymbolPropagation) { + assert(!called, 'side effect called'); + } + assert.deepEqual(res.output, 'bar'); + }); + + it('correctly handles excluded and wrapped reexport assets', async function () { + let b = await bundle( + path.join( + __dirname, + '/integration/scope-hoisting/es6/side-effects-false-wrap-excluded/a.js', + ), + options, + ); + + let res = await run(b, null, {require: false}); + assert.deepEqual(res.output, 4); + }); + + it('supports the package.json sideEffects flag with an array', async function () { + let b = await bundle( + path.join( + __dirname, + '/integration/scope-hoisting/es6/side-effects-array/a.js', + ), + options, + ); + + let calls = []; + let res = await run( + b, + { + sideEffect: caller => { + calls.push(caller); + }, + }, + {require: false}, + ); + + assert(calls.toString() == 'foo', "side effect called for 'foo'"); + assert.deepEqual(res.output, 4); + }); + + it('supports the package.json sideEffects: false flag with shared dependencies', async function () { + let b = await bundle( + path.join( + __dirname, + '/integration/scope-hoisting/es6/side-effects-false-duplicate/a.js', + ), + options, + ); + + let called = false; + let res = await run( + b, + { + sideEffect: () => { + called = true; + }, + }, + {require: false}, + ); + + assert(!called, 'side effect called'); + assert.deepEqual(res.output, 6); + }); + + it('supports the package.json sideEffects: false flag with shared dependencies and code splitting', async function () { + let b = await bundle( + path.join( + __dirname, + '/integration/scope-hoisting/es6/side-effects-split/a.js', + ), + options, + ); + + let res = await run(b, null, {require: false}); + assert.deepEqual(await res.output, 581); + }); + + it('supports the package.json sideEffects: false flag with shared dependencies and code splitting II', async function () { + let b = await bundle( + path.join( + __dirname, + '/integration/scope-hoisting/es6/side-effects-split2/a.js', + ), + options, + ); + + let res = await run(b, null, {require: false}); + assert.deepEqual(await res.output, [{default: 123, foo: 2}, 581]); + }); + + it('missing exports should be replaced with an empty object', async function () { + let b = await bundle( + path.join( + __dirname, + '/integration/scope-hoisting/es6/empty-module/a.js', + ), + options, + ); + + let res = await run(b, null, {require: false}); + assert.deepEqual(res.output, {b: {}}); + }); + + it('supports namespace imports of theoretically excluded reexporting assets', async function () { + let b = await bundle( + path.join( + __dirname, + '/integration/scope-hoisting/es6/import-namespace-sideEffects/index.js', + ), + options, + ); + + let res = await run(b, null, {require: false}); + assert.deepEqual(res.output, {Main: 'main', a: 'foo', b: 'bar'}); + }); + + it('can import from a different bundle via a re-export', async function () { + let b = await bundle( + path.join( + __dirname, + '/integration/scope-hoisting/es6/re-export-bundle-boundary-side-effects/index.js', + ), + options, + ); + + let res = await run(b, null, {require: false}); + assert.deepEqual(await res.output, ['operational', 'ui']); + }); + + it('supports excluding multiple chained namespace reexports', async function () { + let b = await bundle( + path.join( + __dirname, + '/integration/scope-hoisting/es6/side-effects-chained-re-exports-multiple/a.js', + ), + options, + ); + + if (usesSymbolPropagation) { + assert(!findAsset(b, 'symbol1.js')); + } + + let calls = []; + let res = await run( + b, + { + sideEffect: caller => { + calls.push(caller); + }, + }, + {require: false}, + ); + + assert.deepEqual( + calls, + shouldScopeHoist + ? ['message1'] + : [ + 'message1', + 'message2', + 'message', + 'symbol1', + 'symbol2', + 'symbol', + ], + ); + assert.deepEqual(res.output, 'Message 1'); + }); + + it('supports excluding when doing both exports and reexports', async function () { + let b = await bundle( + path.join( + __dirname, + '/integration/scope-hoisting/es6/side-effects-export-reexport/a.js', + ), + options, + ); + + if (usesSymbolPropagation) { + assert(!findAsset(b, 'other.js')); + } + + let calls = []; + let res = await run( + b, + { + sideEffect: caller => { + calls.push(caller); + }, + }, + {require: false}, + ); + + assert.deepEqual( + calls, + usesSymbolPropagation ? ['index'] : ['other', 'index'], + ); + assert.deepEqual(res.output, 'Message 1'); + }); + + it('supports deferring with chained renaming reexports', async function () { + let b = await bundle( + path.join( + __dirname, + '/integration/scope-hoisting/es6/side-effects-re-exports-rename-chained/a.js', + ), + options, + ); + + // assertDependencyWasExcluded(b, 'message.js', './message2'); + + let calls = []; + let res = await run( + b, + { + sideEffect: caller => { + calls.push(caller); + }, + }, + {require: false}, + ); + + assert.deepEqual( + calls, + shouldScopeHoist + ? ['message1'] + : ['message1', 'message2', 'message', 'index2', 'index'], + ); + assert.deepEqual(res.output, 'Message 1'); + }); + + it('supports named and renamed reexports of the same asset (default used)', async function () { + let b = await bundle( + path.join( + __dirname, + '/integration/scope-hoisting/es6/side-effects-re-exports-rename-same2/a.js', + ), + options, + ); + + if (usesSymbolPropagation) { + assert.deepStrictEqual( + new Set(b.getUsedSymbols(nullthrows(findAsset(b, 'other.js')))), + new Set(['bar']), + ); + } + + let calls = []; + let res = await run( + b, + { + sideEffect: caller => { + calls.push(caller); + }, + }, + {require: false}, + ); + + assert.deepEqual( + calls, + shouldScopeHoist ? ['other'] : ['other', 'index'], + ); + assert.deepEqual(res.output, 'bar'); + }); + + it('supports named and renamed reexports of the same asset (named used)', async function () { + let b = await bundle( + path.join( + __dirname, + '/integration/scope-hoisting/es6/side-effects-re-exports-rename-same2/b.js', + ), + options, + ); + + if (usesSymbolPropagation) { + assert.deepStrictEqual( + new Set(b.getUsedSymbols(nullthrows(findAsset(b, 'other.js')))), + new Set(['bar']), + ); + } + + let calls = []; + let res = await run( + b, + { + sideEffect: caller => { + calls.push(caller); + }, + }, + {require: false}, + ); + + assert.deepEqual( + calls, + shouldScopeHoist ? ['other'] : ['other', 'index'], + ); + assert.deepEqual(res.output, 'bar'); + }); + + it('supports named and namespace exports of the same asset (named used)', async function () { + let b = await bundle( + path.join( + __dirname, + '/integration/scope-hoisting/es6/side-effects-re-exports-namespace-same/a.js', + ), + options, + ); + + if (usesSymbolPropagation) { + assert.deepStrictEqual( + new Set(b.getUsedSymbols(nullthrows(findAsset(b, 'index.js')))), + new Set([]), + ); + assert.deepStrictEqual( + new Set(b.getUsedSymbols(nullthrows(findAsset(b, 'other.js')))), + new Set(['default']), + ); + } + + let calls = []; + let res = await run( + b, + { + sideEffect: caller => { + calls.push(caller); + }, + }, + {require: false}, + ); + + assert.deepEqual( + calls, + shouldScopeHoist ? ['other'] : ['other', 'index'], + ); + assert.deepEqual(res.output, ['foo']); + }); + + it('supports named and namespace exports of the same asset (namespace used)', async function () { + let b = await bundle( + path.join( + __dirname, + '/integration/scope-hoisting/es6/side-effects-re-exports-namespace-same/b.js', + ), + options, + ); + + if (usesSymbolPropagation) { + assert.deepStrictEqual( + new Set(b.getUsedSymbols(nullthrows(findAsset(b, 'index.js')))), + new Set([]), + ); + assert.deepStrictEqual( + new Set(b.getUsedSymbols(nullthrows(findAsset(b, 'other.js')))), + new Set(['bar']), + ); + } + + let calls = []; + let res = await run( + b, + { + sideEffect: caller => { + calls.push(caller); + }, + }, + {require: false}, + ); + + assert.deepEqual( + calls, + shouldScopeHoist ? ['other'] : ['other', 'index'], + ); + assert.deepEqual(res.output, ['bar']); + }); + + it('supports named and namespace exports of the same asset (both used)', async function () { + let b = await bundle( + path.join( + __dirname, + '/integration/scope-hoisting/es6/side-effects-re-exports-namespace-same/c.js', + ), + options, + ); + + if (usesSymbolPropagation) { + assert.deepStrictEqual( + new Set(b.getUsedSymbols(nullthrows(findAsset(b, 'index.js')))), + new Set([]), + ); + assert.deepStrictEqual( + new Set(b.getUsedSymbols(nullthrows(findAsset(b, 'other.js')))), + new Set(['default', 'bar']), + ); + } + + let calls = []; + let res = await run( + b, + { + sideEffect: caller => { + calls.push(caller); + }, + }, + {require: false}, + ); + + assert.deepEqual( + calls, + shouldScopeHoist ? ['other'] : ['other', 'index'], + ); + assert.deepEqual(res.output, ['foo', 'bar']); + }); + + it('supports deferring non-weak dependencies that are not used', async function () { + let b = await bundle( + path.join( + __dirname, + '/integration/scope-hoisting/es6/side-effects-semi-weak/a.js', + ), + options, + ); + + // assertDependencyWasExcluded(b, 'esm2.js', './other.js'); + + let calls = []; + let res = await run( + b, + { + sideEffect: caller => { + calls.push(caller); + }, + }, + {require: false}, + ); + + assert.deepEqual( + calls, + shouldScopeHoist ? ['esm1'] : ['esm1', 'other', 'esm2', 'index'], + ); + assert.deepEqual(res.output, 'Message 1'); + }); + + it('supports excluding CommonJS (CommonJS unused)', async function () { + let b = await bundle( + path.join( + __dirname, + '/integration/scope-hoisting/es6/side-effects-commonjs/a.js', + ), + options, + ); + + if (usesSymbolPropagation) { + assert.deepStrictEqual( + new Set(b.getUsedSymbols(nullthrows(findAsset(b, 'esm.js')))), + new Set(['message1']), + ); + // We can't statically analyze commonjs.js, so message1 appears to be used + assert.deepStrictEqual( + new Set(b.getUsedSymbols(nullthrows(findAsset(b, 'commonjs.js')))), + // the exports object is used freely + new Set(['*', 'message1']), + ); + assert.deepStrictEqual( + new Set( + b.getUsedSymbols(findDependency(b, 'index.js', './commonjs.js')), + ), + new Set(['message1']), + ); + } + + let calls = []; + let res = await run( + b, + { + sideEffect: caller => { + calls.push(caller); + }, + }, + {require: false}, + ); + + assert.deepEqual(calls, ['esm', 'commonjs', 'index']); + assert.deepEqual(res.output, 'Message 1'); + }); + + it('supports excluding CommonJS (CommonJS used)', async function () { + let b = await bundle( + path.join( + __dirname, + '/integration/scope-hoisting/es6/side-effects-commonjs/b.js', + ), + options, + ); + + if (usesSymbolPropagation) { + assert(!findAsset(b, 'esm.js')); + assert.deepStrictEqual( + new Set(b.getUsedSymbols(nullthrows(findAsset(b, 'commonjs.js')))), + // the exports object is used freely + new Set(['*', 'message2']), + ); + assert.deepEqual( + new Set( + b.getUsedSymbols(findDependency(b, 'index.js', './commonjs.js')), + ), + new Set(['message2']), + ); + } + + let calls = []; + let res = await run( + b, + { + sideEffect: caller => { + calls.push(caller); + }, + }, + {require: false}, + ); + assert.deepEqual( + calls, + shouldScopeHoist ? ['commonjs'] : ['esm', 'commonjs', 'index'], + ); + assert.deepEqual(res.output, 'Message 2'); + }); + }); + } }); diff --git a/packages/core/integration-tests/test/scope-hoisting.js b/packages/core/integration-tests/test/scope-hoisting.js index 30da9917c8d..159adcdc37b 100644 --- a/packages/core/integration-tests/test/scope-hoisting.js +++ b/packages/core/integration-tests/test/scope-hoisting.js @@ -5,7 +5,7 @@ import {normalizePath} from '@parcel/utils'; import {md} from '@parcel/diagnostic'; import { assertBundles, - assertDependencyWasDeferred, + assertDependencyWasExcluded, bundle as _bundle, bundler as _bundler, distDir, @@ -2222,7 +2222,7 @@ describe('scope hoisting', function () { let output = await run(bundleEvent.bundleGraph); assert.deepEqual(output, [123]); - assertDependencyWasDeferred( + assertDependencyWasExcluded( bundleEvent.bundleGraph, 'a.js', './c.js', @@ -2547,857 +2547,23 @@ describe('scope hoisting', function () { }); }); - describe('sideEffects: false', function () { - it('supports excluding unused CSS imports', async function () { - let b = await bundle( - path.join( - __dirname, - '/integration/scope-hoisting/es6/side-effects-css/index.html', - ), - ); - - assertBundles(b, [ - { - name: 'index.html', - assets: ['index.html'], - }, - { - type: 'js', - assets: ['index.js', 'a.js', 'b1.js'], - }, - { - type: 'css', - assets: ['b1.css'], - }, - ]); - - let calls = []; - let res = await run( - b, - { - output: null, - sideEffect: caller => { - calls.push(caller); - }, - }, - {require: false}, - ); - assert.deepEqual(calls, ['b1']); - assert.deepEqual(res.output, 2); - - let css = await outputFS.readFile( - b.getBundles().find(bundle => bundle.type === 'css').filePath, - 'utf8', - ); - assert(!css.includes('.b2')); - }); - - it("doesn't create new bundles for dynamic imports in excluded assets", async function () { - let b = await bundle( - path.join( - __dirname, - '/integration/scope-hoisting/es6/side-effects-no-new-bundle/index.html', - ), - ); - - assertBundles(b, [ - { - name: 'index.html', - assets: ['index.html'], - }, - { - type: 'js', - assets: ['index.js', 'a.js', 'b1.js'], - }, - ]); - - let calls = []; - let res = await run( - b, - { - output: null, - sideEffect: caller => { - calls.push(caller); - }, - }, - {require: false}, - ); - assert.deepEqual(calls, ['b1']); - assert.deepEqual(res.output, 2); - }); - - it('supports deferring unused ES6 re-exports (namespace used)', async function () { - let b = await bundle( - path.join( - __dirname, - '/integration/scope-hoisting/es6/side-effects-re-exports/a.js', - ), - ); - - assertDependencyWasDeferred(b, 'index.js', './message2.js'); - assert(!findAsset(b, 'message3.js')); - - let calls = []; - let output = await run(b, { - sideEffect: caller => { - calls.push(caller); - }, - }); - - assert.deepEqual(calls, ['message1']); - assert.deepEqual(output, 'Message 1'); - }); - - it('supports deferring an unused ES6 re-export (wildcard, empty, unused)', async function () { - let b = await bundle( - path.join( - __dirname, - '/integration/scope-hoisting/es6/side-effects-re-exports-all-empty/a.js', - ), - ); - - assertDependencyWasDeferred(b, 'index.js', './empty.js'); - - assert.deepEqual(await run(b), 123); - }); - - it('supports deferring unused ES6 re-exports (reexport named used)', async function () { - let b = await bundle( - path.join( - __dirname, - '/integration/scope-hoisting/es6/side-effects-re-exports/b.js', - ), - ); - - assert(!findAsset(b, 'message1.js')); - assert(!findAsset(b, 'message3.js')); - - let calls = []; - let output = await run(b, { - sideEffect: caller => { - calls.push(caller); - }, - }); - - assert.deepEqual(calls, ['message2']); - assert.deepEqual(output, 'Message 2'); - }); - - it('supports deferring unused ES6 re-exports (namespace rename used)', async function () { - let b = await bundle( - path.join( - __dirname, - '/integration/scope-hoisting/es6/side-effects-re-exports/c.js', - ), - ); - - assert(!findAsset(b, 'message1.js')); - assertDependencyWasDeferred(b, 'index.js', './message2.js'); - - let calls = []; - let output = await run(b, { - sideEffect: caller => { - calls.push(caller); - }, - }); - - assert.deepEqual(calls, ['message3']); - assert.deepEqual(output, {default: 'Message 3'}); - }); - - it('supports deferring unused ES6 re-exports (direct export used)', async function () { - let b = await bundle( - path.join( - __dirname, - '/integration/scope-hoisting/es6/side-effects-re-exports/d.js', - ), - ); - - assert(!findAsset(b, 'message1.js')); - assertDependencyWasDeferred(b, 'index.js', './message2.js'); - assert(!findAsset(b, 'message13js')); - - let calls = []; - let output = await run(b, { - sideEffect: caller => { - calls.push(caller); - }, - }); - - assert.deepEqual(calls, ['index']); - assert.deepEqual(output, 'Message 4'); - }); - - it('supports chained ES6 re-exports', async function () { - let b = await bundle( - path.join( - __dirname, - '/integration/scope-hoisting/es6/side-effects-re-exports-chained/index.js', - ), - ); - - assert(!findAsset(b, 'bar.js')); - - let calls = []; - let output = await run(b, { - sideEffect: caller => { - calls.push(caller); - }, - }); - - assert.deepEqual(calls, ['key', 'foo', 'index']); - assert.deepEqual(output, ['key', 'foo']); - }); - - it('should not optimize away an unused ES6 re-export and an used import', async function () { - let b = await bundle( - path.join( - __dirname, - '/integration/scope-hoisting/es6/side-effects-re-exports-import/a.js', - ), - ); - - let output = await run(b); - assert.deepEqual(output, 123); - }); - - it('should not optimize away an unused ES6 re-export and an used import (different symbols)', async function () { - let b = await bundle( - path.join( - __dirname, - '/integration/scope-hoisting/es6/side-effects-re-exports-import-different/a.js', - ), - ); - - let output = await run(b); - assert.deepEqual(output, 123); - }); - - it('correctly handles ES6 re-exports in library mode entries', async function () { - let b = await bundle( - path.join( - __dirname, - '/integration/scope-hoisting/es6/side-effects-re-exports-library/a.js', - ), - ); - - let contents = await outputFS.readFile( - path.join( - __dirname, - '/integration/scope-hoisting/es6/side-effects-re-exports-library/build.js', - ), - 'utf8', - ); - assert(!contents.includes('console.log')); - - let output = await run(b); - assert.deepEqual(output, {c1: 'foo'}); - }); - - it('correctly updates deferred assets that are reexported', async function () { - let testDir = path.join( - __dirname, - '/integration/scope-hoisting/es6/side-effects-update-deferred-reexported', - ); - - let b = bundler(path.join(testDir, 'index.js'), { - inputFS: overlayFS, - outputFS: overlayFS, - }); - - let subscription = await b.watch(); - - let bundleEvent = await getNextBuild(b); - assert(bundleEvent.type === 'buildSuccess'); - let output = await run(bundleEvent.bundleGraph); - assert.deepEqual(output, '12345hello'); - - await overlayFS.mkdirp(path.join(testDir, 'node_modules', 'foo')); - await overlayFS.copyFile( - path.join(testDir, 'node_modules', 'foo', 'foo_updated.js'), - path.join(testDir, 'node_modules', 'foo', 'foo.js'), - ); - - bundleEvent = await getNextBuild(b); - assert(bundleEvent.type === 'buildSuccess'); - output = await run(bundleEvent.bundleGraph); - assert.deepEqual(output, '1234556789'); - - await subscription.unsubscribe(); - }); - - it('correctly updates deferred assets that are reexported and imported directly', async function () { - let testDir = path.join( + it('removes functions that increment variables in object properties', async function () { + let b = await bundle( + path.join( __dirname, - '/integration/scope-hoisting/es6/side-effects-update-deferred-direct', - ); - - let b = bundler(path.join(testDir, 'index.js'), { - inputFS: overlayFS, - outputFS: overlayFS, - }); - - let subscription = await b.watch(); - - let bundleEvent = await getNextBuild(b); - assert(bundleEvent.type === 'buildSuccess'); - let output = await run(bundleEvent.bundleGraph); - assert.deepEqual(output, '12345hello'); - - await overlayFS.mkdirp(path.join(testDir, 'node_modules', 'foo')); - await overlayFS.copyFile( - path.join(testDir, 'node_modules', 'foo', 'foo_updated.js'), - path.join(testDir, 'node_modules', 'foo', 'foo.js'), - ); - - bundleEvent = await getNextBuild(b); - assert(bundleEvent.type === 'buildSuccess'); - output = await run(bundleEvent.bundleGraph); - assert.deepEqual(output, '1234556789'); - - await subscription.unsubscribe(); - }); - - it('removes deferred reexports when imported from multiple asssets', async function () { - let b = await bundle( - path.join( - __dirname, - '/integration/scope-hoisting/es6/side-effects-re-exports-multiple-dynamic/a.js', - ), - ); - - let contents = await outputFS.readFile( - b.getBundles()[0].filePath, - 'utf8', - ); - - assert(!contents.includes('$import$')); - assert(contents.includes('= 1234;')); - assert(!contents.includes('= 5678;')); - - let output = await run(b); - assert.deepEqual(output, [1234, {default: 1234}]); - }); - - it('keeps side effects by default', async function () { - let b = await bundle( - path.join( - __dirname, - '/integration/scope-hoisting/es6/side-effects/a.js', - ), - ); - - let called = false; - let output = await run(b, { - sideEffect: () => { - called = true; - }, - }); - - assert(called, 'side effect not called'); - assert.deepEqual(output, 4); - }); - - it('supports the package.json sideEffects: false flag', async function () { - let b = await bundle( - path.join( - __dirname, - '/integration/scope-hoisting/es6/side-effects-false/a.js', - ), - ); - - let called = false; - let output = await run(b, { - sideEffect: () => { - called = true; + '/integration/scope-hoisting/es6/tree-shaking-increment-object/a.js', + ), + { + defaultTargetOptions: { + shouldOptimize: true, }, - }); - - assert(!called, 'side effect called'); - assert.deepEqual(output, 4); - }); - - it('supports removing a deferred dependency', async function () { - let testDir = path.join( - __dirname, - '/integration/scope-hoisting/es6/side-effects-false', - ); - - let b = bundler(path.join(testDir, 'a.js'), { - inputFS: overlayFS, - outputFS: overlayFS, - }); - - let subscription = await b.watch(); - - try { - let bundleEvent = await getNextBuild(b); - assert.strictEqual(bundleEvent.type, 'buildSuccess'); - let called = false; - let output = await run(bundleEvent.bundleGraph, { - sideEffect: () => { - called = true; - }, - }); - assert(!called, 'side effect called'); - assert.deepEqual(output, 4); - assertDependencyWasDeferred( - bundleEvent.bundleGraph, - 'index.js', - './bar', - ); - - await overlayFS.mkdirp(path.join(testDir, 'node_modules/bar')); - await overlayFS.copyFile( - path.join(testDir, 'node_modules/bar/index.1.js'), - path.join(testDir, 'node_modules/bar/index.js'), - ); + }, + ); - bundleEvent = await getNextBuild(b); - assert.strictEqual(bundleEvent.type, 'buildSuccess'); - called = false; - output = await run(bundleEvent.bundleGraph, { - sideEffect: () => { - called = true; - }, - }); - assert(!called, 'side effect called'); - assert.deepEqual(output, 4); - } finally { - await subscription.unsubscribe(); - } - }); + let content = await outputFS.readFile(b.getBundles()[0].filePath, 'utf8'); + assert(!content.includes('++')); - it('supports wildcards', async function () { - let b = await bundle( - path.join( - __dirname, - '/integration/scope-hoisting/es6/side-effects-false-wildcards/a.js', - ), - ); - let called = false; - let output = await run(b, { - sideEffect: () => { - called = true; - }, - }); - - assert(!called, 'side effect called'); - assert.deepEqual(output, 'bar'); - }); - - it('correctly handles excluded and wrapped reexport assets', async function () { - let b = await bundle( - path.join( - __dirname, - '/integration/scope-hoisting/es6/side-effects-false-wrap-excluded/a.js', - ), - ); - - let output = await run(b); - assert.deepEqual(output, 4); - }); - - it('supports the package.json sideEffects flag with an array', async function () { - let b = await bundle( - path.join( - __dirname, - '/integration/scope-hoisting/es6/side-effects-array/a.js', - ), - ); - - let calls = []; - let output = await run(b, { - sideEffect: caller => { - calls.push(caller); - }, - }); - - assert(calls.toString() == 'foo', "side effect called for 'foo'"); - assert.deepEqual(output, 4); - }); - - it('supports the package.json sideEffects: false flag with shared dependencies', async function () { - let b = await bundle( - path.join( - __dirname, - '/integration/scope-hoisting/es6/side-effects-false-duplicate/a.js', - ), - ); - - let called = false; - let output = await run(b, { - sideEffect: () => { - called = true; - }, - }); - - assert(!called, 'side effect called'); - assert.deepEqual(output, 6); - }); - - it('supports the package.json sideEffects: false flag with shared dependencies and code splitting', async function () { - let b = await bundle( - path.join( - __dirname, - '/integration/scope-hoisting/es6/side-effects-split/a.js', - ), - ); - - assert.deepEqual(await run(b), 581); - }); - - it('supports the package.json sideEffects: false flag with shared dependencies and code splitting II', async function () { - let b = await bundle( - path.join( - __dirname, - '/integration/scope-hoisting/es6/side-effects-split2/a.js', - ), - ); - - assert.deepEqual(await run(b), [{default: 123, foo: 2}, 581]); - }); - - it('missing exports should be replaced with an empty object', async function () { - let b = await bundle( - path.join( - __dirname, - '/integration/scope-hoisting/es6/empty-module/a.js', - ), - ); - - let output = await run(b); - assert.deepEqual(output, {b: {}}); - }); - - it('supports namespace imports of theoretically excluded reexporting assets', async function () { - let b = await bundle( - path.join( - __dirname, - '/integration/scope-hoisting/es6/import-namespace-sideEffects/index.js', - ), - ); - - let output = await run(b); - assert.deepEqual(output, {Main: 'main', a: 'foo', b: 'bar'}); - }); - - it('can import from a different bundle via a re-export', async function () { - let b = await bundle( - path.join( - __dirname, - '/integration/scope-hoisting/es6/re-export-bundle-boundary-side-effects/index.js', - ), - ); - let output = await run(b); - assert.deepEqual(output, ['operational', 'ui']); - }); - - it('supports excluding multiple chained namespace reexports', async function () { - let b = await bundle( - path.join( - __dirname, - '/integration/scope-hoisting/es6/side-effects-chained-re-exports-multiple/a.js', - ), - ); - - assert(!findAsset(b, 'symbol1.js')); - - let calls = []; - let output = await run(b, { - sideEffect: caller => { - calls.push(caller); - }, - }); - - assert.deepEqual(calls, ['message1']); - assert.deepEqual(output, 'Message 1'); - }); - - it('supports excluding when doing both exports and reexports', async function () { - let b = await bundle( - path.join( - __dirname, - '/integration/scope-hoisting/es6/side-effects-export-reexport/a.js', - ), - ); - - assert(!findAsset(b, 'other.js')); - - let calls = []; - let output = await run(b, { - sideEffect: caller => { - calls.push(caller); - }, - }); - - assert.deepEqual(calls, ['index']); - assert.deepEqual(output, 'Message 1'); - }); - - it('supports deferring with chained renaming reexports', async function () { - let b = await bundle( - path.join( - __dirname, - '/integration/scope-hoisting/es6/side-effects-re-exports-rename-chained/a.js', - ), - ); - - // assertDependencyWasDeferred(b, 'message.js', './message2'); - - let calls = []; - let output = await run(b, { - sideEffect: caller => { - calls.push(caller); - }, - }); - - assert.deepEqual(calls, ['message1']); - assert.deepEqual(output, 'Message 1'); - }); - - it('supports named and renamed reexports of the same asset (default used)', async function () { - let b = await bundle( - path.join( - __dirname, - '/integration/scope-hoisting/es6/side-effects-re-exports-rename-same2/a.js', - ), - ); - - assert.deepStrictEqual( - new Set(b.getUsedSymbols(nullthrows(findAsset(b, 'other.js')))), - new Set(['bar']), - ); - - let calls = []; - let output = await run(b, { - sideEffect: caller => { - calls.push(caller); - }, - }); - - assert.deepEqual(calls, ['other']); - assert.deepEqual(output, 'bar'); - }); - - it('supports named and renamed reexports of the same asset (named used)', async function () { - let b = await bundle( - path.join( - __dirname, - '/integration/scope-hoisting/es6/side-effects-re-exports-rename-same2/b.js', - ), - ); - - assert.deepStrictEqual( - new Set(b.getUsedSymbols(nullthrows(findAsset(b, 'other.js')))), - new Set(['bar']), - ); - - let calls = []; - let output = await run(b, { - sideEffect: caller => { - calls.push(caller); - }, - }); - - assert.deepEqual(calls, ['other']); - assert.deepEqual(output, 'bar'); - }); - - it('removes functions that increment variables in object properties', async function () { - let b = await bundle( - path.join( - __dirname, - '/integration/scope-hoisting/es6/tree-shaking-increment-object/a.js', - ), - { - defaultTargetOptions: { - shouldOptimize: true, - }, - }, - ); - - let content = await outputFS.readFile( - b.getBundles()[0].filePath, - 'utf8', - ); - assert(!content.includes('++')); - - await run(b); - }); - - it('supports named and namespace exports of the same asset (named used)', async function () { - let b = await bundle( - path.join( - __dirname, - '/integration/scope-hoisting/es6/side-effects-re-exports-namespace-same/a.js', - ), - ); - - assert.deepStrictEqual( - new Set(b.getUsedSymbols(nullthrows(findAsset(b, 'index.js')))), - new Set([]), - ); - assert.deepStrictEqual( - new Set(b.getUsedSymbols(nullthrows(findAsset(b, 'other.js')))), - new Set(['default']), - ); - - let calls = []; - let output = await run(b, { - sideEffect: caller => { - calls.push(caller); - }, - }); - - assert.deepEqual(calls, ['other']); - assert.deepEqual(output, ['foo']); - }); - - it('supports named and namespace exports of the same asset (namespace used)', async function () { - let b = await bundle( - path.join( - __dirname, - '/integration/scope-hoisting/es6/side-effects-re-exports-namespace-same/b.js', - ), - ); - - assert.deepStrictEqual( - new Set(b.getUsedSymbols(nullthrows(findAsset(b, 'index.js')))), - new Set([]), - ); - assert.deepStrictEqual( - new Set(b.getUsedSymbols(nullthrows(findAsset(b, 'other.js')))), - new Set(['bar']), - ); - - let calls = []; - let output = await run(b, { - sideEffect: caller => { - calls.push(caller); - }, - }); - - assert.deepEqual(calls, ['other']); - assert.deepEqual(output, ['bar']); - }); - - it('supports named and namespace exports of the same asset (both used)', async function () { - let b = await bundle( - path.join( - __dirname, - '/integration/scope-hoisting/es6/side-effects-re-exports-namespace-same/c.js', - ), - ); - - assert.deepStrictEqual( - new Set(b.getUsedSymbols(nullthrows(findAsset(b, 'index.js')))), - new Set([]), - ); - assert.deepStrictEqual( - new Set(b.getUsedSymbols(nullthrows(findAsset(b, 'other.js')))), - new Set(['default', 'bar']), - ); - - let calls = []; - let output = await run(b, { - sideEffect: caller => { - calls.push(caller); - }, - }); - - assert.deepEqual(calls, ['other']); - assert.deepEqual(output, ['foo', 'bar']); - }); - - it('supports deferring non-weak dependencies that are not used', async function () { - let b = await bundle( - path.join( - __dirname, - '/integration/scope-hoisting/es6/side-effects-semi-weak/a.js', - ), - ); - - // assertDependencyWasDeferred(b, 'esm2.js', './other.js'); - - let calls = []; - let output = await run(b, { - sideEffect: caller => { - calls.push(caller); - }, - }); - - assert.deepEqual(calls, ['esm1']); - assert.deepEqual(output, 'Message 1'); - }); - - it('supports excluding CommonJS (CommonJS unused)', async function () { - let b = await bundle( - path.join( - __dirname, - '/integration/scope-hoisting/es6/side-effects-commonjs/a.js', - ), - ); - - assert.deepStrictEqual( - new Set(b.getUsedSymbols(nullthrows(findAsset(b, 'esm.js')))), - new Set(['message1']), - ); - // We can't statically analyze commonjs.js, so message1 appears to be used - assert.deepStrictEqual( - new Set(b.getUsedSymbols(nullthrows(findAsset(b, 'commonjs.js')))), - // the exports object is used freely - new Set(['*', 'message1']), - ); - assert.deepStrictEqual( - new Set( - b.getUsedSymbols(findDependency(b, 'index.js', './commonjs.js')), - ), - new Set(['message1']), - ); - - let calls = []; - let output = await run(b, { - sideEffect: caller => { - calls.push(caller); - }, - }); - - assert.deepEqual(calls, ['esm', 'commonjs', 'index']); - assert.deepEqual(output, 'Message 1'); - }); - - it('supports excluding CommonJS (CommonJS used)', async function () { - let b = await bundle( - path.join( - __dirname, - '/integration/scope-hoisting/es6/side-effects-commonjs/b.js', - ), - ); - - assert(!findAsset(b, 'esm.js')); - assert.deepStrictEqual( - new Set(b.getUsedSymbols(nullthrows(findAsset(b, 'commonjs.js')))), - // the exports object is used freely - new Set(['*', 'message2']), - ); - assert.deepEqual( - new Set( - b.getUsedSymbols(findDependency(b, 'index.js', './commonjs.js')), - ), - new Set(['message2']), - ); - - let calls = []; - let output = await run(b, { - sideEffect: caller => { - calls.push(caller); - }, - }); - - assert.deepEqual(calls, ['commonjs']); - assert.deepEqual(output, 'Message 2'); - }); + await run(b); }); it('ignores missing import specifiers in source assets', async function () { diff --git a/packages/core/test-utils/src/utils.js b/packages/core/test-utils/src/utils.js index 1d988e85b8f..47e1fcca383 100644 --- a/packages/core/test-utils/src/utils.js +++ b/packages/core/test-utils/src/utils.js @@ -193,7 +193,7 @@ export function mergeParcelOptions( }; } -export function assertDependencyWasDeferred( +export function assertDependencyWasExcluded( bundleGraph: BundleGraph, assetFileName: string, specifier: string, From a2bc67c73a0a3d4382902a9ed31fa80115f60a13 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Mon, 1 Nov 2021 16:36:38 +0100 Subject: [PATCH 10/13] Async dependencies aren't analyzed --- packages/transformers/js/src/JSTransformer.js | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/packages/transformers/js/src/JSTransformer.js b/packages/transformers/js/src/JSTransformer.js index a5f42f130d5..08a758971c0 100644 --- a/packages/transformers/js/src/JSTransformer.js +++ b/packages/transformers/js/src/JSTransformer.js @@ -776,9 +776,6 @@ export default (new Transformer({ .getDependencies() .map(dep => [dep.meta.placeholder ?? dep.specifier, dep]), ); - for (let dep of deps.values()) { - dep.symbols.ensure(); - } asset.symbols.ensure(); for (let {exported, local, loc, source} of symbol_result.exports) { @@ -789,6 +786,7 @@ export default (new Transformer({ convertLoc(loc), ); if (dep != null) { + dep.symbols.ensure(); dep.symbols.set( local, `${dep?.id ?? ''}$${local}`, @@ -801,12 +799,14 @@ export default (new Transformer({ for (let {source, local, imported, loc} of symbol_result.imports) { let dep = deps.get(source); if (!dep) continue; + dep.symbols.ensure(); dep.symbols.set(imported, local, convertLoc(loc)); } for (let {source, loc} of symbol_result.exports_all) { let dep = deps.get(source); if (!dep) continue; + dep.symbols.ensure(); dep.symbols.set('*', '*', convertLoc(loc), true); } } From cfdbabea7ae851a70a711b2b82dcaf2c12859c76 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Mon, 1 Nov 2021 21:44:26 +0100 Subject: [PATCH 11/13] Rename to just `Collect` --- .../core/src/{hoist_collect.rs => collect.rs} | 18 +++++++++--------- packages/transformers/js/core/src/fs.rs | 6 +++--- packages/transformers/js/core/src/hoist.rs | 12 ++++++------ packages/transformers/js/core/src/lib.rs | 8 ++++---- 4 files changed, 22 insertions(+), 22 deletions(-) rename packages/transformers/js/core/src/{hoist_collect.rs => collect.rs} (99%) diff --git a/packages/transformers/js/core/src/hoist_collect.rs b/packages/transformers/js/core/src/collect.rs similarity index 99% rename from packages/transformers/js/core/src/hoist_collect.rs rename to packages/transformers/js/core/src/collect.rs index d1524510b32..2fc63dcd36f 100644 --- a/packages/transformers/js/core/src/hoist_collect.rs +++ b/packages/transformers/js/core/src/collect.rs @@ -32,7 +32,7 @@ pub struct Export { pub loc: SourceLocation, } -pub struct HoistCollect { +pub struct Collect { pub source_map: Lrc, pub decls: HashSet, pub ignore_mark: Mark, @@ -84,13 +84,13 @@ struct ExportedAll { } #[derive(Serialize, Debug)] -pub struct HoistCollectResult { +pub struct CollectResult { imports: Vec, exports: Vec, exports_all: Vec, } -impl HoistCollect { +impl Collect { pub fn new( source_map: Lrc, decls: HashSet, @@ -98,7 +98,7 @@ impl HoistCollect { global_mark: Mark, trace_bailouts: bool, ) -> Self { - HoistCollect { + Collect { source_map, decls, ignore_mark, @@ -125,9 +125,9 @@ impl HoistCollect { } } -impl From for HoistCollectResult { - fn from(collect: HoistCollect) -> HoistCollectResult { - HoistCollectResult { +impl From for CollectResult { + fn from(collect: Collect) -> CollectResult { + CollectResult { imports: collect .imports .into_iter() @@ -191,7 +191,7 @@ macro_rules! collect_visit_fn { }; } -impl Visit for HoistCollect { +impl Visit for Collect { fn visit_module(&mut self, node: &Module, _parent: &dyn Node) { self.in_module_this = true; self.in_top_level = true; @@ -822,7 +822,7 @@ impl Visit for HoistCollect { } } -impl HoistCollect { +impl Collect { pub fn match_require(&self, node: &Expr) -> Option { match_require(node, &self.decls, self.ignore_mark) } diff --git a/packages/transformers/js/core/src/fs.rs b/packages/transformers/js/core/src/fs.rs index b92b8442c21..359d59c09fe 100644 --- a/packages/transformers/js/core/src/fs.rs +++ b/packages/transformers/js/core/src/fs.rs @@ -1,5 +1,5 @@ +use crate::collect::{Collect, Import}; use crate::dependency_collector::{DependencyDescriptor, DependencyKind}; -use crate::hoist_collect::{HoistCollect, Import}; use crate::utils::SourceLocation; use data_encoding::{BASE64, HEXLOWER}; use std::collections::HashSet; @@ -26,7 +26,7 @@ pub fn inline_fs<'a>( ) -> impl Fold + 'a { InlineFS { filename: Path::new(filename).to_path_buf(), - collect: HoistCollect::new( + collect: Collect::new( source_map, decls, Mark::fresh(Mark::root()), @@ -41,7 +41,7 @@ pub fn inline_fs<'a>( struct InlineFS<'a> { filename: PathBuf, - collect: HoistCollect, + collect: Collect, global_mark: Mark, project_root: &'a str, deps: &'a mut Vec, diff --git a/packages/transformers/js/core/src/hoist.rs b/packages/transformers/js/core/src/hoist.rs index 93484e86ecc..770831c4bf6 100644 --- a/packages/transformers/js/core/src/hoist.rs +++ b/packages/transformers/js/core/src/hoist.rs @@ -7,7 +7,7 @@ use swc_common::{sync::Lrc, Mark, Span, SyntaxContext, DUMMY_SP}; use swc_ecmascript::ast::*; use swc_ecmascript::visit::{Fold, FoldWith, VisitWith}; -use crate::hoist_collect::{HoistCollect, Import, ImportKind}; +use crate::collect::{Collect, Import, ImportKind}; use crate::id; use crate::utils::{ match_import, match_member_expr, match_require, CodeHighlight, Diagnostic, DiagnosticSeverity, @@ -31,7 +31,7 @@ pub fn hoist( global_mark: Mark, trace_bailouts: bool, ) -> Result<(Module, HoistResult, Vec), Vec> { - let mut collect = HoistCollect::new(source_map, decls, ignore_mark, global_mark, trace_bailouts); + let mut collect = Collect::new(source_map, decls, ignore_mark, global_mark, trace_bailouts); module.visit_with(&Invalid { span: DUMMY_SP } as _, &mut collect); let mut hoist = Hoist::new(module_id, &collect); @@ -67,7 +67,7 @@ struct ImportedSymbol { struct Hoist<'a> { module_id: &'a str, - collect: &'a HoistCollect, + collect: &'a Collect, module_items: Vec, export_decls: HashSet, hoisted_imports: Vec, @@ -95,7 +95,7 @@ pub struct HoistResult { } impl<'a> Hoist<'a> { - fn new(module_id: &'a str, collect: &'a HoistCollect) -> Self { + fn new(module_id: &'a str, collect: &'a Collect) -> Self { Hoist { module_id, collect, @@ -1124,7 +1124,7 @@ mod tests { extern crate indoc; use self::indoc::indoc; - fn parse(code: &str) -> (HoistCollect, String, HoistResult) { + fn parse(code: &str) -> (Collect, String, HoistResult) { let source_map = Lrc::new(SourceMap::default()); let source_file = source_map.new_source_file(FileName::Anon, code.into()); @@ -1148,7 +1148,7 @@ mod tests { let global_mark = Mark::fresh(Mark::root()); let module = module.fold_with(&mut resolver_with_mark(global_mark)); - let mut collect = HoistCollect::new( + let mut collect = Collect::new( source_map.clone(), collect_decls(&module), Mark::fresh(Mark::root()), diff --git a/packages/transformers/js/core/src/lib.rs b/packages/transformers/js/core/src/lib.rs index 0410325cce2..01e3b587a6d 100644 --- a/packages/transformers/js/core/src/lib.rs +++ b/packages/transformers/js/core/src/lib.rs @@ -12,13 +12,13 @@ extern crate serde; extern crate serde_bytes; extern crate sha1; +mod collect; mod decl_collector; mod dependency_collector; mod env_replacer; mod fs; mod global_replacer; mod hoist; -mod hoist_collect; mod modules; mod utils; @@ -45,13 +45,13 @@ use swc_ecmascript::transforms::{ }; use swc_ecmascript::visit::{FoldWith, VisitWith}; +use collect::{Collect, CollectResult}; use decl_collector::*; use dependency_collector::*; use env_replacer::*; use fs::inline_fs; use global_replacer::GlobalReplacer; use hoist::{hoist, HoistResult}; -use hoist_collect::{HoistCollect, HoistCollectResult}; use modules::esm2cjs; use utils::{CodeHighlight, Diagnostic, DiagnosticSeverity, SourceLocation, SourceType}; @@ -97,7 +97,7 @@ pub struct TransformResult { shebang: Option, dependencies: Vec, hoist_result: Option, - symbol_result: Option, + symbol_result: Option, diagnostics: Option>, needs_esm_helpers: bool, used_env: HashSet, @@ -418,7 +418,7 @@ pub fn transform(config: Config) -> Result { } } } else { - let mut symbols_collect = HoistCollect::new( + let mut symbols_collect = Collect::new( source_map.clone(), decls, Mark::fresh(Mark::root()), From 9a7e9ac5d295d50ba903e635a0b5730032585b29 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Sat, 6 Nov 2021 18:27:11 +0100 Subject: [PATCH 12/13] Unify Collect for (no)scopehoist --- packages/transformers/js/core/src/hoist.rs | 25 +++++---------- packages/transformers/js/core/src/lib.rs | 36 +++++++++------------- 2 files changed, 21 insertions(+), 40 deletions(-) diff --git a/packages/transformers/js/core/src/hoist.rs b/packages/transformers/js/core/src/hoist.rs index 770831c4bf6..5a9a9c2dcd4 100644 --- a/packages/transformers/js/core/src/hoist.rs +++ b/packages/transformers/js/core/src/hoist.rs @@ -3,15 +3,15 @@ use std::collections::hash_map::DefaultHasher; use std::collections::{HashMap, HashSet}; use std::hash::Hasher; use swc_atoms::JsWord; -use swc_common::{sync::Lrc, Mark, Span, SyntaxContext, DUMMY_SP}; +use swc_common::{Span, SyntaxContext, DUMMY_SP}; use swc_ecmascript::ast::*; -use swc_ecmascript::visit::{Fold, FoldWith, VisitWith}; +use swc_ecmascript::visit::{Fold, FoldWith}; use crate::collect::{Collect, Import, ImportKind}; use crate::id; use crate::utils::{ match_import, match_member_expr, match_require, CodeHighlight, Diagnostic, DiagnosticSeverity, - IdentId, SourceLocation, + SourceLocation, }; macro_rules! hash { @@ -24,28 +24,16 @@ macro_rules! hash { pub fn hoist( module: Module, - source_map: Lrc, module_id: &str, - decls: HashSet, - ignore_mark: Mark, - global_mark: Mark, - trace_bailouts: bool, + collect: &Collect, ) -> Result<(Module, HoistResult, Vec), Vec> { - let mut collect = Collect::new(source_map, decls, ignore_mark, global_mark, trace_bailouts); - module.visit_with(&Invalid { span: DUMMY_SP } as _, &mut collect); - - let mut hoist = Hoist::new(module_id, &collect); + let mut hoist = Hoist::new(module_id, collect); let module = module.fold_with(&mut hoist); + if !hoist.diagnostics.is_empty() { return Err(hoist.diagnostics); } - if let Some(bailouts) = &collect.bailouts { - hoist - .diagnostics - .extend(bailouts.iter().map(|bailout| bailout.to_diagnostic())); - } - let diagnostics = std::mem::take(&mut hoist.diagnostics); Ok((module, hoist.get_result(), diagnostics)) } @@ -1121,6 +1109,7 @@ mod tests { use swc_ecmascript::parser::lexer::Lexer; use swc_ecmascript::parser::{EsConfig, Parser, StringInput, Syntax}; use swc_ecmascript::transforms::resolver_with_mark; + use swc_ecmascript::visit::VisitWith; extern crate indoc; use self::indoc::indoc; diff --git a/packages/transformers/js/core/src/lib.rs b/packages/transformers/js/core/src/lib.rs index 01e3b587a6d..5d55ec1d3be 100644 --- a/packages/transformers/js/core/src/lib.rs +++ b/packages/transformers/js/core/src/lib.rs @@ -396,16 +396,20 @@ pub fn transform(config: Config) -> Result { return Ok(result); } + let mut collect = Collect::new( + source_map.clone(), + decls, + ignore_mark, + global_mark, + config.trace_bailouts, + ); + module.visit_with(&Invalid { span: DUMMY_SP } as _, &mut collect); + if let Some(bailouts) = &collect.bailouts { + diagnostics.extend(bailouts.iter().map(|bailout| bailout.to_diagnostic())); + } + let module = if config.scope_hoist { - let res = hoist( - module, - source_map.clone(), - config.module_id.as_str(), - decls, - ignore_mark, - global_mark, - config.trace_bailouts, - ); + let res = hoist(module, config.module_id.as_str(), &collect); match res { Ok((module, hoist_result, hoist_diagnostics)) => { result.hoist_result = Some(hoist_result); @@ -418,19 +422,7 @@ pub fn transform(config: Config) -> Result { } } } else { - let mut symbols_collect = Collect::new( - source_map.clone(), - decls, - Mark::fresh(Mark::root()), - global_mark, - config.trace_bailouts, - ); - module.visit_with(&Invalid { span: DUMMY_SP } as _, &mut symbols_collect); - - if let Some(bailouts) = &symbols_collect.bailouts { - diagnostics.extend(bailouts.iter().map(|bailout| bailout.to_diagnostic())); - } - result.symbol_result = Some(symbols_collect.into()); + result.symbol_result = Some(collect.into()); let (module, needs_helpers) = esm2cjs(module, versions); result.needs_esm_helpers = needs_helpers; From 0a38600f158a878cd310457121ab2e429214ed09 Mon Sep 17 00:00:00 2001 From: Niklas Mischkulnig <4586894+mischnic@users.noreply.github.com> Date: Sun, 14 Nov 2021 11:05:20 +0100 Subject: [PATCH 13/13] Move Collect back to ease review --- packages/transformers/js/core/src/collect.rs | 1033 ------------------ packages/transformers/js/core/src/fs.rs | 2 +- packages/transformers/js/core/src/hoist.rs | 1032 ++++++++++++++++- packages/transformers/js/core/src/lib.rs | 6 +- 4 files changed, 1030 insertions(+), 1043 deletions(-) delete mode 100644 packages/transformers/js/core/src/collect.rs diff --git a/packages/transformers/js/core/src/collect.rs b/packages/transformers/js/core/src/collect.rs deleted file mode 100644 index 2fc63dcd36f..00000000000 --- a/packages/transformers/js/core/src/collect.rs +++ /dev/null @@ -1,1033 +0,0 @@ -use serde::Serialize; -use std::collections::{HashMap, HashSet}; -use swc_atoms::JsWord; -use swc_common::{sync::Lrc, Mark, Span, SyntaxContext, DUMMY_SP}; -use swc_ecmascript::ast::*; -use swc_ecmascript::visit::{Node, Visit, VisitWith}; - -use crate::id; -use crate::utils::{ - match_import, match_member_expr, match_require, Bailout, BailoutReason, IdentId, SourceLocation, -}; - -#[derive(Debug, PartialEq, Clone, Copy, Serialize)] -pub enum ImportKind { - Require, - Import, - DynamicImport, -} - -#[derive(Debug)] -pub struct Import { - pub source: JsWord, - pub specifier: JsWord, - pub kind: ImportKind, - pub loc: SourceLocation, -} - -#[derive(Debug)] -pub struct Export { - pub source: Option, - pub specifier: JsWord, - pub loc: SourceLocation, -} - -pub struct Collect { - pub source_map: Lrc, - pub decls: HashSet, - pub ignore_mark: Mark, - pub global_ctxt: SyntaxContext, - pub static_cjs_exports: bool, - pub has_cjs_exports: bool, - pub is_esm: bool, - pub should_wrap: bool, - // local name -> descriptor - pub imports: HashMap, - // exported name -> descriptor - pub exports: HashMap, - // local name -> exported name - pub exports_locals: HashMap, - pub exports_all: HashMap, - pub non_static_access: HashMap>, - pub non_const_bindings: HashMap>, - pub non_static_requires: HashSet, - pub wrapped_requires: HashSet, - pub bailouts: Option>, - in_module_this: bool, - in_top_level: bool, - in_export_decl: bool, - in_function: bool, - in_assign: bool, -} - -#[derive(Debug, Serialize)] -struct ImportedSymbol { - source: JsWord, - local: JsWord, - imported: JsWord, - loc: SourceLocation, - kind: ImportKind, -} - -#[derive(Debug, Serialize)] -struct ExportedSymbol { - source: Option, - local: JsWord, - exported: JsWord, - loc: SourceLocation, -} - -#[derive(Debug, Serialize)] -struct ExportedAll { - source: JsWord, - loc: SourceLocation, -} - -#[derive(Serialize, Debug)] -pub struct CollectResult { - imports: Vec, - exports: Vec, - exports_all: Vec, -} - -impl Collect { - pub fn new( - source_map: Lrc, - decls: HashSet, - ignore_mark: Mark, - global_mark: Mark, - trace_bailouts: bool, - ) -> Self { - Collect { - source_map, - decls, - ignore_mark, - global_ctxt: SyntaxContext::empty().apply_mark(global_mark), - static_cjs_exports: true, - has_cjs_exports: false, - is_esm: false, - should_wrap: false, - imports: HashMap::new(), - exports: HashMap::new(), - exports_locals: HashMap::new(), - exports_all: HashMap::new(), - non_static_access: HashMap::new(), - non_const_bindings: HashMap::new(), - non_static_requires: HashSet::new(), - wrapped_requires: HashSet::new(), - in_module_this: true, - in_top_level: true, - in_export_decl: false, - in_function: false, - in_assign: false, - bailouts: if trace_bailouts { Some(vec![]) } else { None }, - } - } -} - -impl From for CollectResult { - fn from(collect: Collect) -> CollectResult { - CollectResult { - imports: collect - .imports - .into_iter() - .map( - |( - local, - Import { - source, - specifier, - loc, - kind, - }, - )| ImportedSymbol { - source, - local: local.0, - imported: specifier, - loc, - kind, - }, - ) - .collect(), - exports: collect - .exports - .into_iter() - .map( - |( - exported, - Export { - source, - specifier, - loc, - }, - )| ExportedSymbol { - source, - local: specifier, - exported, - loc, - }, - ) - .collect(), - exports_all: collect - .exports_all - .into_iter() - .map(|(source, loc)| ExportedAll { source, loc }) - .collect(), - } - } -} - -macro_rules! collect_visit_fn { - ($name:ident, $type:ident) => { - fn $name(&mut self, node: &$type, _parent: &dyn Node) { - let in_module_this = self.in_module_this; - let in_function = self.in_function; - self.in_module_this = false; - self.in_function = true; - node.visit_children_with(self); - self.in_module_this = in_module_this; - self.in_function = in_function; - } - }; -} - -impl Visit for Collect { - fn visit_module(&mut self, node: &Module, _parent: &dyn Node) { - self.in_module_this = true; - self.in_top_level = true; - self.in_function = false; - node.visit_children_with(self); - self.in_module_this = false; - - if let Some(bailouts) = &mut self.bailouts { - for key in self.imports.keys() { - if let Some(spans) = self.non_static_access.get(key) { - for span in spans { - bailouts.push(Bailout { - loc: SourceLocation::from(&self.source_map, *span), - reason: BailoutReason::NonStaticAccess, - }) - } - } - } - - bailouts.sort_by(|a, b| a.loc.partial_cmp(&b.loc).unwrap()); - } - } - - collect_visit_fn!(visit_function, Function); - collect_visit_fn!(visit_class, Class); - collect_visit_fn!(visit_getter_prop, GetterProp); - collect_visit_fn!(visit_setter_prop, SetterProp); - - fn visit_arrow_expr(&mut self, node: &ArrowExpr, _parent: &dyn Node) { - let in_function = self.in_function; - self.in_function = true; - node.visit_children_with(self); - self.in_function = in_function; - } - - fn visit_module_item(&mut self, node: &ModuleItem, _parent: &dyn Node) { - match node { - ModuleItem::ModuleDecl(_decl) => { - self.is_esm = true; - } - ModuleItem::Stmt(stmt) => { - match stmt { - Stmt::Decl(decl) => { - if let Decl::Var(_var) = decl { - decl.visit_children_with(self); - return; - } - } - Stmt::Expr(expr) => { - // Top-level require(). Do not traverse further so it is not marked as wrapped. - if let Some(_source) = self.match_require(&*expr.expr) { - return; - } - - // TODO: optimize `require('foo').bar` / `require('foo').bar()` as well - } - _ => {} - } - } - } - - self.in_top_level = false; - node.visit_children_with(self); - self.in_top_level = true; - } - - fn visit_import_decl(&mut self, node: &ImportDecl, _parent: &dyn Node) { - for specifier in &node.specifiers { - match specifier { - ImportSpecifier::Named(named) => { - let imported = match &named.imported { - Some(imported) => imported.sym.clone(), - None => named.local.sym.clone(), - }; - self.imports.insert( - id!(named.local), - Import { - source: node.src.value.clone(), - specifier: imported, - kind: ImportKind::Import, - loc: SourceLocation::from(&self.source_map, named.span), - }, - ); - } - ImportSpecifier::Default(default) => { - self.imports.insert( - id!(default.local), - Import { - source: node.src.value.clone(), - specifier: js_word!("default"), - kind: ImportKind::Import, - loc: SourceLocation::from(&self.source_map, default.span), - }, - ); - } - ImportSpecifier::Namespace(namespace) => { - self.imports.insert( - id!(namespace.local), - Import { - source: node.src.value.clone(), - specifier: "*".into(), - kind: ImportKind::Import, - loc: SourceLocation::from(&self.source_map, namespace.span), - }, - ); - } - } - } - } - - fn visit_named_export(&mut self, node: &NamedExport, _parent: &dyn Node) { - for specifier in &node.specifiers { - let source = node.src.as_ref().map(|s| s.value.clone()); - match specifier { - ExportSpecifier::Named(named) => { - let exported = match &named.exported { - Some(exported) => exported.clone(), - None => named.orig.clone(), - }; - self.exports.insert( - exported.sym.clone(), - Export { - specifier: named.orig.sym.clone(), - loc: SourceLocation::from(&self.source_map, exported.span), - source, - }, - ); - self - .exports_locals - .entry(named.orig.sym.clone()) - .or_insert_with(|| exported.sym.clone()); - } - ExportSpecifier::Default(default) => { - self.exports.insert( - js_word!("default"), - Export { - specifier: default.exported.sym.clone(), - loc: SourceLocation::from(&self.source_map, default.exported.span), - source, - }, - ); - self - .exports_locals - .entry(default.exported.sym.clone()) - .or_insert_with(|| js_word!("default")); - } - ExportSpecifier::Namespace(namespace) => { - self.exports.insert( - namespace.name.sym.clone(), - Export { - specifier: "*".into(), - loc: SourceLocation::from(&self.source_map, namespace.span), - source, - }, - ); - // Populating exports_locals with * doesn't make any sense at all - // and hoist doesn't use this anyway. - } - } - } - } - - fn visit_export_decl(&mut self, node: &ExportDecl, _parent: &dyn Node) { - match &node.decl { - Decl::Class(class) => { - self.exports.insert( - class.ident.sym.clone(), - Export { - specifier: class.ident.sym.clone(), - loc: SourceLocation::from(&self.source_map, class.ident.span), - source: None, - }, - ); - self - .exports_locals - .entry(class.ident.sym.clone()) - .or_insert_with(|| class.ident.sym.clone()); - } - Decl::Fn(func) => { - self.exports.insert( - func.ident.sym.clone(), - Export { - specifier: func.ident.sym.clone(), - loc: SourceLocation::from(&self.source_map, func.ident.span), - source: None, - }, - ); - self - .exports_locals - .entry(func.ident.sym.clone()) - .or_insert_with(|| func.ident.sym.clone()); - } - Decl::Var(var) => { - for decl in &var.decls { - self.in_export_decl = true; - decl.name.visit_with(decl, self); - self.in_export_decl = false; - - decl.init.visit_with(decl, self); - } - } - _ => {} - } - - node.visit_children_with(self); - } - - fn visit_export_default_decl(&mut self, node: &ExportDefaultDecl, _parent: &dyn Node) { - match &node.decl { - DefaultDecl::Class(class) => { - if let Some(ident) = &class.ident { - self.exports.insert( - "default".into(), - Export { - specifier: ident.sym.clone(), - loc: SourceLocation::from(&self.source_map, node.span), - source: None, - }, - ); - self - .exports_locals - .entry(ident.sym.clone()) - .or_insert_with(|| "default".into()); - } - } - DefaultDecl::Fn(func) => { - if let Some(ident) = &func.ident { - self.exports.insert( - "default".into(), - Export { - specifier: ident.sym.clone(), - loc: SourceLocation::from(&self.source_map, node.span), - source: None, - }, - ); - self - .exports_locals - .entry(ident.sym.clone()) - .or_insert_with(|| "default".into()); - } - } - _ => { - unreachable!("unsupported export default declaration"); - } - }; - - node.visit_children_with(self); - } - - fn visit_export_all(&mut self, node: &ExportAll, _parent: &dyn Node) { - self.exports_all.insert( - node.src.value.clone(), - SourceLocation::from(&self.source_map, node.span), - ); - } - - fn visit_return_stmt(&mut self, node: &ReturnStmt, _parent: &dyn Node) { - if !self.in_function { - self.should_wrap = true; - self.add_bailout(node.span, BailoutReason::TopLevelReturn); - } - - node.visit_children_with(self) - } - - fn visit_binding_ident(&mut self, node: &BindingIdent, _parent: &dyn Node) { - if self.in_export_decl { - self.exports.insert( - node.id.sym.clone(), - Export { - specifier: node.id.sym.clone(), - loc: SourceLocation::from(&self.source_map, node.id.span), - source: None, - }, - ); - self - .exports_locals - .entry(node.id.sym.clone()) - .or_insert_with(|| node.id.sym.clone()); - } - - if self.in_assign && node.id.span.ctxt() == self.global_ctxt { - self - .non_const_bindings - .entry(id!(node.id)) - .or_default() - .push(node.id.span); - } - } - - fn visit_assign_pat_prop(&mut self, node: &AssignPatProp, _parent: &dyn Node) { - if self.in_export_decl { - self.exports.insert( - node.key.sym.clone(), - Export { - specifier: node.key.sym.clone(), - loc: SourceLocation::from(&self.source_map, node.key.span), - source: None, - }, - ); - self - .exports_locals - .entry(node.key.sym.clone()) - .or_insert_with(|| node.key.sym.clone()); - } - - if self.in_assign && node.key.span.ctxt() == self.global_ctxt { - self - .non_const_bindings - .entry(id!(node.key)) - .or_default() - .push(node.key.span); - } - } - - fn visit_member_expr(&mut self, node: &MemberExpr, _parent: &dyn Node) { - // if module.exports, ensure only assignment or static member expression - // if exports, ensure only static member expression - // if require, could be static access (handle in fold) - - if match_member_expr(node, vec!["module", "exports"], &self.decls) { - self.static_cjs_exports = false; - self.has_cjs_exports = true; - return; - } - - if match_member_expr(node, vec!["module", "hot"], &self.decls) { - return; - } - - if match_member_expr(node, vec!["module", "require"], &self.decls) { - return; - } - - let is_static = match &*node.prop { - Expr::Ident(_) => !node.computed, - Expr::Lit(Lit::Str(_)) => true, - _ => false, - }; - - if let ExprOrSuper::Expr(expr) = &node.obj { - match &**expr { - Expr::Member(member) => { - if match_member_expr(member, vec!["module", "exports"], &self.decls) { - self.has_cjs_exports = true; - if !is_static { - self.static_cjs_exports = false; - self.add_bailout(node.span, BailoutReason::NonStaticExports); - } - } - return; - } - Expr::Ident(ident) => { - let exports: JsWord = "exports".into(); - if ident.sym == exports && !self.decls.contains(&id!(ident)) { - self.has_cjs_exports = true; - if !is_static { - self.static_cjs_exports = false; - self.add_bailout(node.span, BailoutReason::NonStaticExports); - } - } - - if ident.sym == js_word!("module") && !self.decls.contains(&id!(ident)) { - self.has_cjs_exports = true; - self.static_cjs_exports = false; - self.should_wrap = true; - self.add_bailout(node.span, BailoutReason::FreeModule); - } - - // `import` isn't really an identifier... - if !is_static && ident.sym != js_word!("import") { - self - .non_static_access - .entry(id!(ident)) - .or_default() - .push(node.span); - } - return; - } - Expr::This(_this) => { - if self.in_module_this { - self.has_cjs_exports = true; - if !is_static { - self.static_cjs_exports = false; - self.add_bailout(node.span, BailoutReason::NonStaticExports); - } - } - return; - } - _ => {} - } - } - - node.visit_children_with(self); - } - - fn visit_unary_expr(&mut self, node: &UnaryExpr, _parent: &dyn Node) { - if node.op == UnaryOp::TypeOf { - match &*node.arg { - Expr::Ident(ident) - if ident.sym == js_word!("module") && !self.decls.contains(&id!(ident)) => - { - // Do nothing to avoid the ident visitor from marking the module as non-static. - } - _ => node.visit_children_with(self), - } - } else { - node.visit_children_with(self); - } - } - - fn visit_expr(&mut self, node: &Expr, _parent: &dyn Node) { - // If we reached this visitor, this is a non-top-level require that isn't in a variable - // declaration. We need to wrap the referenced module to preserve side effect ordering. - if let Some(source) = self.match_require(node) { - self.wrapped_requires.insert(source); - let span = match node { - Expr::Call(c) => c.span, - _ => unreachable!(), - }; - self.add_bailout(span, BailoutReason::NonTopLevelRequire); - } - - if let Some(source) = match_import(node, self.ignore_mark) { - self.non_static_requires.insert(source.clone()); - self.wrapped_requires.insert(source); - let span = match node { - Expr::Call(c) => c.span, - _ => unreachable!(), - }; - self.add_bailout(span, BailoutReason::NonStaticDynamicImport); - } - - match node { - Expr::Ident(ident) => { - // Bail if `module` or `exports` are accessed non-statically. - let is_module = ident.sym == js_word!("module"); - let exports: JsWord = "exports".into(); - let is_exports = ident.sym == exports; - if (is_module || is_exports) && !self.decls.contains(&id!(ident)) { - self.has_cjs_exports = true; - self.static_cjs_exports = false; - if is_module { - self.should_wrap = true; - self.add_bailout(ident.span, BailoutReason::FreeModule); - } else { - self.add_bailout(ident.span, BailoutReason::FreeExports); - } - } - - // `import` isn't really an identifier... - if ident.sym != js_word!("import") { - self - .non_static_access - .entry(id!(ident)) - .or_default() - .push(ident.span); - } - } - _ => { - node.visit_children_with(self); - } - } - } - - fn visit_this_expr(&mut self, node: &ThisExpr, _parent: &dyn Node) { - if self.in_module_this { - self.has_cjs_exports = true; - self.static_cjs_exports = false; - self.add_bailout(node.span, BailoutReason::FreeExports); - } - } - - fn visit_assign_expr(&mut self, node: &AssignExpr, _parent: &dyn Node) { - // if rhs is a require, record static accesses - // if lhs is `exports`, mark as CJS exports re-assigned - // if lhs is `module.exports` - // if lhs is `module.exports.XXX` or `exports.XXX`, record static export - - self.in_assign = true; - node.left.visit_with(node, self); - self.in_assign = false; - node.right.visit_with(node, self); - - if let PatOrExpr::Pat(pat) = &node.left { - if has_binding_identifier(pat, &"exports".into(), &self.decls) { - // Must wrap for cases like - // ``` - // function logExports() { - // console.log(exports); - // } - // exports.test = 2; - // logExports(); - // exports = {test: 4}; - // logExports(); - // ``` - self.static_cjs_exports = false; - self.has_cjs_exports = true; - self.should_wrap = true; - self.add_bailout(node.span, BailoutReason::ExportsReassignment); - } else if has_binding_identifier(pat, &"module".into(), &self.decls) { - // Same for `module`. If it is reassigned we can't correctly statically analyze. - self.static_cjs_exports = false; - self.has_cjs_exports = true; - self.should_wrap = true; - self.add_bailout(node.span, BailoutReason::ModuleReassignment); - } - } - } - - fn visit_var_declarator(&mut self, node: &VarDeclarator, _parent: &dyn Node) { - // if init is a require call, record static accesses - if let Some(init) = &node.init { - if let Some(source) = self.match_require(init) { - self.add_pat_imports(&node.name, &source, ImportKind::Require); - return; - } - - match &**init { - Expr::Member(member) => { - if let ExprOrSuper::Expr(expr) = &member.obj { - if let Some(source) = self.match_require(&*expr) { - // Convert member expression on require to a destructuring assignment. - // const yx = require('y').x; -> const {x: yx} = require('x'); - let key = match &*member.prop { - Expr::Ident(ident) => { - if !member.computed { - PropName::Ident(ident.clone()) - } else { - PropName::Computed(ComputedPropName { - span: DUMMY_SP, - expr: Box::new(*expr.clone()), - }) - } - } - Expr::Lit(Lit::Str(str_)) => PropName::Str(str_.clone()), - _ => PropName::Computed(ComputedPropName { - span: DUMMY_SP, - expr: Box::new(*expr.clone()), - }), - }; - - self.add_pat_imports( - &Pat::Object(ObjectPat { - optional: false, - span: DUMMY_SP, - type_ann: None, - props: vec![ObjectPatProp::KeyValue(KeyValuePatProp { - key, - value: Box::new(node.name.clone()), - })], - }), - &source, - ImportKind::Require, - ); - return; - } - } - } - Expr::Await(await_exp) => { - // let x = await import('foo'); - // let {x} = await import('foo'); - if let Some(source) = match_import(&*await_exp.arg, self.ignore_mark) { - self.add_pat_imports(&node.name, &source, ImportKind::DynamicImport); - return; - } - } - _ => {} - } - } - - // This is visited via visit_module_item with is_top_level == true, it needs to be - // set to false for called visitors (and restored again). - let in_top_level = self.in_top_level; - self.in_top_level = false; - node.visit_children_with(self); - self.in_top_level = in_top_level; - } - - fn visit_call_expr(&mut self, node: &CallExpr, _parent: &dyn Node) { - if let ExprOrSuper::Expr(expr) = &node.callee { - match &**expr { - Expr::Ident(ident) => { - if ident.sym == js_word!("eval") && !self.decls.contains(&id!(ident)) { - self.should_wrap = true; - self.add_bailout(node.span, BailoutReason::Eval); - } - } - Expr::Member(member) => { - // import('foo').then(foo => ...); - if let ExprOrSuper::Expr(obj) = &member.obj { - if let Some(source) = match_import(&*obj, self.ignore_mark) { - let then: JsWord = "then".into(); - let is_then = match &*member.prop { - Expr::Ident(ident) => !member.computed && ident.sym == then, - Expr::Lit(Lit::Str(str)) => str.value == then, - _ => false, - }; - - if is_then { - if let Some(ExprOrSpread { expr, .. }) = node.args.get(0) { - let param = match &**expr { - Expr::Fn(func) => func.function.params.get(0).map(|param| ¶m.pat), - Expr::Arrow(arrow) => arrow.params.get(0), - _ => None, - }; - - if let Some(param) = param { - self.add_pat_imports(param, &source, ImportKind::DynamicImport); - } else { - self.non_static_requires.insert(source.clone()); - self.wrapped_requires.insert(source); - self.add_bailout(node.span, BailoutReason::NonStaticDynamicImport); - } - - expr.visit_with(node, self); - return; - } - } - } - } - } - _ => {} - } - } - - node.visit_children_with(self); - } -} - -impl Collect { - pub fn match_require(&self, node: &Expr) -> Option { - match_require(node, &self.decls, self.ignore_mark) - } - - fn add_pat_imports(&mut self, node: &Pat, src: &JsWord, kind: ImportKind) { - if !self.in_top_level { - self.wrapped_requires.insert(src.clone()); - if kind != ImportKind::DynamicImport { - self.non_static_requires.insert(src.clone()); - let span = match node { - Pat::Ident(id) => id.id.span, - Pat::Array(arr) => arr.span, - Pat::Object(obj) => obj.span, - Pat::Rest(rest) => rest.span, - Pat::Assign(assign) => assign.span, - Pat::Invalid(i) => i.span, - Pat::Expr(_) => DUMMY_SP, - }; - self.add_bailout(span, BailoutReason::NonTopLevelRequire); - } - } - - match node { - Pat::Ident(ident) => { - // let x = require('y'); - // Need to track member accesses of `x`. - self.imports.insert( - id!(ident.id), - Import { - source: src.clone(), - specifier: "*".into(), - kind, - loc: SourceLocation::from(&self.source_map, ident.id.span), - }, - ); - } - Pat::Object(object) => { - for prop in &object.props { - match prop { - ObjectPatProp::KeyValue(kv) => { - let imported = match &kv.key { - PropName::Ident(ident) => ident.sym.clone(), - PropName::Str(str) => str.value.clone(), - _ => { - // Non-static. E.g. computed property. - self.non_static_requires.insert(src.clone()); - self.add_bailout(object.span, BailoutReason::NonStaticDestructuring); - continue; - } - }; - - match &*kv.value { - Pat::Ident(ident) => { - // let {x: y} = require('y'); - // Need to track `x` as a used symbol. - self.imports.insert( - id!(ident.id), - Import { - source: src.clone(), - specifier: imported, - kind, - loc: SourceLocation::from(&self.source_map, ident.id.span), - }, - ); - - // Mark as non-constant. CJS exports can be mutated by other modules, - // so it's not safe to reference them directly. - self - .non_const_bindings - .entry(id!(ident.id)) - .or_default() - .push(ident.id.span); - } - _ => { - // Non-static. - self.non_static_requires.insert(src.clone()); - self.add_bailout(object.span, BailoutReason::NonStaticDestructuring); - } - } - } - ObjectPatProp::Assign(assign) => { - // let {x} = require('y'); - // let {x = 2} = require('y'); - // Need to track `x` as a used symbol. - self.imports.insert( - id!(assign.key), - Import { - source: src.clone(), - specifier: assign.key.sym.clone(), - kind, - loc: SourceLocation::from(&self.source_map, assign.key.span), - }, - ); - self - .non_const_bindings - .entry(id!(assign.key)) - .or_default() - .push(assign.key.span); - } - ObjectPatProp::Rest(_rest) => { - // let {x, ...y} = require('y'); - // Non-static. We don't know what keys are used. - self.non_static_requires.insert(src.clone()); - self.add_bailout(object.span, BailoutReason::NonStaticDestructuring); - } - } - } - } - _ => { - // Non-static. - self.non_static_requires.insert(src.clone()); - let span = match node { - Pat::Ident(id) => id.id.span, - Pat::Array(arr) => arr.span, - Pat::Object(obj) => obj.span, - Pat::Rest(rest) => rest.span, - Pat::Assign(assign) => assign.span, - Pat::Invalid(i) => i.span, - Pat::Expr(_) => DUMMY_SP, - }; - self.add_bailout(span, BailoutReason::NonStaticDestructuring); - } - } - } - - pub fn get_non_const_binding_idents(&self, node: &Pat, idents: &mut Vec) { - match node { - Pat::Ident(ident) => { - if self.non_const_bindings.contains_key(&id!(ident.id)) { - idents.push(ident.id.clone()); - } - } - Pat::Object(object) => { - for prop in &object.props { - match prop { - ObjectPatProp::KeyValue(kv) => { - self.get_non_const_binding_idents(&*kv.value, idents); - } - ObjectPatProp::Assign(assign) => { - if self.non_const_bindings.contains_key(&id!(assign.key)) { - idents.push(assign.key.clone()); - } - } - ObjectPatProp::Rest(rest) => { - self.get_non_const_binding_idents(&*rest.arg, idents); - } - } - } - } - Pat::Array(array) => { - for el in array.elems.iter().flatten() { - self.get_non_const_binding_idents(el, idents); - } - } - _ => {} - } - } - - fn add_bailout(&mut self, span: Span, reason: BailoutReason) { - if let Some(bailouts) = &mut self.bailouts { - bailouts.push(Bailout { - loc: SourceLocation::from(&self.source_map, span), - reason, - }) - } - } -} - -fn has_binding_identifier(node: &Pat, sym: &JsWord, decls: &HashSet) -> bool { - match node { - Pat::Ident(ident) => { - if ident.id.sym == *sym && !decls.contains(&id!(ident.id)) { - return true; - } - } - Pat::Object(object) => { - for prop in &object.props { - match prop { - ObjectPatProp::KeyValue(kv) => { - if has_binding_identifier(&*kv.value, sym, decls) { - return true; - } - } - ObjectPatProp::Assign(assign) => { - if assign.key.sym == *sym && !decls.contains(&id!(assign.key)) { - return true; - } - } - ObjectPatProp::Rest(rest) => { - if has_binding_identifier(&*rest.arg, sym, decls) { - return true; - } - } - } - } - } - Pat::Array(array) => { - for el in array.elems.iter().flatten() { - if has_binding_identifier(el, sym, decls) { - return true; - } - } - } - _ => {} - } - - false -} diff --git a/packages/transformers/js/core/src/fs.rs b/packages/transformers/js/core/src/fs.rs index 359d59c09fe..b0c33849edc 100644 --- a/packages/transformers/js/core/src/fs.rs +++ b/packages/transformers/js/core/src/fs.rs @@ -1,5 +1,5 @@ -use crate::collect::{Collect, Import}; use crate::dependency_collector::{DependencyDescriptor, DependencyKind}; +use crate::hoist::{Collect, Import}; use crate::utils::SourceLocation; use data_encoding::{BASE64, HEXLOWER}; use std::collections::HashSet; diff --git a/packages/transformers/js/core/src/hoist.rs b/packages/transformers/js/core/src/hoist.rs index 5a9a9c2dcd4..8cd2f85e90b 100644 --- a/packages/transformers/js/core/src/hoist.rs +++ b/packages/transformers/js/core/src/hoist.rs @@ -3,15 +3,14 @@ use std::collections::hash_map::DefaultHasher; use std::collections::{HashMap, HashSet}; use std::hash::Hasher; use swc_atoms::JsWord; -use swc_common::{Span, SyntaxContext, DUMMY_SP}; +use swc_common::{sync::Lrc, Mark, Span, SyntaxContext, DUMMY_SP}; use swc_ecmascript::ast::*; -use swc_ecmascript::visit::{Fold, FoldWith}; +use swc_ecmascript::visit::{Fold, FoldWith, Node, Visit, VisitWith}; -use crate::collect::{Collect, Import, ImportKind}; use crate::id; use crate::utils::{ - match_import, match_member_expr, match_require, CodeHighlight, Diagnostic, DiagnosticSeverity, - SourceLocation, + match_import, match_member_expr, match_require, Bailout, BailoutReason, CodeHighlight, + Diagnostic, DiagnosticSeverity, IdentId, SourceLocation, }; macro_rules! hash { @@ -1098,6 +1097,1028 @@ impl<'a> Hoist<'a> { } } +macro_rules! collect_visit_fn { + ($name:ident, $type:ident) => { + fn $name(&mut self, node: &$type, _parent: &dyn Node) { + let in_module_this = self.in_module_this; + let in_function = self.in_function; + self.in_module_this = false; + self.in_function = true; + node.visit_children_with(self); + self.in_module_this = in_module_this; + self.in_function = in_function; + } + }; +} + +#[derive(Debug, PartialEq, Clone, Copy, Serialize)] +pub enum ImportKind { + Require, + Import, + DynamicImport, +} + +#[derive(Debug)] +pub struct Import { + pub source: JsWord, + pub specifier: JsWord, + pub kind: ImportKind, + pub loc: SourceLocation, +} + +#[derive(Debug)] +pub struct Export { + pub source: Option, + pub specifier: JsWord, + pub loc: SourceLocation, +} + +pub struct Collect { + pub source_map: Lrc, + pub decls: HashSet, + pub ignore_mark: Mark, + pub global_ctxt: SyntaxContext, + pub static_cjs_exports: bool, + pub has_cjs_exports: bool, + pub is_esm: bool, + pub should_wrap: bool, + // local name -> descriptor + pub imports: HashMap, + // exported name -> descriptor + pub exports: HashMap, + // local name -> exported name + pub exports_locals: HashMap, + pub exports_all: HashMap, + pub non_static_access: HashMap>, + pub non_const_bindings: HashMap>, + pub non_static_requires: HashSet, + pub wrapped_requires: HashSet, + pub bailouts: Option>, + in_module_this: bool, + in_top_level: bool, + in_export_decl: bool, + in_function: bool, + in_assign: bool, +} + +#[derive(Debug, Serialize)] +struct CollectImportedSymbol { + source: JsWord, + local: JsWord, + imported: JsWord, + loc: SourceLocation, + kind: ImportKind, +} + +#[derive(Debug, Serialize)] +struct CollectExportedSymbol { + source: Option, + local: JsWord, + exported: JsWord, + loc: SourceLocation, +} + +#[derive(Debug, Serialize)] +struct CollectExportedAll { + source: JsWord, + loc: SourceLocation, +} + +#[derive(Serialize, Debug)] +pub struct CollectResult { + imports: Vec, + exports: Vec, + exports_all: Vec, +} + +impl Collect { + pub fn new( + source_map: Lrc, + decls: HashSet, + ignore_mark: Mark, + global_mark: Mark, + trace_bailouts: bool, + ) -> Self { + Collect { + source_map, + decls, + ignore_mark, + global_ctxt: SyntaxContext::empty().apply_mark(global_mark), + static_cjs_exports: true, + has_cjs_exports: false, + is_esm: false, + should_wrap: false, + imports: HashMap::new(), + exports: HashMap::new(), + exports_locals: HashMap::new(), + exports_all: HashMap::new(), + non_static_access: HashMap::new(), + non_const_bindings: HashMap::new(), + non_static_requires: HashSet::new(), + wrapped_requires: HashSet::new(), + in_module_this: true, + in_top_level: true, + in_export_decl: false, + in_function: false, + in_assign: false, + bailouts: if trace_bailouts { Some(vec![]) } else { None }, + } + } +} + +impl From for CollectResult { + fn from(collect: Collect) -> CollectResult { + CollectResult { + imports: collect + .imports + .into_iter() + .map( + |( + local, + Import { + source, + specifier, + loc, + kind, + }, + )| CollectImportedSymbol { + source, + local: local.0, + imported: specifier, + loc, + kind, + }, + ) + .collect(), + exports: collect + .exports + .into_iter() + .map( + |( + exported, + Export { + source, + specifier, + loc, + }, + )| CollectExportedSymbol { + source, + local: specifier, + exported, + loc, + }, + ) + .collect(), + exports_all: collect + .exports_all + .into_iter() + .map(|(source, loc)| CollectExportedAll { source, loc }) + .collect(), + } + } +} + +impl Visit for Collect { + fn visit_module(&mut self, node: &Module, _parent: &dyn Node) { + self.in_module_this = true; + self.in_top_level = true; + self.in_function = false; + node.visit_children_with(self); + self.in_module_this = false; + + if let Some(bailouts) = &mut self.bailouts { + for key in self.imports.keys() { + if let Some(spans) = self.non_static_access.get(key) { + for span in spans { + bailouts.push(Bailout { + loc: SourceLocation::from(&self.source_map, *span), + reason: BailoutReason::NonStaticAccess, + }) + } + } + } + + bailouts.sort_by(|a, b| a.loc.partial_cmp(&b.loc).unwrap()); + } + } + + collect_visit_fn!(visit_function, Function); + collect_visit_fn!(visit_class, Class); + collect_visit_fn!(visit_getter_prop, GetterProp); + collect_visit_fn!(visit_setter_prop, SetterProp); + + fn visit_arrow_expr(&mut self, node: &ArrowExpr, _parent: &dyn Node) { + let in_function = self.in_function; + self.in_function = true; + node.visit_children_with(self); + self.in_function = in_function; + } + + fn visit_module_item(&mut self, node: &ModuleItem, _parent: &dyn Node) { + match node { + ModuleItem::ModuleDecl(_decl) => { + self.is_esm = true; + } + ModuleItem::Stmt(stmt) => { + match stmt { + Stmt::Decl(decl) => { + if let Decl::Var(_var) = decl { + decl.visit_children_with(self); + return; + } + } + Stmt::Expr(expr) => { + // Top-level require(). Do not traverse further so it is not marked as wrapped. + if let Some(_source) = self.match_require(&*expr.expr) { + return; + } + + // TODO: optimize `require('foo').bar` / `require('foo').bar()` as well + } + _ => {} + } + } + } + + self.in_top_level = false; + node.visit_children_with(self); + self.in_top_level = true; + } + + fn visit_import_decl(&mut self, node: &ImportDecl, _parent: &dyn Node) { + for specifier in &node.specifiers { + match specifier { + ImportSpecifier::Named(named) => { + let imported = match &named.imported { + Some(imported) => imported.sym.clone(), + None => named.local.sym.clone(), + }; + self.imports.insert( + id!(named.local), + Import { + source: node.src.value.clone(), + specifier: imported, + kind: ImportKind::Import, + loc: SourceLocation::from(&self.source_map, named.span), + }, + ); + } + ImportSpecifier::Default(default) => { + self.imports.insert( + id!(default.local), + Import { + source: node.src.value.clone(), + specifier: js_word!("default"), + kind: ImportKind::Import, + loc: SourceLocation::from(&self.source_map, default.span), + }, + ); + } + ImportSpecifier::Namespace(namespace) => { + self.imports.insert( + id!(namespace.local), + Import { + source: node.src.value.clone(), + specifier: "*".into(), + kind: ImportKind::Import, + loc: SourceLocation::from(&self.source_map, namespace.span), + }, + ); + } + } + } + } + + fn visit_named_export(&mut self, node: &NamedExport, _parent: &dyn Node) { + for specifier in &node.specifiers { + let source = node.src.as_ref().map(|s| s.value.clone()); + match specifier { + ExportSpecifier::Named(named) => { + let exported = match &named.exported { + Some(exported) => exported.clone(), + None => named.orig.clone(), + }; + self.exports.insert( + exported.sym.clone(), + Export { + specifier: named.orig.sym.clone(), + loc: SourceLocation::from(&self.source_map, exported.span), + source, + }, + ); + self + .exports_locals + .entry(named.orig.sym.clone()) + .or_insert_with(|| exported.sym.clone()); + } + ExportSpecifier::Default(default) => { + self.exports.insert( + js_word!("default"), + Export { + specifier: default.exported.sym.clone(), + loc: SourceLocation::from(&self.source_map, default.exported.span), + source, + }, + ); + self + .exports_locals + .entry(default.exported.sym.clone()) + .or_insert_with(|| js_word!("default")); + } + ExportSpecifier::Namespace(namespace) => { + self.exports.insert( + namespace.name.sym.clone(), + Export { + specifier: "*".into(), + loc: SourceLocation::from(&self.source_map, namespace.span), + source, + }, + ); + // Populating exports_locals with * doesn't make any sense at all + // and hoist doesn't use this anyway. + } + } + } + } + + fn visit_export_decl(&mut self, node: &ExportDecl, _parent: &dyn Node) { + match &node.decl { + Decl::Class(class) => { + self.exports.insert( + class.ident.sym.clone(), + Export { + specifier: class.ident.sym.clone(), + loc: SourceLocation::from(&self.source_map, class.ident.span), + source: None, + }, + ); + self + .exports_locals + .entry(class.ident.sym.clone()) + .or_insert_with(|| class.ident.sym.clone()); + } + Decl::Fn(func) => { + self.exports.insert( + func.ident.sym.clone(), + Export { + specifier: func.ident.sym.clone(), + loc: SourceLocation::from(&self.source_map, func.ident.span), + source: None, + }, + ); + self + .exports_locals + .entry(func.ident.sym.clone()) + .or_insert_with(|| func.ident.sym.clone()); + } + Decl::Var(var) => { + for decl in &var.decls { + self.in_export_decl = true; + decl.name.visit_with(decl, self); + self.in_export_decl = false; + + decl.init.visit_with(decl, self); + } + } + _ => {} + } + + node.visit_children_with(self); + } + + fn visit_export_default_decl(&mut self, node: &ExportDefaultDecl, _parent: &dyn Node) { + match &node.decl { + DefaultDecl::Class(class) => { + if let Some(ident) = &class.ident { + self.exports.insert( + "default".into(), + Export { + specifier: ident.sym.clone(), + loc: SourceLocation::from(&self.source_map, node.span), + source: None, + }, + ); + self + .exports_locals + .entry(ident.sym.clone()) + .or_insert_with(|| "default".into()); + } + } + DefaultDecl::Fn(func) => { + if let Some(ident) = &func.ident { + self.exports.insert( + "default".into(), + Export { + specifier: ident.sym.clone(), + loc: SourceLocation::from(&self.source_map, node.span), + source: None, + }, + ); + self + .exports_locals + .entry(ident.sym.clone()) + .or_insert_with(|| "default".into()); + } + } + _ => { + unreachable!("unsupported export default declaration"); + } + }; + + node.visit_children_with(self); + } + + fn visit_export_all(&mut self, node: &ExportAll, _parent: &dyn Node) { + self.exports_all.insert( + node.src.value.clone(), + SourceLocation::from(&self.source_map, node.span), + ); + } + + fn visit_return_stmt(&mut self, node: &ReturnStmt, _parent: &dyn Node) { + if !self.in_function { + self.should_wrap = true; + self.add_bailout(node.span, BailoutReason::TopLevelReturn); + } + + node.visit_children_with(self) + } + + fn visit_binding_ident(&mut self, node: &BindingIdent, _parent: &dyn Node) { + if self.in_export_decl { + self.exports.insert( + node.id.sym.clone(), + Export { + specifier: node.id.sym.clone(), + loc: SourceLocation::from(&self.source_map, node.id.span), + source: None, + }, + ); + self + .exports_locals + .entry(node.id.sym.clone()) + .or_insert_with(|| node.id.sym.clone()); + } + + if self.in_assign && node.id.span.ctxt() == self.global_ctxt { + self + .non_const_bindings + .entry(id!(node.id)) + .or_default() + .push(node.id.span); + } + } + + fn visit_assign_pat_prop(&mut self, node: &AssignPatProp, _parent: &dyn Node) { + if self.in_export_decl { + self.exports.insert( + node.key.sym.clone(), + Export { + specifier: node.key.sym.clone(), + loc: SourceLocation::from(&self.source_map, node.key.span), + source: None, + }, + ); + self + .exports_locals + .entry(node.key.sym.clone()) + .or_insert_with(|| node.key.sym.clone()); + } + + if self.in_assign && node.key.span.ctxt() == self.global_ctxt { + self + .non_const_bindings + .entry(id!(node.key)) + .or_default() + .push(node.key.span); + } + } + + fn visit_member_expr(&mut self, node: &MemberExpr, _parent: &dyn Node) { + // if module.exports, ensure only assignment or static member expression + // if exports, ensure only static member expression + // if require, could be static access (handle in fold) + + if match_member_expr(node, vec!["module", "exports"], &self.decls) { + self.static_cjs_exports = false; + self.has_cjs_exports = true; + return; + } + + if match_member_expr(node, vec!["module", "hot"], &self.decls) { + return; + } + + if match_member_expr(node, vec!["module", "require"], &self.decls) { + return; + } + + let is_static = match &*node.prop { + Expr::Ident(_) => !node.computed, + Expr::Lit(Lit::Str(_)) => true, + _ => false, + }; + + if let ExprOrSuper::Expr(expr) = &node.obj { + match &**expr { + Expr::Member(member) => { + if match_member_expr(member, vec!["module", "exports"], &self.decls) { + self.has_cjs_exports = true; + if !is_static { + self.static_cjs_exports = false; + self.add_bailout(node.span, BailoutReason::NonStaticExports); + } + } + return; + } + Expr::Ident(ident) => { + let exports: JsWord = "exports".into(); + if ident.sym == exports && !self.decls.contains(&id!(ident)) { + self.has_cjs_exports = true; + if !is_static { + self.static_cjs_exports = false; + self.add_bailout(node.span, BailoutReason::NonStaticExports); + } + } + + if ident.sym == js_word!("module") && !self.decls.contains(&id!(ident)) { + self.has_cjs_exports = true; + self.static_cjs_exports = false; + self.should_wrap = true; + self.add_bailout(node.span, BailoutReason::FreeModule); + } + + // `import` isn't really an identifier... + if !is_static && ident.sym != js_word!("import") { + self + .non_static_access + .entry(id!(ident)) + .or_default() + .push(node.span); + } + return; + } + Expr::This(_this) => { + if self.in_module_this { + self.has_cjs_exports = true; + if !is_static { + self.static_cjs_exports = false; + self.add_bailout(node.span, BailoutReason::NonStaticExports); + } + } + return; + } + _ => {} + } + } + + node.visit_children_with(self); + } + + fn visit_unary_expr(&mut self, node: &UnaryExpr, _parent: &dyn Node) { + if node.op == UnaryOp::TypeOf { + match &*node.arg { + Expr::Ident(ident) + if ident.sym == js_word!("module") && !self.decls.contains(&id!(ident)) => + { + // Do nothing to avoid the ident visitor from marking the module as non-static. + } + _ => node.visit_children_with(self), + } + } else { + node.visit_children_with(self); + } + } + + fn visit_expr(&mut self, node: &Expr, _parent: &dyn Node) { + // If we reached this visitor, this is a non-top-level require that isn't in a variable + // declaration. We need to wrap the referenced module to preserve side effect ordering. + if let Some(source) = self.match_require(node) { + self.wrapped_requires.insert(source); + let span = match node { + Expr::Call(c) => c.span, + _ => unreachable!(), + }; + self.add_bailout(span, BailoutReason::NonTopLevelRequire); + } + + if let Some(source) = match_import(node, self.ignore_mark) { + self.non_static_requires.insert(source.clone()); + self.wrapped_requires.insert(source); + let span = match node { + Expr::Call(c) => c.span, + _ => unreachable!(), + }; + self.add_bailout(span, BailoutReason::NonStaticDynamicImport); + } + + match node { + Expr::Ident(ident) => { + // Bail if `module` or `exports` are accessed non-statically. + let is_module = ident.sym == js_word!("module"); + let exports: JsWord = "exports".into(); + let is_exports = ident.sym == exports; + if (is_module || is_exports) && !self.decls.contains(&id!(ident)) { + self.has_cjs_exports = true; + self.static_cjs_exports = false; + if is_module { + self.should_wrap = true; + self.add_bailout(ident.span, BailoutReason::FreeModule); + } else { + self.add_bailout(ident.span, BailoutReason::FreeExports); + } + } + + // `import` isn't really an identifier... + if ident.sym != js_word!("import") { + self + .non_static_access + .entry(id!(ident)) + .or_default() + .push(ident.span); + } + } + _ => { + node.visit_children_with(self); + } + } + } + + fn visit_this_expr(&mut self, node: &ThisExpr, _parent: &dyn Node) { + if self.in_module_this { + self.has_cjs_exports = true; + self.static_cjs_exports = false; + self.add_bailout(node.span, BailoutReason::FreeExports); + } + } + + fn visit_assign_expr(&mut self, node: &AssignExpr, _parent: &dyn Node) { + // if rhs is a require, record static accesses + // if lhs is `exports`, mark as CJS exports re-assigned + // if lhs is `module.exports` + // if lhs is `module.exports.XXX` or `exports.XXX`, record static export + + self.in_assign = true; + node.left.visit_with(node, self); + self.in_assign = false; + node.right.visit_with(node, self); + + if let PatOrExpr::Pat(pat) = &node.left { + if has_binding_identifier(pat, &"exports".into(), &self.decls) { + // Must wrap for cases like + // ``` + // function logExports() { + // console.log(exports); + // } + // exports.test = 2; + // logExports(); + // exports = {test: 4}; + // logExports(); + // ``` + self.static_cjs_exports = false; + self.has_cjs_exports = true; + self.should_wrap = true; + self.add_bailout(node.span, BailoutReason::ExportsReassignment); + } else if has_binding_identifier(pat, &"module".into(), &self.decls) { + // Same for `module`. If it is reassigned we can't correctly statically analyze. + self.static_cjs_exports = false; + self.has_cjs_exports = true; + self.should_wrap = true; + self.add_bailout(node.span, BailoutReason::ModuleReassignment); + } + } + } + + fn visit_var_declarator(&mut self, node: &VarDeclarator, _parent: &dyn Node) { + // if init is a require call, record static accesses + if let Some(init) = &node.init { + if let Some(source) = self.match_require(init) { + self.add_pat_imports(&node.name, &source, ImportKind::Require); + return; + } + + match &**init { + Expr::Member(member) => { + if let ExprOrSuper::Expr(expr) = &member.obj { + if let Some(source) = self.match_require(&*expr) { + // Convert member expression on require to a destructuring assignment. + // const yx = require('y').x; -> const {x: yx} = require('x'); + let key = match &*member.prop { + Expr::Ident(ident) => { + if !member.computed { + PropName::Ident(ident.clone()) + } else { + PropName::Computed(ComputedPropName { + span: DUMMY_SP, + expr: Box::new(*expr.clone()), + }) + } + } + Expr::Lit(Lit::Str(str_)) => PropName::Str(str_.clone()), + _ => PropName::Computed(ComputedPropName { + span: DUMMY_SP, + expr: Box::new(*expr.clone()), + }), + }; + + self.add_pat_imports( + &Pat::Object(ObjectPat { + optional: false, + span: DUMMY_SP, + type_ann: None, + props: vec![ObjectPatProp::KeyValue(KeyValuePatProp { + key, + value: Box::new(node.name.clone()), + })], + }), + &source, + ImportKind::Require, + ); + return; + } + } + } + Expr::Await(await_exp) => { + // let x = await import('foo'); + // let {x} = await import('foo'); + if let Some(source) = match_import(&*await_exp.arg, self.ignore_mark) { + self.add_pat_imports(&node.name, &source, ImportKind::DynamicImport); + return; + } + } + _ => {} + } + } + + // This is visited via visit_module_item with is_top_level == true, it needs to be + // set to false for called visitors (and restored again). + let in_top_level = self.in_top_level; + self.in_top_level = false; + node.visit_children_with(self); + self.in_top_level = in_top_level; + } + + fn visit_call_expr(&mut self, node: &CallExpr, _parent: &dyn Node) { + if let ExprOrSuper::Expr(expr) = &node.callee { + match &**expr { + Expr::Ident(ident) => { + if ident.sym == js_word!("eval") && !self.decls.contains(&id!(ident)) { + self.should_wrap = true; + self.add_bailout(node.span, BailoutReason::Eval); + } + } + Expr::Member(member) => { + // import('foo').then(foo => ...); + if let ExprOrSuper::Expr(obj) = &member.obj { + if let Some(source) = match_import(&*obj, self.ignore_mark) { + let then: JsWord = "then".into(); + let is_then = match &*member.prop { + Expr::Ident(ident) => !member.computed && ident.sym == then, + Expr::Lit(Lit::Str(str)) => str.value == then, + _ => false, + }; + + if is_then { + if let Some(ExprOrSpread { expr, .. }) = node.args.get(0) { + let param = match &**expr { + Expr::Fn(func) => func.function.params.get(0).map(|param| ¶m.pat), + Expr::Arrow(arrow) => arrow.params.get(0), + _ => None, + }; + + if let Some(param) = param { + self.add_pat_imports(param, &source, ImportKind::DynamicImport); + } else { + self.non_static_requires.insert(source.clone()); + self.wrapped_requires.insert(source); + self.add_bailout(node.span, BailoutReason::NonStaticDynamicImport); + } + + expr.visit_with(node, self); + return; + } + } + } + } + } + _ => {} + } + } + + node.visit_children_with(self); + } +} + +impl Collect { + pub fn match_require(&self, node: &Expr) -> Option { + match_require(node, &self.decls, self.ignore_mark) + } + + fn add_pat_imports(&mut self, node: &Pat, src: &JsWord, kind: ImportKind) { + if !self.in_top_level { + self.wrapped_requires.insert(src.clone()); + if kind != ImportKind::DynamicImport { + self.non_static_requires.insert(src.clone()); + let span = match node { + Pat::Ident(id) => id.id.span, + Pat::Array(arr) => arr.span, + Pat::Object(obj) => obj.span, + Pat::Rest(rest) => rest.span, + Pat::Assign(assign) => assign.span, + Pat::Invalid(i) => i.span, + Pat::Expr(_) => DUMMY_SP, + }; + self.add_bailout(span, BailoutReason::NonTopLevelRequire); + } + } + + match node { + Pat::Ident(ident) => { + // let x = require('y'); + // Need to track member accesses of `x`. + self.imports.insert( + id!(ident.id), + Import { + source: src.clone(), + specifier: "*".into(), + kind, + loc: SourceLocation::from(&self.source_map, ident.id.span), + }, + ); + } + Pat::Object(object) => { + for prop in &object.props { + match prop { + ObjectPatProp::KeyValue(kv) => { + let imported = match &kv.key { + PropName::Ident(ident) => ident.sym.clone(), + PropName::Str(str) => str.value.clone(), + _ => { + // Non-static. E.g. computed property. + self.non_static_requires.insert(src.clone()); + self.add_bailout(object.span, BailoutReason::NonStaticDestructuring); + continue; + } + }; + + match &*kv.value { + Pat::Ident(ident) => { + // let {x: y} = require('y'); + // Need to track `x` as a used symbol. + self.imports.insert( + id!(ident.id), + Import { + source: src.clone(), + specifier: imported, + kind, + loc: SourceLocation::from(&self.source_map, ident.id.span), + }, + ); + + // Mark as non-constant. CJS exports can be mutated by other modules, + // so it's not safe to reference them directly. + self + .non_const_bindings + .entry(id!(ident.id)) + .or_default() + .push(ident.id.span); + } + _ => { + // Non-static. + self.non_static_requires.insert(src.clone()); + self.add_bailout(object.span, BailoutReason::NonStaticDestructuring); + } + } + } + ObjectPatProp::Assign(assign) => { + // let {x} = require('y'); + // let {x = 2} = require('y'); + // Need to track `x` as a used symbol. + self.imports.insert( + id!(assign.key), + Import { + source: src.clone(), + specifier: assign.key.sym.clone(), + kind, + loc: SourceLocation::from(&self.source_map, assign.key.span), + }, + ); + self + .non_const_bindings + .entry(id!(assign.key)) + .or_default() + .push(assign.key.span); + } + ObjectPatProp::Rest(_rest) => { + // let {x, ...y} = require('y'); + // Non-static. We don't know what keys are used. + self.non_static_requires.insert(src.clone()); + self.add_bailout(object.span, BailoutReason::NonStaticDestructuring); + } + } + } + } + _ => { + // Non-static. + self.non_static_requires.insert(src.clone()); + let span = match node { + Pat::Ident(id) => id.id.span, + Pat::Array(arr) => arr.span, + Pat::Object(obj) => obj.span, + Pat::Rest(rest) => rest.span, + Pat::Assign(assign) => assign.span, + Pat::Invalid(i) => i.span, + Pat::Expr(_) => DUMMY_SP, + }; + self.add_bailout(span, BailoutReason::NonStaticDestructuring); + } + } + } + + fn get_non_const_binding_idents(&self, node: &Pat, idents: &mut Vec) { + match node { + Pat::Ident(ident) => { + if self.non_const_bindings.contains_key(&id!(ident.id)) { + idents.push(ident.id.clone()); + } + } + Pat::Object(object) => { + for prop in &object.props { + match prop { + ObjectPatProp::KeyValue(kv) => { + self.get_non_const_binding_idents(&*kv.value, idents); + } + ObjectPatProp::Assign(assign) => { + if self.non_const_bindings.contains_key(&id!(assign.key)) { + idents.push(assign.key.clone()); + } + } + ObjectPatProp::Rest(rest) => { + self.get_non_const_binding_idents(&*rest.arg, idents); + } + } + } + } + Pat::Array(array) => { + for el in array.elems.iter().flatten() { + self.get_non_const_binding_idents(el, idents); + } + } + _ => {} + } + } + + fn add_bailout(&mut self, span: Span, reason: BailoutReason) { + if let Some(bailouts) = &mut self.bailouts { + bailouts.push(Bailout { + loc: SourceLocation::from(&self.source_map, span), + reason, + }) + } + } +} + +fn has_binding_identifier(node: &Pat, sym: &JsWord, decls: &HashSet) -> bool { + match node { + Pat::Ident(ident) => { + if ident.id.sym == *sym && !decls.contains(&id!(ident.id)) { + return true; + } + } + Pat::Object(object) => { + for prop in &object.props { + match prop { + ObjectPatProp::KeyValue(kv) => { + if has_binding_identifier(&*kv.value, sym, decls) { + return true; + } + } + ObjectPatProp::Assign(assign) => { + if assign.key.sym == *sym && !decls.contains(&id!(assign.key)) { + return true; + } + } + ObjectPatProp::Rest(rest) => { + if has_binding_identifier(&*rest.arg, sym, decls) { + return true; + } + } + } + } + } + Pat::Array(array) => { + for el in array.elems.iter().flatten() { + if has_binding_identifier(el, sym, decls) { + return true; + } + } + } + _ => {} + } + + false +} + #[cfg(test)] mod tests { use super::*; @@ -1109,7 +2130,6 @@ mod tests { use swc_ecmascript::parser::lexer::Lexer; use swc_ecmascript::parser::{EsConfig, Parser, StringInput, Syntax}; use swc_ecmascript::transforms::resolver_with_mark; - use swc_ecmascript::visit::VisitWith; extern crate indoc; use self::indoc::indoc; diff --git a/packages/transformers/js/core/src/lib.rs b/packages/transformers/js/core/src/lib.rs index 5d55ec1d3be..b9a3f51e9a1 100644 --- a/packages/transformers/js/core/src/lib.rs +++ b/packages/transformers/js/core/src/lib.rs @@ -12,7 +12,6 @@ extern crate serde; extern crate serde_bytes; extern crate sha1; -mod collect; mod decl_collector; mod dependency_collector; mod env_replacer; @@ -45,16 +44,17 @@ use swc_ecmascript::transforms::{ }; use swc_ecmascript::visit::{FoldWith, VisitWith}; -use collect::{Collect, CollectResult}; use decl_collector::*; use dependency_collector::*; use env_replacer::*; use fs::inline_fs; use global_replacer::GlobalReplacer; -use hoist::{hoist, HoistResult}; +use hoist::{hoist, CollectResult, HoistResult}; use modules::esm2cjs; use utils::{CodeHighlight, Diagnostic, DiagnosticSeverity, SourceLocation, SourceType}; +use crate::hoist::Collect; + type SourceMapBuffer = Vec<(swc_common::BytePos, swc_common::LineCol)>; #[derive(Serialize, Debug, Deserialize)]