feat(wasm): Expose async facade interfaces (#5352)

This commit is contained in:
OJ Kwon 2022-07-31 20:00:28 -07:00 committed by GitHub
parent f88a8c96ed
commit 281bdd9e97
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
6 changed files with 354 additions and 67 deletions

View File

@ -5,3 +5,4 @@ imports_granularity = "Crate"
reorder_impl_items = true
use_field_init_shorthand = true
wrap_comments = true
edition = "2018"

5
Cargo.lock generated
View File

@ -217,6 +217,7 @@ dependencies = [
"swc_plugin_runner",
"tracing",
"wasm-bindgen",
"wasm-bindgen-futures",
"wasmer",
"wasmer-wasi",
]
@ -4757,9 +4758,9 @@ dependencies = [
[[package]]
name = "wasm-bindgen-futures"
version = "0.4.28"
version = "0.4.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8e8d7523cb1f2a4c96c1317ca690031b714a51cc14e05f712446691f413f5d39"
checksum = "fa76fb221a1f8acddf5b54ace85912606980ad661ac7a503b4570ffd3a624dad"
dependencies = [
"cfg-if 1.0.0",
"js-sys",

View File

@ -25,13 +25,12 @@ plugin = [
"wasmer-wasi",
"wasmer/js-default",
"wasmer-wasi/js-default",
"js-sys",
]
[dependencies]
anyhow = "1.0.58"
console_error_panic_hook = "0.1.7"
js-sys = { version = "0.3.59", optional = true }
js-sys = { version = "0.3.59" }
once_cell = "1.13.0"
parking_lot_core = "0.9.3"
path-clean = "0.1.0"
@ -49,6 +48,7 @@ wasm-bindgen = { version = "0.2.82", features = [
"serde-serialize",
"enable-interning",
] }
wasm-bindgen-futures = "0.4.32"
wasmer = { version = "2.3.0", optional = true, default-features = false }
wasmer-wasi = { version = "2.3.0", optional = true, default-features = false }

View File

@ -1,33 +1,248 @@
const swc = require("../pkg");
it("should be loadable", function () {
const output = swc.transformSync("class Foo {}", {});
describe("transform", () => {
it("should work", function () {
const output = swc.transformSync("class Foo {}", {});
expect(output).toMatchInlineSnapshot(`
Object {
"code": "function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError(\\"Cannot call a class as a function\\");
}
}
var Foo = function Foo() {
\\"use strict\\";
_classCallCheck(this, Foo);
};
",
}
`);
});
it("should work with async facade", async () => {
const output = await swc.transform("class Foo {}", {});
expect(output).toMatchInlineSnapshot(`
Object {
"code": "function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError(\\"Cannot call a class as a function\\");
}
}
var Foo = function Foo() {
\\"use strict\\";
_classCallCheck(this, Foo);
};
",
}
`);
});
it("should work with program object", async () => {
const input = swc.parseSync("class Foo {}", {
syntax: "typescript",
target: "es2021",
});
const output = await swc.transform(input, {});
expect(output).toMatchInlineSnapshot(`
Object {
"code": "function _classCallCheck(instance, Constructor) {
if (!(instance instanceof Constructor)) {
throw new TypeError(\\"Cannot call a class as a function\\");
}
}
var Foo = function Foo() {
\\"use strict\\";
_classCallCheck(this, Foo);
};
",
}
`);
});
it("should support 'paths' and 'baseUrl'", () => {
const { code } = swc.transformSync(
`
import foo from '@src/app';
console.log(foo)
`,
{
filename: "main.js",
jsc: {
parser: {
syntax: "typescript",
},
target: "es2021",
transform: {},
baseUrl: __dirname,
paths: {
"@src/*": ["bar/*"],
},
},
module: {
type: "commonjs",
},
}
);
expect(code).toContain(`bar/app`);
});
});
it("should support 'paths' and 'baseUrl'", async () => {
const { code } = await swc.transformSync(
`
import foo from '@src/app';
console.log(foo)
`,
{
filename: "main.js",
jsc: {
parser: {
syntax: "typescript",
},
target: "es2021",
transform: {},
baseUrl: __dirname,
paths: {
"@src/*": ["bar/*"],
},
},
module: {
type: "commonjs",
},
}
);
describe("parse", () => {
it("should work", () => {
const output = swc.parseSync("class Foo {}", {
syntax: "typescript",
target: "es2021",
});
expect(code).toContain(`bar/app`);
expect(output).toMatchInlineSnapshot(`
Object {
"body": Array [
Object {
"body": Array [],
"declare": false,
"decorators": Array [],
"identifier": Object {
"optional": false,
"span": Object {
"ctxt": 0,
"end": 394,
"start": 391,
},
"type": "Identifier",
"value": "Foo",
},
"implements": Array [],
"isAbstract": false,
"span": Object {
"ctxt": 0,
"end": 397,
"start": 385,
},
"superClass": null,
"superTypeParams": null,
"type": "ClassDeclaration",
"typeParams": null,
},
],
"interpreter": null,
"span": Object {
"ctxt": 0,
"end": 397,
"start": 385,
},
"type": "Module",
}
`);
});
it("should work with async facade", async () => {
const output = await swc.parse("class Foo {}", {
syntax: "typescript",
target: "es2021",
});
expect(output).toMatchInlineSnapshot(`
Object {
"body": Array [
Object {
"body": Array [],
"declare": false,
"decorators": Array [],
"identifier": Object {
"optional": false,
"span": Object {
"ctxt": 0,
"end": 407,
"start": 404,
},
"type": "Identifier",
"value": "Foo",
},
"implements": Array [],
"isAbstract": false,
"span": Object {
"ctxt": 0,
"end": 410,
"start": 398,
},
"superClass": null,
"superTypeParams": null,
"type": "ClassDeclaration",
"typeParams": null,
},
],
"interpreter": null,
"span": Object {
"ctxt": 0,
"end": 410,
"start": 398,
},
"type": "Module",
}
`);
});
});
describe("minify", () => {
it("should work", () => {
const output = swc.minifySync(
"const somename = 1; console.log(somename);"
);
expect(output).toMatchInlineSnapshot(`
Object {
"code": "const a=1;console.log(1)",
}
`);
});
it("should work with async facade", async () => {
const output = await swc.minify(
"const somename = 1; console.log(somename);"
);
expect(output).toMatchInlineSnapshot(`
Object {
"code": "const a=1;console.log(1)",
}
`);
});
});
describe("print", () => {
it("should work", () => {
const input = swc.parseSync("class Foo {}", {
syntax: "typescript",
target: "es2021",
});
const output = swc.printSync(input);
expect(output).toMatchInlineSnapshot(`
Object {
"code": "class Foo {
}
",
}
`);
});
it("should work with async facade", async () => {
const input = swc.parseSync("class Foo {}", {
syntax: "typescript",
target: "es2021",
});
const output = await swc.print(input);
expect(output).toMatchInlineSnapshot(`
Object {
"code": "class Foo {
}
",
}
`);
});
});

View File

@ -1,9 +1,11 @@
#![allow(unused)]
#![deny(warnings)]
#![allow(clippy::unused_unit)]
use std::sync::Arc;
use anyhow::{Context, Error};
use js_sys::{JsString, JSON};
use once_cell::sync::Lazy;
use swc::{
config::{ErrorFormat, JsMinifyOptions, Options, ParseOptions, SourceMapsConfig},
@ -11,7 +13,8 @@ use swc::{
};
use swc_common::{comments::Comments, FileName, FilePathMapping, SourceMap};
use swc_ecmascript::ast::{EsVersion, Program};
use wasm_bindgen::prelude::*;
use wasm_bindgen::{prelude::*, JsCast};
use wasm_bindgen_futures::{future_to_promise, spawn_local, JsFuture};
mod types;
@ -23,24 +26,39 @@ fn convert_err(err: Error, error_format: ErrorFormat) -> JsValue {
/// auto generated one, which is not reflecting most of types in detail.
#[wasm_bindgen(typescript_custom_section)]
const INTERFACE_DEFINITIONS: &'static str = r#"
export function minify(src: string, opts?: JsMinifyOptions): Promise<Output>;
export function minifySync(code: string, opts?: JsMinifyOptions): Output;
export function parseSync(
src: string,
options: ParseOptions & { isModule: false }
): Script;
export function parse(src: string, options: ParseOptions & {
isModule: false;
}): Promise<Script>;
export function parse(src: string, options?: ParseOptions): Promise<Module>;
export function parseSync(src: string, options: ParseOptions & {
isModule: false;
}): Script;
export function parseSync(src: string, options?: ParseOptions): Module;
export function parseSync(src: string, options?: ParseOptions): Program;
export function print(m: Program, options?: Options): Promise<Output>;
export function printSync(m: Program, options?: Options): Output
/**
* @param {string} code
* @param {Options} opts
* @param {Record<string, ArrayBuffer>} experimental_plugin_bytes_resolver An object contains bytes array for the plugin
* specified in config. Key of record represents the name of the plugin specified in config. Note this is an experimental
* interface, likely will change.
* @returns {Output}
*/
export function transformSync(code: string, opts: Options, experimental_plugin_bytes_resolver?: any): Output;
* Note: this interface currently does not do _actual_ async work, only provides
* a corresponding async interfaces to the `@swc/core`'s interface.
*/
export function transform(
code: string | Program,
options?: Options,
experimental_plugin_bytes_resolver?: any
): Promise<Output>;
/**
* @param {string} code
* @param {Options} opts
* @param {Record<string, ArrayBuffer>} experimental_plugin_bytes_resolver An object contains bytes array for the plugin
* specified in config. Key of record represents the name of the plugin specified in config. Note this is an experimental
* interface, likely will change.
* @returns {Output}
*/
export function transformSync(code: string | Program, opts?: Options, experimental_plugin_bytes_resolver?: any): Output;
"#;
#[wasm_bindgen(
@ -48,7 +66,7 @@ export function transformSync(code: string, opts: Options, experimental_plugin_b
typescript_type = "minifySync",
skip_typescript
)]
pub fn minify_sync(s: &str, opts: JsValue) -> Result<JsValue, JsValue> {
pub fn minify_sync(s: JsString, opts: JsValue) -> Result<JsValue, JsValue> {
console_error_panic_hook::set_once();
let c = compiler();
@ -60,7 +78,11 @@ pub fn minify_sync(s: &str, opts: JsValue) -> Result<JsValue, JsValue> {
},
|handler| {
c.run(|| {
let opts: JsMinifyOptions = opts.into_serde().context("failed to parse options")?;
let opts = if opts.is_null() || opts.is_undefined() {
Default::default()
} else {
opts.into_serde().context("failed to parse options")?
};
let fm = c.cm.new_source_file(FileName::Anon, s.into());
let program = c
@ -74,8 +96,15 @@ pub fn minify_sync(s: &str, opts: JsValue) -> Result<JsValue, JsValue> {
.map_err(|e| convert_err(e, ErrorFormat::Normal))
}
#[wasm_bindgen(js_name = "minify", typescript_type = "minify", skip_typescript)]
pub fn minify(s: JsString, opts: JsValue) -> js_sys::Promise {
// TODO: This'll be properly scheduled once wasm have standard backed thread
// support.
future_to_promise(async { minify_sync(s, opts) })
}
#[wasm_bindgen(js_name = "parseSync", typescript_type = "parseSync", skip_typescript)]
pub fn parse_sync(s: &str, opts: JsValue) -> Result<JsValue, JsValue> {
pub fn parse_sync(s: JsString, opts: JsValue) -> Result<JsValue, JsValue> {
console_error_panic_hook::set_once();
let c = compiler();
@ -87,7 +116,11 @@ pub fn parse_sync(s: &str, opts: JsValue) -> Result<JsValue, JsValue> {
},
|handler| {
c.run(|| {
let opts: ParseOptions = opts.into_serde().context("failed to parse options")?;
let opts: ParseOptions = if opts.is_null() || opts.is_undefined() {
Default::default()
} else {
opts.into_serde().context("failed to parse options")?
};
let fm = c.cm.new_source_file(FileName::Anon, s.into());
@ -116,6 +149,13 @@ pub fn parse_sync(s: &str, opts: JsValue) -> Result<JsValue, JsValue> {
.map_err(|e| convert_err(e, ErrorFormat::Normal))
}
#[wasm_bindgen(js_name = "parse", typescript_type = "parse", skip_typescript)]
pub fn parse(s: JsString, opts: JsValue) -> js_sys::Promise {
// TODO: This'll be properly scheduled once wasm have standard backed thread
// support.
future_to_promise(async { parse_sync(s, opts) })
}
#[wasm_bindgen(js_name = "printSync", typescript_type = "printSync", skip_typescript)]
pub fn print_sync(s: JsValue, opts: JsValue) -> Result<JsValue, JsValue> {
console_error_panic_hook::set_once();
@ -129,7 +169,11 @@ pub fn print_sync(s: JsValue, opts: JsValue) -> Result<JsValue, JsValue> {
},
|_handler| {
c.run(|| {
let opts: Options = opts.into_serde().context("failed to parse options")?;
let opts: Options = if opts.is_null() || opts.is_undefined() {
Default::default()
} else {
opts.into_serde().context("failed to parse options")?
};
let program: Program = s.into_serde().context("failed to deserialize program")?;
@ -159,6 +203,13 @@ pub fn print_sync(s: JsValue, opts: JsValue) -> Result<JsValue, JsValue> {
.map_err(|e| convert_err(e, ErrorFormat::Normal))
}
#[wasm_bindgen(js_name = "print", typescript_type = "print", skip_typescript)]
pub fn print(s: JsValue, opts: JsValue) -> js_sys::Promise {
// TODO: This'll be properly scheduled once wasm have standard backed thread
// support.
future_to_promise(async { print_sync(s, opts) })
}
#[wasm_bindgen(
js_name = "transformSync",
typescript_type = "transformSync",
@ -166,7 +217,7 @@ pub fn print_sync(s: JsValue, opts: JsValue) -> Result<JsValue, JsValue> {
)]
#[allow(unused_variables)]
pub fn transform_sync(
s: &str,
s: JsValue,
opts: JsValue,
experimental_plugin_bytes_resolver: JsValue,
) -> Result<JsValue, JsValue> {
@ -219,10 +270,13 @@ pub fn transform_sync(
}
}
let opts: Options = opts
.into_serde()
.context("failed to parse options")
.map_err(|e| convert_err(e, ErrorFormat::Normal))?;
let opts: Options = if opts.is_null() || opts.is_undefined() {
Default::default()
} else {
opts.into_serde()
.context("failed to parse options")
.map_err(|e| convert_err(e, ErrorFormat::Normal))?
};
let error_format = opts.experimental.error_format.unwrap_or_default();
@ -233,17 +287,22 @@ pub fn transform_sync(
},
|handler| {
c.run(|| {
let fm = c.cm.new_source_file(
if opts.filename.is_empty() {
FileName::Anon
} else {
FileName::Real(opts.filename.clone().into())
},
s.into(),
);
let out = c
.process_js_file(fm, handler, &opts)
.context("failed to process input file")?;
let s = s.dyn_into::<js_sys::JsString>();
let out = match s {
Ok(s) => {
let fm = c.cm.new_source_file(
if opts.filename.is_empty() {
FileName::Anon
} else {
FileName::Real(opts.filename.clone().into())
},
s.into(),
);
c.process_js_file(fm, handler, &opts)
.context("failed to process input file")?
}
Err(v) => unsafe { c.process_js(handler, v.into_serde().expect(""), &opts)? },
};
JsValue::from_serde(&out).context("failed to serialize json")
})
@ -252,6 +311,17 @@ pub fn transform_sync(
.map_err(|e| convert_err(e, error_format))
}
#[wasm_bindgen(js_name = "transform", typescript_type = "transform", skip_typescript)]
pub fn transform(
s: JsValue,
opts: JsValue,
experimental_plugin_bytes_resolver: JsValue,
) -> js_sys::Promise {
// TODO: This'll be properly scheduled once wasm have standard backed thread
// support.
future_to_promise(async { transform_sync(s, opts, experimental_plugin_bytes_resolver) })
}
/// Get global sourcemap
fn compiler() -> Arc<Compiler> {
static C: Lazy<Arc<Compiler>> = Lazy::new(|| {

View File

@ -848,7 +848,7 @@ pub struct Config {
}
/// Second argument of `minify`.
#[derive(Debug, Clone, Serialize, Deserialize)]
#[derive(Debug, Clone, Serialize, Deserialize, Default)]
#[serde(deny_unknown_fields, rename_all = "camelCase")]
pub struct JsMinifyOptions {
#[serde(default)]