diff --git a/packages/transformers/js/core/src/global_replacer.rs b/packages/transformers/js/core/src/global_replacer.rs index f784584c832..087e237c95e 100644 --- a/packages/transformers/js/core/src/global_replacer.rs +++ b/packages/transformers/js/core/src/global_replacer.rs @@ -1,18 +1,18 @@ use std::path::Path; -use swc_core::ecma::utils::stack_size::maybe_grow_default; use indexmap::IndexMap; use path_slash::PathBufExt; +use swc_core::common::sync::Lrc; use swc_core::common::Mark; use swc_core::common::SourceMap; use swc_core::common::SyntaxContext; use swc_core::common::DUMMY_SP; -use swc_core::ecma::ast::ComputedPropName; -use swc_core::ecma::ast::{self}; +use swc_core::ecma::ast::{self, Expr}; +use swc_core::ecma::ast::{ComputedPropName, Module}; use swc_core::ecma::atoms::js_word; use swc_core::ecma::atoms::JsWord; -use swc_core::ecma::visit::Fold; -use swc_core::ecma::visit::FoldWith; +use swc_core::ecma::visit::VisitMut; +use swc_core::ecma::visit::VisitMutWith; use crate::dependency_collector::DependencyDescriptor; use crate::dependency_collector::DependencyKind; @@ -22,10 +22,42 @@ use crate::utils::is_unresolved; use crate::utils::SourceLocation; use crate::utils::SourceType; +/// Replaces a few node.js constants with literals or require statements. +/// This duplicates some logic in [`NodeReplacer`] +/// +/// (TODO: why is this needed?). +/// +/// In particular, the following constants are replaced: +/// +/// * `process` - Replaced with a dependency to a magic 'process' module +/// * `Buffer` - Replaced with a dependency to a magic 'buffer' module +/// * `__dirname` and `__filename` - Replaced with the file path +/// * `global` - Replaced with `arguments[3]`. +/// +/// Instead of being replaced in-place the identifiers are left in their +/// location, but a declaration statement is added for the identifier at +/// the top of the module. +/// +/// For example if a module contains: +/// ```skip +/// function test() { +/// console.log(process); +/// } +/// ``` +/// +/// It should be converted into: +/// ```skip +/// const process = require('process'); +/// function test() { +/// console.log(process); +/// } +/// ``` pub struct GlobalReplacer<'a> { - pub source_map: &'a SourceMap, + pub source_map: Lrc, + /// Require statements that are inserted into the file will be added to this list. pub items: &'a mut Vec, pub global_mark: Mark, + /// Internal structure for inserted global statements. pub globals: IndexMap, pub project_root: &'a Path, pub filename: &'a Path, @@ -33,133 +65,114 @@ pub struct GlobalReplacer<'a> { pub scope_hoist: bool, } -impl<'a> Fold for GlobalReplacer<'a> { - fn fold_expr(&mut self, node: ast::Expr) -> ast::Expr { +impl VisitMut for GlobalReplacer<'_> { + fn visit_mut_expr(&mut self, node: &mut Expr) { use ast::Expr::*; - use ast::Ident; use ast::MemberExpr; use ast::MemberProp; - // Do not traverse into the `prop` side of member expressions unless computed. - let mut node = match node { - Member(expr) => { - if let MemberProp::Computed(_) = expr.prop { - Member(MemberExpr { - obj: expr.obj.fold_with(self), - prop: expr.prop.fold_with(self), - ..expr - }) - } else { + let Ident(id) = node else { + node.visit_mut_children_with(self); + return; + }; + + // Only handle global variables + if !is_unresolved(&id, self.unresolved_mark) { + return; + } + + let unresolved_mark = self.unresolved_mark; + match id.sym.to_string().as_str() { + "process" => { + if self.update_binding(id, |_| { + Call(create_require(js_word!("process"), unresolved_mark)) + }) { + let specifier = id.sym.clone(); + self.items.push(DependencyDescriptor { + kind: DependencyKind::Require, + loc: SourceLocation::from(&self.source_map, id.span), + specifier, + attributes: None, + is_optional: false, + is_helper: false, + source_type: Some(SourceType::Module), + placeholder: None, + }); + } + } + "Buffer" => { + let specifier = swc_core::ecma::atoms::JsWord::from("buffer"); + if self.update_binding(id, |_| { Member(MemberExpr { - obj: expr.obj.fold_with(self), - ..expr + obj: Box::new(Call(create_require(specifier.clone(), unresolved_mark))), + prop: MemberProp::Ident(ast::Ident::new("Buffer".into(), DUMMY_SP)), + span: DUMMY_SP, }) + }) { + self.items.push(DependencyDescriptor { + kind: DependencyKind::Require, + loc: SourceLocation::from(&self.source_map, id.span), + specifier, + attributes: None, + is_optional: false, + is_helper: false, + source_type: Some(SourceType::Module), + placeholder: None, + }); } } - _ => maybe_grow_default(|| node.fold_children_with(self)), - }; + "__filename" => { + self.update_binding(id, |this| { + let filename = + if let Some(relative) = pathdiff::diff_paths(this.filename, this.project_root) { + relative.to_slash_lossy() + } else if let Some(filename) = this.filename.file_name() { + format!("/{}", filename.to_string_lossy()) + } else { + String::from("/unknown.js") + }; - if let Ident(id) = &mut node { - // Only handle global variables - if !is_unresolved(&id, self.unresolved_mark) { - return node; + Lit(ast::Lit::Str( + swc_core::ecma::atoms::JsWord::from(filename).into(), + )) + }); } - - let unresolved_mark = self.unresolved_mark; - match id.sym.to_string().as_str() { - "process" => { - if self.update_binding(id, |_| { - Call(create_require(js_word!("process"), unresolved_mark)) - }) { - let specifier = id.sym.clone(); - self.items.push(DependencyDescriptor { - kind: DependencyKind::Require, - loc: SourceLocation::from(self.source_map, id.span), - specifier, - attributes: None, - is_optional: false, - is_helper: false, - source_type: Some(SourceType::Module), - placeholder: None, - }); - } - } - "Buffer" => { - let specifier = swc_core::ecma::atoms::JsWord::from("buffer"); - if self.update_binding(id, |_| { + "__dirname" => { + self.update_binding(id, |this| { + let dirname = if let Some(dirname) = this.filename.parent() { + if let Some(relative) = pathdiff::diff_paths(dirname, this.project_root) { + relative.to_slash_lossy() + } else { + String::from("/") + } + } else { + String::from("/") + }; + Lit(ast::Lit::Str( + swc_core::ecma::atoms::JsWord::from(dirname).into(), + )) + }); + } + "global" => { + if !self.scope_hoist { + self.update_binding(id, |_| { Member(MemberExpr { - obj: Box::new(Call(create_require(specifier.clone(), unresolved_mark))), - prop: MemberProp::Ident(ast::Ident::new("Buffer".into(), DUMMY_SP)), + obj: Box::new(Ident(ast::Ident::new(js_word!("arguments"), DUMMY_SP))), + prop: MemberProp::Computed(ComputedPropName { + span: DUMMY_SP, + expr: Box::new(Lit(ast::Lit::Num(3.into()))), + }), span: DUMMY_SP, }) - }) { - self.items.push(DependencyDescriptor { - kind: DependencyKind::Require, - loc: SourceLocation::from(self.source_map, id.span), - specifier, - attributes: None, - is_optional: false, - is_helper: false, - source_type: Some(SourceType::Module), - placeholder: None, - }); - } - } - "__filename" => { - self.update_binding(id, |this| { - let filename = - if let Some(relative) = pathdiff::diff_paths(this.filename, this.project_root) { - relative.to_slash_lossy() - } else if let Some(filename) = this.filename.file_name() { - format!("/{}", filename.to_string_lossy()) - } else { - String::from("/unknown.js") - }; - ast::Expr::Lit(ast::Lit::Str( - swc_core::ecma::atoms::JsWord::from(filename).into(), - )) - }); - } - "__dirname" => { - self.update_binding(id, |this| { - let dirname = if let Some(dirname) = this.filename.parent() { - if let Some(relative) = pathdiff::diff_paths(dirname, this.project_root) { - relative.to_slash_lossy() - } else { - String::from("/") - } - } else { - String::from("/") - }; - ast::Expr::Lit(ast::Lit::Str( - swc_core::ecma::atoms::JsWord::from(dirname).into(), - )) }); } - "global" => { - if !self.scope_hoist { - self.update_binding(id, |_| { - ast::Expr::Member(ast::MemberExpr { - obj: Box::new(Ident(Ident::new(js_word!("arguments"), DUMMY_SP))), - prop: MemberProp::Computed(ComputedPropName { - span: DUMMY_SP, - expr: Box::new(Lit(ast::Lit::Num(3.into()))), - }), - span: DUMMY_SP, - }) - }); - } - } - _ => {} } + _ => {} } - - node } - fn fold_module(&mut self, node: ast::Module) -> ast::Module { - // Insert globals at the top of the program - let mut node = swc_core::ecma::visit::fold_module(self, node); + fn visit_mut_module(&mut self, node: &mut Module) { + node.visit_mut_children_with(self); node.body.splice( 0..0, self @@ -167,26 +180,114 @@ impl<'a> Fold for GlobalReplacer<'a> { .values() .map(|(_, stmt)| ast::ModuleItem::Stmt(stmt.clone())), ); - node } } impl GlobalReplacer<'_> { fn update_binding(&mut self, id: &mut ast::Ident, expr: F) -> bool where - F: FnOnce(&Self) -> ast::Expr, + F: FnOnce(&Self) -> Expr, { - if let Some((ctxt, _)) = self.globals.get(&id.sym) { - id.span.ctxt = *ctxt; + if let Some((syntax_context, _)) = self.globals.get(&id.sym) { + id.span.ctxt = *syntax_context; false } else { - let (decl, ctxt) = create_global_decl_stmt(id.sym.clone(), expr(self), self.global_mark); + let (decl, syntax_context) = + create_global_decl_stmt(id.sym.clone(), expr(self), self.global_mark); - id.span.ctxt = ctxt; + id.span.ctxt = syntax_context; - self.globals.insert(id.sym.clone(), (ctxt, decl)); + self.globals.insert(id.sym.clone(), (syntax_context, decl)); true } } } + +#[cfg(test)] +mod test { + use std::path::Path; + + use swc_core::ecma::atoms::JsWord; + + use crate::global_replacer::GlobalReplacer; + use crate::test_utils::{run_visit, RunTestContext, RunVisitResult}; + use crate::{DependencyDescriptor, DependencyKind}; + + fn make_global_replacer( + run_test_context: RunTestContext, + items: &mut Vec, + ) -> GlobalReplacer { + GlobalReplacer { + source_map: run_test_context.source_map, + items, + global_mark: run_test_context.global_mark, + globals: Default::default(), + project_root: Path::new("project-root"), + filename: Path::new("filename"), + unresolved_mark: run_test_context.unresolved_mark, + scope_hoist: false, + } + } + + #[test] + fn test_globals_visitor_with_require_process() { + let mut items = vec![]; + + let RunVisitResult { output_code, .. } = run_visit( + r#" +console.log(process.test); + "#, + |run_test_context: RunTestContext| make_global_replacer(run_test_context, &mut items), + ); + assert_eq!( + output_code, + r#"var process = require("process"); +console.log(process.test); +"# + ); + assert_eq!(items.len(), 1); + assert_eq!(items[0].kind, DependencyKind::Require); + assert_eq!(items[0].specifier, JsWord::from("process")); + } + + #[test] + fn test_transforms_computed_property() { + let mut items = vec![]; + + let RunVisitResult { output_code, .. } = run_visit( + r#" +object[process.test]; +object[__dirname]; + "#, + |run_test_context: RunTestContext| make_global_replacer(run_test_context, &mut items), + ); + assert_eq!( + output_code, + r#"var process = require("process"); +var __dirname = ".."; +object[process.test]; +object[__dirname]; +"# + ); + } + + #[test] + fn test_does_not_transform_member_property() { + let mut items = vec![]; + + let RunVisitResult { output_code, .. } = run_visit( + r#" +object.process.test; +object.__filename; + "#, + |run_test_context: RunTestContext| make_global_replacer(run_test_context, &mut items), + ); + assert_eq!( + output_code, + r#"object.process.test; +object.__filename; +"# + ); + } +} diff --git a/packages/transformers/js/core/src/lib.rs b/packages/transformers/js/core/src/lib.rs index f7eafa5b97f..fbb87ce33e5 100644 --- a/packages/transformers/js/core/src/lib.rs +++ b/packages/transformers/js/core/src/lib.rs @@ -7,6 +7,8 @@ mod global_replacer; mod hoist; mod modules; mod node_replacer; +#[cfg(test)] +mod test_utils; mod typeof_replacer; mod utils; @@ -411,8 +413,8 @@ pub fn transform( let mut passes = chain!( // Insert dependencies for node globals Optional::new( - GlobalReplacer { - source_map: &source_map, + as_folder(GlobalReplacer { + source_map: source_map.clone(), items: &mut global_deps, global_mark, globals: IndexMap::new(), @@ -420,7 +422,7 @@ pub fn transform( filename: Path::new(&config.filename), unresolved_mark, scope_hoist: config.scope_hoist - }, + }), config.insert_node_globals ), // Transpile new syntax to older syntax if needed diff --git a/packages/transformers/js/core/src/test_utils.rs b/packages/transformers/js/core/src/test_utils.rs new file mode 100644 index 00000000000..2433ad6a936 --- /dev/null +++ b/packages/transformers/js/core/src/test_utils.rs @@ -0,0 +1,143 @@ +use swc_core::common::input::StringInput; +use swc_core::common::sync::Lrc; +use swc_core::common::util::take::Take; +use swc_core::common::{FileName, Globals, Mark, SourceMap, GLOBALS}; +use swc_core::ecma::ast::Module; +use swc_core::ecma::codegen::text_writer::JsWriter; +use swc_core::ecma::parser::lexer::Lexer; +use swc_core::ecma::parser::Parser; +use swc_core::ecma::transforms::base::resolver; +use swc_core::ecma::visit::{Fold, FoldWith, VisitMut, VisitMutWith}; + +pub(crate) struct RunTestContext { + /// Source-map in use + pub source_map: Lrc, + /// Global mark from SWC resolver + pub global_mark: Mark, + /// Unresolved mark from SWC resolver + pub unresolved_mark: Mark, +} + +pub(crate) struct RunVisitResult { + pub output_code: String, + #[allow(unused)] + pub visitor: V, +} + +/// Helper to test SWC visitors. +/// +/// * Parse `code` with SWC +/// * Run a visitor over it +/// * Return the result +/// +pub(crate) fn run_visit( + code: &str, + make_visit: impl FnOnce(RunTestContext) -> V, +) -> RunVisitResult { + let (output_code, visitor) = run_with_transformation( + code, + |run_test_context: RunTestContext, module: &mut Module| { + let mut visit = make_visit(run_test_context); + module.visit_mut_with(&mut visit); + visit + }, + ); + RunVisitResult { + output_code, + visitor, + } +} + +/// Same as `run_visit` but for `Fold` instances +#[allow(unused)] +pub(crate) fn run_fold( + code: &str, + make_fold: impl FnOnce(RunTestContext) -> V, +) -> RunVisitResult { + let (output_code, visitor) = run_with_transformation( + code, + |run_test_context: RunTestContext, module: &mut Module| { + let mut visit = make_fold(run_test_context); + *module = module.take().fold_with(&mut visit); + visit + }, + ); + RunVisitResult { + output_code, + visitor, + } +} + +/// Parse code, run resolver over it, then run the `tranform` function with the parsed module +/// codegen and return the results. +fn run_with_transformation( + code: &str, + transform: impl FnOnce(RunTestContext, &mut Module) -> R, +) -> (String, R) { + let source_map = Lrc::new(SourceMap::default()); + let source_file = source_map.new_source_file(FileName::Anon, code.into()); + + let lexer = Lexer::new( + Default::default(), + Default::default(), + StringInput::from(&*source_file), + None, + ); + + let mut parser = Parser::new_from(lexer); + let module = parser.parse_module().unwrap(); + + GLOBALS.set(&Globals::new(), || { + let global_mark = Mark::new(); + let unresolved_mark = Mark::new(); + let mut module = module.fold_with(&mut resolver(unresolved_mark, global_mark, false)); + + let result = transform( + RunTestContext { + source_map: source_map.clone(), + global_mark, + unresolved_mark, + }, + &mut module, + ); + + let mut output_buffer = vec![]; + let writer = JsWriter::new(source_map.clone(), "\n", &mut output_buffer, None); + let mut emitter = swc_core::ecma::codegen::Emitter { + cfg: Default::default(), + cm: source_map, + comments: None, + wr: writer, + }; + emitter.emit_module(&module).unwrap(); + let output_code = String::from_utf8(output_buffer).unwrap(); + + (output_code, result) + }) +} + +#[cfg(test)] +mod test { + use swc_core::ecma::ast::{Lit, Str}; + use swc_core::ecma::visit::VisitMut; + + use super::*; + + #[test] + fn test_example() { + struct Visitor; + impl VisitMut for Visitor { + fn visit_mut_lit(&mut self, n: &mut Lit) { + *n = Lit::Str(Str::from("replacement")); + } + } + + let code = r#"console.log('test!')"#; + let RunVisitResult { output_code, .. } = run_visit(code, |_: RunTestContext| Visitor); + assert_eq!( + output_code, + r#"console.log("replacement"); +"# + ); + } +} diff --git a/packages/transformers/js/core/src/typeof_replacer.rs b/packages/transformers/js/core/src/typeof_replacer.rs index fb4ec4eed91..2121f6cc652 100644 --- a/packages/transformers/js/core/src/typeof_replacer.rs +++ b/packages/transformers/js/core/src/typeof_replacer.rs @@ -82,14 +82,7 @@ impl VisitMut for TypeofReplacer { #[cfg(test)] mod test { - use swc_core::common::input::StringInput; - use swc_core::common::sync::Lrc; - use swc_core::common::{FileName, Globals, SourceMap, GLOBALS}; - use swc_core::ecma::codegen::text_writer::JsWriter; - use swc_core::ecma::parser::lexer::Lexer; - use swc_core::ecma::parser::Parser; - use swc_core::ecma::transforms::base::resolver; - use swc_core::ecma::visit::{FoldWith, VisitMutWith}; + use crate::test_utils::run_visit; use super::*; @@ -103,7 +96,8 @@ const e = typeof exports; let output_code = run_visit(code, |context| TypeofReplacer { unresolved_mark: context.unresolved_mark, - }); + }) + .output_code; let expected_code = r#" const x = "function"; @@ -122,7 +116,8 @@ const x = typeof require === 'function'; let output_code = run_visit(code, |context| TypeofReplacer { unresolved_mark: context.unresolved_mark, - }); + }) + .output_code; let expected_code = r#" const x = "function" === 'function'; @@ -143,7 +138,8 @@ function wrapper({ require, exports }) { let output_code = run_visit(code, |context| TypeofReplacer { unresolved_mark: context.unresolved_mark, - }); + }) + .output_code; let expected_code = r#" function wrapper({ require, exports }) { @@ -155,50 +151,4 @@ function wrapper({ require, exports }) { .trim_start(); assert_eq!(output_code, expected_code); } - - struct RunTestContext { - #[allow(unused)] - global_mark: Mark, - unresolved_mark: Mark, - } - - fn run_visit(code: &str, make_visit: impl FnOnce(RunTestContext) -> V) -> String { - let source_map = Lrc::new(SourceMap::default()); - let source_file = source_map.new_source_file(FileName::Anon, code.into()); - - let lexer = Lexer::new( - Default::default(), - Default::default(), - StringInput::from(&*source_file), - None, - ); - - let mut parser = Parser::new_from(lexer); - let module = parser.parse_module().unwrap(); - - let output_code = GLOBALS.set(&Globals::new(), || { - let global_mark = Mark::new(); - let unresolved_mark = Mark::new(); - let mut module = module.fold_with(&mut resolver(unresolved_mark, global_mark, false)); - - let mut visit = make_visit(RunTestContext { - global_mark, - unresolved_mark, - }); - module.visit_mut_with(&mut visit); - - let mut output_buffer = vec![]; - let writer = JsWriter::new(source_map.clone(), "\n", &mut output_buffer, None); - let mut emitter = swc_core::ecma::codegen::Emitter { - cfg: Default::default(), - cm: source_map, - comments: None, - wr: writer, - }; - emitter.emit_module(&module).unwrap(); - let output_code = String::from_utf8(output_buffer).unwrap(); - output_code - }); - output_code - } }