diff --git a/Cargo.lock b/Cargo.lock index dd58066c56ae5a..dbf49bd0399ade 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1,5 +1,15 @@ # This file is automatically @generated by Cargo. # It is not intended for manual editing. +[[package]] +name = "Inflector" +version = "0.11.4" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "fe438c63458706e03479442743baae6c88256498e6431708f6dfc520a26515d3" +dependencies = [ + "lazy_static", + "regex", +] + [[package]] name = "adler32" version = "1.0.4" @@ -493,6 +503,7 @@ dependencies = [ "serde_derive", "serde_json", "sourcemap", + "swc_ecma_visit", "sys-info", "tempfile", "termcolor", @@ -2463,6 +2474,33 @@ dependencies = [ "syn 1.0.17", ] +[[package]] +name = "swc_ecma_visit" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2bdd4d87e6499ff8cc3b32981ab2a3917cea4002a0c4523868181f59d14f4638" +dependencies = [ + "num-bigint", + "swc_atoms", + "swc_common", + "swc_ecma_ast", + "swc_ecma_visit_macros", +] + +[[package]] +name = "swc_ecma_visit_macros" +version = "0.1.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "ceb3a3184ba505b3f94fa4132a72e754d361ad9913b7e0dc4f01ab38649a9d26" +dependencies = [ + "Inflector", + "pmutil", + "proc-macro2 1.0.10", + "quote 1.0.3", + "swc_macros_common", + "syn 1.0.17", +] + [[package]] name = "swc_macros_common" version = "0.3.1" diff --git a/cli/Cargo.toml b/cli/Cargo.toml index ed3941f3c4950c..7cc3c62d830a40 100644 --- a/cli/Cargo.toml +++ b/cli/Cargo.toml @@ -64,6 +64,7 @@ walkdir = "2.3.1" warp = "0.2.2" semver-parser = "0.9.0" uuid = { version = "0.8.1", features = ["v4"] } +swc_ecma_visit = { version = "=0.1.0" } [target.'cfg(windows)'.dependencies] winapi = "0.3.8" diff --git a/cli/swc_util.rs b/cli/swc_util.rs index 2086a5ded33af7..a2e5d50fdc5fe2 100644 --- a/cli/swc_util.rs +++ b/cli/swc_util.rs @@ -18,6 +18,8 @@ use crate::swc_ecma_parser::Session; use crate::swc_ecma_parser::SourceFileInput; use crate::swc_ecma_parser::Syntax; use crate::swc_ecma_parser::TsConfig; +use swc_ecma_visit::Node; +use swc_ecma_visit::Visit; use std::error::Error; use std::fmt; @@ -162,3 +164,156 @@ impl AstParser { .unwrap_or_else(|| vec![]) } } + +struct DependencyVisitor { + dependencies: Vec, + analyze_dynamic_imports: bool, +} + +impl Visit for DependencyVisitor { + fn visit_import_decl( + &mut self, + import_decl: &swc_ecma_ast::ImportDecl, + _parent: &dyn Node, + ) { + let src_str = import_decl.src.value.to_string(); + self.dependencies.push(src_str); + } + + fn visit_named_export( + &mut self, + named_export: &swc_ecma_ast::NamedExport, + _parent: &dyn Node, + ) { + if let Some(src) = &named_export.src { + let src_str = src.value.to_string(); + self.dependencies.push(src_str); + } + } + + fn visit_export_all( + &mut self, + export_all: &swc_ecma_ast::ExportAll, + _parent: &dyn Node, + ) { + let src_str = export_all.src.value.to_string(); + self.dependencies.push(src_str); + } + + fn visit_call_expr( + &mut self, + call_expr: &swc_ecma_ast::CallExpr, + _parent: &dyn Node, + ) { + if !self.analyze_dynamic_imports { + return; + } + + use swc_ecma_ast::Expr::*; + use swc_ecma_ast::ExprOrSuper::*; + + let boxed_expr = match call_expr.callee.clone() { + Super(_) => return, + Expr(boxed) => boxed, + }; + + match &*boxed_expr { + Ident(ident) => { + if &ident.sym.to_string() != "import" { + return; + } + } + _ => return, + }; + + if let Some(arg) = call_expr.args.get(0) { + match &*arg.expr { + Lit(lit) => { + if let swc_ecma_ast::Lit::Str(str_) = lit { + let src_str = str_.value.to_string(); + self.dependencies.push(src_str); + } + } + _ => return, + } + } + } +} + +/// Given file name and source code return vector +/// of unresolved import specifiers. +/// +/// Returned vector may contain duplicate entries. +/// +/// Second argument allows to configure if dynamic +/// imports should be analyzed. +/// +/// NOTE: Only statically analyzable dynamic imports +/// are considered; ie. the ones that have plain string specifier: +/// +/// await import("./fizz.ts") +/// +/// These imports will be ignored: +/// +/// await import(`./${dir}/fizz.ts`) +/// await import("./" + "fizz.ts") +#[allow(unused)] +pub fn analyze_dependencies( + source_code: &str, + analyze_dynamic_imports: bool, +) -> Result, SwcDiagnosticBuffer> { + let parser = AstParser::new(); + parser.parse_module("root.ts", source_code, |parse_result| { + let module = parse_result?; + let mut collector = DependencyVisitor { + dependencies: vec![], + analyze_dynamic_imports, + }; + collector.visit_module(&module, &module); + Ok(collector.dependencies) + }) +} + +#[test] +fn test_analyze_dependencies() { + let source = r#" +import { foo } from "./foo.ts"; +export { bar } from "./foo.ts"; +export * from "./bar.ts"; +"#; + + let dependencies = + analyze_dependencies(source, false).expect("Failed to parse"); + assert_eq!( + dependencies, + vec![ + "./foo.ts".to_string(), + "./foo.ts".to_string(), + "./bar.ts".to_string(), + ] + ); +} + +#[test] +fn test_analyze_dependencies_dyn_imports() { + let source = r#" +import { foo } from "./foo.ts"; +export { bar } from "./foo.ts"; +export * from "./bar.ts"; + +const a = await import("./fizz.ts"); +const a = await import("./" + "buzz.ts"); +"#; + + let dependencies = + analyze_dependencies(source, true).expect("Failed to parse"); + assert_eq!( + dependencies, + vec![ + "./foo.ts".to_string(), + "./foo.ts".to_string(), + "./bar.ts".to_string(), + "./fizz.ts".to_string(), + ] + ); +}