From 8865f6dfc8432f98690dce2c11c4538880aed4ce Mon Sep 17 00:00:00 2001 From: David Alsh Date: Mon, 18 Nov 2024 11:53:38 +1100 Subject: [PATCH] Added magic comment visitor and wired it up to the transformer --- Cargo.lock | 1 + .../src/js_transformer/conversion.rs | 11 +- packages/transformers/js/core/Cargo.toml | 1 + packages/transformers/js/core/src/lib.rs | 10 + .../js/core/src/magic_comments/mod.rs | 5 + .../js/core/src/magic_comments/visitor.rs | 72 +++++ .../core/src/magic_comments/visitor_test.rs | 253 ++++++++++++++++++ 7 files changed, 352 insertions(+), 1 deletion(-) create mode 100644 packages/transformers/js/core/src/magic_comments/mod.rs create mode 100644 packages/transformers/js/core/src/magic_comments/visitor.rs create mode 100644 packages/transformers/js/core/src/magic_comments/visitor_test.rs diff --git a/Cargo.lock b/Cargo.lock index 1f2787686..8193a4295 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -302,6 +302,7 @@ name = "atlaspack-js-swc-core" version = "0.1.0" dependencies = [ "Inflector", + "anyhow", "atlaspack-macros", "atlaspack_contextual_imports", "atlaspack_core", diff --git a/crates/atlaspack_plugin_transformer_js/src/js_transformer/conversion.rs b/crates/atlaspack_plugin_transformer_js/src/js_transformer/conversion.rs index 637a097d0..c34941260 100644 --- a/crates/atlaspack_plugin_transformer_js/src/js_transformer/conversion.rs +++ b/crates/atlaspack_plugin_transformer_js/src/js_transformer/conversion.rs @@ -3,6 +3,7 @@ use std::sync::Arc; use atlaspack_core::diagnostic; use indexmap::IndexMap; +use std::collections::HashMap; use swc_core::atoms::{Atom, JsWord}; use atlaspack_core::plugin::{PluginOptions, TransformResult}; @@ -44,6 +45,7 @@ pub(crate) fn convert_result( &options.project_root, transformer_config, result.dependencies, + result.magic_comments, &asset, )?; @@ -381,6 +383,7 @@ pub(crate) fn convert_dependencies( project_root: &Path, transformer_config: &atlaspack_js_swc_core::Config, dependencies: Vec, + magic_comments: HashMap, asset: &Asset, ) -> Result<(IndexMap, Vec), Vec> { let mut dependency_by_specifier = IndexMap::new(); @@ -400,7 +403,13 @@ pub(crate) fn convert_dependencies( )?; match result { - DependencyConversionResult::Dependency(dependency) => { + DependencyConversionResult::Dependency(mut dependency) => { + if let Some(chunk_name) = magic_comments.get(&dependency.specifier) { + dependency.meta.insert( + "chunkNameMagicComment".to_string(), + chunk_name.clone().into(), + ); + } dependency_by_specifier.insert(placeholder, dependency); } DependencyConversionResult::InvalidateOnFileChange(file_path) => { diff --git a/packages/transformers/js/core/Cargo.toml b/packages/transformers/js/core/Cargo.toml index 78f6fd359..ce8854f73 100644 --- a/packages/transformers/js/core/Cargo.toml +++ b/packages/transformers/js/core/Cargo.toml @@ -47,5 +47,6 @@ atlaspack_contextual_imports = { path = "../../../../crates/atlaspack_contextual atlaspack_swc_runner = { path = "../../../../crates/atlaspack_swc_runner" } parking_lot = { workspace = true } regex = { workspace = true } +anyhow = { workspace = true } tracing = { workspace = true } tracing-test = { workspace = true } diff --git a/packages/transformers/js/core/src/lib.rs b/packages/transformers/js/core/src/lib.rs index 1b8ee9cff..651f6f7d9 100644 --- a/packages/transformers/js/core/src/lib.rs +++ b/packages/transformers/js/core/src/lib.rs @@ -5,6 +5,7 @@ mod env_replacer; mod fs; mod global_replacer; mod hoist; +mod magic_comments; mod modules; mod node_replacer; pub mod test_utils; @@ -39,6 +40,7 @@ pub use hoist::ExportedSymbol; use hoist::HoistResult; pub use hoist::ImportedSymbol; use indexmap::IndexMap; +use magic_comments::MagicCommentsVisitor; use modules::esm2cjs; use node_replacer::NodeReplacer; use path_slash::PathExt; @@ -152,6 +154,7 @@ pub struct TransformResult { pub has_node_replacements: bool, pub is_constant_module: bool, pub conditions: HashSet, + pub magic_comments: HashMap, } fn targets_to_versions(targets: &Option>) -> Option { @@ -266,6 +269,13 @@ pub fn transform( let global_mark = Mark::fresh(Mark::root()); let unresolved_mark = Mark::fresh(Mark::root()); + + if code.contains("webpackChunkName") { + let mut magic_comment_visitor = MagicCommentsVisitor::new(code); + module.visit_with(&mut magic_comment_visitor); + result.magic_comments = magic_comment_visitor.magic_comments; + } + let module = module.fold_with(&mut chain!( resolver(unresolved_mark, global_mark, config.is_type_script), // Decorators can use type information, so must run before the TypeScript pass. diff --git a/packages/transformers/js/core/src/magic_comments/mod.rs b/packages/transformers/js/core/src/magic_comments/mod.rs new file mode 100644 index 000000000..bb9f892c4 --- /dev/null +++ b/packages/transformers/js/core/src/magic_comments/mod.rs @@ -0,0 +1,5 @@ +mod visitor; +#[cfg(test)] +mod visitor_test; + +pub use self::visitor::*; diff --git a/packages/transformers/js/core/src/magic_comments/visitor.rs b/packages/transformers/js/core/src/magic_comments/visitor.rs new file mode 100644 index 000000000..3b9fef6ef --- /dev/null +++ b/packages/transformers/js/core/src/magic_comments/visitor.rs @@ -0,0 +1,72 @@ +use std::collections::HashMap; +use std::rc::Rc; + +use regex::Regex; +use swc_core::common::Span; +use swc_core::ecma::ast::*; +use swc_core::ecma::utils::ExprExt; +use swc_core::ecma::visit::{Visit, VisitWith}; + +thread_local! { + static RE_CHUNK_NAME: Rc = Rc::new(Regex::new(r#"webpackChunkName:\s*['"](?[^'"]+)['"]"#).unwrap()); +} + +#[derive(Debug)] +pub struct MagicCommentsVisitor<'a> { + pub magic_comments: HashMap, + pub offset: u32, + pub code: &'a str, +} + +impl<'a> MagicCommentsVisitor<'a> { + pub fn new(code: &'a str) -> Self { + Self { + magic_comments: Default::default(), + offset: 0, + code, + } + } + + fn fix_callee(&self, span: &Span) -> (u32, u32) { + let start = span.lo().0 - self.offset; + let end = span.hi().0 - self.offset; + (start, end) + } +} + +impl<'a> Visit for MagicCommentsVisitor<'a> { + fn visit_module(&mut self, node: &Module) { + self.offset = node.span.lo().0; + node.visit_children_with(self); + } + + fn visit_call_expr(&mut self, node: &CallExpr) { + if !node.callee.is_import() || node.args.len() == 0 || !node.args[0].expr.is_str() { + node.visit_children_with(self); + return; + } + + let (code_start, code_end) = self.fix_callee(&node.span); + let code_start_index = (code_start - 1) as usize; + let code_end_index = (code_end - 1) as usize; + let inner = &self.code[code_start_index..code_end_index]; + + let re = RE_CHUNK_NAME.with(|re| re.clone()); + + let Some(caps) = re.captures(inner) else { + return; + }; + + let Some(found) = caps.name("name") else { + return; + }; + + let Expr::Lit(Lit::Str(specifier)) = &*node.args[0].expr else { + return; + }; + + self + .magic_comments + .insert(specifier.value.to_string(), found.as_str().to_string()); + } +} diff --git a/packages/transformers/js/core/src/magic_comments/visitor_test.rs b/packages/transformers/js/core/src/magic_comments/visitor_test.rs new file mode 100644 index 000000000..db603867e --- /dev/null +++ b/packages/transformers/js/core/src/magic_comments/visitor_test.rs @@ -0,0 +1,253 @@ +use std::path::PathBuf; + +use swc_core::common::comments::SingleThreadedComments; +use swc_core::common::sync::Lrc; +use swc_core::common::*; +use swc_core::ecma::ast::*; +use swc_core::ecma::parser::lexer::Lexer; +use swc_core::ecma::parser::*; +use swc_core::ecma::visit::VisitWith; + +use super::MagicCommentsVisitor; + +pub fn parse(code: &str) -> anyhow::Result { + let source_map = Lrc::new(SourceMap::default()); + let source_file = + source_map.new_source_file(Lrc::new(FileName::Real(PathBuf::new())), code.into()); + + let comments = SingleThreadedComments::default(); + let syntax = { + let mut tsconfig = TsSyntax::default(); + tsconfig.tsx = true; + Syntax::Typescript(tsconfig) + }; + + let lexer = Lexer::new( + syntax, + EsVersion::latest(), + StringInput::from(&*source_file), + Some(&comments), + ); + + let mut parser = Parser::new_from(lexer); + + let program = match parser.parse_program() { + Err(err) => anyhow::bail!("{:?}", err), + Ok(program) => program, + }; + + Ok(program) +} + +#[test] +fn it_should_not_set_chunk_name_if_code_does_not_contain_a_magic_comment() -> anyhow::Result<()> { + let code = r#"import('./foo')"#; + + let mut visitor = MagicCommentsVisitor::new(code); + parse(code)?.visit_with(&mut visitor); + + assert!( + visitor.magic_comments.len() == 0, + "Expected no magic comments to be set" + ); + + Ok(()) +} + +#[test] +fn it_should_set_chunk_name_if_code_contains_magic_comment() -> anyhow::Result<()> { + let code = r#"import(/* webpackChunkName: "foo-chunk" */ "./foo")"#; + + let mut visitor = MagicCommentsVisitor::new(code); + parse(code)?.visit_with(&mut visitor); + + assert!( + visitor.magic_comments.len() == 1, + "Expected magic comment to be set" + ); + + let Some(chunk_name) = visitor.magic_comments.get("./foo") else { + assert!(false, "Expected magic comment to be set"); + anyhow::bail!("") + }; + + assert!( + chunk_name == "foo-chunk", + "Expected magic comment to be set" + ); + + Ok(()) +} + +#[test] +fn it_should_set_chunk_name_if_code_contains_multiple_magic_comment() -> anyhow::Result<()> { + let code = r#" + import(/* webpackChunkName: "foo-chunk" */ "./foo") + import(/* webpackChunkName: "bar-chunk" */ "./bar") + "#; + + let mut visitor = MagicCommentsVisitor::new(code); + parse(code)?.visit_with(&mut visitor); + + assert!( + visitor.magic_comments.len() == 2, + "Expected magic comment to be set" + ); + + let Some(chunk_name_foo) = visitor.magic_comments.get("./foo") else { + assert!(false, "Expected magic comment to be set"); + anyhow::bail!("") + }; + + let Some(chunk_name_bar) = visitor.magic_comments.get("./bar") else { + assert!(false, "Expected magic comment to be set"); + anyhow::bail!("") + }; + + assert!( + chunk_name_foo == "foo-chunk", + "Expected magic comment to be set" + ); + + assert!( + chunk_name_bar == "bar-chunk", + "Expected magic comment to be set" + ); + + Ok(()) +} + +#[test] +fn it_should_work_with_current_dir_import() -> anyhow::Result<()> { + let code = r#"import(/* webpackChunkName: "foo-chunk" */ ".");"#; + + let mut visitor = MagicCommentsVisitor::new(code); + parse(code)?.visit_with(&mut visitor); + + assert!( + visitor.magic_comments.len() == 1, + "Expected magic comment to be set" + ); + + let Some(chunk_name) = visitor.magic_comments.get(".") else { + assert!(false, "Expected magic comment to be set"); + anyhow::bail!("") + }; + + assert!( + chunk_name == "foo-chunk", + "Expected magic comment to be set" + ); + + Ok(()) +} + +#[test] +fn it_should_work_with_current_dir_import_2() -> anyhow::Result<()> { + let code = r#"import(/* webpackChunkName: "foo-chunk" */ "./");"#; + + let mut visitor = MagicCommentsVisitor::new(code); + parse(code)?.visit_with(&mut visitor); + + assert!( + visitor.magic_comments.len() == 1, + "Expected magic comment to be set" + ); + + let Some(chunk_name) = visitor.magic_comments.get("./") else { + assert!(false, "Expected magic comment to be set"); + anyhow::bail!("") + }; + + assert!( + chunk_name == "foo-chunk", + "Expected magic comment to be set" + ); + + Ok(()) +} + +#[test] +fn it_should_work_with_parent_dir_import() -> anyhow::Result<()> { + let code = r#"import(/* webpackChunkName: "foo-chunk" */ "..");"#; + + let mut visitor = MagicCommentsVisitor::new(code); + parse(code)?.visit_with(&mut visitor); + + assert!( + visitor.magic_comments.len() == 1, + "Expected magic comment to be set" + ); + + let Some(chunk_name) = visitor.magic_comments.get("..") else { + assert!(false, "Expected magic comment to be set"); + anyhow::bail!("") + }; + + assert!( + chunk_name == "foo-chunk", + "Expected magic comment to be set" + ); + + Ok(()) +} + +#[test] +fn it_should_work_with_parent_dir_import_2() -> anyhow::Result<()> { + let code = r#"import(/* webpackChunkName: "foo-chunk" */ "../");"#; + + let mut visitor = MagicCommentsVisitor::new(code); + parse(code)?.visit_with(&mut visitor); + + assert!( + visitor.magic_comments.len() == 1, + "Expected magic comment to be set" + ); + + let Some(chunk_name) = visitor.magic_comments.get("../") else { + assert!(false, "Expected magic comment to be set"); + anyhow::bail!("") + }; + + assert!( + chunk_name == "foo-chunk", + "Expected magic comment to be set" + ); + + Ok(()) +} + +#[test] +fn it_parses_lazy_imports() -> anyhow::Result<()> { + let code = r#" + const LazyPermalinkButton: ComponentType = lazyAfterPaint( + () => + import(/* webpackChunkName: "async-issue-view-permalink-button" */ './view').then( + (exportedModule) => exportedModule.PermalinkButton, + ), + { + ssr: false, + }, + ); + "#; + + let mut visitor = MagicCommentsVisitor::new(code); + parse(code)?.visit_with(&mut visitor); + + assert!( + visitor.magic_comments.len() == 1, + "Expected magic comment to be set" + ); + + let Some(chunk_name) = visitor.magic_comments.get("./view") else { + assert!(false, "Expected magic comment to be set"); + anyhow::bail!("") + }; + + assert!( + chunk_name == "async-issue-view-permalink-button", + "Expected magic comment to be set" + ); + + Ok(()) +}