diff --git a/.github/ISSUE_TEMPLATE/crash_report.md b/.github/ISSUE_TEMPLATE/crash_report.md index eb5d332c2d7c..1415bbeee260 100644 --- a/.github/ISSUE_TEMPLATE/crash_report.md +++ b/.github/ISSUE_TEMPLATE/crash_report.md @@ -13,3 +13,7 @@ assignees: '' 3. Error message gained from `swc --sync ` (--sync is required to get panic message) + +4. Backtrace + +(You can get it by invoking swc by setting an environment varaible`SWC_DEBUG` to `1`, invoke swc like `SWC_DEBUG=1 npx swc foo.js` on linux / darwin) \ No newline at end of file diff --git a/common/Cargo.toml b/common/Cargo.toml index e551df0f9f2f..1a59e9e7fbf5 100644 --- a/common/Cargo.toml +++ b/common/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "swc_common" -version = "0.5.13" +version = "0.5.14" authors = ["강동윤 "] license = "Apache-2.0/MIT" repository = "https://github.com/swc-project/swc.git" diff --git a/ecmascript/transforms/src/macros.rs b/ecmascript/transforms/src/macros.rs index 9fe60d5385f9..6c7c95ffedb7 100644 --- a/ecmascript/transforms/src/macros.rs +++ b/ecmascript/transforms/src/macros.rs @@ -240,6 +240,7 @@ macro_rules! validate { }}; } +#[macro_export] macro_rules! noop_fold_type { ($F:ty, $N:tt) => { impl Fold for $F { @@ -316,6 +317,7 @@ macro_rules! noop_fold_type { }; } +#[macro_export] macro_rules! noop_visit_type { ($F:ty, $N:tt) => { impl Visit for $F { diff --git a/native/Cargo.toml b/native/Cargo.toml index 262b0504d1e6..2bf6d09e3d6c 100644 --- a/native/Cargo.toml +++ b/native/Cargo.toml @@ -18,6 +18,7 @@ neon-build = "0.4.0" [dependencies] swc = { path = "../" } spack = { path = "../spack" } +backtrace = "0.3" fxhash = "0.2" anyhow = "1" serde_json = "1" diff --git a/native/src/bundle.rs b/native/src/bundle.rs index f90cd6164edb..2720e59a7bb4 100644 --- a/native/src/bundle.rs +++ b/native/src/bundle.rs @@ -4,12 +4,14 @@ use fxhash::FxHashMap; use neon::prelude::*; use serde::Deserialize; use spack::{ - config::EntryConfig, load::Load, resolve::{NodeResolver, Resolve}, BundleKind, }; -use std::{path::PathBuf, sync::Arc}; +use std::{ + panic::{catch_unwind, AssertUnwindSafe}, + sync::Arc, +}; use swc::{config::SourceMapsConfig, Compiler, TransformOutput}; struct ConfigItem { @@ -38,77 +40,73 @@ impl Task for BundleTask { type JsEvent = JsValue; fn perform(&self) -> Result { - let working_dir = PathBuf::from(self.config.static_items.working_dir.clone()); - let bundler = spack::Bundler::new( - working_dir.clone(), - 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, - &self.config.loader, - ); - let entries = match &self.config.static_items.config.entry { - EntryConfig::File(name) => { - let mut m = FxHashMap::default(); - m.insert(name.clone(), working_dir.join(name)); - m - } - EntryConfig::Multiple(files) => { - let mut m = FxHashMap::default(); - for name in files { - m.insert(name.clone(), working_dir.join(name)); - } - m - } - EntryConfig::Files(v) => v + 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, + &self.config.loader, + ); + + let result = bundler.bundle(&self.config.static_items.config)?; + + let result = result .into_iter() - .map(|(k, v)| (k.clone(), working_dir.clone().join(v))) - .collect(), + .map(|bundle| match bundle.kind { + BundleKind::Named { name } | BundleKind::Lib { name } => { + Ok((name, bundle.module)) + } + BundleKind::Dynamic => bail!("unimplemented: dynamic code splitting"), + }) + .map(|res| { + res.and_then(|(k, m)| { + // TODO: Source map + let minify = self + .config + .static_items + .config + .options + .as_ref() + .map(|v| { + v.config + .as_ref() + .map(|v| v.minify) + .flatten() + .unwrap_or(false) + }) + .unwrap_or(false); + + let output = + self.swc + .print(&m, SourceMapsConfig::Bool(true), None, minify)?; + + Ok((k, output)) + }) + }) + .collect::>()?; + + Ok(result) + })); + + let err = match res { + Ok(v) => return v, + Err(err) => err, }; - let result = bundler.bundle(entries)?; - - let result = result - .into_iter() - .map(|bundle| match bundle.kind { - BundleKind::Named { name } | BundleKind::Lib { name } => Ok((name, bundle.module)), - BundleKind::Dynamic => bail!("unimplemented: dynamic code splitting"), - }) - .map(|res| { - res.and_then(|(k, m)| { - // TODO: Source map - let minify = self - .config - .static_items - .config - .options - .as_ref() - .map(|v| { - v.config - .as_ref() - .map(|v| v.minify) - .flatten() - .unwrap_or(false) - }) - .unwrap_or(false); - - let output = self - .swc - .print(&m, SourceMapsConfig::Bool(true), None, minify)?; - - Ok((k, output)) - }) - }) - .collect::>()?; + if let Some(s) = err.downcast_ref::() { + bail!("panic detected: {}", s); + } - Ok(result) + bail!("panic detected") } fn complete( diff --git a/native/src/lib.rs b/native/src/lib.rs index fedc2faec1df..3d4405f8fbf4 100644 --- a/native/src/lib.rs +++ b/native/src/lib.rs @@ -10,9 +10,12 @@ extern crate serde; extern crate swc; use anyhow::{Context as _, Error}; +use backtrace::Backtrace; use neon::prelude::*; use path_clean::clean; use std::{ + env, + panic::set_hook, path::{Path, PathBuf}, sync::Arc, }; @@ -26,6 +29,13 @@ use swc::{ mod bundle; fn init(_cx: MethodContext) -> NeonResult { + if cfg!(debug_assertions) || env::var("SWC_DEBUG").unwrap_or_else(|_| String::new()) == "1" { + set_hook(Box::new(|_panic_info| { + let backtrace = Backtrace::new(); + println!("Backtrace: {:?}", backtrace); + })); + } + let cm = Arc::new(SourceMap::new(FilePathMapping::empty())); let handler = Arc::new(Handler::with_tty_emitter( diff --git a/node-swc/__tests__/spack/config_test.js b/node-swc/__tests__/spack/config_test.js index 3512a6406300..f93c4280233e 100644 --- a/node-swc/__tests__/spack/config_test.js +++ b/node-swc/__tests__/spack/config_test.js @@ -10,4 +10,14 @@ it('should handle multiple entry in spack.config.js', async () => { expect(result.b).toBeTruthy(); expect(result.b.code.replace('\n', '')).toBe(`console.log('bar');`); +}); + +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.b).toBeTruthy(); + expect(result.b.code).toContain(`require('./common-`); }); \ No newline at end of file diff --git a/node-swc/__tests__/spack/multi_entry_test.js b/node-swc/__tests__/spack/multi_entry_test.js new file mode 100644 index 000000000000..c13297b80f73 --- /dev/null +++ b/node-swc/__tests__/spack/multi_entry_test.js @@ -0,0 +1,34 @@ +const swc = require("../../.."); +const path = require('path'); + + +it('should handle multiple entries on same level', async () => { + const result = await swc.bundle({ + entry: { + a: path.join(__dirname, '../../tests/spack/mutli-entry-same-level/src/a.js'), + b: path.join(__dirname, '../../tests/spack/mutli-entry-same-level/src/b.js'), + } + }); + + expect(result.a).toBeTruthy(); + expect(result.a.code).toContain(`import { foo } from './common-`); + + expect(result.b).toBeTruthy(); + expect(result.b.code).toContain(`import { foo } from './common-`); +}); + + +it('should handle multiple entries on different level', async () => { + const result = await swc.bundle({ + entry: { + web: path.join(__dirname, '../../tests/spack/mutli-entry-different-level/src/web/index.js'), + a: path.join(__dirname, '../../tests/spack/mutli-entry-different-level/src/a.js'), + } + }); + + expect(result.a).toBeTruthy(); + expect(result.a.code).toContain(`import { foo } from './common-`); + + expect(result.web).toBeTruthy(); + expect(result.web.code).toContain(`../common`); +}); \ No newline at end of file diff --git a/node-swc/src/index.ts b/node-swc/src/index.ts index 42375c808ea8..670dc8146a38 100644 --- a/node-swc/src/index.ts +++ b/node-swc/src/index.ts @@ -221,6 +221,20 @@ export class Compiler extends wrapNativeSuper(native.Compiler) { async bundle(options?: BundleInput | string): Promise<{ [name: string]: Output }> { const opts = await compileBundleOptions(options); + if (Array.isArray(opts)) { + const all = await Promise.all(opts.map(async (opt) => { + return this.bundle(opt) + })); + let obj = {} as any; + for (const o of all) { + obj = { + ...obj, + ...o, + }; + } + return obj; + } + return new Promise((resolve, reject) => { super.bundle({ ...opts, diff --git a/node-swc/src/spack/index.ts b/node-swc/src/spack/index.ts index 1b39feed1111..bb0463035323 100644 --- a/node-swc/src/spack/index.ts +++ b/node-swc/src/spack/index.ts @@ -8,13 +8,21 @@ export async function compileBundleOptions(c: BundleInput | string | undefined): try { const file = typeof f === 'string' ? f : path.resolve('spack.config.js'); - let configFromFile = require(file); - if (configFromFile.default) { - configFromFile = configFromFile.default; + let configFromFile: BundleInput = require(file); + if ((configFromFile as any).default) { + configFromFile = (configFromFile as any).default; + } + if (Array.isArray(configFromFile)) { + if (Array.isArray(f)) { + return [...configFromFile, ...f] + } + if (typeof f !== 'string') { + configFromFile.push(f); + } + return configFromFile; } return Object.assign({}, configFromFile, c); } catch (e) { - console.log(e); if (typeof f === 'string') { throw new Error(`Config file does not exist at ${c}`) } diff --git a/node-swc/tests/spack/config-swcrc/.swcrc b/node-swc/tests/spack/config-swcrc/.swcrc new file mode 100644 index 000000000000..33ac9a40aa14 --- /dev/null +++ b/node-swc/tests/spack/config-swcrc/.swcrc @@ -0,0 +1,5 @@ +{ + "module": { + "type": "commonjs" + } +} diff --git a/node-swc/tests/spack/config-swcrc/common.js b/node-swc/tests/spack/config-swcrc/common.js new file mode 100644 index 000000000000..31047e22e600 --- /dev/null +++ b/node-swc/tests/spack/config-swcrc/common.js @@ -0,0 +1 @@ +console.log('Prevent inlinig') \ No newline at end of file diff --git a/node-swc/tests/spack/config-swcrc/entry1.js b/node-swc/tests/spack/config-swcrc/entry1.js new file mode 100644 index 000000000000..483e6d53514e --- /dev/null +++ b/node-swc/tests/spack/config-swcrc/entry1.js @@ -0,0 +1 @@ +import './common' \ No newline at end of file diff --git a/node-swc/tests/spack/config-swcrc/entry2.js b/node-swc/tests/spack/config-swcrc/entry2.js new file mode 100644 index 000000000000..483e6d53514e --- /dev/null +++ b/node-swc/tests/spack/config-swcrc/entry2.js @@ -0,0 +1 @@ +import './common' \ No newline at end of file diff --git a/node-swc/tests/spack/config-swcrc/spack.config.js b/node-swc/tests/spack/config-swcrc/spack.config.js new file mode 100644 index 000000000000..b938a77c25c1 --- /dev/null +++ b/node-swc/tests/spack/config-swcrc/spack.config.js @@ -0,0 +1,8 @@ +const path = require('path'); + +module.exports = { + entry: { + a: path.join(__dirname, 'entry1.js'), + b: path.join(__dirname, 'entry2.js'), + }, +} \ No newline at end of file diff --git a/node-swc/tests/spack/mutli-entry-different-level/src/a.js b/node-swc/tests/spack/mutli-entry-different-level/src/a.js new file mode 100644 index 000000000000..dd8f7c43c465 --- /dev/null +++ b/node-swc/tests/spack/mutli-entry-different-level/src/a.js @@ -0,0 +1,3 @@ +import { foo } from './common' + +console.log(foo) \ No newline at end of file diff --git a/node-swc/tests/spack/mutli-entry-different-level/src/common.js b/node-swc/tests/spack/mutli-entry-different-level/src/common.js new file mode 100644 index 000000000000..e116966ddd21 --- /dev/null +++ b/node-swc/tests/spack/mutli-entry-different-level/src/common.js @@ -0,0 +1,5 @@ +console.log('loading foo') +export const foo = 'FOO'; +console.log('loaded foo') + + diff --git a/node-swc/tests/spack/mutli-entry-different-level/src/web/index.js b/node-swc/tests/spack/mutli-entry-different-level/src/web/index.js new file mode 100644 index 000000000000..4dee13a4f686 --- /dev/null +++ b/node-swc/tests/spack/mutli-entry-different-level/src/web/index.js @@ -0,0 +1 @@ +import '../common'; \ No newline at end of file diff --git a/node-swc/tests/spack/mutli-entry-same-level/src/a.js b/node-swc/tests/spack/mutli-entry-same-level/src/a.js new file mode 100644 index 000000000000..dd8f7c43c465 --- /dev/null +++ b/node-swc/tests/spack/mutli-entry-same-level/src/a.js @@ -0,0 +1,3 @@ +import { foo } from './common' + +console.log(foo) \ No newline at end of file diff --git a/node-swc/tests/spack/mutli-entry-same-level/src/b.js b/node-swc/tests/spack/mutli-entry-same-level/src/b.js new file mode 100644 index 000000000000..5eb18456469c --- /dev/null +++ b/node-swc/tests/spack/mutli-entry-same-level/src/b.js @@ -0,0 +1,3 @@ +import { foo } from './common' + +console.log(foo) diff --git a/node-swc/tests/spack/mutli-entry-same-level/src/common.js b/node-swc/tests/spack/mutli-entry-same-level/src/common.js new file mode 100644 index 000000000000..e116966ddd21 --- /dev/null +++ b/node-swc/tests/spack/mutli-entry-same-level/src/common.js @@ -0,0 +1,5 @@ +console.log('loading foo') +export const foo = 'FOO'; +console.log('loaded foo') + + diff --git a/package.json b/package.json index cae2eefa8e59..4b6ba4511ce6 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "@swc/core", - "version": "1.2.2", + "version": "1.2.3", "description": "Super-fast alternative for babel", "main": "./index.js", "author": "강동윤 ", diff --git a/spack/.gitignore b/spack/.gitignore index 91d2a07c323d..8d0f61c986bf 100644 --- a/spack/.gitignore +++ b/spack/.gitignore @@ -1,3 +1,3 @@ -*.js +/*.js *.d.ts *.map \ No newline at end of file diff --git a/spack/Cargo.toml b/spack/Cargo.toml index 8182faa944c3..0260ee93cc9d 100644 --- a/spack/Cargo.toml +++ b/spack/Cargo.toml @@ -14,6 +14,7 @@ edition = "2018" swc_atoms = { path = "../atoms" } swc_common = { path = "../common" } swc_ecma_ast = { path = "../ecmascript/ast", features = ["fold"] } +swc_ecma_codegen = { path = "../ecmascript/codegen" } swc_ecma_parser = { path = "../ecmascript/parser", features = ["verify"] } swc_ecma_transforms = { path = "../ecmascript/transforms" } swc_ecma_utils = { path = "../ecmascript/utils" } @@ -22,7 +23,9 @@ string_enum = { version = "0.3", path ="../macros/string_enum" } regex = "1" serde = { version = "1", features = ["derive"] } anyhow = "1" +crc = "1.8" dashmap = "=3.5.1" +radix_fmt = "1" rayon = "1" log = "0.4.8" node-resolve = "2.2.0" @@ -31,6 +34,7 @@ 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/chunk/merge.rs b/spack/src/bundler/chunk/merge.rs index 49f6a264d086..ec1b1ac9c091 100644 --- a/spack/src/bundler/chunk/merge.rs +++ b/spack/src/bundler/chunk/merge.rs @@ -14,7 +14,7 @@ use swc_common::{ DUMMY_SP, }; use swc_ecma_ast::*; -use swc_ecma_transforms::hygiene; +use swc_ecma_transforms::{hygiene, noop_fold_type}; use swc_ecma_utils::{find_ids, DestructuringFinder, StmtLike}; impl Bundler<'_> { @@ -198,6 +198,8 @@ impl Bundler<'_> { /// `export var a = 1` => `var a = 1` struct Unexporter; +noop_fold_type!(Unexporter); + impl Fold for Unexporter { fn fold(&mut self, item: ModuleItem) -> ModuleItem { match item { @@ -230,6 +232,8 @@ 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); @@ -407,6 +411,8 @@ struct ActualMarker<'a> { imports: &'a [Specifier], } +noop_fold_type!(ActualMarker<'_>); + impl Fold for ActualMarker<'_> { fn fold(&mut self, ident: Ident) -> Ident { if let Some(mut ident) = self.imports.iter().find_map(|s| match s { @@ -444,6 +450,8 @@ struct LocalMarker<'a> { excluded: Vec, } +noop_fold_type!(LocalMarker<'_>); + impl<'a> LocalMarker<'a> { /// Searches for i, and fold T. #[allow(dead_code)] @@ -627,6 +635,8 @@ struct GlobalMarker { 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/export.rs b/spack/src/bundler/export.rs index 7127767ee845..8ddbd9c358f2 100644 --- a/spack/src/bundler/export.rs +++ b/spack/src/bundler/export.rs @@ -7,6 +7,7 @@ use fxhash::FxHashMap; use swc_atoms::js_word; use swc_common::{SyntaxContext, Visit, VisitWith}; use swc_ecma_ast::*; +use swc_ecma_transforms::noop_visit_type; use swc_ecma_utils::find_ids; impl Bundler<'_> { @@ -47,6 +48,8 @@ struct ExportFinder { info: RawExports, } +noop_visit_type!(ExportFinder); + impl Visit for ExportFinder { fn visit(&mut self, item: &ModuleItem) { match item { diff --git a/spack/src/bundler/import/mod.rs b/spack/src/bundler/import/mod.rs index b96f4613f9a2..112d0fceb629 100644 --- a/spack/src/bundler/import/mod.rs +++ b/spack/src/bundler/import/mod.rs @@ -9,6 +9,7 @@ use std::{ use swc_atoms::{js_word, JsWord}; use swc_common::{util::move_map::MoveMap, Fold, FoldWith, Mark, DUMMY_SP}; use swc_ecma_ast::*; +use swc_ecma_transforms::noop_fold_type; use swc_ecma_utils::{find_ids, ident::IdentLike, Id}; #[cfg(test)] @@ -96,6 +97,8 @@ struct ImportHandler<'a, 'b> { deglob_phase: bool, } +noop_fold_type!(ImportHandler<'_, '_>); + impl ImportHandler<'_, '_> { fn mark_for(&self, src: &str) -> Option { let path = self.bundler.resolve(self.path, src).ok()?; diff --git a/spack/src/bundler/load_transformed/mod.rs b/spack/src/bundler/load_transformed/mod.rs index c9fd00442d9d..a775c0efb2ce 100644 --- a/spack/src/bundler/load_transformed/mod.rs +++ b/spack/src/bundler/load_transformed/mod.rs @@ -222,8 +222,6 @@ impl Bundler<'_> { || { let p = match fm.name { FileName::Real(ref p) => p, - // stdin compilation - FileName::Anon => &self.working_dir, _ => unreachable!("{} module in spack", fm.name), }; diff --git a/spack/src/bundler/mod.rs b/spack/src/bundler/mod.rs index 9f420c210a38..0f85c31cc3a6 100644 --- a/spack/src/bundler/mod.rs +++ b/spack/src/bundler/mod.rs @@ -1,11 +1,16 @@ use self::scope::Scope; use crate::{ - bundler::load_transformed::TransformedModule, load::Load, resolve::Resolve, Config, ModuleId, + 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; @@ -13,15 +18,13 @@ mod chunk; mod export; mod import; mod load_transformed; +mod rename; mod scope; #[cfg(test)] mod tests; mod usage_analysis; pub struct Bundler<'a> { - working_dir: PathBuf, - _config: Config, - /// Javascript compiler. swc: Arc, swc_options: swc::config::Options, @@ -55,7 +58,6 @@ pub struct Bundle { impl<'a> Bundler<'a> { pub fn new( - working_dir: PathBuf, swc: Arc, mut swc_options: swc::config::Options, resolver: &'a dyn Resolve, @@ -73,9 +75,16 @@ impl<'a> Bundler<'a> { 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 { - working_dir, - _config: Config { tree_shake: true }, swc, swc_options, loader, @@ -86,11 +95,28 @@ impl<'a> Bundler<'a> { } } - pub fn bundle(&self, entries: FxHashMap) -> Result, Error> { + 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(&self.working_dir, &path.to_string_lossy())?; + let path = self.resolve(&config.working_dir, &path.to_string_lossy())?; let res = self .load_transformed(path) .context("load_transformed failed")?; @@ -113,7 +139,9 @@ impl<'a> Bundler<'a> { Ok(output) })?; - self.chunk(local) + let bundles = self.chunk(local)?; + + Ok(self.rename(bundles)?) } pub fn swc(&self) -> &swc::Compiler { diff --git a/spack/src/bundler/rename.rs b/spack/src/bundler/rename.rs new file mode 100644 index 000000000000..80d9aef0f08a --- /dev/null +++ b/spack/src/bundler/rename.rs @@ -0,0 +1,262 @@ +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, Fold, FoldWith, Span}; +use swc_ecma_ast::{ImportDecl, Module, Str}; +use swc_ecma_codegen::{text_writer::WriteJs, Emitter}; +use swc_ecma_transforms::noop_fold_type; + +impl Bundler<'_> { + pub(super) fn rename(&self, bundles: Vec) -> Result, Error> { + let mut new = Vec::with_capacity(bundles.len()); + let mut renamed = FxHashMap::default(); + + for bundle in bundles { + match bundle.kind { + 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 &mut buf as Box, + handlers: box 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(&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/tests.rs b/spack/src/bundler/tests.rs index 7d8a0557e82b..41d86ba0ff50 100644 --- a/spack/src/bundler/tests.rs +++ b/spack/src/bundler/tests.rs @@ -2,7 +2,7 @@ use super::Bundler; use crate::{loaders::swc::SwcLoader, resolve::NodeResolver, util::HygieneRemover}; use pretty_assertions::assert_eq; -use std::{env, path::PathBuf, sync::Arc}; +use std::{path::PathBuf, sync::Arc}; use swc_common::{fold::FoldWith, FileName}; use swc_ecma_ast::*; use swc_ecma_parser::{EsConfig, Syntax}; @@ -58,7 +58,6 @@ where let compiler = Arc::new(swc::Compiler::new(cm.clone(), Arc::new(handler))); let loader = SwcLoader::new(compiler.clone(), Default::default()); let bundler = Bundler::new( - env::current_dir().unwrap(), compiler.clone(), swc::config::Options { swcrc: true, diff --git a/spack/src/config/mod.rs b/spack/src/config/mod.rs index 79c775378290..fef7d0f6a4f3 100644 --- a/spack/src/config/mod.rs +++ b/spack/src/config/mod.rs @@ -16,6 +16,9 @@ mod resolve; #[derive(Debug, Deserialize)] pub struct Config { + #[serde(default)] + pub working_dir: PathBuf, + #[serde(default)] pub mode: Mode, diff --git a/spack/src/lib.rs b/spack/src/lib.rs index 7b7475e200af..10a75129d191 100644 --- a/spack/src/lib.rs +++ b/spack/src/lib.rs @@ -21,8 +21,3 @@ pub mod loaders; mod normalize; pub mod resolve; mod util; - -#[derive(Debug, Clone, PartialEq, Eq)] -pub struct Config { - pub tree_shake: bool, -} diff --git a/spack/tests/fixture.rs b/spack/tests/fixture.rs index 4d0e727b2f6c..e4f2d2701baf 100644 --- a/spack/tests/fixture.rs +++ b/spack/tests/fixture.rs @@ -6,7 +6,11 @@ extern crate test; use fxhash::FxHashMap; -use spack::{loaders::swc::SwcLoader, BundleKind, Bundler}; +use spack::{ + config::{Config, EntryConfig}, + loaders::swc::SwcLoader, + BundleKind, Bundler, +}; use std::{ env, fs::{create_dir_all, read_dir}, @@ -123,8 +127,17 @@ fn reference_tests(tests: &mut Vec, errors: bool) -> Result<(), i ..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( - env::current_dir().unwrap(), compiler.clone(), swc::config::Options { swcrc: true, @@ -134,9 +147,7 @@ fn reference_tests(tests: &mut Vec, errors: bool) -> Result<(), i &loader, ); - assert_ne!(entries.len(), 0); - - let modules = bundler.bundle(entries).expect("failed to bundle module"); + let modules = bundler.bundle(&config).expect("failed to bundle module"); log::info!("Bundled as {} modules", modules.len()); let mut error = false; diff --git a/spack/tests/pass/multi-entry/simple/output/d.js b/spack/tests/pass/multi-entry/simple/output/d-175xpbs4ddbvu.js similarity index 100% rename from spack/tests/pass/multi-entry/simple/output/d.js rename to spack/tests/pass/multi-entry/simple/output/d-175xpbs4ddbvu.js diff --git a/spack/tests/pass/multi-entry/simple/output/entry-a.js b/spack/tests/pass/multi-entry/simple/output/entry-a.js index 522507a23455..a48661bf0097 100644 --- a/spack/tests/pass/multi-entry/simple/output/entry-a.js +++ b/spack/tests/pass/multi-entry/simple/output/entry-a.js @@ -1,4 +1,4 @@ -import { foo } from './d'; +import { foo } from './d-175xpbs4ddbvu.js'; console.log('loading c.js'); function c() { foo(); diff --git a/spack/tests/pass/multi-entry/simple/output/entry-b.js b/spack/tests/pass/multi-entry/simple/output/entry-b.js index e2cfc77518ce..5712c892c374 100644 --- a/spack/tests/pass/multi-entry/simple/output/entry-b.js +++ b/spack/tests/pass/multi-entry/simple/output/entry-b.js @@ -1,2 +1,2 @@ -import { foo } from './d'; +import { foo } from './d-175xpbs4ddbvu.js'; foo(); diff --git a/src/builder.rs b/src/builder.rs index 74f5675123c7..52768de44033 100644 --- a/src/builder.rs +++ b/src/builder.rs @@ -122,7 +122,7 @@ impl<'a, 'b, P: Pass> PassBuilder<'a, 'b, P> { Some(ModuleConfig::CommonJs(ref c)) => !c.no_interop, Some(ModuleConfig::Amd(ref c)) => !c.config.no_interop, Some(ModuleConfig::Umd(ref c)) => !c.config.no_interop, - None => false, + Some(ModuleConfig::Es6) | None => false, }; // compat diff --git a/src/config.rs b/src/config.rs index 38b132a0939b..3fcfdea67a5f 100644 --- a/src/config.rs +++ b/src/config.rs @@ -530,6 +530,8 @@ pub enum ModuleConfig { Umd(modules::umd::Config), #[serde(rename = "amd")] Amd(modules::amd::Config), + #[serde(rename = "es6")] + Es6, } impl ModuleConfig { @@ -539,7 +541,7 @@ impl ModuleConfig { config: Option, ) -> Box { match config { - None => box noop(), + None | Some(ModuleConfig::Es6) => box noop(), Some(ModuleConfig::CommonJs(config)) => { box modules::common_js::common_js(root_mark, config) } diff --git a/src/lib.rs b/src/lib.rs index 3c6e7c957270..44039850dccf 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -281,23 +281,16 @@ impl Compiler { } } - /// This method handles merging of config. - /// - /// This method does **not** parse module. - pub fn config_for_file( - &self, - opts: &Options, - name: &FileName, - ) -> Result, Error> { + pub fn read_config(&self, opts: &Options, name: &FileName) -> Result { self.run(|| -> Result<_, Error> { let Options { ref root, root_mode, swcrc, config_file, - is_module, .. } = opts; + let root = root.clone().unwrap_or_else(|| { if cfg!(target_arch = "wasm32") { PathBuf::new() @@ -328,9 +321,8 @@ impl Compiler { if let Some(config_file) = config_file { config.merge(&config_file.into_config(Some(path))?) } - let built = - opts.build(&self.cm, &self.handler, *is_module, Some(config)); - return Ok(built); + + return Ok(config); } if dir == root && *root_mode == RootMode::Root { @@ -341,26 +333,32 @@ impl Compiler { } let config_file = config_file.unwrap_or_else(|| Rc::default()); - let built = opts.build( - &self.cm, - &self.handler, - *is_module, - Some(config_file.into_config(Some(path))?), - ); - return Ok(built); + let config = config_file.into_config(Some(path))?; + + return Ok(config); } _ => {} } - let built = opts.build( - &self.cm, - &self.handler, - *is_module, - match config_file { - Some(config_file) => Some(config_file.into_config(None)?), - None => Some(Rc::default().into_config(None)?), - }, - ); + Ok(match config_file { + Some(config_file) => config_file.into_config(None)?, + None => Rc::default().into_config(None)?, + }) + }) + .with_context(|| format!("failed to read swcrc file ('{:?}')", name)) + } + + /// This method handles merging of config. + /// + /// This method does **not** parse module. + pub fn config_for_file( + &self, + opts: &Options, + name: &FileName, + ) -> Result, Error> { + self.run(|| -> Result<_, Error> { + let config = self.read_config(opts, name)?; + let built = opts.build(&self.cm, &self.handler, opts.is_module, Some(config)); Ok(built) }) .with_context(|| format!("failed to load config for file '{:?}'", name))