From fa756a1b4819e4f2966f152204afd91b80450cec Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=EA=B0=95=EB=8F=99=EC=9C=A4?= Date: Fri, 18 Sep 2020 18:30:02 +0900 Subject: [PATCH] bundler: Handle export * properly (#1083) --- bundler/Cargo.toml | 2 +- bundler/src/bundler/chunk/export.rs | 61 +++++++++-- bundler/src/bundler/chunk/merge.rs | 3 + spack/tests/pass/deno-002/full/input/.swcrc | 9 ++ .../deno-002/full/input/async/deferred.ts | 26 +++++ .../pass/deno-002/full/input/async/delay.ts | 9 ++ .../pass/deno-002/full/input/async/mod.ts | 5 + .../full/input/async/mux_async_iterator.ts | 69 ++++++++++++ .../pass/deno-002/full/input/async/pool.ts | 46 ++++++++ spack/tests/pass/deno-002/full/input/entry.js | 1 + .../tests/pass/deno-002/full/output/entry.js | 101 ++++++++++++++++++ 11 files changed, 321 insertions(+), 11 deletions(-) create mode 100644 spack/tests/pass/deno-002/full/input/.swcrc create mode 100644 spack/tests/pass/deno-002/full/input/async/deferred.ts create mode 100644 spack/tests/pass/deno-002/full/input/async/delay.ts create mode 100644 spack/tests/pass/deno-002/full/input/async/mod.ts create mode 100644 spack/tests/pass/deno-002/full/input/async/mux_async_iterator.ts create mode 100644 spack/tests/pass/deno-002/full/input/async/pool.ts create mode 100644 spack/tests/pass/deno-002/full/input/entry.js create mode 100644 spack/tests/pass/deno-002/full/output/entry.js diff --git a/bundler/Cargo.toml b/bundler/Cargo.toml index 663ffd5ff582..8bf36bba6ff8 100644 --- a/bundler/Cargo.toml +++ b/bundler/Cargo.toml @@ -6,7 +6,7 @@ edition = "2018" license = "Apache-2.0/MIT" name = "swc_bundler" repository = "https://github.com/swc-project/swc.git" -version = "0.7.3" +version = "0.7.4" # See more keys and their definitions at https://doc.rust-lang.org/cargo/reference/manifest.html [features] diff --git a/bundler/src/bundler/chunk/export.rs b/bundler/src/bundler/chunk/export.rs index c5a838d5be1b..a098552bc132 100644 --- a/bundler/src/bundler/chunk/export.rs +++ b/bundler/src/bundler/chunk/export.rs @@ -126,8 +126,19 @@ where .collect::>(); { - let mut decls = vec![]; + let mut normal_reexports = vec![]; + let mut star_reexports = vec![]; for (src, mut specifiers) in additional_modules { + // If a dependency is indirect, we need to export items from it manually. + let is_indirect = !nomral_plan.chunks.contains(&src.module_id); + + let add_to = if specifiers.is_empty() && is_indirect { + // User provided code like `export * from './foo';`, but planner decide to merge + // it within dependency module. So we reexport them using a named export. + &mut star_reexports + } else { + &mut normal_reexports + }; if specifiers.is_empty() { // let dep = self.scope.get_module(src.module_id).unwrap(); @@ -146,26 +157,56 @@ where unimplemented!("namespaced re-export: local={:?}, all={}", local, all) } }; - let var = VarDeclarator { - span: DUMMY_SP, - name: Pat::Ident(imported), - init: Some(Box::new(Expr::Ident(exported))), - definite: false, - }; - decls.push(var); + + add_to.push((imported, exported)); } } - if !decls.is_empty() { + if !normal_reexports.is_empty() { entry .body .push(ModuleItem::Stmt(Stmt::Decl(Decl::Var(VarDecl { span: DUMMY_SP, kind: VarDeclKind::Const, declare: false, - decls, + decls: normal_reexports + .into_iter() + .map(|(imported, exported)| { + let var = VarDeclarator { + span: DUMMY_SP, + name: Pat::Ident(imported), + init: Some(Box::new(Expr::Ident(exported))), + definite: false, + }; + + var + }) + .collect(), })))); } + + if !star_reexports.is_empty() { + entry + .body + .push(ModuleItem::ModuleDecl(ModuleDecl::ExportNamed( + NamedExport { + span: DUMMY_SP, + specifiers: star_reexports + .into_iter() + .map(|(imported, exported)| { + ExportNamedSpecifier { + span: DUMMY_SP, + orig: exported.clone(), + exported: Some(imported.clone()), + } + .into() + }) + .collect(), + src: None, + type_only: false, + }, + ))); + } } for dep in deps { diff --git a/bundler/src/bundler/chunk/merge.rs b/bundler/src/bundler/chunk/merge.rs index 255301646cfd..6cd4e5ca7e33 100644 --- a/bundler/src/bundler/chunk/merge.rs +++ b/bundler/src/bundler/chunk/merge.rs @@ -326,6 +326,7 @@ where // } if is_entry { + // print_hygiene("done", &self.cm, &entry); self.finalize_merging_of_entry(plan, &mut entry); } @@ -336,6 +337,8 @@ where fn finalize_merging_of_entry(&self, plan: &Plan, entry: &mut Module) { entry.body.retain_mut(|item| { match item { + ModuleItem::ModuleDecl(ModuleDecl::ExportAll(..)) => return false, + ModuleItem::ModuleDecl(ModuleDecl::ExportNamed(export)) => { export.src = None; } diff --git a/spack/tests/pass/deno-002/full/input/.swcrc b/spack/tests/pass/deno-002/full/input/.swcrc new file mode 100644 index 000000000000..fc5920ef248b --- /dev/null +++ b/spack/tests/pass/deno-002/full/input/.swcrc @@ -0,0 +1,9 @@ +{ + "jsc": { + "target": "es2020", + "parser": { + "syntax": "typescript", + "decorators": true + } + } +} diff --git a/spack/tests/pass/deno-002/full/input/async/deferred.ts b/spack/tests/pass/deno-002/full/input/async/deferred.ts new file mode 100644 index 000000000000..109a1a37e336 --- /dev/null +++ b/spack/tests/pass/deno-002/full/input/async/deferred.ts @@ -0,0 +1,26 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +// TODO(ry) It'd be better to make Deferred a class that inherits from +// Promise, rather than an interface. This is possible in ES2016, however +// typescript produces broken code when targeting ES5 code. +// See https://github.com/Microsoft/TypeScript/issues/15202 +// At the time of writing, the github issue is closed but the problem remains. +export interface Deferred extends Promise { + resolve: (value?: T | PromiseLike) => void; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + reject: (reason?: any) => void; +} + +/** Creates a Promise with the `reject` and `resolve` functions + * placed as methods on the promise object itself. It allows you to do: + * + * const p = deferred(); + * // ... + * p.resolve(42); + */ +export function deferred(): Deferred { + let methods; + const promise = new Promise((resolve, reject): void => { + methods = { resolve, reject }; + }); + return Object.assign(promise, methods) as Deferred; +} diff --git a/spack/tests/pass/deno-002/full/input/async/delay.ts b/spack/tests/pass/deno-002/full/input/async/delay.ts new file mode 100644 index 000000000000..e3aec368f53d --- /dev/null +++ b/spack/tests/pass/deno-002/full/input/async/delay.ts @@ -0,0 +1,9 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +/* Resolves after the given number of milliseconds. */ +export function delay(ms: number): Promise { + return new Promise((res): number => + setTimeout((): void => { + res(); + }, ms) + ); +} diff --git a/spack/tests/pass/deno-002/full/input/async/mod.ts b/spack/tests/pass/deno-002/full/input/async/mod.ts new file mode 100644 index 000000000000..4dc0b5eabb06 --- /dev/null +++ b/spack/tests/pass/deno-002/full/input/async/mod.ts @@ -0,0 +1,5 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +export * from "./deferred"; +export * from "./delay"; +export * from "./mux_async_iterator"; +export * from "./pool"; diff --git a/spack/tests/pass/deno-002/full/input/async/mux_async_iterator.ts b/spack/tests/pass/deno-002/full/input/async/mux_async_iterator.ts new file mode 100644 index 000000000000..f9f110e5a80c --- /dev/null +++ b/spack/tests/pass/deno-002/full/input/async/mux_async_iterator.ts @@ -0,0 +1,69 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +import { Deferred, deferred } from "./deferred.ts"; + +interface TaggedYieldedValue { + iterator: AsyncIterableIterator; + value: T; +} + +/** The MuxAsyncIterator class multiplexes multiple async iterators into a + * single stream. It currently makes an assumption: + * - The final result (the value returned and not yielded from the iterator) + * does not matter; if there is any, it is discarded. + */ +export class MuxAsyncIterator implements AsyncIterable { + private iteratorCount = 0; + private yields: Array> = []; + // eslint-disable-next-line @typescript-eslint/no-explicit-any + private throws: any[] = []; + private signal: Deferred = deferred(); + + add(iterator: AsyncIterableIterator): void { + ++this.iteratorCount; + this.callIteratorNext(iterator); + } + + private async callIteratorNext( + iterator: AsyncIterableIterator, + ): Promise { + try { + const { value, done } = await iterator.next(); + if (done) { + --this.iteratorCount; + } else { + this.yields.push({ iterator, value }); + } + } catch (e) { + this.throws.push(e); + } + this.signal.resolve(); + } + + async *iterate(): AsyncIterableIterator { + while (this.iteratorCount > 0) { + // Sleep until any of the wrapped iterators yields. + await this.signal; + + // Note that while we're looping over `yields`, new items may be added. + for (let i = 0; i < this.yields.length; i++) { + const { iterator, value } = this.yields[i]; + yield value; + this.callIteratorNext(iterator); + } + + if (this.throws.length) { + for (const e of this.throws) { + throw e; + } + this.throws.length = 0; + } + // Clear the `yields` list and reset the `signal` promise. + this.yields.length = 0; + this.signal = deferred(); + } + } + + [Symbol.asyncIterator](): AsyncIterableIterator { + return this.iterate(); + } +} diff --git a/spack/tests/pass/deno-002/full/input/async/pool.ts b/spack/tests/pass/deno-002/full/input/async/pool.ts new file mode 100644 index 000000000000..fc85170414cc --- /dev/null +++ b/spack/tests/pass/deno-002/full/input/async/pool.ts @@ -0,0 +1,46 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. + +/** + * pooledMap transforms values from an (async) iterable into another async + * iterable. The transforms are done concurrently, with a max concurrency + * defined by the poolLimit. + * + * @param poolLimit The maximum count of items being processed concurrently. + * @param array The input array for mapping. + * @param iteratorFn The function to call for every item of the array. + */ +export function pooledMap( + poolLimit: number, + array: Iterable | AsyncIterable, + iteratorFn: (data: T) => Promise, +): AsyncIterableIterator { + // Create the async iterable that is returned from this function. + const res = new TransformStream, R>({ + async transform( + p: Promise, + controller: TransformStreamDefaultController, + ): Promise { + controller.enqueue(await p); + }, + }); + // Start processing items from the iterator + (async (): Promise => { + const writer = res.writable.getWriter(); + const executing: Array> = []; + for await (const item of array) { + const p = Promise.resolve().then(() => iteratorFn(item)); + writer.write(p); + const e: Promise = p.then(() => + executing.splice(executing.indexOf(e), 1) + ); + executing.push(e); + if (executing.length >= poolLimit) { + await Promise.race(executing); + } + } + // Wait until all ongoing events have processed, then close the writer. + await Promise.all(executing); + writer.close(); + })(); + return res.readable.getIterator(); +} diff --git a/spack/tests/pass/deno-002/full/input/entry.js b/spack/tests/pass/deno-002/full/input/entry.js new file mode 100644 index 000000000000..9ecb78292266 --- /dev/null +++ b/spack/tests/pass/deno-002/full/input/entry.js @@ -0,0 +1 @@ +export * from './async/mod' \ No newline at end of file diff --git a/spack/tests/pass/deno-002/full/output/entry.js b/spack/tests/pass/deno-002/full/output/entry.js new file mode 100644 index 000000000000..bbe81e76b56a --- /dev/null +++ b/spack/tests/pass/deno-002/full/output/entry.js @@ -0,0 +1,101 @@ +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +/* Resolves after the given number of milliseconds. */ export function delay(ms) { + return new Promise((res)=>setTimeout(()=>{ + res(); + }, ms) + ); +} +function deferred1() { + let methods; + const promise = new Promise((resolve, reject)=>{ + }); + return Object.assign(promise, methods); +} +var tmp = Symbol.asyncIterator; +/** The MuxAsyncIterator class multiplexes multiple async iterators into a + * single stream. It currently makes an assumption: + * - The final result (the value returned and not yielded from the iterator) + * does not matter; if there is any, it is discarded. + */ export class MuxAsyncIterator { + add(iterator) { + ++this.iteratorCount; + this.callIteratorNext(iterator); + } + async callIteratorNext(iterator) { + try { + const { value , done } = await iterator.next(); + if (done) --this.iteratorCount; + else this.yields.push({ + iterator, + value + }); + } catch (e) { + this.throws.push(e); + } + this.signal.resolve(); + } + async *iterate() { + while(this.iteratorCount > 0){ + // Sleep until any of the wrapped iterators yields. + await this.signal; + // Note that while we're looping over `yields`, new items may be added. + for(let i = 0; i < this.yields.length; i++){ + const { iterator , value } = this.yields[i]; + yield value; + this.callIteratorNext(iterator); + } + if (this.throws.length) { + for (const e of this.throws)throw e; + this.throws.length = 0; + } + // Clear the `yields` list and reset the `signal` promise. + this.yields.length = 0; + this.signal = deferred1(); + } + } + [tmp]() { + return this.iterate(); + } + constructor(){ + this.iteratorCount = 0; + this.yields = []; + this.throws = []; + this.signal = deferred1(); + } +} +// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license. +/** + * pooledMap transforms values from an (async) iterable into another async + * iterable. The transforms are done concurrently, with a max concurrency + * defined by the poolLimit. + * + * @param poolLimit The maximum count of items being processed concurrently. + * @param array The input array for mapping. + * @param iteratorFn The function to call for every item of the array. + */ export function pooledMap(poolLimit, array, iteratorFn) { + // Create the async iterable that is returned from this function. + const res = new TransformStream({ + async transform (p, controller) { + controller.enqueue(await p); + } + }); + // Start processing items from the iterator + (async ()=>{ + const writer = res.writable.getWriter(); + const executing = []; + for await (const item of array){ + const p = Promise.resolve().then(()=>iteratorFn(item) + ); + writer.write(p); + const e = p.then(()=>executing.splice(executing.indexOf(e), 1) + ); + executing.push(e); + if (executing.length >= poolLimit) await Promise.race(executing); + } + // Wait until all ongoing events have processed, then close the writer. + await Promise.all(executing); + writer.close(); + })(); + return res.readable.getIterator(); +} +export { deferred1 as deferred };