diff --git a/.github/workflows/compilation.yml b/.github/workflows/compilation.yml index a5d8ba3c3ca..d318d19bc8e 100644 --- a/.github/workflows/compilation.yml +++ b/.github/workflows/compilation.yml @@ -42,3 +42,7 @@ jobs: - name: Check swc_ecma_transforms run: | (cd ecmascript/transforms && cargo hack check --feature-powerset --no-dev-deps) + + - name: Check swc_bundler + run: | + (cd bundler && cargo hack check --feature-powerset --no-dev-deps) diff --git a/.npmignore b/.npmignore index 127fafb804a..de680478566 100644 --- a/.npmignore +++ b/.npmignore @@ -15,6 +15,7 @@ package-lock.json wasm/ # Reduce package size +**/tests.rs **/tests/ **/benches/ **/target/ diff --git a/Cargo.toml b/Cargo.toml index ebda9075ab7..095b8207a11 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -31,7 +31,7 @@ serde_json = "1" once_cell = "1" regex = "1" either = "1" -dashmap = "=3.5.1" +dashmap = "3" sourcemap = "6" base64 = "0.12.0" diff --git a/bundler/Cargo.toml b/bundler/Cargo.toml new file mode 100644 index 00000000000..0159c8ba3d9 --- /dev/null +++ b/bundler/Cargo.toml @@ -0,0 +1,39 @@ +[package] +name = "swc_bundler" +version = "0.1.0" +authors = ["강동윤 "] +license = "Apache-2.0/MIT" +repository = "https://github.com/swc-project/swc.git" +documentation = "https://swc-project.github.io/rustdoc/swc_bundler/" +description = "Very fast ecmascript bundler" +edition = "2018" + +# See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html +[features] +default = [] +concurrent = ["swc_common/concurrent", "dashmap", "rayon"] + + +[dependencies] +swc_atoms = { version = "0.2", path = "../atoms" } +swc_common = { version = "0.9", path = "../common" } +swc_ecma_ast = { version = "0.28", path = "../ecmascript/ast" } +swc_ecma_codegen = { version = "0.31", path = "../ecmascript/codegen" } +swc_ecma_parser = { version = "0.33", path = "../ecmascript/parser" } +swc_ecma_transforms = { version = "0.19", path = "../ecmascript/transforms" } +swc_ecma_utils = { version = "0.17", path = "../ecmascript/utils" } +swc_ecma_visit = { version = "0.13", path = "../ecmascript/visit" } +anyhow = "1" +crc = "1.8" +radix_fmt = "1" +relative-path = "1.2" +log = "0.4" +petgraph = "0.5" +once_cell = "1" +dashmap = { version = "3", optional = true } +rayon = { version = "1", optional = true } +fxhash = "0.2.1" +is-macro = "0.1" + +[dev-dependencies] +testing = { path = "../testing" } \ No newline at end of file diff --git a/bundler/README.md b/bundler/README.md new file mode 100644 index 00000000000..f9b0b73a98c --- /dev/null +++ b/bundler/README.md @@ -0,0 +1,13 @@ +# swc_bundler + +Bundler for the swc project. + +## Features + +- Clean merging (generated code is easy to optimize) +- Parallel file loading +- Tree shaking +- Common js support (aka `require`) +- Circular imports + +Tests live at `/spack`. diff --git a/bundler/assets/a.js b/bundler/assets/a.js new file mode 100644 index 00000000000..bb4813d79ba --- /dev/null +++ b/bundler/assets/a.js @@ -0,0 +1,7 @@ +export const FOO = 1; + + +export class A { + foo() { + } +} \ No newline at end of file diff --git a/bundler/assets/main.js b/bundler/assets/main.js new file mode 100644 index 00000000000..b034d39d8ba --- /dev/null +++ b/bundler/assets/main.js @@ -0,0 +1,3 @@ +import { A, FOO } from './a'; + +console.log(A, FOO); \ No newline at end of file diff --git a/bundler/examples/path.rs b/bundler/examples/path.rs new file mode 100644 index 00000000000..1762aead1f2 --- /dev/null +++ b/bundler/examples/path.rs @@ -0,0 +1,112 @@ +use anyhow::Error; +use fxhash::FxHashMap; +use std::io::stdout; +use swc_bundler::{BundleKind, Bundler, Config, Load, Resolve}; +use swc_common::{sync::Lrc, FileName, FilePathMapping, Globals, SourceMap}; +use swc_ecma_codegen::{text_writer::JsWriter, Emitter}; +use swc_ecma_parser::{lexer::Lexer, EsConfig, Parser, StringInput, Syntax}; + +fn main() { + testing::init(); + + let globals = Globals::new(); + let cm = Lrc::new(SourceMap::new(FilePathMapping::empty())); + // This example does not use core modules. + let external_modules = vec![]; + let bundler = Bundler::new( + &globals, + cm.clone(), + PathLoader { cm: cm.clone() }, + PathResolver, + Config { + require: true, + external_modules, + }, + ); + let mut entries = FxHashMap::default(); + entries.insert("main".to_string(), FileName::Real("assets/main.js".into())); + + let mut bundles = bundler.bundle(entries).expect("failed to bundle"); + assert_eq!( + bundles.len(), + 1, + "There's no conditional / dynamic imports and we provided only one entry" + ); + let bundle = bundles.pop().unwrap(); + assert_eq!( + bundle.kind, + BundleKind::Named { + name: "main".into() + }, + "We provided it" + ); + + let wr = stdout(); + let mut emitter = Emitter { + cfg: swc_ecma_codegen::Config { minify: false }, + cm: cm.clone(), + comments: None, + wr: Box::new(JsWriter::new(cm.clone(), "\n", wr.lock(), None)), + handlers: Box::new(Handllers), + }; + + emitter.emit_module(&bundle.module).unwrap(); +} + +/// I should remove this... +struct Handllers; + +impl swc_ecma_codegen::Handlers for Handllers {} + +struct PathLoader { + cm: Lrc, +} + +impl Load for PathLoader { + fn load( + &self, + file: &FileName, + ) -> Result<(Lrc, swc_ecma_ast::Module), Error> { + let file = match file { + FileName::Real(v) => v, + _ => unreachable!(), + }; + + let fm = self.cm.load_file(file)?; + let lexer = Lexer::new( + Syntax::Es(EsConfig { + ..Default::default() + }), + Default::default(), + StringInput::from(&*fm), + None, + ); + + let mut parser = Parser::new_from(lexer); + let module = parser.parse_module().expect("This should not happen"); + + Ok((fm, module)) + } +} +struct PathResolver; + +impl Resolve for PathResolver { + fn resolve(&self, base: &FileName, module_specifier: &str) -> Result { + assert!( + module_specifier.starts_with("."), + "We are not using node_modules within this example" + ); + + let base = match base { + FileName::Real(v) => v, + _ => unreachable!(), + }; + + Ok(FileName::Real( + base.parent() + .unwrap() + .join(module_specifier) + .with_extension("js"), + )) + } +} diff --git a/bundler/src/bundler/chunk/circular.rs b/bundler/src/bundler/chunk/circular.rs new file mode 100644 index 00000000000..f917c496413 --- /dev/null +++ b/bundler/src/bundler/chunk/circular.rs @@ -0,0 +1,242 @@ +use super::merge::{LocalMarker, Unexporter}; +use crate::{bundler::load::TransformedModule, Bundler, Load, ModuleId, Resolve}; +use hygiene::top_level_ident_folder; +use std::iter::once; +use swc_common::DUMMY_SP; +use swc_ecma_ast::*; +use swc_ecma_visit::{FoldWith, Node, Visit, VisitWith}; + +mod hygiene; + +/// Circular imports are hard to handle. +/// +/// We use some dedicated method to handle circular dependencies. +impl Bundler<'_, L, R> +where + L: Load, + R: Resolve, +{ + pub(super) fn merge_circular_modules( + &self, + entry_id: ModuleId, + circular_modules: &mut Vec, + ) -> Module { + assert!( + circular_modules.len() >= 1, + "# of circular modules should be 2 or greater than 2 including entry. Got {:?}", + circular_modules + ); + debug_assert!( + self.scope.is_circular(entry_id), + "merge_circular_modules should only be called for circular entries" + ); + + let entry_module = self.scope.get_module(entry_id).unwrap(); + + let modules = circular_modules + .iter() + .chain(once(&entry_id)) + .map(|&id| self.scope.get_module(id).unwrap()) + .collect::>(); + + let mut entry = self.process_circular_module(&modules, entry_module); + + for &dep in &*circular_modules { + let new_module = self.merge_two_circular_modules(&modules, entry, dep); + + entry = new_module; + } + + // All circular modules are inlined + circular_modules.clear(); + circular_modules.push(entry_id); + + entry + } + + /// Merges `a` and `b` into one module. + fn merge_two_circular_modules( + &self, + circular_modules: &[TransformedModule], + mut entry: Module, + dep: ModuleId, + ) -> Module { + self.run(|| { + // print_hygiene("START: merge_two_circular_modules", &self.cm, &entry); + + let dep_info = self.scope.get_module(dep).unwrap(); + let mut dep = self.process_circular_module(circular_modules, dep_info); + + dep = dep.fold_with(&mut Unexporter); + + // Merge code + entry.body = merge_respecting_order(entry.body, dep.body); + + // print_hygiene("END :merge_two_circular_modules", &self.cm, &entry); + entry + }) + } + + /// + /// - Remove cicular imnports + fn process_circular_module( + &self, + circular_modules: &[TransformedModule], + entry: TransformedModule, + ) -> Module { + let mut module = (*entry.module).clone(); + // print_hygiene("START: process_circular_module", &self.cm, &module); + + module.body.retain(|item| { + match item { + ModuleItem::ModuleDecl(ModuleDecl::Import(import)) => { + // Drop if it's one of circular import + for circular_module in circular_modules { + if entry + .imports + .specifiers + .iter() + .any(|v| v.0.module_id == circular_module.id && v.0.src == import.src) + { + log::debug!("Dropping circular import"); + return false; + } + } + } + _ => {} + } + + true + }); + + for circular_module in circular_modules { + for (src, specifiers) in entry.imports.specifiers.iter() { + if circular_module.id == src.module_id { + module = module.fold_with(&mut LocalMarker { + mark: circular_module.mark(), + specifiers: &specifiers, + excluded: vec![], + }); + break; + } + } + } + + module = module.fold_with(&mut top_level_ident_folder( + self.top_level_mark, + entry.mark(), + )); + + // print_hygiene("END: process_circular_module", &self.cm, &module); + + module + } +} + +/// Originally, this method should create a dependency graph, but +fn merge_respecting_order(mut entry: Vec, mut dep: Vec) -> Vec { + let mut new = Vec::with_capacity(entry.len() + dep.len()); + + // While looping over items from entry, we check for dependency. + loop { + if entry.is_empty() { + log::debug!("entry is empty"); + break; + } + let item = entry.drain(..=0).next().unwrap(); + + // Everything from dep is injected + if dep.is_empty() { + log::trace!("dep is empty"); + new.push(item); + new.extend(entry); + break; + } + + // If the code of entry depends on dependency, we insert dependency source code + // at the position. + if let Some(pos) = dependency_index(&item, &dep) { + log::trace!("Found depndency: {}", pos); + + new.extend(dep.drain(..=pos)); + new.push(item); + continue; + } + + // We checked the length of `dep` + if let Some(pos) = dependency_index(&dep[0], &[item.clone()]) { + log::trace!("Found reverse depndency (index[0]): {}", pos); + + new.extend(entry.drain(..=pos)); + new.extend(dep.drain(..=0)); + continue; + } + + if let Some(pos) = dependency_index(&dep[0], &entry) { + log::trace!("Found reverse depndency: {}", pos); + + new.extend(entry.drain(..=pos)); + new.extend(dep.drain(..=0)); + continue; + } + + log::debug!("No dependency"); + + new.push(item); + } + + // Append remaining statements. + new.extend(dep); + + new +} + +fn dependency_index(item: &ModuleItem, deps: &[ModuleItem]) -> Option { + let mut v = DepFinder { deps, idx: None }; + item.visit_with(&Invalid { span: DUMMY_SP }, &mut v); + v.idx +} + +struct DepFinder<'a> { + deps: &'a [ModuleItem], + idx: Option, +} + +impl Visit for DepFinder<'_> { + fn visit_ident(&mut self, i: &Ident, _: &dyn Node) { + if self.idx.is_some() { + return; + } + + for (idx, dep) in self.deps.iter().enumerate() { + match dep { + ModuleItem::Stmt(Stmt::Decl(Decl::Class(decl))) => { + log::trace!( + "Decl (from dep) = {}{:?}, Ident = {}{:?}", + decl.ident.sym, + decl.ident.span.ctxt, + i.sym, + i.span.ctxt + ); + if decl.ident.sym == i.sym && decl.ident.span.ctxt == i.span.ctxt { + self.idx = Some(idx); + break; + } + } + _ => {} + } + } + } + + fn visit_member_expr(&mut self, e: &MemberExpr, _: &dyn Node) { + e.obj.visit_with(e as _, self); + + if e.computed { + e.prop.visit_with(e as _, self) + } + } + + fn visit_class_member(&mut self, _: &ClassMember, _: &dyn Node) {} + fn visit_function(&mut self, _: &Function, _: &dyn Node) {} + fn visit_arrow_expr(&mut self, _: &ArrowExpr, _: &dyn Node) {} +} diff --git a/bundler/src/bundler/chunk/circular/hygiene.rs b/bundler/src/bundler/chunk/circular/hygiene.rs new file mode 100644 index 00000000000..b2b2ece3269 --- /dev/null +++ b/bundler/src/bundler/chunk/circular/hygiene.rs @@ -0,0 +1,164 @@ +use crate::id::Id; +use fxhash::FxHashSet; +use swc_common::{Mark, SyntaxContext}; +use swc_ecma_ast::*; +use swc_ecma_visit::{Fold, FoldWith}; + +pub fn top_level_ident_folder(top_level_mark: Mark, module_mark: Mark) -> impl 'static + Fold { + MergeFolder { + scope: Default::default(), + top_level_mark, + module_mark, + } +} + +#[derive(Default)] +struct Scope<'a> { + parent: Option<&'a Scope<'a>>, + binding_idents: FxHashSet, +} + +/// Modifies mark of top-level identifiers so they can be merged cleanly. +struct MergeFolder<'a> { + scope: Scope<'a>, + /// Global marker for the top-level identifiers + top_level_mark: Mark, + /// THe marker for the module's top-level identifiers. + module_mark: Mark, +} + +impl<'a> Scope<'a> { + pub fn new(parent: Option<&'a Scope<'a>>) -> Self { + Scope { + parent, + ..Default::default() + } + } + + pub fn contains(&self, i: &Ident) -> bool { + if self.binding_idents.contains(&Id::from(i)) { + return true; + } + + self.parent.map(|p| p.contains(i)).unwrap_or(false) + } +} + +/// TODO: This is incomplete +impl<'a> MergeFolder<'a> { + fn fold_bindine_ident(&mut self, mut i: Ident) -> Ident { + log::trace!("BindingIdent: {}{:?}", i.sym, i.span.ctxt); + + let mut ctxt = i.span.clone(); + if self.top_level_mark == ctxt.remove_mark() { + i.span = i + .span + .with_ctxt(SyntaxContext::empty().apply_mark(self.module_mark)); + } + + self.scope.binding_idents.insert((&i).into()); + i + } + + fn fold_ref_ident(&mut self, mut i: Ident) -> Ident { + // Skip reference to globals. + if !self.scope.contains(&i) { + // eprintln!("Preserving {}{:?}", i.sym, i.span.ctxt); + return i; + } + log::trace!("Changing context of ident ref: {}{:?}", i.sym, i.span.ctxt); + + let mut ctxt = i.span.clone(); + if self.top_level_mark == ctxt.remove_mark() { + i.span = i + .span + .with_ctxt(SyntaxContext::empty().apply_mark(self.module_mark)); + } + i + } + + fn child(&'a self) -> MergeFolder<'a> { + MergeFolder { + top_level_mark: self.top_level_mark, + module_mark: self.module_mark, + scope: Scope::new(Some(&self.scope)), + } + } +} + +impl Fold for MergeFolder<'_> { + fn fold_class_decl(&mut self, c: ClassDecl) -> ClassDecl { + ClassDecl { + ident: self.fold_bindine_ident(c.ident), + class: c.class.fold_with(self), + ..c + } + } + + fn fold_class_expr(&mut self, c: ClassExpr) -> ClassExpr { + ClassExpr { + ident: c.ident.map(|i| self.fold_bindine_ident(i)), + class: c.class.fold_with(self), + ..c + } + } + + fn fold_member_expr(&mut self, e: MemberExpr) -> MemberExpr { + if e.computed { + MemberExpr { + obj: e.obj.fold_with(self), + prop: e.prop.fold_with(self), + ..e + } + } else { + MemberExpr { + obj: e.obj.fold_with(self), + ..e + } + } + } + + fn fold_expr(&mut self, mut e: Expr) -> Expr { + e = e.fold_children_with(self); + + match e { + Expr::Ident(i) => Expr::Ident(self.fold_ref_ident(i)), + _ => e, + } + } + + fn fold_fn_decl(&mut self, decl: FnDecl) -> FnDecl { + let ident = self.fold_bindine_ident(decl.ident); + + let mut child = self.child(); + let function = decl.function.fold_with(&mut child); + + FnDecl { + ident, + function, + ..decl + } + } + + fn fold_fn_expr(&mut self, f: FnExpr) -> FnExpr { + let ident = f.ident.map(|i| self.fold_bindine_ident(i)); + + let mut child = self.child(); + let function = f.function.fold_with(&mut child); + + FnExpr { + ident, + function, + ..f + } + } + + fn fold_pat(&mut self, mut p: Pat) -> Pat { + p = p.fold_children_with(self); + + match p { + Pat::Ident(i) => Pat::Ident(self.fold_bindine_ident(i)), + _ => p, + } + } +} diff --git a/spack/src/bundler/chunk/merge.rs b/bundler/src/bundler/chunk/merge.rs similarity index 87% rename from spack/src/bundler/chunk/merge.rs rename to bundler/src/bundler/chunk/merge.rs index a555a6c1a85..e4e86d8b45b 100644 --- a/spack/src/bundler/chunk/merge.rs +++ b/bundler/src/bundler/chunk/merge.rs @@ -1,7 +1,9 @@ -use super::Bundler; use crate::{ - bundler::{export::Exports, load_transformed::Specifier}, - Id, ModuleId, + bundler::{export::Exports, load::Specifier}, + id::{Id, ModuleId}, + load::Load, + resolve::Resolve, + Bundler, }; use anyhow::{Context, Error}; use std::{ @@ -12,57 +14,89 @@ use std::{ use swc_atoms::{js_word, JsWord}; use swc_common::{Mark, Span, Spanned, SyntaxContext, DUMMY_SP}; use swc_ecma_ast::*; -use swc_ecma_transforms::noop_fold_type; use swc_ecma_utils::{ find_ids, prepend, private_ident, undefined, DestructuringFinder, ExprFactory, StmtLike, }; use swc_ecma_visit::{Fold, FoldWith, VisitMut, VisitMutWith, VisitWith}; -impl Bundler<'_> { +impl Bundler<'_, L, R> +where + L: Load, + R: Resolve, +{ /// Merge `targets` into `entry`. pub(super) fn merge_modules( &self, entry: ModuleId, targets: &mut Vec, ) -> Result { - log::trace!("merge_modules({})", entry); + self.run(|| { + let is_circular = self.scope.is_circular(entry); + + log::trace!( + "merge_modules({}) <- {:?}; circular = {}", + entry, + targets, + is_circular + ); - self.swc.run(|| { let info = self.scope.get_module(entry).unwrap(); - - let mut entry: Module = (*info.module).clone(); if targets.is_empty() { return Ok((*info.module).clone()); } - log::info!("Merge: {} <= {:?}", info.fm.name, targets); + if is_circular { + log::info!("Circular dependency detected"); + // TODO: provide only circular imports. + return Ok(self.merge_circular_modules(entry, targets)); + } - // { - // let code = self - // .swc - // .print( - // &entry.clone().fold_with(&mut HygieneVisualizer), - // SourceMapsConfig::Bool(false), - // None, - // false, - // ) - // .unwrap() - // .code; - // - // println!("Before merging:\n{}\n\n\n", code); - // } + let mut entry: Module = (*info.module).clone(); + + log::info!("Merge: ({}){} <= {:?}", info.id, info.fm.name, targets); + + // print_hygiene("before:merge", &self.cm, &entry); for (src, specifiers) in &info.imports.specifiers { if !targets.contains(&src.module_id) { + // Already merged by recursive call to merge_modules. log::debug!( - "Not merging: not in target: ({}):{} <= ({}):{}", + "Not merging: already merged: ({}):{} <= ({}):{}", info.id, info.fm.name, src.module_id, src.src.value, ); + + if let Some(imported) = self.scope.get_module(src.module_id) { + // Respan using imported module's syntax context. + entry = entry.fold_with(&mut LocalMarker { + mark: imported.mark(), + specifiers: &specifiers, + excluded: vec![], + }); + } + + // Drop imports, as they are already merged. + entry.body.retain(|item| { + match item { + ModuleItem::ModuleDecl(ModuleDecl::Import(import)) => { + // Drop if it's one of circular import + if info.imports.specifiers.iter().any(|v| { + v.0.module_id == src.module_id && v.0.src == import.src + }) { + log::debug!("Dropping import"); + return false; + } + } + _ => {} + } + + true + }); continue; } + log::debug!("Merging: {} <= {}", info.fm.name, src.src.value); if specifiers.iter().any(|v| v.is_namespace()) { @@ -75,6 +109,10 @@ impl Bundler<'_> { if let Some(imported) = self.scope.get_module(src.module_id) { info.helpers.extend(&imported.helpers); + if let Some(pos) = targets.iter().position(|x| *x == src.module_id) { + targets.remove(pos); + } + // In the case of // // a <- b @@ -93,43 +131,13 @@ impl Bundler<'_> { ) })?; - if let Some(pos) = targets.iter().position(|x| *x == info.id) { - targets.remove(pos); - } - if imported.is_es6 { - //{ - // let code = self - // .swc - // .print( - // &dep.clone().fold_with(&mut HygieneVisualizer), - // info.fm.clone(), - // false, - // false, - // ) - // .unwrap() - // .code; - // - // println!("Dep before drop_unused:\n{}\n\n\n", code); - //} + // print_hygiene("dep:before:tree-shaking", &self.cm, &dep); // Tree-shaking - dep = self.drop_unused(imported.fm.clone(), dep, Some(&specifiers)); + dep = self.drop_unused(dep, Some(&specifiers)); - //{ - // let code = self - // .swc - // .print( - // &dep.clone().fold_with(&mut HygieneVisualizer), - // info.fm.clone(), - // false, - // false, - // ) - // .unwrap() - // .code; - // - // println!("Dep after drop_unused:\n{}\n\n\n", code); - //} + // print_hygiene("dep:after:tree-shaking", &self.cm, &dep); if let Some(imports) = info .imports @@ -163,11 +171,15 @@ impl Bundler<'_> { // }); } + // print_hygiene("dep:before:global-mark", &self.cm, &dep); + dep = dep.fold_with(&mut GlobalMarker { used_mark: self.used_mark, module_mark: imported.mark(), }); + // print_hygiene("dep:after:global-mark", &self.cm, &dep); + // { // let code = self // .swc @@ -183,21 +195,6 @@ impl Bundler<'_> { // println!("Dep:\n{}\n\n\n", code); // } - // { - // let code = self - // .swc - // .print( - // &entry.clone().fold_with(&mut HygieneVisualizer), - // SourceMapsConfig::Bool(false), - // None, - // false, - // ) - // .unwrap() - // .code; - // - // println!("@: Before merging:\n{}\n\n\n", code); - // } - // Replace import statement / require with module body let mut injector = Es6ModuleInjector { imported: dep.body.clone(), @@ -205,28 +202,14 @@ impl Bundler<'_> { }; entry.body.visit_mut_with(&mut injector); - // { - // let code = self - // .swc - // .print( - // &entry.clone().fold_with(&mut - // HygieneVisualizer), - // SourceMapsConfig::Bool(false), - // None, - // false, - // ) - // .unwrap() - // .code; - // - // println!("Merged:\n{}\n\n\n", code); - // } + // print_hygiene("entry:after:injection", &self.cm, &entry); if injector.imported.is_empty() { continue; } } - { + if self.config.require { // common js module is transpiled as // // Src: @@ -356,16 +339,44 @@ impl Bundler<'_> { } /// `export var a = 1` => `var a = 1` -struct Unexporter; - -noop_fold_type!(Unexporter); +pub(super) struct Unexporter; impl Fold for Unexporter { fn fold_module_item(&mut self, item: ModuleItem) -> ModuleItem { match item { ModuleItem::ModuleDecl(decl) => match decl { ModuleDecl::ExportDecl(decl) => ModuleItem::Stmt(Stmt::Decl(decl.decl)), - ModuleDecl::ExportDefaultExpr(..) => { + + ModuleDecl::ExportDefaultDecl(export) => match export.decl { + DefaultDecl::Class(ClassExpr { ident: None, .. }) + | DefaultDecl::Fn(FnExpr { ident: None, .. }) => { + ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP })) + } + DefaultDecl::TsInterfaceDecl(decl) => { + ModuleItem::Stmt(Stmt::Decl(Decl::TsInterface(decl))) + } + + DefaultDecl::Class(ClassExpr { + ident: Some(ident), + class, + }) => ModuleItem::Stmt(Stmt::Decl(Decl::Class(ClassDecl { + declare: false, + ident, + class, + }))), + + DefaultDecl::Fn(FnExpr { + ident: Some(ident), + function, + }) => ModuleItem::Stmt(Stmt::Decl(Decl::Fn(FnDecl { + declare: false, + function, + ident, + }))), + }, + + // Empty statement + ModuleDecl::ExportAll(..) | ModuleDecl::ExportDefaultExpr(..) => { ModuleItem::Stmt(Stmt::Empty(EmptyStmt { span: DUMMY_SP })) } ModuleDecl::ExportNamed(ref n) if n.src.is_none() => { @@ -373,8 +384,7 @@ impl Fold for Unexporter { } ModuleDecl::Import(..) => ModuleItem::ModuleDecl(decl), - // TODO: Handle all - _ => unimplemented!("Unexporter: {:?}", decl), + _ => unimplemented!("Unexported: {:?}", decl), }, _ => item, @@ -392,8 +402,6 @@ struct ExportRenamer<'a> { extras: Vec, } -noop_fold_type!(ExportRenamer<'_>); - impl ExportRenamer<'_> { pub fn aliased_import(&self, sym: &JsWord) -> Option { log::debug!("aliased_import({})\n{:?}\n\n\n", sym, self.imports); @@ -575,8 +583,6 @@ struct ActualMarker<'a> { imports: &'a [Specifier], } -noop_fold_type!(ActualMarker<'_>); - impl Fold for ActualMarker<'_> { fn fold_expr(&mut self, node: Expr) -> Expr { node @@ -605,15 +611,13 @@ impl Fold for ActualMarker<'_> { } /// Applied to the importer module, and marks (connects) imported idents. -struct LocalMarker<'a> { +pub(super) struct LocalMarker<'a> { /// Mark applied to imported idents. - mark: Mark, - specifiers: &'a [Specifier], - excluded: Vec, + pub mark: Mark, + pub specifiers: &'a [Specifier], + pub excluded: Vec, } -noop_fold_type!(LocalMarker<'_>); - impl<'a> LocalMarker<'a> { /// Searches for i, and fold T. #[allow(dead_code)] @@ -772,13 +776,11 @@ impl VisitMut for Es6ModuleInjector { } } -struct GlobalMarker { - used_mark: Mark, - module_mark: Mark, +pub(super) struct GlobalMarker { + pub used_mark: Mark, + pub module_mark: Mark, } -noop_fold_type!(GlobalMarker); - impl GlobalMarker { fn is_marked_as_used(&self, span: Span) -> bool { let mut ctxt = span.ctxt(); diff --git a/spack/src/bundler/chunk/mod.rs b/bundler/src/bundler/chunk/mod.rs similarity index 81% rename from spack/src/bundler/chunk/mod.rs rename to bundler/src/bundler/chunk/mod.rs index 3b48b35a926..0c0979a198d 100644 --- a/spack/src/bundler/chunk/mod.rs +++ b/bundler/src/bundler/chunk/mod.rs @@ -1,15 +1,16 @@ -use super::Bundler; +use super::{load::TransformedModule, Bundler}; use crate::{ - bundler::{load_transformed::TransformedModule, Bundle, BundleKind}, - ModuleId, + id::ModuleId, load::Load, resolve::Resolve, util::IntoParallelIterator, Bundle, BundleKind, }; use anyhow::{Context, Error}; use fxhash::{FxHashMap, FxHashSet}; use petgraph::{graphmap::DiGraphMap, visit::Bfs}; -use rayon::prelude::*; -use swc_ecma_transforms::{fixer, hygiene, optimization::simplify::dce::dce}; +#[cfg(feature = "rayon")] +use rayon::iter::ParallelIterator; +use swc_ecma_transforms::{hygiene, optimization::simplify::dce}; use swc_ecma_visit::FoldWith; +mod circular; mod merge; pub(super) type ModuleGraph = DiGraphMap; @@ -34,7 +35,11 @@ struct State { common_libs: FxHashSet, } -impl Bundler<'_> { +impl Bundler<'_, L, R> +where + L: Load, + R: Resolve, +{ /// `entries` - Entry modules (provided by user) by it's basename. /// /// # How it works @@ -50,16 +55,15 @@ impl Bundler<'_> { .into_par_iter() .map( |(kind, id, mut module_ids_to_merge): (BundleKind, ModuleId, _)| { - self.swc().run(|| { + self.run(|| { let module = self .merge_modules(id, &mut module_ids_to_merge) .context("failed to merge module") .unwrap(); // TODO let module = module - .fold_with(&mut dce(Default::default())) - .fold_with(&mut hygiene()) - .fold_with(&mut fixer(Some(&self.swc.comments() as _))); + .fold_with(&mut dce::dce(Default::default())) + .fold_with(&mut hygiene()); Bundle { kind, id, module } }) @@ -139,6 +143,8 @@ impl Bundler<'_> { } fn add_to_graph(&self, graph: &mut ModuleGraph, module_id: ModuleId) { + let contains = graph.contains_node(module_id); + graph.add_node(module_id); let m = self @@ -146,8 +152,20 @@ impl Bundler<'_> { .get_module(module_id) .expect("failed to get module"); + // Prevent dejavu + if contains { + for (src, _) in &m.imports.specifiers { + if graph.contains_node(src.module_id) { + self.scope.mark_as_circular(module_id); + self.scope.mark_as_circular(src.module_id); + return; + } + } + } + for (src, _) in &*m.imports.specifiers { // + self.add_to_graph(graph, src.module_id); graph.add_edge( module_id, diff --git a/spack/src/bundler/export.rs b/bundler/src/bundler/export.rs similarity index 95% rename from spack/src/bundler/export.rs rename to bundler/src/bundler/export.rs index bab59ffcc2c..d4cfe8d2c35 100644 --- a/spack/src/bundler/export.rs +++ b/bundler/src/bundler/export.rs @@ -1,17 +1,20 @@ -use super::Bundler; -use crate::{ - bundler::load_transformed::{Source, Specifier}, - Id, +use super::{ + load::{Source, Specifier}, + Bundler, }; +use crate::{id::Id, load::Load, resolve::Resolve}; use fxhash::FxHashMap; use swc_atoms::js_word; use swc_common::{SyntaxContext, DUMMY_SP}; use swc_ecma_ast::*; -use swc_ecma_transforms::noop_visit_type; use swc_ecma_utils::find_ids; use swc_ecma_visit::{Node, Visit, VisitWith}; -impl Bundler<'_> { +impl Bundler<'_, L, R> +where + L: Load, + R: Resolve, +{ /// This method removes exported pure constants from the module. /// /// A pure constant is a exported literal. @@ -20,7 +23,7 @@ impl Bundler<'_> { /// TODO: Support pattern like /// export const [a, b] = [1, 2] pub(super) fn extract_export_info(&self, module: &Module) -> RawExports { - self.swc.run(|| { + self.run(|| { let mut v = ExportFinder::default(); module.visit_with(&Invalid { span: DUMMY_SP } as _, &mut v); @@ -32,14 +35,12 @@ impl Bundler<'_> { #[derive(Debug, Default)] pub(super) struct RawExports { - pub pure_constants: Vec<(Id, Lit)>, /// Key is None if it's exported from the module itself. pub items: FxHashMap, Vec>, } #[derive(Debug, Default)] pub(super) struct Exports { - pub pure_constants: Vec<(Id, Lit)>, pub items: Vec, pub reexports: FxHashMap>, } @@ -49,8 +50,6 @@ struct ExportFinder { info: RawExports, } -noop_visit_type!(ExportFinder); - impl Visit for ExportFinder { fn visit_module_item(&mut self, item: &ModuleItem, _: &dyn Node) { match item { diff --git a/bundler/src/bundler/finalize.rs b/bundler/src/bundler/finalize.rs new file mode 100644 index 00000000000..c3df4d35422 --- /dev/null +++ b/bundler/src/bundler/finalize.rs @@ -0,0 +1,170 @@ +use crate::{hash::calc_hash, Bundle, BundleKind, Bundler, Load, Resolve}; +use anyhow::Error; +use fxhash::FxHashMap; +use relative_path::RelativePath; +use std::path::{Path, PathBuf}; +use swc_common::{util::move_map::MoveMap, FileName}; +use swc_ecma_ast::{ImportDecl, Str}; +use swc_ecma_transforms::noop_fold_type; +use swc_ecma_visit::{Fold, FoldWith}; + +impl Bundler<'_, L, R> +where + L: Load, + R: Resolve, +{ + /// This method do + /// + /// - inject helpers + /// - rename chunks + pub(super) fn finalize(&self, bundles: Vec) -> Result, Error> { + self.run(|| { + let mut new = Vec::with_capacity(bundles.len()); + let mut renamed = FxHashMap::default(); + + for mut bundle in bundles { + match bundle.kind { + BundleKind::Named { .. } => { + // Inject helpers + let helpers = self + .scope + .get_module(bundle.id) + .expect("module should exist at this point") + .helpers; + + helpers.append_to(&mut bundle.module.body); + + new.push(Bundle { ..bundle }); + } + BundleKind::Lib { name } => { + let hash = calc_hash(self.cm.clone(), &bundle.module)?; + let mut new_name = PathBuf::from(name); + let key = new_name.clone(); + let file_name = new_name + .file_name() + .map(|path| -> PathBuf { + let path = Path::new(path); + let ext = path.extension(); + if let Some(ext) = ext { + return format!( + "{}-{}.{}", + path.file_stem().unwrap().to_string_lossy(), + hash, + ext.to_string_lossy() + ) + .into(); + } + return format!( + "{}-{}", + path.file_stem().unwrap().to_string_lossy(), + hash, + ) + .into(); + }) + .expect("javascript file should have name"); + new_name.pop(); + new_name = new_name.join(file_name.clone()); + + renamed.insert(key, new_name.to_string_lossy().to_string()); + + new.push(Bundle { + kind: BundleKind::Named { + name: file_name.display().to_string(), + }, + ..bundle + }) + } + _ => new.push(bundle), + } + } + + if new.len() == 1 { + return Ok(new); + } + + new = new.move_map(|bundle| { + let path = match self.scope.get_module(bundle.id).unwrap().fm.name { + FileName::Real(ref v) => v.clone(), + _ => { + log::error!("Cannot rename: not a real file"); + return bundle; + } + }; + + let module = { + // Change imports + let mut v = Renamer { + resolver: &self.resolver, + base: &path, + renamed: &renamed, + }; + bundle.module.fold_with(&mut v) + }; + + Bundle { module, ..bundle } + }); + + Ok(new) + }) + } +} + +/// Import renamer. This pass changes import path. +struct Renamer<'a, R> +where + R: Resolve, +{ + resolver: R, + base: &'a PathBuf, + renamed: &'a FxHashMap, +} + +noop_fold_type!(Renamer<'_, '_>); + +impl Fold for Renamer<'_, R> +where + R: Resolve, +{ + fn fold_import_decl(&mut self, import: ImportDecl) -> ImportDecl { + let resolved = match self + .resolver + .resolve(&FileName::Real(self.base.clone()), &import.src.value) + { + Ok(v) => match v { + FileName::Real(v) => v, + _ => panic!("rename_bundles called with non-path module"), + }, + Err(_) => return import, + }; + + if let Some(v) = self.renamed.get(&resolved) { + // We use parent because RelativePath uses ../common-[hash].js + // if we use `entry-a.js` as a base. + // + // entry-a.js + // common.js + let base = self + .base + .parent() + .unwrap_or(self.base) + .as_os_str() + .to_string_lossy(); + let base = RelativePath::new(&*base); + let v = base.relative(&*v); + let value = v.as_str(); + return ImportDecl { + src: Str { + value: if value.starts_with(".") { + value.into() + } else { + format!("./{}", value).into() + }, + ..import.src + }, + ..import + }; + } + + import + } +} diff --git a/spack/src/bundler/helpers/_require.js b/bundler/src/bundler/helpers/_require.js similarity index 100% rename from spack/src/bundler/helpers/_require.js rename to bundler/src/bundler/helpers/_require.js diff --git a/spack/src/bundler/helpers/mod.rs b/bundler/src/bundler/helpers/mod.rs similarity index 100% rename from spack/src/bundler/helpers/mod.rs rename to bundler/src/bundler/helpers/mod.rs diff --git a/spack/src/bundler/import/mod.rs b/bundler/src/bundler/import/mod.rs similarity index 83% rename from spack/src/bundler/import/mod.rs rename to bundler/src/bundler/import/mod.rs index 3ead6c583b5..3ea8c96705d 100644 --- a/spack/src/bundler/import/mod.rs +++ b/bundler/src/bundler/import/mod.rs @@ -1,57 +1,62 @@ use super::Bundler; +use crate::{load::Load, resolve::Resolve}; use anyhow::{Context, Error}; use fxhash::{FxHashMap, FxHashSet}; -use node_resolve::is_core_module; -use std::{ - mem::replace, - path::{Path, PathBuf}, - sync::Arc, -}; +use std::mem::replace; use swc_atoms::{js_word, JsWord}; -use swc_common::{util::move_map::MoveMap, Mark, Spanned, DUMMY_SP}; +use swc_common::{sync::Lrc, util::move_map::MoveMap, FileName, Mark, Spanned, DUMMY_SP}; use swc_ecma_ast::*; -use swc_ecma_transforms::noop_fold_type; use swc_ecma_utils::{find_ids, ident::IdentLike, Id}; use swc_ecma_visit::{Fold, FoldWith}; #[cfg(test)] mod tests; -impl Bundler<'_> { - /// This de-globs imports if possible. +impl Bundler<'_, L, R> +where + L: Load, + R: Resolve, +{ + /// This method de-globs imports if possible. pub(super) fn extract_import_info( &self, - path: &Path, + path: &FileName, module: &mut Module, _mark: Mark, ) -> RawImports { - let body = replace(&mut module.body, vec![]); + self.run(|| { + let body = replace(&mut module.body, vec![]); - let mut v = ImportHandler { - path, - bundler: self, - top_level: false, - info: Default::default(), - forces_ns: Default::default(), - ns_usage: Default::default(), - deglob_phase: false, - }; - let body = body.fold_with(&mut v); - v.deglob_phase = true; - let body = body.fold_with(&mut v); - module.body = body; + let mut v = ImportHandler { + path, + bundler: self, + top_level: false, + info: Default::default(), + forces_ns: Default::default(), + ns_usage: Default::default(), + deglob_phase: false, + }; + let body = body.fold_with(&mut v); + v.deglob_phase = true; + let body = body.fold_with(&mut v); + module.body = body; - v.info + v.info + }) } - pub(super) fn resolve(&self, base: &Path, s: &str) -> Result, Error> { - self.swc.run(|| { + pub(super) fn resolve( + &self, + base: &FileName, + module_specifier: &str, + ) -> Result, Error> { + self.run(|| { let path = self .resolver - .resolve(base, s) - .with_context(|| format!("failed to resolve {} from {}", s, base.display()))?; + .resolve(base, module_specifier) + .with_context(|| format!("failed to resolve {} from {}", module_specifier, base))?; - let path = Arc::new(path); + let path = Lrc::new(path); Ok(path) }) @@ -77,9 +82,13 @@ pub(super) struct RawImports { pub dynamic_imports: Vec, } -struct ImportHandler<'a, 'b> { - path: &'a Path, - bundler: &'a Bundler<'b>, +struct ImportHandler<'a, 'b, L, R> +where + L: Load, + R: Resolve, +{ + path: &'a FileName, + bundler: &'a Bundler<'b, L, R>, top_level: bool, info: RawImports, /// Contains namespace imports accessed with computed key. @@ -99,11 +108,20 @@ struct ImportHandler<'a, 'b> { deglob_phase: bool, } -noop_fold_type!(ImportHandler<'_, '_>); - -impl ImportHandler<'_, '_> { +impl ImportHandler<'_, '_, L, R> +where + L: Load, + R: Resolve, +{ fn mark_for(&self, src: &str) -> Option { - if is_core_module(src) { + // Don't apply mark if it's a core module. + if self + .bundler + .config + .external_modules + .iter() + .any(|v| v == src) + { return None; } let path = self.bundler.resolve(self.path, src).ok()?; @@ -112,10 +130,20 @@ impl ImportHandler<'_, '_> { } } -impl Fold for ImportHandler<'_, '_> { +impl Fold for ImportHandler<'_, '_, L, R> +where + L: Load, + R: Resolve, +{ fn fold_import_decl(&mut self, import: ImportDecl) -> ImportDecl { if !self.deglob_phase { - if is_core_module(&import.src.value) { + // Ignore if it's a core module. + if self + .bundler + .config + .external_modules + .contains(&import.src.value) + { return import; } @@ -334,13 +362,14 @@ impl Fold for ImportHandler<'_, '_> { match &e.callee { ExprOrSuper::Expr(callee) - if match &**callee { - Expr::Ident(Ident { - sym: js_word!("require"), - .. - }) => true, - _ => false, - } => + if self.bundler.config.require + && match &**callee { + Expr::Ident(Ident { + sym: js_word!("require"), + .. + }) => true, + _ => false, + } => { let span = callee.span(); @@ -399,13 +428,15 @@ impl Fold for ImportHandler<'_, '_> { callee: ExprOrSuper::Expr(ref callee), ref args, .. - }) if match &**callee { - Expr::Ident(Ident { - sym: js_word!("require"), - .. - }) => true, - _ => false, - } && args.len() == 1 => + }) if self.bundler.config.require + && match &**callee { + Expr::Ident(Ident { + sym: js_word!("require"), + .. + }) => true, + _ => false, + } + && args.len() == 1 => { let span = *span; let src = match args.first().unwrap() { @@ -415,7 +446,8 @@ impl Fold for ImportHandler<'_, '_> { }, _ => return node, }; - if is_core_module(&src.value) { + // Ignore core modules. + if self.bundler.config.external_modules.contains(&src.value) { return node; } diff --git a/spack/src/bundler/import/tests.rs b/bundler/src/bundler/import/tests.rs similarity index 85% rename from spack/src/bundler/import/tests.rs rename to bundler/src/bundler/import/tests.rs index f95cb8128a7..739da8d7019 100644 --- a/spack/src/bundler/import/tests.rs +++ b/bundler/src/bundler/import/tests.rs @@ -1,5 +1,7 @@ -use crate::bundler::{import::ImportHandler, tests::test_bundler}; +use super::ImportHandler; +use crate::bundler::tests::test_bundler; use std::path::Path; +use swc_common::FileName; use swc_ecma_visit::FoldWith; #[test] @@ -13,7 +15,7 @@ ns.foo(); ", ); let mut v = ImportHandler { - path: &Path::new("index.js"), + path: &FileName::Real(Path::new("index.js").to_path_buf()), bundler: &t.bundler, top_level: false, info: Default::default(), @@ -41,7 +43,7 @@ ns.bar(); ", ); let mut v = ImportHandler { - path: &Path::new("index.js"), + path: &FileName::Real(Path::new("index.js").to_path_buf()), bundler: &t.bundler, top_level: false, info: Default::default(), diff --git a/bundler/src/bundler/load.rs b/bundler/src/bundler/load.rs new file mode 100644 index 00000000000..de0961bfad6 --- /dev/null +++ b/bundler/src/bundler/load.rs @@ -0,0 +1,422 @@ +use super::{export::Exports, helpers::Helpers, Bundler}; +use crate::{ + bundler::{export::RawExports, import::RawImports}, + id::{Id, ModuleId}, + util, + util::IntoParallelIterator, + Load, Resolve, +}; +use anyhow::{Context, Error}; +use is_macro::Is; +#[cfg(feature = "rayon")] +use rayon::iter::ParallelIterator; +use swc_atoms::js_word; +use swc_common::{sync::Lrc, FileName, Mark, SourceFile, DUMMY_SP}; +use swc_ecma_ast::{ + Expr, ExprOrSuper, ImportDecl, ImportSpecifier, Invalid, MemberExpr, Module, ModuleDecl, Str, +}; +use swc_ecma_transforms::resolver_with_mark; +use swc_ecma_visit::{FoldWith, Node, Visit, VisitWith}; +/// Module after applying transformations. +#[derive(Debug, Clone)] +pub(super) struct TransformedModule { + pub id: ModuleId, + pub fm: Lrc, + pub module: Lrc, + pub imports: Lrc, + pub exports: Lrc, + + /// If false, the module will be wrapped with a small helper function. + pub is_es6: bool, + + /// Used helpers + pub helpers: Lrc, + + mark: Mark, +} + +impl TransformedModule { + /// THe marker for the module's top-level identifiers. + pub fn mark(&self) -> Mark { + self.mark + } +} + +impl Bundler<'_, L, R> +where + L: Load, + R: Resolve, +{ + /// Phase 1 (discovery) + /// + /// We apply transforms at this phase to make cache efficient. + /// As we cache in this phase, changing dependency does not affect cache. + pub(super) fn load_transformed( + &self, + file_name: &FileName, + ) -> Result, Error> { + self.run(|| { + log::trace!("load_transformed: ({})", file_name); + + // In case of common module + if let Some(cached) = self.scope.get_module_by_path(&file_name) { + log::info!("Cached: {}", file_name); + return Ok(Some(cached)); + } + + let (_, fm, module) = self.load(&file_name).context("Bundler.load() failed")?; + let (v, mut files) = self + .analyze(&file_name, fm.clone(), module) + .context("failed to analyze module")?; + files.dedup_by_key(|v| v.1.clone()); + + log::info!("Storing module: {}", file_name); + self.scope.store_module(v.clone()); + + // Load dependencies and store them in the `Scope` + let results = files + .into_par_iter() + .map(|(_src, path)| { + log::debug!("loading dependency: {}", path); + self.load_transformed(&path) + }) + .collect::>(); + + // Do tasks in parallel, and then wait for result + for result in results { + let res = result?; + dbg!(res.is_none()); + } + + Ok(Some(v)) + }) + } + + fn load(&self, file_name: &FileName) -> Result<(ModuleId, Lrc, Module), Error> { + self.run(|| { + let (module_id, _) = self.scope.module_id_gen.gen(file_name); + + let (fm, module) = self + .loader + .load(&file_name) + .with_context(|| format!("Bundler.loader.load({}) failed", file_name))?; + self.scope.mark_as_loaded(module_id); + Ok((module_id, fm, module)) + }) + } + + /// This methods returns [Source]s which should be loaded. + fn analyze( + &self, + file_name: &FileName, + fm: Lrc, + mut module: Module, + ) -> Result<(TransformedModule, Vec<(Source, Lrc)>), Error> { + self.run(|| { + log::trace!("transform_module({})", fm.name); + module = module.fold_with(&mut resolver_with_mark(self.top_level_mark)); + + let (id, mark) = self.scope.module_id_gen.gen(file_name); + + // { + // let code = self + // .swc + // .print( + // &module.clone().fold_with(&mut HygieneVisualizer), + // SourceMapsConfig::Bool(false), + // None, + // false, + // ) + // .unwrap() + // .code; + // + // println!("Resolved:\n{}\n\n", code); + // } + + let imports = self.extract_import_info(file_name, &mut module, mark); + + // { + // let code = self + // .swc + // .print( + // &module.clone().fold_with(&mut HygieneVisualizer), + // SourceMapsConfig::Bool(false), + // None, + // false, + // ) + // .unwrap() + // .code; + // + // println!("After imports:\n{}\n", code,); + // } + + let exports = self.extract_export_info(&module); + + let is_es6 = { + let mut v = Es6ModuleDetector { + forced_es6: false, + found_other: false, + }; + module.visit_with(&Invalid { span: DUMMY_SP } as _, &mut v); + v.forced_es6 || !v.found_other + }; + if is_es6 { + module = self.drop_unused(module, None); + } + + let (imports, exports) = util::join( + || self.resolve_imports(file_name, imports), + || self.resolve_exports(file_name, exports), + ); + let (imports, mut import_files) = imports?; + let (exports, reexport_files) = exports?; + import_files.extend(reexport_files); + + let module = Lrc::new(module); + + Ok(( + TransformedModule { + id, + fm, + module, + imports: Lrc::new(imports), + exports: Lrc::new(exports), + is_es6, + helpers: Default::default(), + mark, + }, + import_files, + )) + }) + } + + /// Resolve re-exports. + fn resolve_exports( + &self, + base: &FileName, + raw: RawExports, + ) -> Result<(Exports, Vec<(Source, Lrc)>), Error> { + self.run(|| { + log::trace!("resolve_exports({})", base); + let mut files = vec![]; + + let mut exports = Exports::default(); + + let items = raw + .items + .into_par_iter() + .map(|(src, ss)| -> Result<_, Error> { + self.run(|| { + let info = match src { + Some(src) => { + let name = self.resolve(base, &src.value)?; + let (id, _) = self.scope.module_id_gen.gen(&name); + Some((id, name, src)) + } + None => None, + }; + + Ok((info, ss)) + }) + }) + .collect::>(); + + for res in items { + let (info, specifiers) = res?; + + match info { + None => exports.items.extend(specifiers), + Some((id, name, src)) => { + // + let src = Source { + is_loaded_synchronously: true, + is_unconditional: false, + module_id: id, + src, + }; + exports + .reexports + .entry(src.clone()) + .or_default() + .extend(specifiers); + files.push((src, name)); + } + } + } + + Ok((exports, files)) + }) + } + + /// Resolve dependencies + fn resolve_imports( + &self, + base: &FileName, + info: RawImports, + ) -> Result<(Imports, Vec<(Source, Lrc)>), Error> { + self.run(|| { + log::trace!("resolve_imports({})", base); + let mut files = vec![]; + + let mut merged = Imports::default(); + let RawImports { + imports, + lazy_imports, + dynamic_imports, + } = info; + + let loaded = imports + .into_par_iter() + .map(|v| (v, false, true)) + .chain(lazy_imports.into_par_iter().map(|v| (v, false, false))) + .chain(dynamic_imports.into_par_iter().map(|src| { + ( + ImportDecl { + span: src.span, + specifiers: vec![], + src, + type_only: false, + }, + true, + false, + ) + })) + .map(|(decl, dynamic, unconditional)| -> Result<_, Error> { + self.run(|| { + // + let file_name = self.resolve(base, &decl.src.value)?; + let (id, _) = self.scope.module_id_gen.gen(&file_name); + + Ok((id, file_name, decl, dynamic, unconditional)) + }) + }) + .collect::>(); + + for res in loaded { + // TODO: Report error and proceed instead of returning an error + let (id, file_name, decl, is_dynamic, is_unconditional) = res?; + + let src = Source { + is_loaded_synchronously: !is_dynamic, + is_unconditional, + module_id: id, + src: decl.src, + }; + files.push((src.clone(), file_name)); + + // TODO: Handle rename + let mut specifiers = vec![]; + for s in decl.specifiers { + match s { + ImportSpecifier::Named(s) => specifiers.push(Specifier::Specific { + local: s.local.into(), + alias: s.imported.map(From::from), + }), + ImportSpecifier::Default(s) => specifiers.push(Specifier::Specific { + local: s.local.into(), + alias: Some(Id::new(js_word!("default"), s.span.ctxt())), + }), + ImportSpecifier::Namespace(s) => { + specifiers.push(Specifier::Namespace { + local: s.local.into(), + }); + } + } + } + + merged.specifiers.push((src, specifiers)); + } + + Ok((merged, files)) + }) + } +} + +#[derive(Debug, Default)] +pub(super) struct Imports { + /// If imported ids are empty, it is a side-effect import. + pub specifiers: Vec<(Source, Vec)>, +} + +/// Clone is relatively cheap +#[derive(Debug, Clone, Is)] +pub(super) enum Specifier { + Specific { local: Id, alias: Option }, + Namespace { local: Id }, +} + +impl Specifier { + pub fn local(&self) -> &Id { + match self { + Specifier::Specific { local, .. } => local, + Specifier::Namespace { local, .. } => local, + } + } +} + +#[derive(Debug, Clone, PartialEq, Eq, Hash)] +pub(super) struct Source { + pub is_loaded_synchronously: bool, + pub is_unconditional: bool, + + pub module_id: ModuleId, + // Clone is relatively cheap, thanks to string_cache. + pub src: Str, +} + +struct Es6ModuleDetector { + /// If import statement or export is detected, it's an es6 module regardless + /// of other codes. + forced_es6: bool, + /// True if other module system is detected. + found_other: bool, +} + +impl Visit for Es6ModuleDetector { + fn visit_member_expr(&mut self, e: &MemberExpr, _: &dyn Node) { + e.obj.visit_with(e as _, self); + + if e.computed { + e.prop.visit_with(e as _, self); + } + + match &e.obj { + ExprOrSuper::Expr(e) => { + match &**e { + Expr::Ident(i) => { + // TODO: Check syntax context (Check if marker is the global mark) + if i.sym == *"module" { + self.found_other = true; + } + + if i.sym == *"exports" { + self.found_other = true; + } + } + + _ => {} + } + } + _ => {} + } + + // + } + + fn visit_module_decl(&mut self, decl: &ModuleDecl, _: &dyn Node) { + match decl { + ModuleDecl::Import(_) + | ModuleDecl::ExportDecl(_) + | ModuleDecl::ExportNamed(_) + | ModuleDecl::ExportDefaultDecl(_) + | ModuleDecl::ExportDefaultExpr(_) + | ModuleDecl::ExportAll(_) => { + self.forced_es6 = true; + } + + ModuleDecl::TsImportEquals(_) => {} + ModuleDecl::TsExportAssignment(_) => {} + ModuleDecl::TsNamespaceExport(_) => {} + } + } +} diff --git a/bundler/src/bundler/mod.rs b/bundler/src/bundler/mod.rs new file mode 100644 index 00000000000..ff661e249df --- /dev/null +++ b/bundler/src/bundler/mod.rs @@ -0,0 +1,150 @@ +use self::scope::Scope; +use crate::{Load, ModuleId, Resolve}; +use anyhow::{Context, Error}; +use fxhash::FxHashMap; +use swc_atoms::JsWord; +use swc_common::{sync::Lrc, FileName, Globals, Mark, SourceMap, DUMMY_SP, GLOBALS}; +use swc_ecma_ast::Module; + +mod chunk; +mod export; +mod finalize; +mod helpers; +mod import; +mod load; +mod scope; +#[cfg(test)] +mod tests; +mod usage_analysis; + +#[derive(Debug)] +pub struct Config { + /// If it's true, [Bundler] searches for require calls. + pub require: bool, + /// List of modules which should be preserved. + pub external_modules: Vec, +} + +#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash)] +pub enum BundleKind { + /// User-provided entry + Named { name: String }, + /// Auto-generated entry (created by import expression) + Dynamic, + /// A lazy-loaded shared library + Lib { name: String }, +} + +/// Built bundle +#[derive(Debug)] +pub struct Bundle { + pub kind: BundleKind, + pub id: ModuleId, + /// Merged module. + /// + /// You **should** run fixer. + pub module: Module, +} + +pub struct Bundler<'a, L, R> +where + L: Load, + R: Resolve, +{ + config: Config, + + globals: &'a Globals, + cm: Lrc, + loader: L, + resolver: R, + + /// [Mark] used while tree shaking + used_mark: Mark, + /// [Mark] used while tree shaking + top_level_mark: Mark, + + scope: Scope, +} + +impl<'a, L, R> Bundler<'a, L, R> +where + L: Load, + R: Resolve, +{ + pub fn new( + globals: &'a Globals, + cm: Lrc, + loader: L, + resolver: R, + config: Config, + ) -> Self { + GLOBALS.set(&globals, || { + let used_mark = Mark::fresh(Mark::root()); + log::info!("Used mark: {:?}", DUMMY_SP.apply_mark(used_mark).ctxt()); + let top_level_mark = Mark::fresh(Mark::root()); + log::info!( + "top-level mark: {:?}", + DUMMY_SP.apply_mark(top_level_mark).ctxt() + ); + + Bundler { + cm, + loader, + resolver, + used_mark, + top_level_mark, + scope: Default::default(), + globals, + config, + } + }) + } + + /// + /// + /// + /// Note: This method will panic if entries references each other in + /// circular manner. However, it applies only to the provided `entries`, and + /// dependencies with circular reference is ok. + pub fn bundle(&self, entries: FxHashMap) -> Result, Error> { + let results = entries + .into_iter() + .map(|(name, path)| -> Result<_, Error> { + let res = self + .load_transformed(&path) + .context("load_transformed failed")?; + Ok((name, res)) + }) + .collect::>(); + + // We collect at here to handle dynamic imports + // TODO: Handle dynamic imports + + let local = { + let mut output = FxHashMap::default(); + + for res in results { + let (name, m) = res?; + let m = m.unwrap(); + + output.insert(name, m); + } + + output + }; + + let bundles = self.chunk(local)?; + + let bundles = self.finalize(bundles)?; + Ok(bundles) + } + + /// Sets `swc_common::GLOBALS` + #[inline] + fn run(&self, op: F) -> Ret + where + F: FnOnce() -> Ret, + { + GLOBALS.set(self.globals, op) + } +} diff --git a/bundler/src/bundler/scope.rs b/bundler/src/bundler/scope.rs new file mode 100644 index 00000000000..f0643a3f169 --- /dev/null +++ b/bundler/src/bundler/scope.rs @@ -0,0 +1,47 @@ +use super::load::TransformedModule; +use crate::{ + id::{ModuleId, ModuleIdGenerator}, + util::CloneMap, +}; +use swc_common::FileName; + +#[derive(Debug, Default)] +pub(super) struct Scope { + pub module_id_gen: ModuleIdGenerator, + + circular_modules: CloneMap, + loaded_modules: CloneMap, + + /// Cached after applying basical transformations. + transformed_modules: CloneMap, +} + +impl Scope { + pub fn is_circular(&self, id: ModuleId) -> bool { + self.circular_modules.get(&id).is_some() + } + + pub fn mark_as_circular(&self, id: ModuleId) { + self.circular_modules.insert(id, ()); + } + + pub fn mark_as_loaded(&self, id: ModuleId) { + self.loaded_modules.insert(id, ()); + } + + /// Stores module information. The information should contain only + /// information gotten from module itself. In other words, it should not + /// contains information from a dependency. + pub fn store_module(&self, info: TransformedModule) { + self.transformed_modules.insert(info.id, info); + } + + pub fn get_module_by_path(&self, file_name: &FileName) -> Option { + let (id, _) = self.module_id_gen.gen(file_name); + self.get_module(id) + } + + pub fn get_module(&self, id: ModuleId) -> Option { + Some(self.transformed_modules.get(&id)?.clone()) + } +} diff --git a/bundler/src/bundler/tests.rs b/bundler/src/bundler/tests.rs new file mode 100644 index 00000000000..f0ae1293bbe --- /dev/null +++ b/bundler/src/bundler/tests.rs @@ -0,0 +1,89 @@ +//! Utilities for testing. +use super::{Bundler, Config}; +use crate::{util::HygieneRemover, Load, Resolve}; +use anyhow::Error; +use std::path::PathBuf; +use swc_common::{sync::Lrc, FileName, SourceFile, SourceMap, GLOBALS}; +use swc_ecma_ast::*; +use swc_ecma_parser::{lexer::Lexer, Parser, StringInput}; +use swc_ecma_utils::drop_span; +use swc_ecma_visit::FoldWith; + +pub struct Tester<'a> { + pub cm: Lrc, + pub bundler: Bundler<'a, Loader, Resolver>, +} + +#[derive(Debug, Default)] +pub struct Loader; + +impl Load for Loader { + fn load(&self, _: &FileName) -> Result<(Lrc, Module), Error> { + unreachable!("swc_bundler: tester.load") + } +} + +#[derive(Debug, Default)] +pub struct Resolver; + +impl Resolve for Resolver { + fn resolve(&self, _: &FileName, _: &str) -> Result { + unreachable!("swc_bundler: tester.resolve") + } +} + +impl<'a> Tester<'a> { + pub fn parse(&self, s: &str) -> Module { + let fm = self + .cm + .new_source_file(FileName::Real(PathBuf::from("input.js")), s.into()); + + let lexer = Lexer::new( + Default::default(), + Default::default(), + StringInput::from(&*fm), + None, + ); + let mut parser = Parser::new_from(lexer); + parser.parse_module().unwrap() + } + + pub fn assert_eq(&self, m: &Module, expected: &str) { + let expected = self.parse(expected); + + let m = drop_span(m.clone().fold_with(&mut HygieneRemover)); + let expected = drop_span(expected); + + assert_eq!(m, expected) + } +} + +pub fn test_bundler(op: F) +where + F: FnOnce(&mut Tester), +{ + testing::run_test2(true, |cm, _| { + GLOBALS.with(|globals| { + let bundler = Bundler::new( + globals, + cm.clone(), + Default::default(), + Default::default(), + Config { + require: true, + external_modules: vec![], + }, + ); + + let mut t = Tester { + cm: cm.clone(), + bundler, + }; + + op(&mut t); + + Ok(()) + }) + }) + .expect("WTF?"); +} diff --git a/spack/src/bundler/usage_analysis.rs b/bundler/src/bundler/usage_analysis.rs similarity index 79% rename from spack/src/bundler/usage_analysis.rs rename to bundler/src/bundler/usage_analysis.rs index a90dee99147..8a6f63e97bf 100644 --- a/spack/src/bundler/usage_analysis.rs +++ b/bundler/src/bundler/usage_analysis.rs @@ -1,23 +1,22 @@ -use crate::{bundler::load_transformed::Specifier, Bundler}; -use std::{borrow::Cow, sync::Arc}; -use swc_common::SourceFile; +use super::load::Specifier; +use crate::{Bundler, Load, Resolve}; +use std::borrow::Cow; use swc_ecma_ast::*; use swc_ecma_transforms::optimization::simplify::dce; use swc_ecma_utils::ident::IdentLike; use swc_ecma_visit::FoldWith; -impl Bundler<'_> { +impl Bundler<'_, L, R> +where + L: Load, + R: Resolve, +{ /// If used_exports is [None], all exports are treated as exported. /// /// Note: Context of used_exports is ignored, as the specifiers comes from /// other module. - pub(super) fn drop_unused( - &self, - _fm: Arc, - node: Module, - used_exports: Option<&[Specifier]>, - ) -> Module { - self.swc.run(|| { + pub(super) fn drop_unused(&self, node: Module, used_exports: Option<&[Specifier]>) -> Module { + self.run(|| { let mut used = vec![]; if let Some(used_exports) = used_exports { diff --git a/bundler/src/debug/mod.rs b/bundler/src/debug/mod.rs new file mode 100644 index 00000000000..57fc2174c6e --- /dev/null +++ b/bundler/src/debug/mod.rs @@ -0,0 +1,41 @@ +#![allow(dead_code)] + +use std::io::{stdout, Write}; +use swc_common::{sync::Lrc, SourceMap}; +use swc_ecma_ast::{Ident, Module}; +use swc_ecma_codegen::{text_writer::JsWriter, Emitter}; +use swc_ecma_visit::{Fold, FoldWith}; + +pub(crate) fn print_hygiene(event: &str, cm: &Lrc, t: &Module) { + let module = t.clone().fold_with(&mut HygieneVisualizer); + + let stdout = stdout(); + let mut w = stdout.lock(); + + writeln!(w, "==================== @ {} ====================", event).unwrap(); + Emitter { + cfg: swc_ecma_codegen::Config { minify: false }, + cm: cm.clone(), + comments: None, + wr: Box::new(JsWriter::new(cm.clone(), "\n", &mut w, None)), + handlers: Box::new(Handlers), + } + .emit_module(&module) + .unwrap(); + writeln!(w, "==================== @ ====================").unwrap(); +} + +impl swc_ecma_codegen::Handlers for Handlers {} + +struct Handlers; + +struct HygieneVisualizer; + +impl Fold for HygieneVisualizer { + fn fold_ident(&mut self, node: Ident) -> Ident { + Ident { + sym: format!("{}{:?}", node.sym, node.span.ctxt()).into(), + ..node + } + } +} diff --git a/bundler/src/hash.rs b/bundler/src/hash.rs new file mode 100644 index 00000000000..e89cf30f33e --- /dev/null +++ b/bundler/src/hash.rs @@ -0,0 +1,117 @@ +use anyhow::{Context, Error}; +use crc::{crc64, crc64::Digest, Hasher64}; +use std::io; +use swc_common::{sync::Lrc, SourceMap, Span}; +use swc_ecma_ast::Module; +use swc_ecma_codegen::{text_writer::WriteJs, Emitter}; + +pub(crate) fn calc_hash(cm: Lrc, m: &Module) -> Result { + let digest = crc64::Digest::new(crc64::ECMA); + let mut buf = Hasher { digest }; + + { + let mut emitter = Emitter { + cfg: Default::default(), + cm, + comments: None, + wr: Box::new(&mut buf) as Box, + handlers: Box::new(Handlers), + }; + + emitter + .emit_module(&m) + .context("failed to emit module to calculate hash")?; + } + // + + let result = buf.digest.sum64(); + Ok(radix_fmt::radix(result, 36).to_string()) +} + +impl swc_ecma_codegen::Handlers for Handlers {} +struct Handlers; + +struct Hasher { + digest: Digest, +} + +impl Hasher { + fn w(&mut self, s: &str) { + self.digest.write(s.as_bytes()); + } +} + +impl WriteJs for &mut Hasher { + fn increase_indent(&mut self) -> io::Result<()> { + Ok(()) + } + + fn decrease_indent(&mut self) -> io::Result<()> { + Ok(()) + } + + fn write_semi(&mut self) -> io::Result<()> { + self.w(";"); + Ok(()) + } + + fn write_space(&mut self) -> io::Result<()> { + self.w(" "); + Ok(()) + } + + fn write_keyword(&mut self, _: Option, s: &'static str) -> io::Result<()> { + self.w(s); + Ok(()) + } + + fn write_operator(&mut self, s: &str) -> io::Result<()> { + self.w(s); + Ok(()) + } + + fn write_param(&mut self, s: &str) -> io::Result<()> { + self.w(s); + Ok(()) + } + + fn write_property(&mut self, s: &str) -> io::Result<()> { + self.w(s); + Ok(()) + } + + fn write_line(&mut self) -> io::Result<()> { + self.w("\n"); + Ok(()) + } + + fn write_lit(&mut self, _: Span, s: &str) -> io::Result<()> { + self.w(s); + Ok(()) + } + + fn write_comment(&mut self, _: Span, s: &str) -> io::Result<()> { + self.w(s); + Ok(()) + } + + fn write_str_lit(&mut self, _: Span, s: &str) -> io::Result<()> { + self.w(s); + Ok(()) + } + + fn write_str(&mut self, s: &str) -> io::Result<()> { + self.w(s); + Ok(()) + } + + fn write_symbol(&mut self, _: Span, s: &str) -> io::Result<()> { + self.w(s); + Ok(()) + } + + fn write_punct(&mut self, s: &'static str) -> io::Result<()> { + self.w(s); + Ok(()) + } +} diff --git a/spack/src/id.rs b/bundler/src/id.rs similarity index 77% rename from spack/src/id.rs rename to bundler/src/id.rs index a268d52ae6a..72331a2ddda 100644 --- a/spack/src/id.rs +++ b/bundler/src/id.rs @@ -1,14 +1,10 @@ -use dashmap::DashMap; +use fxhash::FxHashMap; use std::{ fmt, - path::PathBuf, - sync::{ - atomic::{AtomicU64, Ordering::SeqCst}, - Arc, - }, + sync::atomic::{AtomicU64, Ordering::SeqCst}, }; use swc_atoms::JsWord; -use swc_common::{Mark, SyntaxContext, DUMMY_SP}; +use swc_common::{sync::Lock, FileName, Mark, SyntaxContext, DUMMY_SP}; use swc_ecma_ast::Ident; use swc_ecma_utils::ident::IdentLike; @@ -24,18 +20,19 @@ impl fmt::Display for ModuleId { #[derive(Debug, Default)] pub(crate) struct ModuleIdGenerator { v: AtomicU64, - cache: DashMap, (ModuleId, Mark)>, + cache: Lock>, } impl ModuleIdGenerator { - pub fn gen(&self, path: &Arc) -> (ModuleId, Mark) { - if let Some(v) = self.cache.get(path) { - return *v.value(); + pub fn gen(&self, file_name: &FileName) -> (ModuleId, Mark) { + let mut w = self.cache.lock(); + if let Some(v) = w.get(file_name) { + return v.clone(); } let id = ModuleId(self.v.fetch_add(1, SeqCst)); let mark = Mark::fresh(Mark::root()); - self.cache.insert(path.clone(), (id, mark)); + w.insert(file_name.clone(), (id, mark)); (id, mark) } } @@ -62,11 +59,6 @@ impl Id { Ident::new(self.0, DUMMY_SP.with_ctxt(self.1)) } - pub fn append_mark(mut self, mark: Mark) -> Self { - self.1 = self.1.apply_mark(mark); - self - } - pub fn replace_mark(mut self, mark: Mark) -> Self { self.1 = SyntaxContext::empty().apply_mark(mark); self @@ -110,5 +102,3 @@ impl PartialEq for Id { self.0 == *other } } - -pub type QualifiedId = (ModuleId, Id); diff --git a/bundler/src/lib.rs b/bundler/src/lib.rs new file mode 100644 index 00000000000..a5de0feba12 --- /dev/null +++ b/bundler/src/lib.rs @@ -0,0 +1,14 @@ +pub use self::{ + bundler::{Bundle, BundleKind, Bundler, Config}, + id::ModuleId, + load::Load, + resolve::Resolve, +}; + +mod bundler; +mod debug; +mod hash; +mod id; +mod load; +mod resolve; +mod util; diff --git a/bundler/src/load.rs b/bundler/src/load.rs new file mode 100644 index 00000000000..21adc60c514 --- /dev/null +++ b/bundler/src/load.rs @@ -0,0 +1,27 @@ +use anyhow::Error; +use swc_common::{sync::Lrc, FileName, SourceFile}; +use swc_ecma_ast::Module; + +/// Responsible for providing files to the bundler. +/// +/// Note: Resolve and Load are separate trait because multiple module can depend +/// on a single module. Due to the possibility of 'common' module, bundler +/// should implement some caching. The bundler uses [FileName] as a key of the +/// cache. +/// +/// This trait is designed to allow passing pre-parsed module. +pub trait Load: swc_common::sync::Send + swc_common::sync::Sync { + fn load(&self, file: &FileName) -> Result<(Lrc, Module), Error>; +} + +impl Load for Box { + fn load(&self, file: &FileName) -> Result<(Lrc, Module), Error> { + (**self).load(file) + } +} + +impl<'a, T: ?Sized + Load> Load for &'a T { + fn load(&self, file: &FileName) -> Result<(Lrc, Module), Error> { + (**self).load(file) + } +} diff --git a/bundler/src/resolve.rs b/bundler/src/resolve.rs new file mode 100644 index 00000000000..317242528fe --- /dev/null +++ b/bundler/src/resolve.rs @@ -0,0 +1,18 @@ +use anyhow::Error; +use swc_common::FileName; + +pub trait Resolve: swc_common::sync::Send + swc_common::sync::Sync { + fn resolve(&self, base: &FileName, module_specifier: &str) -> Result; +} + +impl Resolve for Box { + fn resolve(&self, base: &FileName, module_specifier: &str) -> Result { + (**self).resolve(base, module_specifier) + } +} + +impl<'a, T: ?Sized + Resolve> Resolve for &'a T { + fn resolve(&self, base: &FileName, module_specifier: &str) -> Result { + (**self).resolve(base, module_specifier) + } +} diff --git a/bundler/src/util.rs b/bundler/src/util.rs new file mode 100644 index 00000000000..cf90f0cd668 --- /dev/null +++ b/bundler/src/util.rs @@ -0,0 +1,96 @@ +use fxhash::FxBuildHasher; +use std::hash::Hash; +use swc_common::{Span, SyntaxContext}; +use swc_ecma_visit::Fold; + +#[derive(Debug)] +pub(crate) struct CloneMap +where + K: Eq + Hash, + V: Clone, +{ + #[cfg(feature = "concurrent")] + inner: dashmap::DashMap, + #[cfg(not(feature = "concurrent"))] + inner: std::cell::RefCell>, +} + +impl Default for CloneMap +where + K: Eq + Hash, + V: Clone, +{ + fn default() -> Self { + Self { + inner: Default::default(), + } + } +} + +impl CloneMap +where + K: Eq + Hash, + V: Clone, +{ + #[cfg(feature = "concurrent")] + pub fn get(&self, k: &K) -> Option { + if let Some(v) = self.inner.get(k) { + Some(v.value().clone()) + } else { + None + } + } + + #[cfg(not(feature = "concurrent"))] + pub fn get(&self, k: &K) -> Option { + if let Some(v) = self.inner.borrow().get(k) { + Some(v.clone()) + } else { + None + } + } + + #[cfg(feature = "concurrent")] + pub fn insert(&self, k: K, v: V) { + self.inner.insert(k, v); + } + + #[cfg(not(feature = "concurrent"))] + pub fn insert(&self, k: K, v: V) { + self.inner.borrow_mut().insert(k, v); + } +} + +pub(crate) struct HygieneRemover; + +impl Fold for HygieneRemover { + fn fold_span(&mut self, s: Span) -> Span { + s.with_ctxt(SyntaxContext::empty()) + } +} + +#[cfg(feature = "rayon")] +pub(crate) use rayon::join; + +#[cfg(not(feature = "rayon"))] +pub(crate) fn join(oper_a: A, oper_b: B) -> (RA, RB) +where + A: FnOnce() -> RA, + B: FnOnce() -> RB, +{ + (oper_a(), oper_b()) +} + +#[cfg(feature = "rayon")] +pub(crate) use rayon::iter::IntoParallelIterator; + +/// Fake trait +#[cfg(not(feature = "rayon"))] +pub(crate) trait IntoParallelIterator: Sized + IntoIterator { + fn into_par_iter(self) -> ::IntoIter { + self.into_iter() + } +} + +#[cfg(not(feature = "rayon"))] +impl IntoParallelIterator for T where T: IntoIterator {} diff --git a/common/Cargo.toml b/common/Cargo.toml index a6348f89562..dba71f846fd 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "swc_common" -version = "0.9.0" +version = "0.9.1" authors = ["강동윤 "] license = "Apache-2.0/MIT" repository = "https://github.com/swc-project/swc.git" diff --git a/common/src/sync.rs b/common/src/sync.rs index b54d3898570..ac56cf8f977 100644 --- a/common/src/sync.rs +++ b/common/src/sync.rs @@ -103,28 +103,18 @@ impl Lock { // self.0.get_mut() // } - // #[cfg(parallel_compiler)] + // #[cfg(feature = "concurrent")] // #[inline(always)] // pub fn try_lock(&self) -> Option> { // self.0.try_lock() // } // - // #[cfg(not(parallel_compiler))] + // #[cfg(not(feature = "concurrent"))] // #[inline(always)] // pub fn try_lock(&self) -> Option> { // self.0.try_borrow_mut().ok() // } - #[cfg(parallel_compiler)] - #[inline(always)] - pub fn lock(&self) -> LockGuard<'_, T> { - if ERROR_CHECKING { - self.0.try_lock().expect("lock was already held") - } else { - self.0.lock() - } - } - #[cfg(feature = "concurrent")] #[inline(always)] pub fn lock(&self) -> LockGuard<'_, T> { @@ -253,7 +243,7 @@ impl Ord for LockCell { } } -#[derive(Debug)] +#[derive(Debug, Default)] pub struct RwLock(InnerRwLock); impl RwLock { @@ -278,6 +268,50 @@ impl RwLock { pub fn borrow(&self) -> ReadGuard<'_, T> { self.read() } + + #[inline(always)] + pub fn get_mut(&mut self) -> &mut T { + self.0.get_mut() + } + + #[inline(always)] + pub fn with_read_lock R, R>(&self, f: F) -> R { + f(&*self.read()) + } + + #[cfg(not(feature = "concurrent"))] + #[inline(always)] + pub fn try_write(&self) -> Result, ()> { + self.0.try_borrow_mut().map_err(|_| ()) + } + + #[cfg(feature = "concurrent")] + #[inline(always)] + pub fn try_write(&self) -> Result, ()> { + self.0.try_write().ok_or(()) + } + + #[cfg(not(feature = "concurrent"))] + #[inline(always)] + pub fn write(&self) -> WriteGuard<'_, T> { + self.0.borrow_mut() + } + + #[cfg(feature = "concurrent")] + #[inline(always)] + pub fn write(&self) -> WriteGuard<'_, T> { + self.0.write() + } + + #[inline(always)] + pub fn with_write_lock R, R>(&self, f: F) -> R { + f(&mut *self.write()) + } + + #[inline(always)] + pub fn borrow_mut(&self) -> WriteGuard<'_, T> { + self.write() + } } // FIXME: Probably a bad idea diff --git a/ecmascript/parser/Cargo.toml b/ecmascript/parser/Cargo.toml index da0402b72e8..08be8168bb8 100644 --- a/ecmascript/parser/Cargo.toml +++ b/ecmascript/parser/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "swc_ecma_parser" -version = "0.33.2" +version = "0.33.3" authors = ["강동윤 "] license = "Apache-2.0/MIT" repository = "https://github.com/swc-project/swc.git" diff --git a/ecmascript/parser/examples/lexer.rs b/ecmascript/parser/examples/lexer.rs index 1c4323b6b83..8c1f90796d6 100644 --- a/ecmascript/parser/examples/lexer.rs +++ b/ecmascript/parser/examples/lexer.rs @@ -7,40 +7,38 @@ use swc_common::{ use swc_ecma_parser::{lexer::Lexer, Capturing, Parser, StringInput, Syntax}; fn main() { - swc_common::GLOBALS.set(&swc_common::Globals::new(), || { - let cm: Lrc = Default::default(); - let handler = Handler::with_tty_emitter(ColorConfig::Auto, true, false, Some(cm.clone())); + let cm: Lrc = Default::default(); + let handler = Handler::with_tty_emitter(ColorConfig::Auto, true, false, Some(cm.clone())); - // Real usage - // let fm = cm - // .load_file(Path::new("test.js")) - // .expect("failed to load test.js"); + // Real usage + // let fm = cm + // .load_file(Path::new("test.js")) + // .expect("failed to load test.js"); - let fm = cm.new_source_file( - FileName::Custom("test.js".into()), - "function foo() {}".into(), - ); + let fm = cm.new_source_file( + FileName::Custom("test.js".into()), + "function foo() {}".into(), + ); - let lexer = Lexer::new( - Syntax::Es(Default::default()), - Default::default(), - StringInput::from(&*fm), - None, - ); + let lexer = Lexer::new( + Syntax::Es(Default::default()), + Default::default(), + StringInput::from(&*fm), + None, + ); - let capturing = Capturing::new(lexer); + let capturing = Capturing::new(lexer); - let mut parser = Parser::new_from(capturing); + let mut parser = Parser::new_from(capturing); - for e in parser.take_errors() { - e.into_diagnostic(&handler).emit(); - } + for e in parser.take_errors() { + e.into_diagnostic(&handler).emit(); + } - let _module = parser - .parse_module() - .map_err(|e| e.into_diagnostic(&handler).emit()) - .expect("Failed to parse module."); + let _module = parser + .parse_module() + .map_err(|e| e.into_diagnostic(&handler).emit()) + .expect("Failed to parse module."); - println!("Tokens: {:?}", parser.input().take()); - }); + println!("Tokens: {:?}", parser.input().take()); } diff --git a/ecmascript/parser/examples/typescript.rs b/ecmascript/parser/examples/typescript.rs index cd83de58dd6..f98aa77237d 100644 --- a/ecmascript/parser/examples/typescript.rs +++ b/ecmascript/parser/examples/typescript.rs @@ -7,40 +7,38 @@ use swc_common::{ use swc_ecma_parser::{lexer::Lexer, Capturing, Parser, StringInput, Syntax}; fn main() { - swc_common::GLOBALS.set(&swc_common::Globals::new(), || { - let cm: Lrc = Default::default(); - let handler = Handler::with_tty_emitter(ColorConfig::Auto, true, false, Some(cm.clone())); + let cm: Lrc = Default::default(); + let handler = Handler::with_tty_emitter(ColorConfig::Auto, true, false, Some(cm.clone())); - // Real usage - // let fm = cm - // .load_file(Path::new("test.js")) - // .expect("failed to load test.js"); + // Real usage + // let fm = cm + // .load_file(Path::new("test.js")) + // .expect("failed to load test.js"); - let fm = cm.new_source_file( - FileName::Custom("test.js".into()), - "interface Foo {}".into(), - ); + let fm = cm.new_source_file( + FileName::Custom("test.js".into()), + "interface Foo {}".into(), + ); - let lexer = Lexer::new( - Syntax::Typescript(Default::default()), - Default::default(), - StringInput::from(&*fm), - None, - ); + let lexer = Lexer::new( + Syntax::Typescript(Default::default()), + Default::default(), + StringInput::from(&*fm), + None, + ); - let capturing = Capturing::new(lexer); + let capturing = Capturing::new(lexer); - let mut parser = Parser::new_from(capturing); + let mut parser = Parser::new_from(capturing); - for e in parser.take_errors() { - e.into_diagnostic(&handler).emit(); - } + for e in parser.take_errors() { + e.into_diagnostic(&handler).emit(); + } - let _module = parser - .parse_typescript_module() - .map_err(|e| e.into_diagnostic(&handler).emit()) - .expect("Failed to parse module."); + let _module = parser + .parse_typescript_module() + .map_err(|e| e.into_diagnostic(&handler).emit()) + .expect("Failed to parse module."); - println!("Tokens: {:?}", parser.input().take()); - }); + println!("Tokens: {:?}", parser.input().take()); } diff --git a/ecmascript/parser/src/lib.rs b/ecmascript/parser/src/lib.rs index bf38810c8e9..1c62ec0eba2 100644 --- a/ecmascript/parser/src/lib.rs +++ b/ecmascript/parser/src/lib.rs @@ -40,7 +40,7 @@ //! #[macro_use] //! extern crate swc_common; //! extern crate swc_ecma_parser; -//! use std::sync::Arc; +//! use swc_common::sync::Lrc; //! use swc_common::{ //! errors::{ColorConfig, Handler}, //! FileName, FilePathMapping, SourceMap, @@ -48,43 +48,41 @@ //! use swc_ecma_parser::{lexer::Lexer, Parser, StringInput, Syntax}; //! //! fn main() { -//! swc_common::GLOBALS.set(&swc_common::Globals::new(), || { -//! let cm: Arc = Default::default(); -//! let handler = -//! Handler::with_tty_emitter(ColorConfig::Auto, true, false, -//! Some(cm.clone())); +//! let cm: Lrc = Default::default(); +//! let handler = +//! Handler::with_tty_emitter(ColorConfig::Auto, true, false, +//! Some(cm.clone())); //! -//! // Real usage -//! // let fm = cm -//! // .load_file(Path::new("test.js")) -//! // .expect("failed to load test.js"); -//! let fm = cm.new_source_file( -//! FileName::Custom("test.js".into()), -//! "function foo() {}".into(), -//! ); -//! let lexer = Lexer::new( -//! // We want to parse ecmascript -//! Syntax::Es(Default::default()), -//! // JscTarget defaults to es5 -//! Default::default(), -//! StringInput::from(&*fm), -//! None, -//! ); +//! // Real usage +//! // let fm = cm +//! // .load_file(Path::new("test.js")) +//! // .expect("failed to load test.js"); +//! let fm = cm.new_source_file( +//! FileName::Custom("test.js".into()), +//! "function foo() {}".into(), +//! ); +//! let lexer = Lexer::new( +//! // We want to parse ecmascript +//! Syntax::Es(Default::default()), +//! // JscTarget defaults to es5 +//! Default::default(), +//! StringInput::from(&*fm), +//! None, +//! ); //! -//! let mut parser = Parser::new_from(lexer); +//! let mut parser = Parser::new_from(lexer); //! -//! for e in parser.take_errors() { -//! e.into_diagnostic(&handler).emit(); -//! } +//! for e in parser.take_errors() { +//! e.into_diagnostic(&handler).emit(); +//! } //! -//! let _module = parser -//! .parse_module() -//! .map_err(|mut e| { -//! // Unrecoverable fatal error occurred -//! e.into_diagnostic(&handler).emit() -//! }) -//! .expect("failed to parser module"); -//! }); +//! let _module = parser +//! .parse_module() +//! .map_err(|mut e| { +//! // Unrecoverable fatal error occurred +//! e.into_diagnostic(&handler).emit() +//! }) +//! .expect("failed to parser module"); //! } //! ``` //! diff --git a/ecmascript/parser/src/parser/macros.rs b/ecmascript/parser/src/parser/macros.rs index 067a31fce79..cf01e03f3e8 100644 --- a/ecmascript/parser/src/parser/macros.rs +++ b/ecmascript/parser/src/parser/macros.rs @@ -115,7 +115,7 @@ macro_rules! assert_and_bump { /// if token has data like string. macro_rules! eat { ($p:expr, ';') => {{ - log::trace!("eat(';'): cur={:?}", cur!($p, true)); + log::trace!("eat(';'): cur={:?}", cur!($p, false)); $p.input.eat(&Token::Semi) || eof!($p) || is!($p, '}') diff --git a/ecmascript/parser/tests/typescript/eof-issue/input.tsx b/ecmascript/parser/tests/typescript/eof-issue/input.tsx new file mode 100644 index 00000000000..cd3fb5a19cc --- /dev/null +++ b/ecmascript/parser/tests/typescript/eof-issue/input.tsx @@ -0,0 +1 @@ +export const Divider =
\ No newline at end of file diff --git a/ecmascript/parser/tests/typescript/eof-issue/input.tsx.json b/ecmascript/parser/tests/typescript/eof-issue/input.tsx.json new file mode 100644 index 00000000000..20413cd65f3 --- /dev/null +++ b/ecmascript/parser/tests/typescript/eof-issue/input.tsx.json @@ -0,0 +1,83 @@ +{ + "type": "Module", + "span": { + "start": 0, + "end": 29, + "ctxt": 0 + }, + "body": [ + { + "type": "ExportDeclaration", + "span": { + "start": 0, + "end": 29, + "ctxt": 0 + }, + "declaration": { + "type": "VariableDeclaration", + "span": { + "start": 7, + "end": 29, + "ctxt": 0 + }, + "kind": "const", + "declare": false, + "declarations": [ + { + "type": "VariableDeclarator", + "span": { + "start": 13, + "end": 29, + "ctxt": 0 + }, + "id": { + "type": "Identifier", + "span": { + "start": 13, + "end": 20, + "ctxt": 0 + }, + "value": "Divider", + "typeAnnotation": null, + "optional": false + }, + "init": { + "type": "JSXElement", + "span": { + "start": 23, + "end": 29, + "ctxt": 0 + }, + "opening": { + "type": "JSXOpeningElement", + "name": { + "type": "Identifier", + "span": { + "start": 24, + "end": 27, + "ctxt": 0 + }, + "value": "div", + "typeAnnotation": null, + "optional": false + }, + "span": { + "start": 23, + "end": 29, + "ctxt": 0 + }, + "attributes": [], + "selfClosing": true, + "typeArguments": null + }, + "children": [], + "closing": null + }, + "definite": false + } + ] + } + } + ], + "interpreter": null +} diff --git a/ecmascript/preset_env/Cargo.toml b/ecmascript/preset_env/Cargo.toml index c25622ccc3b..a6d1aa412fb 100644 --- a/ecmascript/preset_env/Cargo.toml +++ b/ecmascript/preset_env/Cargo.toml @@ -21,7 +21,7 @@ semver = { version = "0.9.0", features = ["serde"] } once_cell = "1.2.0" st-map = "0.1.2" fxhash = "0.2.1" -dashmap = "=3.5.1" +dashmap = "3" [dev-dependencies] swc_ecma_codegen = { path = "../codegen" } diff --git a/ecmascript/transforms/Cargo.toml b/ecmascript/transforms/Cargo.toml index bd91ecee8b4..67ec94b4fd6 100644 --- a/ecmascript/transforms/Cargo.toml +++ b/ecmascript/transforms/Cargo.toml @@ -20,7 +20,7 @@ swc_ecma_ast = { version = "0.28.0", path ="../ast" } swc_ecma_utils = { version = "0.17.0", path ="../utils" } swc_ecma_parser = { version = "0.33.0", path ="../parser" } swc_ecma_visit = { version = "0.13.0", path ="../visit" } -dashmap = { version = "=3.5.1", optional = true } +dashmap = { version = "3", optional = true } either = "1.5" fxhash = "0.2" indexmap = "1" diff --git a/ecmascript/transforms/src/optimization/simplify/dce/mod.rs b/ecmascript/transforms/src/optimization/simplify/dce/mod.rs index 336551bb8c7..deff25503e2 100644 --- a/ecmascript/transforms/src/optimization/simplify/dce/mod.rs +++ b/ecmascript/transforms/src/optimization/simplify/dce/mod.rs @@ -153,11 +153,12 @@ impl Fold for Dce<'_> { return node; } - let stmts = node.stmts.fold_with(self); + let mut stmts = node.stmts.fold_with(self); let mut span = node.span; - if stmts.iter().any(|stmt| self.is_marked(stmt.span())) { + if self.marking_phase || stmts.iter().any(|stmt| self.is_marked(stmt.span())) { span = span.apply_mark(self.config.used_mark); + stmts = self.fold_in_marking_phase(stmts); } BlockStmt { span, stmts } @@ -170,6 +171,7 @@ impl Fold for Dce<'_> { if self.marking_phase || self.included.contains(&node.ident.to_id()) { node.class.span = node.class.span.apply_mark(self.config.used_mark); + node.class.super_class = self.fold_in_marking_phase(node.class.super_class); } node.fold_children_with(self) @@ -307,7 +309,9 @@ impl Fold for Dce<'_> { if self.marking_phase || self.included.contains(&f.ident.to_id()) { f.function.span = f.function.span.apply_mark(self.config.used_mark); + f.function.params = self.fold_in_marking_phase(f.function.params); f.function.body = self.fold_in_marking_phase(f.function.body); + return f; } f.fold_children_with(self) @@ -445,6 +449,7 @@ impl Fold for Dce<'_> { } // Drop unused imports. + log::debug!("Removing unused import specifiers"); import.specifiers.retain(|s| self.should_include(s)); if !import.specifiers.is_empty() { @@ -508,13 +513,9 @@ impl Fold for Dce<'_> { if self.is_marked(node.span) { return node; } + node.span = node.span.apply_mark(self.config.used_mark); - - let mut node = node.fold_children_with(self); - - if self.is_marked(node.arg.span()) { - node.arg = self.fold_in_marking_phase(node.arg) - } + node.arg = self.fold_in_marking_phase(node.arg); node } @@ -611,7 +612,6 @@ impl Fold for Dce<'_> { return var; } - log::trace!("VarDecl"); var = var.fold_children_with(self); var.decls = var.decls.move_flat_map(|decl| { @@ -676,6 +676,10 @@ impl Dce<'_> { T: StmtLike + FoldWith + Spanned + std::fmt::Debug, T: for<'any> VisitWith> + VisitWith, { + if self.marking_phase { + return items.move_map(|item| self.fold_in_marking_phase(item)); + } + let old = self.changed; let mut preserved = FxHashSet::default(); diff --git a/ecmascript/transforms/tests/optimization_simplify_dce.rs b/ecmascript/transforms/tests/optimization_simplify_dce.rs index f2d8acb605e..9da02cd92e6 100644 --- a/ecmascript/transforms/tests/optimization_simplify_dce.rs +++ b/ecmascript/transforms/tests/optimization_simplify_dce.rs @@ -424,3 +424,34 @@ var load = function(){} var { progress } = load(); console.log(progress);" ); + +noop!( + spack_issue_008, + "class B { + } + class A extends B { + } + console.log('foo'); + new A();" +); + +noop!( + spack_issue_009, + " +class A { + +} +function a() { + return new A(); +} +console.log(a, a()); +" +); + +noop!( + spack_issue_010, + " +class A {} +console.log(new A()); +" +); diff --git a/native/Cargo.toml b/native/Cargo.toml index b721b6b69f6..64899683488 100644 --- a/native/Cargo.toml +++ b/native/Cargo.toml @@ -17,6 +17,7 @@ neon-build = "0.4.0" [dependencies] swc = { path = "../" } +swc_bundler = { path = "../bundler" } swc_common = { path = "../common", features = ["tty-emitter", "sourcemap"] } swc_ecma_ast = { path = "../ecmascript/ast" } swc_ecma_parser = { path = "../ecmascript/parser" } diff --git a/native/src/bundle.rs b/native/src/bundle.rs index 3defec5e198..d68d82abef2 100644 --- a/native/src/bundle.rs +++ b/native/src/bundle.rs @@ -3,16 +3,13 @@ use anyhow::{bail, Error}; use fxhash::FxHashMap; use neon::prelude::*; use serde::Deserialize; -use spack::{ - load::Load, - resolve::{NodeResolver, Resolve}, - BundleKind, -}; +use spack::resolvers::NodeResolver; use std::{ panic::{catch_unwind, AssertUnwindSafe}, sync::Arc, }; use swc::{config::SourceMapsConfig, Compiler, TransformOutput}; +use swc_bundler::{BundleKind, Bundler, Load, Resolve}; struct ConfigItem { loader: Box, @@ -41,23 +38,55 @@ impl Task for BundleTask { fn perform(&self) -> Result { let res = catch_unwind(AssertUnwindSafe(|| { - let bundler = spack::Bundler::new( - self.swc.clone(), - self.config - .static_items - .config - .options - .as_ref() - .map(|options| options.clone()) - .unwrap_or_else(|| { - serde_json::from_value(serde_json::Value::Object(Default::default())) - .unwrap() - }), - &self.config.resolver, + let bundler = Bundler::new( + self.swc.globals(), + self.swc.cm.clone(), &self.config.loader, + &self.config.resolver, + swc_bundler::Config { + require: true, + external_modules: vec![ + "assert", + "buffer", + "child_process", + "console", + "cluster", + "crypto", + "dgram", + "dns", + "events", + "fs", + "http", + "http2", + "https", + "net", + "os", + "path", + "perf_hooks", + "process", + "querystring", + "readline", + "repl", + "stream", + "string_decoder", + "timers", + "tls", + "tty", + "url", + "util", + "v8", + "vm", + "wasi", + "worker", + "zlib", + ] + .into_iter() + .map(From::from) + .collect(), + }, ); - let result = bundler.bundle(&self.config.static_items.config)?; + let result = bundler.bundle(self.config.static_items.config.entry.clone().into())?; let result = result .into_iter() @@ -134,7 +163,7 @@ pub(crate) fn bundle(mut cx: MethodContext) -> JsResult { let opt = cx.argument::(0)?; let callback = cx.argument::(1)?; - let static_items = neon_serde::from_value(&mut cx, opt.upcast())?; + let static_items: StaticConfigItem = neon_serde::from_value(&mut cx, opt.upcast())?; let loader = opt .get(&mut cx, "loader")? @@ -150,7 +179,15 @@ pub(crate) fn bundle(mut cx: MethodContext) -> JsResult { .unwrap_or_else(|_| { Box::new(spack::loaders::swc::SwcLoader::new( c.clone(), - Default::default(), + static_items + .config + .options + .as_ref() + .cloned() + .unwrap_or_else(|| { + serde_json::from_value(serde_json::Value::Object(Default::default())) + .unwrap() + }), )) }); @@ -158,7 +195,7 @@ pub(crate) fn bundle(mut cx: MethodContext) -> JsResult { swc: c.clone(), config: ConfigItem { loader, - resolver: Box::new(NodeResolver) as Box<_>, + resolver: Box::new(NodeResolver::new()) as Box<_>, static_items, }, } diff --git a/node-swc/__tests__/spack/config_test.js b/node-swc/__tests__/spack/config_test.js index 8ed2b1ef2d2..4caf9ac3c6b 100644 --- a/node-swc/__tests__/spack/config_test.js +++ b/node-swc/__tests__/spack/config_test.js @@ -16,8 +16,8 @@ it('should respect .swcrc', async () => { const result = await swc.bundle(path.join(__dirname, '../../tests/spack/config-swcrc/spack.config.js')); expect(result.a).toBeTruthy(); - expect(result.a.code).toContain(`require("./common-`); + expect(result.a.code).toContain(`require("./common`); expect(result.b).toBeTruthy(); - expect(result.b.code).toContain(`require("./common-`); + expect(result.b.code).toContain(`require("./common`); }); \ No newline at end of file diff --git a/package.json b/package.json index 84eae446463..6be1fd5f8d6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@swc/core", - "version": "1.2.18", + "version": "1.2.19", "description": "Super-fast alternative for babel", "main": "./index.js", "author": "강동윤 ", diff --git a/spack/Cargo.toml b/spack/Cargo.toml index ae184040ab2..ff7619cfc94 100644 --- a/spack/Cargo.toml +++ b/spack/Cargo.toml @@ -12,6 +12,7 @@ edition = "2018" [dependencies] swc_atoms = { path = "../atoms" } +swc_bundler = { path = "../bundler", features = ["concurrent"] } swc_common = { path = "../common" } swc_ecma_ast = { path = "../ecmascript/ast" } swc_ecma_codegen = { path = "../ecmascript/codegen" } @@ -25,18 +26,13 @@ regex = "1" once_cell = "1" serde = { version = "1", features = ["derive"] } anyhow = "1" -crc = "1.8" -dashmap = "=3.5.1" -radix_fmt = "1" -rayon = "1" +dashmap = "3" log = "0.4.8" node-resolve = "2.2.0" -petgraph = "0.5" fxhash = "0.2.1" is-macro = "0.1.8" neon = { version = "0.4.0", features = ["event-handler-api"] } neon-sys = "0.4.0" -relative-path = "1.2" [dev-dependencies] pretty_assertions = "0.6.1" diff --git a/spack/src/bundler/config.rs b/spack/src/bundler/config.rs deleted file mode 100644 index bd5e68c3a10..00000000000 --- a/spack/src/bundler/config.rs +++ /dev/null @@ -1,57 +0,0 @@ -use crate::Bundler; -use anyhow::{Error, Result}; -use dashmap::DashMap; -use std::{collections::HashMap, env}; -use swc_atoms::JsWord; -use swc_common::FileName; -use swc_ecma_ast::Expr; -use swc_ecma_parser::{lexer::Lexer, Parser, StringInput}; - -#[derive(Debug, Default)] -pub(super) struct Cache { - exprs: DashMap, -} - -impl Bundler<'_> { - #[inline] - fn get_or_parse_expr(&self, key: &str, s: String) -> Result { - if let Some(v) = self.cache.exprs.get(key) { - return Ok((*v).clone()); - } - - let cm = self.swc.cm.clone(); - let fm = cm.new_source_file(FileName::Anon, s); - - let lexer = Lexer::new( - Default::default(), - Default::default(), - StringInput::from(&*fm), - None, - ); - - let mut parser = Parser::new_from(lexer); - - let expr = parser.parse_expr().map_err(|err| { - Error::msg(format!( - "config: failed parse `{}` as expression: (key = `{}`): {:?}", - fm.src, key, err - )) - })?; - - self.cache.exprs.insert(key.to_string(), *expr.clone()); - - Ok(*expr) - } - - /// Has`NODE_ENV` - pub(super) fn envs(&self) -> Result> { - let mut envs = HashMap::with_capacity(1); - - let node_env = env::var("NODE_ENV").unwrap_or_else(|_| "development".to_string()); - - let v = self.get_or_parse_expr("NODE_ENV", node_env)?; - envs.insert("NODE_ENV".into(), v); - - Ok(envs) - } -} diff --git a/spack/src/bundler/load_transformed/mod.rs b/spack/src/bundler/load_transformed/mod.rs deleted file mode 100644 index d78b7d795b6..00000000000 --- a/spack/src/bundler/load_transformed/mod.rs +++ /dev/null @@ -1,466 +0,0 @@ -use super::Bundler; -use crate::{ - bundler::{ - export::{Exports, RawExports}, - helpers::Helpers, - import::RawImports, - }, - debug::assert_clean, - Id, ModuleId, -}; -use anyhow::{Context, Error}; -use is_macro::Is; -use rayon::prelude::*; -use std::{ - path::{Path, PathBuf}, - sync::Arc, -}; -use swc_atoms::js_word; -use swc_common::{FileName, Mark, SourceFile, DUMMY_SP}; -use swc_ecma_ast::{ - Expr, ExprOrSuper, ImportDecl, ImportSpecifier, Invalid, MemberExpr, Module, ModuleDecl, - Program, Str, -}; -use swc_ecma_transforms::{ - optimization::{simplify::dead_branch_remover, InlineGlobals}, - resolver::resolver_with_mark, -}; -use swc_ecma_visit::{FoldWith, Node, Visit, VisitWith}; - -#[cfg(test)] -mod tests; - -/// Module after applying transformations. -#[derive(Debug, Clone)] -pub(super) struct TransformedModule { - pub id: ModuleId, - pub fm: Arc, - pub module: Arc, - pub imports: Arc, - pub exports: Arc, - - /// If false, the module will be wrapped with helper function just like - /// wwbpack. - pub is_es6: bool, - - /// Used helpers - pub helpers: Arc, - - mark: Mark, -} - -impl TransformedModule { - /// Marks applied to bindings - pub fn mark(&self) -> Mark { - self.mark - } -} - -#[derive(Debug, Default)] -pub(super) struct Imports { - /// If imported ids are empty, it is a side-effect import. - pub specifiers: Vec<(Source, Vec)>, -} - -/// Clone is relatively cheap -#[derive(Debug, Clone, Is)] -pub(super) enum Specifier { - Specific { local: Id, alias: Option }, - Namespace { local: Id }, -} - -impl Specifier { - pub fn local(&self) -> &Id { - match self { - Specifier::Specific { local, .. } => local, - Specifier::Namespace { local, .. } => local, - } - } -} - -#[derive(Debug, Clone, PartialEq, Eq, Hash)] -pub(super) struct Source { - pub is_loaded_synchronously: bool, - pub is_unconditional: bool, - - pub module_id: ModuleId, - // Clone is relatively cheap, thanks to string_cache. - pub src: Str, -} - -impl Bundler<'_> { - /// Phase 1 (discovery) - /// - /// We apply transforms at this phase to make cache efficient. - /// As we cache in this phase, changing dependency does not affect cache. - pub(super) fn load_transformed(&self, path: Arc) -> Result { - Ok(self.load_transformed_inner(path)?.1) - } - - fn load_transformed_inner( - &self, - path: Arc, - ) -> Result<(Arc, TransformedModule), Error> { - log::trace!("load_transformed: ({})", path.display()); - - self.swc.run(|| { - if let Some(cached) = self.scope.get_module_by_path(&path) { - return Ok((path, cached.clone())); - } - - let (_, fm, module) = self.load(&path).context("Bundler.load failed")?; - let v = self - .transform_module(&path, fm.clone(), module) - .context("failed to transform module")?; - - self.scope.store_module(path.clone(), v.clone()); - - //{ - // let code = self - // .swc - // .print( - // &(*v.module).clone().fold_with(&mut HygieneVisualizer), - // fm, - // false, - // false, - // ) - // .unwrap() - // .code; - // - // println!( - // "Fully loaded:\n{}\nImports: {:?}\nExports: {:?}\n", - // code, v.imports, v.exports - // ); - //} - - Ok((path, v)) - }) - } - - fn load(&self, path: &Arc) -> Result<(ModuleId, Arc, Module), Error> { - self.swc.run(|| { - let (module_id, _) = self.scope.module_id_gen.gen(path); - - let path = Arc::new(path); - - let (fm, module) = self - .loader - .load(&path) - .with_context(|| format!("Loader.load({}) failed", path.display()))?; - assert_clean(&module); - - Ok((module_id, fm, module)) - }) - } - - fn transform_module( - &self, - path: &Arc, - fm: Arc, - mut module: Module, - ) -> Result { - self.swc.run(|| { - log::trace!("transform_module({})", fm.name); - module = module.fold_with(&mut resolver_with_mark(self.top_level_mark)); - module = module.fold_with(&mut InlineGlobals { - envs: self.envs()?, - globals: Default::default(), - }); - module = module.fold_with(&mut dead_branch_remover()); - - let (id, mark) = self.scope.module_id_gen.gen(path); - - // { - // let code = self - // .swc - // .print( - // &module.clone().fold_with(&mut HygieneVisualizer), - // SourceMapsConfig::Bool(false), - // None, - // false, - // ) - // .unwrap() - // .code; - // - // println!("Resolved:\n{}\n\n", code); - // } - - let imports = self.extract_import_info(path, &mut module, mark); - - // { - // let code = self - // .swc - // .print( - // &module.clone().fold_with(&mut HygieneVisualizer), - // SourceMapsConfig::Bool(false), - // None, - // false, - // ) - // .unwrap() - // .code; - // - // println!("After imports:\n{}\n", code,); - // } - - let exports = self.extract_export_info(&module); - - // TODO: Exclude resolver (for performance) - let (module, (imports, exports)) = rayon::join( - || -> Result<_, Error> { - self.swc.run(|| { - // Process module - let config = self - .swc - .config_for_file(&self.swc_options, &fm.name) - .context("failed to parse .swcrc")?; - - let program = self.swc.transform( - Program::Module(module), - config.external_helpers, - config.pass, - ); - - // { - // let code = self - // .swc - // .print( - // &program.clone().fold_with(&mut HygieneVisualizer), - // SourceMapsConfig::Bool(false), - // None, - // false, - // ) - // .unwrap() - // .code; - // - // println!("loaded using swc:\n{}\n\n", code); - // } - - match program { - Program::Module(module) => Ok(module), - _ => unreachable!(), - } - }) - }, - || { - let p = match fm.name { - FileName::Real(ref p) => p, - _ => unreachable!("{} module in spack", fm.name), - }; - - rayon::join( - || self.swc.run(|| self.load_imports(&p, imports)), - || self.swc.run(|| self.load_exports(&p, exports)), - ) - }, - ); - - let imports = imports?; - let exports = exports?; - let mut module = module?; - let is_es6 = { - let mut v = Es6ModuleDetector { - forced_es6: false, - found_other: false, - }; - module.visit_with(&Invalid { span: DUMMY_SP } as _, &mut v); - v.forced_es6 || !v.found_other - }; - if is_es6 { - module = self.drop_unused(fm.clone(), module, None); - } - - let module = Arc::new(module); - - Ok(TransformedModule { - id, - fm, - module, - imports: Arc::new(imports), - exports: Arc::new(exports), - is_es6, - helpers: Default::default(), - mark, - }) - }) - } - - fn load_exports(&self, base: &Path, raw: RawExports) -> Result { - self.swc.run(|| { - log::trace!("load_exports({})", base.display()); - - let mut exports = Exports::default(); - exports.pure_constants = raw.pure_constants; - - let items = raw - .items - .into_par_iter() - .map(|(src, ss)| -> Result<_, Error> { - let info = match src { - Some(src) => { - let path = self.resolve(base, &src.value)?; - Some((self.load_transformed_inner(path)?, src)) - } - None => None, - }; - - Ok((info, ss)) - }) - .collect::>(); - - for res in items { - let (info, specifiers): (Option<((Arc, TransformedModule), Str)>, _) = - res?; - - match info { - None => exports.items.extend(specifiers), - Some(info) => exports - .reexports - .entry(Source { - is_loaded_synchronously: true, - is_unconditional: false, - module_id: (info.0).1.id, - src: info.1, - }) - .or_default() - .extend(specifiers), - } - } - - Ok(exports) - }) - } - - /// Load dependencies - fn load_imports(&self, base: &Path, info: RawImports) -> Result { - self.swc.run(|| { - log::trace!("load_imports({})", base.display()); - - let mut merged = Imports::default(); - let RawImports { - imports, - lazy_imports, - dynamic_imports, - } = info; - - let loaded = imports - .into_par_iter() - .map(|v| (v, false, true)) - .chain(lazy_imports.into_par_iter().map(|v| (v, false, false))) - .chain(dynamic_imports.into_par_iter().map(|src| { - ( - ImportDecl { - span: src.span, - specifiers: vec![], - src, - type_only: false, - }, - true, - false, - ) - })) - .map(|(decl, dynamic, unconditional)| -> Result<_, Error> { - // - let path = self.resolve(base, &decl.src.value)?; - let res = self.load_transformed_inner(path)?; - - Ok((res, decl, dynamic, unconditional)) - }) - .collect::>(); - - for res in loaded { - // TODO: Report error and proceed instead of returning an error - let ((path, _res), decl, is_dynamic, is_unconditional) = res?; - - if let Some(src) = self.scope.get_module_by_path(&path) { - let src = Source { - is_loaded_synchronously: !is_dynamic, - is_unconditional, - module_id: src.id, - src: decl.src, - }; - - // TODO: Handle rename - let mut specifiers = vec![]; - for s in decl.specifiers { - match s { - ImportSpecifier::Named(s) => specifiers.push(Specifier::Specific { - local: s.local.into(), - alias: s.imported.map(From::from), - }), - ImportSpecifier::Default(s) => specifiers.push(Specifier::Specific { - local: s.local.into(), - alias: Some(Id::new(js_word!("default"), s.span.ctxt())), - }), - ImportSpecifier::Namespace(s) => { - specifiers.push(Specifier::Namespace { - local: s.local.into(), - }); - } - } - } - - merged.specifiers.push((src, specifiers)); - } - } - - Ok(merged) - }) - } -} - -struct Es6ModuleDetector { - /// If import statement or export is detected, it's an es6 module regardless - /// of other codes. - forced_es6: bool, - /// True if other module system is detected. - found_other: bool, -} - -impl Visit for Es6ModuleDetector { - fn visit_member_expr(&mut self, e: &MemberExpr, _: &dyn Node) { - e.obj.visit_with(e as _, self); - - if e.computed { - e.prop.visit_with(e as _, self); - } - - match &e.obj { - ExprOrSuper::Expr(e) => { - match &**e { - Expr::Ident(i) => { - // TODO: Check syntax context (Check if marker is the global mark) - if i.sym == *"module" { - self.found_other = true; - } - - if i.sym == *"exports" { - self.found_other = true; - } - } - - _ => {} - } - } - _ => {} - } - - // - } - - fn visit_module_decl(&mut self, decl: &ModuleDecl, _: &dyn Node) { - match decl { - ModuleDecl::Import(_) - | ModuleDecl::ExportDecl(_) - | ModuleDecl::ExportNamed(_) - | ModuleDecl::ExportDefaultDecl(_) - | ModuleDecl::ExportDefaultExpr(_) - | ModuleDecl::ExportAll(_) => { - self.forced_es6 = true; - } - - ModuleDecl::TsImportEquals(_) => {} - ModuleDecl::TsExportAssignment(_) => {} - ModuleDecl::TsNamespaceExport(_) => {} - } - } -} diff --git a/spack/src/bundler/load_transformed/tests.rs b/spack/src/bundler/load_transformed/tests.rs deleted file mode 100644 index e48b1a5dc91..00000000000 --- a/spack/src/bundler/load_transformed/tests.rs +++ /dev/null @@ -1,8 +0,0 @@ -use crate::bundler::tests::test_bundler; - -#[test] -fn basic() { - test_bundler(|t| { - t.parse(""); - }); -} diff --git a/spack/src/bundler/mod.rs b/spack/src/bundler/mod.rs deleted file mode 100644 index a58206cd06b..00000000000 --- a/spack/src/bundler/mod.rs +++ /dev/null @@ -1,154 +0,0 @@ -use self::{config::Cache, scope::Scope}; -use crate::{ - bundler::load_transformed::TransformedModule, - config::{Config, EntryConfig}, - load::Load, - resolve::Resolve, - ModuleId, -}; -use anyhow::{Context, Error}; -use fxhash::FxHashMap; -use rayon::prelude::*; -use std::{path::PathBuf, sync::Arc}; -use swc::config::ModuleConfig; -use swc_common::{Mark, DUMMY_SP}; -use swc_ecma_ast::Module; - -mod chunk; -mod config; -mod export; -mod helpers; -mod import; -mod load_transformed; -mod rename; -mod scope; -#[cfg(test)] -mod tests; -mod usage_analysis; - -pub struct Bundler<'a> { - cache: Cache, - /// Javascript compiler. - swc: Arc, - swc_options: swc::config::Options, - used_mark: Mark, - top_level_mark: Mark, - - resolver: &'a dyn Resolve, - loader: &'a dyn Load, - - scope: Scope, -} - -#[derive(Debug)] -pub enum BundleKind { - /// User-provided entry - Named { name: String }, - /// Auto-generated entry (created by import expression) - Dynamic, - /// A lazy-loaded shared library - Lib { name: String }, -} - -/// Built bundle -#[derive(Debug)] -pub struct Bundle { - pub kind: BundleKind, - pub id: ModuleId, - /// Merged module - pub module: Module, -} - -impl<'a> Bundler<'a> { - pub fn new( - swc: Arc, - mut swc_options: swc::config::Options, - resolver: &'a dyn Resolve, - loader: &'a dyn Load, - ) -> Self { - let used_mark = swc.run(|| Mark::fresh(Mark::root())); - log::info!("Used mark: {:?}", DUMMY_SP.apply_mark(used_mark).ctxt()); - let top_level_mark = swc.run(|| Mark::fresh(Mark::root())); - log::info!( - "top-level mark: {:?}", - DUMMY_SP.apply_mark(top_level_mark).ctxt() - ); - - swc_options.disable_fixer = true; - swc_options.disable_hygiene = true; - swc_options.global_mark = Some(top_level_mark); - - if swc_options.config.is_none() { - swc_options.config = Some(Default::default()); - } - - if let Some(c) = &mut swc_options.config { - // Preserve es6 modules - c.module = Some(ModuleConfig::Es6); - } - - Bundler { - swc, - cache: Default::default(), - swc_options, - used_mark, - top_level_mark, - resolver, - loader, - scope: Default::default(), - } - } - - pub fn bundle(&self, config: &Config) -> Result, Error> { - let entries = { - let mut map = FxHashMap::default(); - match &config.entry { - EntryConfig::File(f) => { - map.insert(f.clone(), PathBuf::from(f.clone())); - } - EntryConfig::Multiple(files) => { - for f in files { - map.insert(f.clone(), f.clone().into()); - } - } - EntryConfig::Files(files) => map = files.clone(), - } - - map - }; - - let results = entries - .into_par_iter() - .map(|(name, path)| -> Result<_, Error> { - let path = self.resolve(&config.working_dir, &path.to_string_lossy())?; - let res = self - .load_transformed(path) - .context("load_transformed failed")?; - Ok((name, res)) - }) - .collect::>(); - - // We collect at here to handle dynamic imports - // TODO: Handle dynamic imports - - let local = self.swc.run(|| -> Result<_, Error> { - let mut output = FxHashMap::default(); - - for res in results { - let (name, m): (String, TransformedModule) = res?; - - output.insert(name, m); - } - - Ok(output) - })?; - - let bundles = self.chunk(local)?; - - Ok(self.finalize(bundles)?) - } - - pub fn swc(&self) -> &swc::Compiler { - &self.swc - } -} diff --git a/spack/src/bundler/rename.rs b/spack/src/bundler/rename.rs deleted file mode 100644 index c00d6f0803e..00000000000 --- a/spack/src/bundler/rename.rs +++ /dev/null @@ -1,276 +0,0 @@ -use crate::{Bundle, BundleKind, Bundler}; -use anyhow::{Context, Error}; -use crc::{crc64, crc64::Digest, Hasher64}; -use fxhash::FxHashMap; -use relative_path::RelativePath; -use std::{ - io, - path::{Path, PathBuf}, -}; -use swc::config::Options; -use swc_common::{util::move_map::MoveMap, FileName, Span}; -use swc_ecma_ast::{ImportDecl, Module, Str}; -use swc_ecma_codegen::{text_writer::WriteJs, Emitter}; -use swc_ecma_transforms::noop_fold_type; -use swc_ecma_visit::{Fold, FoldWith}; - -impl Bundler<'_> { - pub(super) fn finalize(&self, bundles: Vec) -> Result, Error> { - let mut new = Vec::with_capacity(bundles.len()); - let mut renamed = FxHashMap::default(); - - for mut bundle in bundles { - match bundle.kind { - BundleKind::Named { .. } => { - // Inject helpers - let helpers = self - .scope - .get_module(bundle.id) - .expect("module should exist at this point") - .helpers; - - self.swc - .run_transform(true, || helpers.append_to(&mut bundle.module.body)); - - new.push(Bundle { ..bundle }); - } - BundleKind::Lib { name } => { - let hash = self.calc_hash(&bundle.module)?; - let mut new_name = PathBuf::from(name); - let key = new_name.clone(); - let file_name = new_name - .file_name() - .map(|path| -> PathBuf { - let path = Path::new(path); - let ext = path.extension(); - if let Some(ext) = ext { - return format!( - "{}-{}.{}", - path.file_stem().unwrap().to_string_lossy(), - hash, - ext.to_string_lossy() - ) - .into(); - } - return format!( - "{}-{}", - path.file_stem().unwrap().to_string_lossy(), - hash, - ) - .into(); - }) - .expect("javascript file should have name"); - new_name.pop(); - new_name = new_name.join(file_name.clone()); - - renamed.insert(key, new_name.to_string_lossy().to_string()); - - new.push(Bundle { - kind: BundleKind::Named { - name: file_name.display().to_string(), - }, - ..bundle - }) - } - _ => new.push(bundle), - } - } - - new = new.move_map(|bundle| { - let path = match self.scope.get_module(bundle.id).unwrap().fm.name { - FileName::Real(ref v) => v.clone(), - _ => { - log::error!("Cannot rename: not a real file"); - return bundle; - } - }; - - let module = { - // Change imports - let mut v = Renamer { - bundler: self, - path: &path, - renamed: &renamed, - }; - bundle.module.fold_with(&mut v) - }; - - let module = self.swc.run(|| { - let opts = Options { - ..self.swc_options.clone() - }; - let file_name = FileName::Real(path); - let config = self.swc.read_config(&opts, &file_name).unwrap_or_default(); - let mut module_pass = swc::config::ModuleConfig::build( - self.swc.cm.clone(), - self.top_level_mark, - config.module, - ); - module.fold_with(&mut module_pass) - }); - - Bundle { module, ..bundle } - }); - - Ok(new) - } - - fn calc_hash(&self, m: &Module) -> Result { - let digest = crc64::Digest::new(crc64::ECMA); - let mut buf = Hasher { digest }; - - { - let mut emitter = Emitter { - cfg: Default::default(), - cm: self.swc.cm.clone(), - comments: None, - wr: Box::new(&mut buf) as Box, - handlers: Box::new(Handlers), - }; - - emitter - .emit_module(&m) - .context("failed to emit module to calculate hash")?; - } - // - - let result = buf.digest.sum64(); - Ok(radix_fmt::radix(result, 36).to_string()) - } -} - -/// Import renamer. This pass changes import path. -struct Renamer<'a, 'b> { - bundler: &'a Bundler<'b>, - path: &'a Path, - renamed: &'a FxHashMap, -} - -noop_fold_type!(Renamer<'_, '_>); - -impl Fold for Renamer<'_, '_> { - fn fold_import_decl(&mut self, import: ImportDecl) -> ImportDecl { - let resolved = match self.bundler.resolve(self.path, &import.src.value) { - Ok(v) => v, - Err(_) => return import, - }; - - if let Some(v) = self.renamed.get(&*resolved) { - // We use parent because RelativePath uses ../common-[hash].js - // if we use `entry-a.js` as a base. - // - // entry-a.js - // common.js - let base = self - .path - .parent() - .unwrap_or(self.path) - .as_os_str() - .to_string_lossy(); - let base = RelativePath::new(&*base); - let v = base.relative(&*v); - let value = v.as_str(); - return ImportDecl { - src: Str { - value: if value.starts_with(".") { - value.into() - } else { - format!("./{}", value).into() - }, - ..import.src - }, - ..import - }; - } - - import - } -} - -impl swc_ecma_codegen::Handlers for Handlers {} -struct Handlers; - -struct Hasher { - digest: Digest, -} - -impl Hasher { - fn w(&mut self, s: &str) { - self.digest.write(s.as_bytes()); - } -} - -impl WriteJs for &mut Hasher { - fn increase_indent(&mut self) -> io::Result<()> { - Ok(()) - } - - fn decrease_indent(&mut self) -> io::Result<()> { - Ok(()) - } - - fn write_semi(&mut self) -> io::Result<()> { - self.w(";"); - Ok(()) - } - - fn write_space(&mut self) -> io::Result<()> { - self.w(" "); - Ok(()) - } - - fn write_keyword(&mut self, _: Option, s: &'static str) -> io::Result<()> { - self.w(s); - Ok(()) - } - - fn write_operator(&mut self, s: &str) -> io::Result<()> { - self.w(s); - Ok(()) - } - - fn write_param(&mut self, s: &str) -> io::Result<()> { - self.w(s); - Ok(()) - } - - fn write_property(&mut self, s: &str) -> io::Result<()> { - self.w(s); - Ok(()) - } - - fn write_line(&mut self) -> io::Result<()> { - self.w("\n"); - Ok(()) - } - - fn write_lit(&mut self, _: Span, s: &str) -> io::Result<()> { - self.w(s); - Ok(()) - } - - fn write_comment(&mut self, _: Span, s: &str) -> io::Result<()> { - self.w(s); - Ok(()) - } - - fn write_str_lit(&mut self, _: Span, s: &str) -> io::Result<()> { - self.w(s); - Ok(()) - } - - fn write_str(&mut self, s: &str) -> io::Result<()> { - self.w(s); - Ok(()) - } - - fn write_symbol(&mut self, _: Span, s: &str) -> io::Result<()> { - self.w(s); - Ok(()) - } - - fn write_punct(&mut self, s: &'static str) -> io::Result<()> { - self.w(s); - Ok(()) - } -} diff --git a/spack/src/bundler/scope.rs b/spack/src/bundler/scope.rs deleted file mode 100644 index 1405e76cdf7..00000000000 --- a/spack/src/bundler/scope.rs +++ /dev/null @@ -1,30 +0,0 @@ -use crate::{bundler::load_transformed::TransformedModule, id::ModuleIdGenerator, ModuleId}; -use dashmap::DashMap; -use fxhash::FxBuildHasher; -use std::{path::PathBuf, sync::Arc}; - -#[derive(Debug, Default)] -pub(super) struct Scope { - pub module_id_gen: ModuleIdGenerator, - - /// Phase 1 cache - modules: DashMap, -} - -impl Scope { - /// Stores module information. The information should contain only - /// information gotten from module itself. In other words, it should not - /// contains information from a dependency. - pub fn store_module(&self, _path: Arc, info: TransformedModule) { - self.modules.insert(info.id, info); - } - - pub fn get_module_by_path(&self, path: &Arc) -> Option { - let (id, _) = self.module_id_gen.gen(path); - self.get_module(id) - } - - pub fn get_module(&self, id: ModuleId) -> Option { - Some(self.modules.get(&id)?.value().clone()) - } -} diff --git a/spack/src/bundler/tests.rs b/spack/src/bundler/tests.rs deleted file mode 100644 index 4e6b8627c38..00000000000 --- a/spack/src/bundler/tests.rs +++ /dev/null @@ -1,78 +0,0 @@ -//! Utilities for testing. -use super::Bundler; -use crate::{loaders::swc::SwcLoader, resolve::NodeResolver, util::HygieneRemover}; -use pretty_assertions::assert_eq; -use std::{path::PathBuf, sync::Arc}; -use swc_common::FileName; -use swc_ecma_ast::*; -use swc_ecma_parser::{EsConfig, Syntax}; -use swc_ecma_utils::drop_span; -use swc_ecma_visit::FoldWith; - -pub struct Tester<'a> { - pub bundler: Bundler<'a>, -} - -impl<'a> Tester<'a> { - pub fn parse(&self, s: &str) -> Module { - let fm = self - .bundler - .swc - .cm - .new_source_file(FileName::Real(PathBuf::from("input.js")), s.into()); - let p = self - .bundler - .swc - .parse_js( - fm, - Default::default(), - Syntax::Es(EsConfig { - dynamic_import: true, - ..Default::default() - }), - true, - true, - ) - .expect("failed to parse"); - - match p { - Program::Module(m) => m, - Program::Script(_) => unreachable!(), - } - } - - pub fn assert_eq(&self, m: &Module, expected: &str) { - let expected = self.parse(expected); - - let m = drop_span(m.clone().fold_with(&mut HygieneRemover)); - let expected = drop_span(expected); - - assert_eq!(m, expected) - } -} - -pub fn test_bundler(op: F) -where - F: FnOnce(&mut Tester), -{ - testing::run_test2(true, |cm, handler| { - let compiler = Arc::new(swc::Compiler::new(cm.clone(), Arc::new(handler))); - let loader = SwcLoader::new(compiler.clone(), Default::default()); - let bundler = Bundler::new( - compiler.clone(), - swc::config::Options { - swcrc: true, - ..Default::default() - }, - &NodeResolver, - &loader, - ); - - let mut t = Tester { bundler }; - - op(&mut t); - - Ok(()) - }) - .expect("WTF?"); -} diff --git a/spack/src/config/mod.rs b/spack/src/config/mod.rs index fef7d0f6a4f..290be8af378 100644 --- a/spack/src/config/mod.rs +++ b/spack/src/config/mod.rs @@ -8,6 +8,7 @@ use fxhash::FxHashMap; use serde::Deserialize; use std::{fmt, marker::PhantomData, path::PathBuf}; use string_enum::StringEnum; +use swc_common::FileName; mod module; mod optimization; @@ -56,7 +57,7 @@ impl Default for Mode { } } -#[derive(Debug, Deserialize)] +#[derive(Debug, Clone, Deserialize)] #[serde(untagged, rename = "Entry")] pub enum EntryConfig { File(String), @@ -64,6 +65,36 @@ pub enum EntryConfig { Files(FxHashMap), } +impl From for FxHashMap { + fn from(c: EntryConfig) -> Self { + let mut m = FxHashMap::default(); + + match c { + EntryConfig::File(f) => { + let path = PathBuf::from(f); + let file_name = path + .file_name() + .expect("entry must be a file, instead of a directory"); + m.insert(file_name.to_string_lossy().into(), FileName::Real(path)); + } + EntryConfig::Multiple(files) => { + for f in files { + let path = PathBuf::from(f); + let file_name = path + .file_name() + .expect("entry must be a file, instead of a directory"); + m.insert(file_name.to_string_lossy().into(), FileName::Real(path)); + } + } + EntryConfig::Files(f) => { + return f.into_iter().map(|(k, v)| (k, FileName::Real(v))).collect() + } + } + + m + } +} + pub struct JsCallback { _f: Box Ret>, _phantom: PhantomData<(T, Ret)>, diff --git a/spack/src/debug/mod.rs b/spack/src/debug/mod.rs deleted file mode 100644 index ba572fceae6..00000000000 --- a/spack/src/debug/mod.rs +++ /dev/null @@ -1,30 +0,0 @@ -use swc_common::{Span, SyntaxContext, DUMMY_SP}; -use swc_ecma_ast::{Ident, Invalid}; -use swc_ecma_visit::{Fold, Node, Visit, VisitWith}; - -pub(crate) struct HygieneVisualizer; - -impl Fold for HygieneVisualizer { - fn fold_ident(&mut self, node: Ident) -> Ident { - Ident { - sym: format!("{}{:?}", node.sym, node.span.ctxt()).into(), - ..node - } - } -} - -pub(crate) struct AssertClean; - -impl Visit for AssertClean { - fn visit_span(&mut self, s: &Span, _: &dyn Node) { - debug_assert_eq!( - s.ctxt(), - SyntaxContext::empty(), - "Hygiene info should be clean at this moment" - ); - } -} - -pub(crate) fn assert_clean>(m: &T) { - m.visit_with(&Invalid { span: DUMMY_SP } as _, &mut AssertClean) -} diff --git a/spack/src/lib.rs b/spack/src/lib.rs index 9164c7a2c47..ca6eaf32b8a 100644 --- a/spack/src/lib.rs +++ b/spack/src/lib.rs @@ -3,17 +3,6 @@ #[cfg(test)] extern crate test; -pub use self::{ - bundler::{Bundle, BundleKind, Bundler}, - id::{Id, ModuleId, QualifiedId}, -}; - -mod bundler; pub mod config; -mod debug; -mod id; -pub mod load; pub mod loaders; -mod normalize; -pub mod resolve; -mod util; +pub mod resolvers; diff --git a/spack/src/load.rs b/spack/src/load.rs deleted file mode 100644 index 31dcc02d0b8..00000000000 --- a/spack/src/load.rs +++ /dev/null @@ -1,20 +0,0 @@ -use anyhow::Error; -use std::{path::Path, sync::Arc}; -use swc_common::SourceFile; -use swc_ecma_ast::Module; - -pub trait Load: Send + Sync { - fn load(&self, path: &Path) -> Result<(Arc, Module), Error>; -} - -impl Load for Box { - fn load(&self, path: &Path) -> Result<(Arc, Module), Error> { - T::load(self, path) - } -} - -impl<'a, T: ?Sized + Load> Load for &'a T { - fn load(&self, path: &Path) -> Result<(Arc, Module), Error> { - T::load(self, path) - } -} diff --git a/spack/src/loaders/neon.rs b/spack/src/loaders/neon.rs index 46b299280cf..e6d03bff0c5 100644 --- a/spack/src/loaders/neon.rs +++ b/spack/src/loaders/neon.rs @@ -1,11 +1,7 @@ -use crate::load::Load; use anyhow::{Context as _, Error}; use neon::prelude::*; - -use std::{ - path::Path, - sync::{mpsc::channel, Arc}, -}; +use std::sync::{mpsc::channel, Arc}; +use swc_bundler::Load; use swc_common::{FileName, SourceFile}; use swc_ecma_ast::{Module, Program}; @@ -16,8 +12,8 @@ pub struct NeonLoader { } impl Load for NeonLoader { - fn load(&self, p: &Path) -> Result<(Arc, Module), Error> { - let path = p.to_string_lossy().to_string(); + fn load(&self, name: &FileName) -> Result<(Arc, Module), Error> { + let path = name.to_string(); let (tx, rx) = channel(); self.handler.schedule_with(move |cx, _value, f| { @@ -58,10 +54,7 @@ impl Load for NeonLoader { .context("failed to receive output from js loader")?; let code = code?; - let fm = self - .swc - .cm - .new_source_file(FileName::Real(p.to_path_buf()), code); + let fm = self.swc.cm.new_source_file(name.clone(), code); let config = self.swc.config_for_file( &swc::config::Options { diff --git a/spack/src/loaders/swc.rs b/spack/src/loaders/swc.rs index 7e111aeea76..cd60f9e50d5 100644 --- a/spack/src/loaders/swc.rs +++ b/spack/src/loaders/swc.rs @@ -1,7 +1,7 @@ -use crate::load::Load; -use anyhow::Error; -use std::{path::Path, sync::Arc}; -use swc_common::SourceFile; +use anyhow::{bail, Context, Error}; +use std::sync::Arc; +use swc_bundler::Load; +use swc_common::{FileName, SourceFile}; use swc_ecma_ast::{Module, Program}; use swc_ecma_parser::JscTarget; @@ -23,7 +23,28 @@ impl SwcLoader { v.module = None; v.minify = Some(false); - v.jsc.target = JscTarget::Es2019; + v.jsc.target = JscTarget::Es2020; + + if v.jsc.transform.is_none() { + v.jsc.transform = Some(Default::default()); + } + + let mut transform = v.jsc.transform.as_mut().unwrap(); + if transform.optimizer.is_none() { + transform.optimizer = Some(Default::default()); + } + + let mut opt = transform.optimizer.as_mut().unwrap(); + if opt.globals.is_none() { + opt.globals = Some(Default::default()); + } + + // Always inline NODE_ENV + opt.globals + .as_mut() + .unwrap() + .envs + .insert("NODE_ENV".to_string()); } SwcLoader { compiler, options } @@ -31,35 +52,40 @@ impl SwcLoader { } impl Load for SwcLoader { - fn load(&self, path: &Path) -> Result<(Arc, Module), Error> { - self.compiler.run(|| { - log::debug!("JsLoader.load({})", path.display()); + fn load(&self, name: &FileName) -> Result<(Arc, Module), Error> { + log::debug!("JsLoader.load({})", name); - let fm = self.compiler.cm.load_file(path)?; + let fm = self + .compiler + .cm + .load_file(match name { + FileName::Real(v) => &v, + _ => bail!("swc-loader only accepts path. Got `{}`", name), + }) + .with_context(|| format!("failed to load file `{}`", name))?; - log::trace!("JsLoader.load: loaded"); + log::trace!("JsLoader.load: loaded"); - let config = self.compiler.config_for_file(&self.options, &fm.name)?; + let config = self.compiler.config_for_file(&self.options, &fm.name)?; - log::trace!("JsLoader.load: loaded config"); + log::trace!("JsLoader.load: loaded config"); - // We run transform at this phase to strip out unused dependencies. - // - // Note that we don't apply compat transform at loading phase. - let program = - self.compiler - .parse_js(fm.clone(), JscTarget::Es2019, config.syntax, true, true)?; + // We run transform at this phase to strip out unused dependencies. + // + // Note that we don't apply compat transform at loading phase. + let program = + self.compiler + .parse_js(fm.clone(), JscTarget::Es2019, config.syntax, true, true)?; - log::trace!("JsLoader.load: parsed"); + log::trace!("JsLoader.load: parsed"); - let program = self.compiler.transform(program, true, config.pass); + let program = self.compiler.transform(program, true, config.pass); - log::trace!("JsLoader.load: applied transforms"); + log::trace!("JsLoader.load: applied transforms"); - match program { - Program::Module(module) => Ok((fm, module)), - _ => unreachable!(), - } - }) + match program { + Program::Module(module) => Ok((fm, module)), + _ => unreachable!(), + } } } diff --git a/spack/src/normalize/mod.rs b/spack/src/normalize/mod.rs deleted file mode 100644 index 8b137891791..00000000000 --- a/spack/src/normalize/mod.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/spack/src/resolve.rs b/spack/src/resolve.rs deleted file mode 100644 index 69233c9c155..00000000000 --- a/spack/src/resolve.rs +++ /dev/null @@ -1,45 +0,0 @@ -use anyhow::{Context, Error}; -use std::path::{Path, PathBuf}; - -pub trait Resolve: Send + Sync { - /// - /// Returned filename will be hashed if possible and used to generate module - /// id. - fn resolve(&self, base: &Path, import: &str) -> Result; -} - -impl Resolve for Box { - fn resolve(&self, base: &Path, import: &str) -> Result { - T::resolve(self, base, import) - } -} - -impl<'a, T: ?Sized + Resolve> Resolve for &'a T { - fn resolve(&self, base: &Path, import: &str) -> Result { - T::resolve(self, base, import) - } -} - -pub struct NodeResolver; - -impl Resolve for NodeResolver { - fn resolve(&self, base: &Path, import: &str) -> Result { - let base_dir = base - .parent() - .map(Path::to_path_buf) - .unwrap_or_else(|| PathBuf::from(".")); - - Ok(node_resolve::Resolver::new() - .with_extensions(&[".ts", ".tsx", ".js", ".jsx", ".json", ".node"]) - .with_main_fields(&["swc-main", "esnext", "main"]) - .with_basedir(base_dir.clone()) - .resolve(import) - .with_context(|| { - format!( - "node-resolve failed; basedir = {}, import = {}", - base_dir.display(), - import - ) - })?) - } -} diff --git a/spack/src/resolvers/mod.rs b/spack/src/resolvers/mod.rs new file mode 100644 index 00000000000..b1048e2b3ac --- /dev/null +++ b/spack/src/resolvers/mod.rs @@ -0,0 +1,43 @@ +use anyhow::{bail, Context, Error}; +use std::path::{Path, PathBuf}; +use swc_bundler::Resolve; +use swc_common::FileName; + +pub struct NodeResolver(node_resolve::Resolver); + +impl NodeResolver { + pub fn new() -> Self { + Self( + node_resolve::Resolver::new() + .with_extensions(&[".ts", ".tsx", ".js", ".jsx", ".json", ".node"]) + .with_main_fields(&["swc-main", "esnext", "main"]), + ) + } +} + +impl Resolve for NodeResolver { + fn resolve(&self, base: &FileName, module_specifier: &str) -> Result { + let base = match base { + FileName::Real(v) => v, + _ => bail!("node-resolver supports only files"), + }; + + let base_dir = base + .parent() + .map(Path::to_path_buf) + .unwrap_or_else(|| PathBuf::from(".")); + + let path = self + .0 + .with_basedir(base_dir.clone()) + .resolve(module_specifier) + .with_context(|| { + format!( + "node-resolver failed; basedir = {}, import = {}", + base_dir.display(), + module_specifier + ) + })?; + Ok(FileName::Real(path)) + } +} diff --git a/spack/src/util.rs b/spack/src/util.rs deleted file mode 100644 index 45a5403912c..00000000000 --- a/spack/src/util.rs +++ /dev/null @@ -1,10 +0,0 @@ -use swc_common::{Span, SyntaxContext}; -use swc_ecma_visit::Fold; - -pub struct HygieneRemover; - -impl Fold for HygieneRemover { - fn fold_span(&mut self, s: Span) -> Span { - s.with_ctxt(SyntaxContext::empty()) - } -} diff --git a/spack/tests/fixture.rs b/spack/tests/fixture.rs index 7bba75713ac..c929c3d5399 100644 --- a/spack/tests/fixture.rs +++ b/spack/tests/fixture.rs @@ -3,11 +3,7 @@ extern crate test; use fxhash::FxHashMap; -use spack::{ - config::{Config, EntryConfig}, - loaders::swc::SwcLoader, - BundleKind, Bundler, -}; +use spack::{loaders::swc::SwcLoader, resolvers::NodeResolver}; use std::{ env, fs::{create_dir_all, read_dir}, @@ -16,6 +12,8 @@ use std::{ sync::Arc, }; use swc::config::SourceMapsConfig; +use swc_bundler::{BundleKind, Bundler, Config}; +use swc_common::{FileName, GLOBALS}; use test::{ test_main, DynTestFn, Options, ShouldPanic::No, TestDesc, TestDescAndFn, TestName, TestType, }; @@ -93,7 +91,10 @@ fn reference_tests(tests: &mut Vec, errors: bool) -> Result<(), i }) .map(|e| -> Result<_, io::Error> { let e = e?; - Ok((e.file_name().to_string_lossy().to_string(), e.path())) + Ok(( + e.file_name().to_string_lossy().to_string(), + FileName::Real(e.path()), + )) }) .collect::, _>>()?; @@ -116,73 +117,103 @@ fn reference_tests(tests: &mut Vec, errors: bool) -> Result<(), i eprintln!("\n\n========== Running reference test {}\n", dir_name); testing::run_test2(false, |cm, handler| { - let compiler = Arc::new(swc::Compiler::new(cm.clone(), Arc::new(handler))); - let loader = SwcLoader::new( - compiler.clone(), - swc::config::Options { - swcrc: true, - ..Default::default() - }, - ); - let config = Config { - working_dir: Default::default(), - mode: Default::default(), - entry: EntryConfig::Files(entries), - output: None, - module: Default::default(), - optimization: None, - resolve: None, - options: None, - }; - let bundler = Bundler::new( - compiler.clone(), - swc::config::Options { - swcrc: true, - ..Default::default() - }, - &spack::resolve::NodeResolver, - &loader, - ); + GLOBALS.with(|globals| { + let compiler = Arc::new(swc::Compiler::new(cm.clone(), Arc::new(handler))); + let loader = SwcLoader::new( + compiler.clone(), + swc::config::Options { + swcrc: true, + ..Default::default() + }, + ); + let bundler = Bundler::new( + globals, + cm.clone(), + &loader, + NodeResolver::new(), + Config { + require: true, + external_modules: vec![ + "assert", + "buffer", + "child_process", + "console", + "cluster", + "crypto", + "dgram", + "dns", + "events", + "fs", + "http", + "http2", + "https", + "net", + "os", + "path", + "perf_hooks", + "process", + "querystring", + "readline", + "repl", + "stream", + "string_decoder", + "timers", + "tls", + "tty", + "url", + "util", + "v8", + "vm", + "wasi", + "worker", + "zlib", + ] + .into_iter() + .map(From::from) + .collect(), + }, + ); - let modules = bundler.bundle(&config).expect("failed to bundle module"); - log::info!("Bundled as {} modules", modules.len()); + let modules = bundler.bundle(entries).map_err(|_| ())?; + log::info!("Bundled as {} modules", modules.len()); - let mut error = false; + let mut error = false; - for bundled in modules { - let code = bundler - .swc() - .print(&bundled.module, SourceMapsConfig::Bool(false), None, false) - .expect("failed to emit bundle") - .code; + for bundled in modules { + let code = compiler + .print(&bundled.module, SourceMapsConfig::Bool(false), None, false) + .expect("failed to print?") + .code; - let name = match bundled.kind { - BundleKind::Named { name } | BundleKind::Lib { name } => { - PathBuf::from(name) - } - BundleKind::Dynamic => format!("dynamic.{}.js", bundled.id).into(), - }; + let name = match bundled.kind { + BundleKind::Named { name } | BundleKind::Lib { name } => { + PathBuf::from(name) + } + BundleKind::Dynamic => format!("dynamic.{}.js", bundled.id).into(), + }; - let output_path = entry.path().join("output").join(name.file_name().unwrap()); + let output_path = + entry.path().join("output").join(name.file_name().unwrap()); - log::info!("Printing {}", output_path.display()); + log::info!("Printing {}", output_path.display()); - let s = NormalizedOutput::from(code); + let s = NormalizedOutput::from(code); - match s.compare_to_file(&output_path) { - Ok(_) => {} - Err(err) => { - println!("{:?}", err); - error = true; + match s.compare_to_file(&output_path) { + Ok(_) => {} + Err(err) => { + println!("{:?}", err); + error = true; + } } } - } - if error { - return Err(()); - } + if error { + return Err(()); + } - Ok(()) + Ok(()) + }) }) .expect("failed to process a module"); }); diff --git a/spack/tests/pass/basic/class-inheritance/input/a.js b/spack/tests/pass/basic/class-inheritance/input/a.js new file mode 100644 index 00000000000..8fdddf0ca49 --- /dev/null +++ b/spack/tests/pass/basic/class-inheritance/input/a.js @@ -0,0 +1,5 @@ +import { B } from './b'; + +export class A extends B { + +} \ No newline at end of file diff --git a/spack/tests/pass/basic/class-inheritance/input/b.js b/spack/tests/pass/basic/class-inheritance/input/b.js new file mode 100644 index 00000000000..88e7710df1c --- /dev/null +++ b/spack/tests/pass/basic/class-inheritance/input/b.js @@ -0,0 +1,3 @@ +export class B { + +} \ No newline at end of file diff --git a/spack/tests/pass/basic/class-inheritance/input/entry.ts b/spack/tests/pass/basic/class-inheritance/input/entry.ts new file mode 100644 index 00000000000..5c05f02e7da --- /dev/null +++ b/spack/tests/pass/basic/class-inheritance/input/entry.ts @@ -0,0 +1,5 @@ +import { A } from './a'; +import './b'; + +console.log('foo'); +new A(); \ No newline at end of file diff --git a/spack/tests/pass/basic/class-inheritance/output/entry.ts b/spack/tests/pass/basic/class-inheritance/output/entry.ts new file mode 100644 index 00000000000..f930767136b --- /dev/null +++ b/spack/tests/pass/basic/class-inheritance/output/entry.ts @@ -0,0 +1,6 @@ +class B { +} +class A extends B { +} +console.log('foo'); +new A(); diff --git a/spack/tests/pass/basic/extends/input/a.js b/spack/tests/pass/basic/extends/input/a.js new file mode 100644 index 00000000000..7ab009c2e59 --- /dev/null +++ b/spack/tests/pass/basic/extends/input/a.js @@ -0,0 +1,3 @@ +import { B } from './b'; + +export class A extends B { } \ No newline at end of file diff --git a/spack/tests/pass/basic/extends/input/b.js b/spack/tests/pass/basic/extends/input/b.js new file mode 100644 index 00000000000..a7907694215 --- /dev/null +++ b/spack/tests/pass/basic/extends/input/b.js @@ -0,0 +1 @@ +export class B { } \ No newline at end of file diff --git a/spack/tests/pass/basic/extends/input/entry.js b/spack/tests/pass/basic/extends/input/entry.js new file mode 100644 index 00000000000..0154d355cfb --- /dev/null +++ b/spack/tests/pass/basic/extends/input/entry.js @@ -0,0 +1,4 @@ +import { A } from './a'; +import { B } from './b'; + +console.log(A, B); \ No newline at end of file diff --git a/spack/tests/pass/basic/extends/output/entry.js b/spack/tests/pass/basic/extends/output/entry.js new file mode 100644 index 00000000000..38b9a858e1a --- /dev/null +++ b/spack/tests/pass/basic/extends/output/entry.js @@ -0,0 +1,5 @@ +class B { +} +class A extends B { +} +console.log(A, B); diff --git a/spack/tests/pass/circular/complex-class-function/input/a.js b/spack/tests/pass/circular/complex-class-function/input/a.js new file mode 100644 index 00000000000..9641eb224a8 --- /dev/null +++ b/spack/tests/pass/circular/complex-class-function/input/a.js @@ -0,0 +1,13 @@ +import { getC } from './c'; + +export function a() { + return new A() +} + +export class A extends getC() { + +} + +export function getA() { + return A; +} \ No newline at end of file diff --git a/spack/tests/pass/circular/complex-class-function/input/b.js b/spack/tests/pass/circular/complex-class-function/input/b.js new file mode 100644 index 00000000000..7faa656055c --- /dev/null +++ b/spack/tests/pass/circular/complex-class-function/input/b.js @@ -0,0 +1,4 @@ +import { A, getA, a } from './a'; + + +export { A, getA, a } \ No newline at end of file diff --git a/spack/tests/pass/circular/complex-class-function/input/c.js b/spack/tests/pass/circular/complex-class-function/input/c.js new file mode 100644 index 00000000000..50cb014a86e --- /dev/null +++ b/spack/tests/pass/circular/complex-class-function/input/c.js @@ -0,0 +1,7 @@ +import './b'; + +export function getC() { + return C; +} + +export class C { } \ No newline at end of file diff --git a/spack/tests/pass/circular/complex-class-function/input/entry.js b/spack/tests/pass/circular/complex-class-function/input/entry.js new file mode 100644 index 00000000000..c7a54a28c36 --- /dev/null +++ b/spack/tests/pass/circular/complex-class-function/input/entry.js @@ -0,0 +1,3 @@ +import { a } from './a'; + +console.log(a, a()) \ No newline at end of file diff --git a/spack/tests/pass/circular/complex-class-function/output/entry.js b/spack/tests/pass/circular/complex-class-function/output/entry.js new file mode 100644 index 00000000000..c9a7b8d71c9 --- /dev/null +++ b/spack/tests/pass/circular/complex-class-function/output/entry.js @@ -0,0 +1,4 @@ +function a() { + return new A(); +} +console.log(a, a()); diff --git a/spack/tests/pass/circular/hygiene/class-inheritance/input/a.js b/spack/tests/pass/circular/hygiene/class-inheritance/input/a.js new file mode 100644 index 00000000000..7ab009c2e59 --- /dev/null +++ b/spack/tests/pass/circular/hygiene/class-inheritance/input/a.js @@ -0,0 +1,3 @@ +import { B } from './b'; + +export class A extends B { } \ No newline at end of file diff --git a/spack/tests/pass/circular/hygiene/class-inheritance/input/b.js b/spack/tests/pass/circular/hygiene/class-inheritance/input/b.js new file mode 100644 index 00000000000..b624107afcc --- /dev/null +++ b/spack/tests/pass/circular/hygiene/class-inheritance/input/b.js @@ -0,0 +1,8 @@ +import { A } from './a'; +import { C } from './c'; + +export class B extends C { + a() { + return new A(); + } +} \ No newline at end of file diff --git a/spack/tests/pass/circular/hygiene/class-inheritance/input/c.js b/spack/tests/pass/circular/hygiene/class-inheritance/input/c.js new file mode 100644 index 00000000000..63345921163 --- /dev/null +++ b/spack/tests/pass/circular/hygiene/class-inheritance/input/c.js @@ -0,0 +1,10 @@ +import { B } from './b'; + +export class C { + a() { + throw new Error('Unimplemented') + } + b() { + return new B(); + } +} \ No newline at end of file diff --git a/spack/tests/pass/circular/hygiene/class-inheritance/input/entry.js b/spack/tests/pass/circular/hygiene/class-inheritance/input/entry.js new file mode 100644 index 00000000000..81899701aad --- /dev/null +++ b/spack/tests/pass/circular/hygiene/class-inheritance/input/entry.js @@ -0,0 +1,4 @@ +import { A } from './a'; +import './b'; +import './c'; +console.log(A, 'Loaded!'); \ No newline at end of file diff --git a/spack/tests/pass/circular/hygiene/class-inheritance/output/entry.js b/spack/tests/pass/circular/hygiene/class-inheritance/output/entry.js new file mode 100644 index 00000000000..5f1a8b9a724 --- /dev/null +++ b/spack/tests/pass/circular/hygiene/class-inheritance/output/entry.js @@ -0,0 +1,16 @@ +class C { + a() { + throw new Error('Unimplemented'); + } + b() { + return new B(); + } +} +class B extends C { + a() { + return new A(); + } +} +class A extends B { +} +console.log(A, 'Loaded!'); diff --git a/spack/tests/pass/circular/many/input/a.js b/spack/tests/pass/circular/many/input/a.js new file mode 100644 index 00000000000..250e707803a --- /dev/null +++ b/spack/tests/pass/circular/many/input/a.js @@ -0,0 +1 @@ +import './b'; \ No newline at end of file diff --git a/spack/tests/pass/circular/many/input/b.js b/spack/tests/pass/circular/many/input/b.js new file mode 100644 index 00000000000..400b54362cb --- /dev/null +++ b/spack/tests/pass/circular/many/input/b.js @@ -0,0 +1 @@ +import './c'; \ No newline at end of file diff --git a/spack/tests/pass/circular/many/input/c.js b/spack/tests/pass/circular/many/input/c.js new file mode 100644 index 00000000000..b7bd80c5fb7 --- /dev/null +++ b/spack/tests/pass/circular/many/input/c.js @@ -0,0 +1 @@ +import './d'; \ No newline at end of file diff --git a/spack/tests/pass/circular/many/input/d.js b/spack/tests/pass/circular/many/input/d.js new file mode 100644 index 00000000000..674450f7dfe --- /dev/null +++ b/spack/tests/pass/circular/many/input/d.js @@ -0,0 +1 @@ +import './a'; \ No newline at end of file diff --git a/spack/tests/pass/circular/many/input/entry.js b/spack/tests/pass/circular/many/input/entry.js new file mode 100644 index 00000000000..674450f7dfe --- /dev/null +++ b/spack/tests/pass/circular/many/input/entry.js @@ -0,0 +1 @@ +import './a'; \ No newline at end of file diff --git a/spack/tests/pass/circular/mixed/input/a.js b/spack/tests/pass/circular/mixed/input/a.js new file mode 100644 index 00000000000..c9b903da7d9 --- /dev/null +++ b/spack/tests/pass/circular/mixed/input/a.js @@ -0,0 +1,9 @@ +import { B } from './b' +import './c'; + +export class A { + method() { + return new B(); + } +} + diff --git a/spack/tests/pass/circular/mixed/input/b.js b/spack/tests/pass/circular/mixed/input/b.js new file mode 100644 index 00000000000..5eed81df897 --- /dev/null +++ b/spack/tests/pass/circular/mixed/input/b.js @@ -0,0 +1,6 @@ +import { A } from "./a"; +import './c'; + +export class B extends A { + +} \ No newline at end of file diff --git a/spack/tests/pass/circular/mixed/input/c.js b/spack/tests/pass/circular/mixed/input/c.js new file mode 100644 index 00000000000..fef60a588ac --- /dev/null +++ b/spack/tests/pass/circular/mixed/input/c.js @@ -0,0 +1 @@ +console.log('c'); \ No newline at end of file diff --git a/spack/tests/pass/circular/mixed/input/entry.js b/spack/tests/pass/circular/mixed/input/entry.js new file mode 100644 index 00000000000..0154d355cfb --- /dev/null +++ b/spack/tests/pass/circular/mixed/input/entry.js @@ -0,0 +1,4 @@ +import { A } from './a'; +import { B } from './b'; + +console.log(A, B); \ No newline at end of file diff --git a/spack/tests/pass/circular/mixed/output/entry.js b/spack/tests/pass/circular/mixed/output/entry.js new file mode 100644 index 00000000000..a2d26eee95d --- /dev/null +++ b/spack/tests/pass/circular/mixed/output/entry.js @@ -0,0 +1,9 @@ +class A { + method() { + return new B(); + } +} +class B extends A { +} +console.log('c'); +console.log(A, B); diff --git a/spack/tests/pass/circular/simple/input/a.js b/spack/tests/pass/circular/simple/input/a.js new file mode 100644 index 00000000000..0e91128a205 --- /dev/null +++ b/spack/tests/pass/circular/simple/input/a.js @@ -0,0 +1,8 @@ +import { B } from './b' + +export class A { + method() { + return new B(); + } +} + diff --git a/spack/tests/pass/circular/simple/input/b.js b/spack/tests/pass/circular/simple/input/b.js new file mode 100644 index 00000000000..41114a161bc --- /dev/null +++ b/spack/tests/pass/circular/simple/input/b.js @@ -0,0 +1,5 @@ +import { A } from "./a"; + +export class B extends A { + +} \ No newline at end of file diff --git a/spack/tests/pass/circular/simple/input/entry.js b/spack/tests/pass/circular/simple/input/entry.js new file mode 100644 index 00000000000..0154d355cfb --- /dev/null +++ b/spack/tests/pass/circular/simple/input/entry.js @@ -0,0 +1,4 @@ +import { A } from './a'; +import { B } from './b'; + +console.log(A, B); \ No newline at end of file diff --git a/spack/tests/pass/circular/simple/output/entry.js b/spack/tests/pass/circular/simple/output/entry.js new file mode 100644 index 00000000000..a531fd4867d --- /dev/null +++ b/spack/tests/pass/circular/simple/output/entry.js @@ -0,0 +1,8 @@ +class A { + method() { + return new B(); + } +} +class B extends A { +} +console.log(A, B); diff --git a/spack/tests/pass/circular/top-level-idents/input/a.js b/spack/tests/pass/circular/top-level-idents/input/a.js new file mode 100644 index 00000000000..ab32b6fce10 --- /dev/null +++ b/spack/tests/pass/circular/top-level-idents/input/a.js @@ -0,0 +1,3 @@ +import './b'; + +console.log('a'); \ No newline at end of file diff --git a/spack/tests/pass/circular/top-level-idents/input/b.js b/spack/tests/pass/circular/top-level-idents/input/b.js new file mode 100644 index 00000000000..e75d9bcbc49 --- /dev/null +++ b/spack/tests/pass/circular/top-level-idents/input/b.js @@ -0,0 +1,3 @@ +import './c'; + +console.log('b'); \ No newline at end of file diff --git a/spack/tests/pass/circular/top-level-idents/input/c.js b/spack/tests/pass/circular/top-level-idents/input/c.js new file mode 100644 index 00000000000..0dea3a930e0 --- /dev/null +++ b/spack/tests/pass/circular/top-level-idents/input/c.js @@ -0,0 +1,3 @@ +import './a'; + +console.log('c'); \ No newline at end of file diff --git a/spack/tests/pass/circular/top-level-idents/input/entry.js b/spack/tests/pass/circular/top-level-idents/input/entry.js new file mode 100644 index 00000000000..a8106067e71 --- /dev/null +++ b/spack/tests/pass/circular/top-level-idents/input/entry.js @@ -0,0 +1,3 @@ +import './a'; + +console.log('entry'); \ No newline at end of file diff --git a/spack/tests/pass/circular/top-level-idents/output/entry.js b/spack/tests/pass/circular/top-level-idents/output/entry.js new file mode 100644 index 00000000000..3330e73d33d --- /dev/null +++ b/spack/tests/pass/circular/top-level-idents/output/entry.js @@ -0,0 +1,4 @@ +console.log('a'); +console.log('b'); +console.log('c'); +console.log('entry'); diff --git a/spack/tests/pass/dynamic-import/namespace/dynamic-key/input/entry.js b/spack/tests/pass/dynamic-import/namespace/dynamic-key/input/entry.js index cc2700fda4e..89178c08efd 100644 --- a/spack/tests/pass/dynamic-import/namespace/dynamic-key/input/entry.js +++ b/spack/tests/pass/dynamic-import/namespace/dynamic-key/input/entry.js @@ -2,4 +2,4 @@ function foo() { return Math.random() > 0.5 ? 'a' : 'b' } -import(`./lib/${foo()}`) \ No newline at end of file +import(`./lib/${foo()}`); \ No newline at end of file diff --git a/spack/tests/pass/swcrc/jsx/issue-884/input/entry.jsx b/spack/tests/pass/swcrc/jsx/issue-884/input/entry.jsx index acdffd7d7d3..c26b478e762 100644 --- a/spack/tests/pass/swcrc/jsx/issue-884/input/entry.jsx +++ b/spack/tests/pass/swcrc/jsx/issue-884/input/entry.jsx @@ -1,4 +1,4 @@ /** * Don't ask why it is named as divider. */ -export const Divider =
\ No newline at end of file +export const Divider =
; diff --git a/spack/tests/pass/swcrc/jsx/issue-884/output/entry.jsx b/spack/tests/pass/swcrc/jsx/issue-884/output/entry.jsx index 5f95f3bde1d..fa258e3b0fb 100644 --- a/spack/tests/pass/swcrc/jsx/issue-884/output/entry.jsx +++ b/spack/tests/pass/swcrc/jsx/issue-884/output/entry.jsx @@ -1,4 +1,4 @@ /** * Don't ask why it is named as divider. */ -export var Divider = React.createElement("div", null); +export const Divider = React.createElement("div", null); diff --git a/spack/tests/pass/tree-shaking/in-module/output/entry.js b/spack/tests/pass/tree-shaking/in-module/output/entry.js index 93f63904e26..e00b68aa31a 100644 --- a/spack/tests/pass/tree-shaking/in-module/output/entry.js +++ b/spack/tests/pass/tree-shaking/in-module/output/entry.js @@ -1,2 +1 @@ -let a = 2; -console.log(a); +console.log(2); diff --git a/src/lib.rs b/src/lib.rs index 6e2b73d38c4..0bb9953e6c5 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -54,6 +54,10 @@ pub struct TransformOutput { /// These are **low-level** apis. impl Compiler { + pub fn globals(&self) -> &Globals { + &self.globals + } + pub fn comments(&self) -> &SwcComments { &self.comments }