feat(binding): Create Wasm package for stripping only TypeScript (#9124)

**Description:**

This PR adds a Wasm binding which is only capable of stripping TypeScript types.


**Related issue:**

 - https://github.com/marco-ippolito/node/pull/2
This commit is contained in:
Donny/강동윤 2024-07-03 09:50:59 +09:00 committed by GitHub
parent 1597c5dee5
commit 6b3c0da755
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
14 changed files with 333 additions and 47 deletions

View File

@ -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

22
bindings/Cargo.lock generated
View File

@ -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",

View File

@ -4,6 +4,7 @@ members = [
"binding_core_wasm",
"binding_minifier_node",
"binding_minifier_wasm",
"binding_typescript_wasm",
"swc_cli",
]
resolver = "2"

View File

@ -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,
"{}",

View File

@ -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<Transf
..Default::default()
};
let comments = SingleThreadedComments::default();
let module = parse_js(
cm.clone(),
fm.clone(),
handler,
target,
Syntax::Es(EsSyntax {
jsx: true,
decorators: true,
decorators_before_export: true,
import_attributes: true,
..Default::default()
}),
options.module,
Some(&comments),
)
.context("failed to parse input file")?;
// top_level defaults to true if module is true
// https://github.com/swc-project/swc/issues/2254
if options.module {
if module.is_module() {
if let Some(opts) = &mut min_opts.compress {
if opts.top_level.is_none() {
opts.top_level = Some(TopLevelOptions { functions: true });
@ -136,25 +154,6 @@ fn do_work(input: MinifyTarget, options: JsMinifyOptions) -> napi::Result<Transf
}
}
let comments = SingleThreadedComments::default();
let module = parse_js(
cm.clone(),
fm.clone(),
handler,
target,
Syntax::Es(EsConfig {
jsx: true,
decorators: true,
decorators_before_export: true,
import_attributes: true,
..Default::default()
}),
IsModule::Bool(options.module),
Some(&comments),
)
.context("failed to parse input file")?;
let source_map_names = if source_map.enabled() {
let mut v = IdentCollector {
names: Default::default(),
@ -172,7 +171,7 @@ fn do_work(input: MinifyTarget, options: JsMinifyOptions) -> napi::Result<Transf
let is_mangler_enabled = min_opts.mangle.is_some();
let module = (|| {
let module = {
let module = module.fold_with(&mut resolver(unresolved_mark, top_level_mark, false));
let mut module = swc_core::ecma::minifier::optimize(
@ -192,7 +191,7 @@ fn do_work(input: MinifyTarget, options: JsMinifyOptions) -> napi::Result<Transf
}
module.visit_mut_with(&mut fixer(Some(&comments as &dyn Comments)));
module
})();
};
let preserve_comments = options
.format
@ -211,7 +210,7 @@ fn do_work(input: MinifyTarget, options: JsMinifyOptions) -> napi::Result<Transf
inline_sources_content: options.inline_sources_content,
source_map,
source_map_names: &source_map_names,
orig: orig.as_ref(),
orig,
comments: Some(&comments),
emit_source_map_columns: options.emit_source_map_columns,
preamble: &options.format.preamble,

View File

@ -1 +0,0 @@
wasm-pack build --debug --scope swc -t nodejs --features plugin --features getrandom/js $@

View File

@ -1,4 +0,0 @@
#!/bin/bash
# run this script from the wasm folder ./scripts/build_nodejs_release.sh
npx wasm-pack build --scope swc -t nodejs --features plugin

View File

@ -1,4 +0,0 @@
#!/bin/bash
# run this script from the wasm folder ./scripts/build_web_release.sh
npx wasm-pack build --scope swc --features plugin

View File

@ -1,6 +0,0 @@
#!/usr/bin/env bash
set -eu
./scripts/build.sh
npx jest $@

View File

@ -0,0 +1,40 @@
[package]
authors = ["강동윤 <kdy1997.dev@gmail.com>"]
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

View File

@ -0,0 +1,5 @@
{
"devDependencies": {
"jest": "^25.1.0"
}
}

View File

@ -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<TransformOutput>;
export function transformSync(src: string, opts?: Options): TransformOutput;
"#;
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Options {
#[serde(default)]
pub module: Option<bool>,
#[serde(default)]
pub filename: Option<String>,
#[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<String>,
}
#[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<JsValue, JsValue> {
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<TransformOutput, Error> {
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<ErrorFormat>,
) -> 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()
}
}

View File

@ -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)

View File

@ -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"