diff --git a/.github/workflows/publish-core.yml b/.github/workflows/publish-core.yml index dd87925d91ef..ea0d44db7a3a 100644 --- a/.github/workflows/publish-core.yml +++ b/.github/workflows/publish-core.yml @@ -508,11 +508,14 @@ jobs: matrix: settings: - crate: "binding_core_wasm" - npm: "@swc/wasm" + npm: "@swc\\/wasm" target: nodejs - crate: "binding_core_wasm" - npm: "@swc/wasm-web" + npm: "@swc\\/wasm-web" target: web + - crate: "binding_typescript_wasm" + npm: "@swc\\/wasm-typescript" + target: no-modules steps: - uses: actions/checkout@v4 diff --git a/bindings/Cargo.lock b/bindings/Cargo.lock index 170234f990f2..542c351c1df0 100644 --- a/bindings/Cargo.lock +++ b/bindings/Cargo.lock @@ -302,6 +302,23 @@ dependencies = [ "wasm-bindgen", ] +[[package]] +name = "binding_typescript_wasm" +version = "1.6.6" +dependencies = [ + "anyhow", + "getrandom", + "serde", + "serde-wasm-bindgen", + "serde_json", + "swc_core", + "swc_ecma_codegen", + "swc_error_reporters", + "tracing", + "wasm-bindgen", + "wasm-bindgen-futures", +] + [[package]] name = "bitflags" version = "1.3.2" @@ -2653,9 +2670,9 @@ dependencies = [ [[package]] name = "serde_json" -version = "1.0.115" +version = "1.0.120" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "12dc5c46daa8e9fdf4f5e71b6cf9a53f2487da0e86e55808e2d35539666497dd" +checksum = "4e0d21c9a8cae1235ad58a00c11cb40d4b1e5c784f1ef2c537876ed6ffd8b7c5" dependencies = [ "itoa", "ryu", @@ -3208,6 +3225,7 @@ dependencies = [ "swc_ecma_minifier", "swc_ecma_parser", "swc_ecma_transforms_base", + "swc_ecma_transforms_typescript", "swc_ecma_visit", "swc_malloc", "swc_node_bundler", diff --git a/bindings/Cargo.toml b/bindings/Cargo.toml index d9ae05ef04cf..8dcc3014dd95 100644 --- a/bindings/Cargo.toml +++ b/bindings/Cargo.toml @@ -4,6 +4,7 @@ members = [ "binding_core_wasm", "binding_minifier_node", "binding_minifier_wasm", + "binding_typescript_wasm", "swc_cli", ] resolver = "2" diff --git a/bindings/binding_core_node/build.rs b/bindings/binding_core_node/build.rs index 968e821ff76e..a6220cd0337c 100644 --- a/bindings/binding_core_node/build.rs +++ b/bindings/binding_core_node/build.rs @@ -17,7 +17,7 @@ fn main() { let out_dir = env::var("OUT_DIR").expect("Outdir should exist"); let dest_path = Path::new(&out_dir).join("triple.txt"); let mut f = - BufWriter::new(File::create(&dest_path).expect("Failed to create target triple text")); + BufWriter::new(File::create(dest_path).expect("Failed to create target triple text")); write!( f, "{}", diff --git a/bindings/binding_minifier_node/src/minify.rs b/bindings/binding_minifier_node/src/minify.rs index 952477051f54..141b499fc15c 100644 --- a/bindings/binding_minifier_node/src/minify.rs +++ b/bindings/binding_minifier_node/src/minify.rs @@ -7,8 +7,7 @@ use napi::{ }; use serde::Deserialize; use swc_compiler_base::{ - minify_file_comments, parse_js, IdentCollector, IsModule, PrintArgs, SourceMapsConfig, - TransformOutput, + minify_file_comments, parse_js, IdentCollector, PrintArgs, SourceMapsConfig, TransformOutput, }; use swc_config::config_types::BoolOr; use swc_core::{ @@ -23,7 +22,7 @@ use swc_core::{ js::{JsMinifyCommentOption, JsMinifyOptions}, option::{MinifyOptions, TopLevelOptions}, }, - parser::{EsConfig, Syntax}, + parser::{EsSyntax, Syntax}, transforms::base::{fixer::fixer, hygiene::hygiene, resolver}, visit::{FoldWith, VisitMutWith, VisitWith}, }, @@ -109,11 +108,30 @@ fn do_work(input: MinifyTarget, options: JsMinifyOptions) -> napi::Result napi::Result napi::Result napi::Result napi::Result"] +description = "wasm module for swc" +edition = "2021" +license = "Apache-2.0" +name = "binding_typescript_wasm" +publish = false +repository = "https://github.com/swc-project/swc.git" +version = "1.6.6" + +[lib] +bench = false +crate-type = ["cdylib"] + +[features] +default = ["swc_v1"] +swc_v1 = [] +swc_v2 = [] + +[dependencies] +anyhow = "1.0.66" +getrandom = { version = "0.2.10", features = ["js"] } +serde = { version = "1", features = ["derive"] } +serde-wasm-bindgen = "0.4.5" +serde_json = "1.0.120" +swc_core = { version = "0.95.10", features = [ + "ecma_ast_serde", + "ecma_codegen", + "ecma_transforms", + "ecma_transforms_typescript", + "ecma_visit", +] } +swc_ecma_codegen = { version = "0.151.1", features = ["serde-impl"] } +swc_error_reporters = "0.18.0" +tracing = { version = "0.1.37", features = ["max_level_off"] } +wasm-bindgen = { version = "0.2.82", features = ["enable-interning"] } +wasm-bindgen-futures = { version = "0.4.41" } + +[package.metadata.wasm-pack.profile.release] +wasm-opt = false diff --git a/bindings/binding_typescript_wasm/package.json b/bindings/binding_typescript_wasm/package.json new file mode 100644 index 000000000000..62046512ab73 --- /dev/null +++ b/bindings/binding_typescript_wasm/package.json @@ -0,0 +1,5 @@ +{ + "devDependencies": { + "jest": "^25.1.0" + } +} diff --git a/bindings/binding_typescript_wasm/src/lib.rs b/bindings/binding_typescript_wasm/src/lib.rs new file mode 100644 index 000000000000..33cebd37c752 --- /dev/null +++ b/bindings/binding_typescript_wasm/src/lib.rs @@ -0,0 +1,227 @@ +use anyhow::{Context, Error}; +use serde::{Deserialize, Serialize}; +use swc_core::{ + base::{config::ErrorFormat, HandlerOpts}, + common::{ + comments::SingleThreadedComments, errors::ColorConfig, source_map::SourceMapGenConfig, + sync::Lrc, FileName, Mark, SourceMap, GLOBALS, + }, + ecma::{ + ast::{EsVersion, Program}, + codegen::text_writer::JsWriter, + parser::{ + parse_file_as_module, parse_file_as_program, parse_file_as_script, Syntax, TsSyntax, + }, + transforms::base::{ + fixer::fixer, + helpers::{inject_helpers, Helpers, HELPERS}, + hygiene::hygiene, + resolver, + }, + visit::VisitMutWith, + }, +}; +use swc_error_reporters::handler::try_with_handler; +use wasm_bindgen::prelude::*; +use wasm_bindgen_futures::{ + future_to_promise, + js_sys::{JsString, Promise}, +}; + +/// Custom interface definitions for the @swc/wasm's public interface instead of +/// 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 transform(src: string, opts?: Options): Promise; +export function transformSync(src: string, opts?: Options): TransformOutput; +"#; + +#[derive(Deserialize)] +#[serde(rename_all = "camelCase")] +pub struct Options { + #[serde(default)] + pub module: Option, + #[serde(default)] + pub filename: Option, + + #[serde(default)] + pub parser: TsSyntax, + + #[serde(default)] + pub external_helpers: bool, + + #[serde(default)] + pub source_maps: bool, + + #[serde(default)] + pub transform: swc_core::ecma::transforms::typescript::Config, + + #[serde(default)] + pub codegen: swc_core::ecma::codegen::Config, +} + +#[derive(Serialize)] +pub struct TransformOutput { + code: String, + map: Option, +} + +#[wasm_bindgen] +pub fn transform(input: JsString, options: JsValue) -> Promise { + future_to_promise(async move { transform_sync(input, options) }) +} + +#[wasm_bindgen] +pub fn transform_sync(input: JsString, options: JsValue) -> Result { + let options: Options = serde_wasm_bindgen::from_value(options)?; + + let input = input.as_string().unwrap(); + + let result = GLOBALS + .set(&Default::default(), || operate(input, options)) + .map_err(|err| convert_err(err, None))?; + + Ok(serde_wasm_bindgen::to_value(&result)?) +} + +fn operate(input: String, options: Options) -> Result { + let cm = Lrc::new(SourceMap::default()); + + try_with_handler( + cm.clone(), + HandlerOpts { + color: ColorConfig::Never, + skip_filename: true, + }, + |handler| { + let filename = options + .filename + .map_or(FileName::Anon, |f| FileName::Real(f.into())); + + let fm = cm.new_source_file(filename, input); + + let syntax = Syntax::Typescript(options.parser); + let target = EsVersion::latest(); + + let comments = SingleThreadedComments::default(); + let mut errors = vec![]; + + let program = match options.module { + Some(true) => { + parse_file_as_module(&fm, syntax, target, Some(&comments), &mut errors) + .map(Program::Module) + } + Some(false) => { + parse_file_as_script(&fm, syntax, target, Some(&comments), &mut errors) + .map(Program::Script) + } + None => parse_file_as_program(&fm, syntax, target, Some(&comments), &mut errors), + }; + + let mut program = match program { + Ok(program) => program, + Err(err) => { + err.into_diagnostic(handler).emit(); + + for e in errors { + e.into_diagnostic(handler).emit(); + } + + return Err(anyhow::anyhow!("failed to parse")); + } + }; + + if !errors.is_empty() { + for e in errors { + e.into_diagnostic(handler).emit(); + } + + return Err(anyhow::anyhow!("failed to parse")); + } + + let unresolved_mark = Mark::new(); + let top_level_mark = Mark::new(); + + HELPERS.set(&Helpers::new(options.external_helpers), || { + // Apply resolver + + program.visit_mut_with(&mut resolver(unresolved_mark, top_level_mark, true)); + + // Strip typescript types + + program.visit_mut_with(&mut swc_core::ecma::transforms::typescript::typescript( + options.transform, + top_level_mark, + )); + + // Apply external helpers + + program.visit_mut_with(&mut inject_helpers(unresolved_mark)); + + // Apply hygiene + + program.visit_mut_with(&mut hygiene()); + + // Apply fixer + + program.visit_mut_with(&mut fixer(Some(&comments))); + }); + + // Generate code + + let mut buf = vec![]; + let mut src_map_buf = if options.source_maps { + Some(vec![]) + } else { + None + }; + + { + let wr = JsWriter::new(cm.clone(), "\n", &mut buf, src_map_buf.as_mut()); + let mut emitter = swc_core::ecma::codegen::Emitter { + cfg: options.codegen, + cm: cm.clone(), + comments: Some(&comments), + wr, + }; + + emitter.emit_program(&program).unwrap(); + } + + let code = String::from_utf8(buf).context("generated code is not utf-8")?; + + let map = if let Some(src_map_buf) = src_map_buf { + let mut wr = vec![]; + let map = cm.build_source_map_with_config(&src_map_buf, None, TsSourceMapGenConfig); + + map.to_writer(&mut wr) + .context("failed to write source map")?; + + let map = String::from_utf8(wr).context("source map is not utf-8")?; + Some(map) + } else { + None + }; + + Ok(TransformOutput { code, map }) + }, + ) +} + +pub fn convert_err( + err: Error, + error_format: Option, +) -> wasm_bindgen::prelude::JsValue { + error_format + .unwrap_or(ErrorFormat::Normal) + .format(&err) + .into() +} + +struct TsSourceMapGenConfig; + +impl SourceMapGenConfig for TsSourceMapGenConfig { + fn file_name_to_source(&self, f: &FileName) -> String { + f.to_string() + } +} diff --git a/scripts/publish.sh b/scripts/publish.sh index a9f578bfd667..eb56920bf059 100755 --- a/scripts/publish.sh +++ b/scripts/publish.sh @@ -19,7 +19,7 @@ echo "Publishing $version with swc_core $swc_core_version" # Update version (cd ./packages/core && npm version "$version" --no-git-tag-version --allow-same-version || true) (cd ./packages/minifier && npm version "$version" --no-git-tag-version --allow-same-version || true) -(cd ./bindings && cargo set-version $version -p binding_core_wasm -p binding_minifier_wasm) +(cd ./bindings && cargo set-version $version -p binding_core_wasm -p binding_minifier_wasm -p binding_typescript_wasm) (cd ./bindings && cargo set-version --bump patch -p swc_cli) diff --git a/yarn.lock b/yarn.lock index 96956a2e8f70..b577206f2f1d 100644 --- a/yarn.lock +++ b/yarn.lock @@ -6323,6 +6323,14 @@ __metadata: languageName: unknown linkType: soft +"binding_typescript_wasm-2142ce@workspace:bindings/binding_typescript_wasm": + version: 0.0.0-use.local + resolution: "binding_typescript_wasm-2142ce@workspace:bindings/binding_typescript_wasm" + dependencies: + jest: "npm:^25.1.0" + languageName: unknown + linkType: soft + "bindings@npm:^1.5.0": version: 1.5.0 resolution: "bindings@npm:1.5.0"