feat(es/typescript): Add swc_fast_ts_strip (#9143)

**Description:**

This PR also adds a CI process for each Wasm binding.

---------

Co-authored-by: magic-akari <akari.ccino@gmail.com>
This commit is contained in:
Donny/강동윤 2024-07-05 21:07:24 +09:00 committed by GitHub
parent ce78baa17d
commit b129343c94
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
51 changed files with 689 additions and 364 deletions

View File

@ -166,6 +166,14 @@ jobs:
if: >-
${{ !contains(github.event.head_commit.message, 'chore: ') }}
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
pkg:
- binding_core_wasm
- binding_minifier_wasm
- binding_typescript_wasm
steps:
- uses: actions/checkout@v4
@ -200,7 +208,7 @@ jobs:
- name: Test
run: |
(cd bindings/binding_core_wasm && ./scripts/test.sh)
(cd bindings/${{ matrix.pkg }} && ./scripts/test.sh)
cargo-test:
name: Test - ${{ matrix.settings.crate }} - ${{ matrix.settings.os }}
@ -460,6 +468,8 @@ jobs:
runner: ubuntu-latest
- crate: swc_fast_graph
os: ubuntu-latest
- crate: swc_fast_ts_strip
os: ubuntu-latest
runner: ubuntu-latest
- crate: swc_graph_analyzer
os: ubuntu-latest

13
Cargo.lock generated
View File

@ -4904,6 +4904,19 @@ dependencies = [
"swc_common",
]
[[package]]
name = "swc_fast_ts_strip"
version = "0.1.1"
dependencies = [
"anyhow",
"serde",
"swc_common",
"swc_ecma_ast",
"swc_ecma_parser",
"swc_ecma_visit",
"testing",
]
[[package]]
name = "swc_graph_analyzer"
version = "0.23.0"

View File

@ -24,6 +24,7 @@ members = [
"crates/swc_x_optimizer",
"crates/swc_transform_common",
"crates/swc_typescript",
"crates/swc_fast_ts_strip",
]
resolver = "2"

118
bindings/Cargo.lock generated
View File

@ -311,9 +311,9 @@ dependencies = [
"serde",
"serde-wasm-bindgen",
"serde_json",
"swc_core",
"swc_ecma_codegen",
"swc_common",
"swc_error_reporters",
"swc_fast_ts_strip",
"tracing",
"wasm-bindgen",
"wasm-bindgen-futures",
@ -465,8 +465,8 @@ dependencies = [
"once_cell",
"strsim",
"termcolor",
"terminal_size 0.2.6",
"textwrap 0.16.0",
"terminal_size",
"textwrap",
]
[[package]]
@ -1480,12 +1480,6 @@ dependencies = [
"syn 2.0.48",
]
[[package]]
name = "is_ci"
version = "1.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "616cde7c720bb2bb5824a224687d8f77bfd38922027f01d825cd7453be5099fb"
[[package]]
name = "itertools"
version = "0.13.0"
@ -1682,33 +1676,27 @@ dependencies = [
[[package]]
name = "miette"
version = "4.7.1"
version = "7.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "1c90329e44f9208b55f45711f9558cec15d7ef8295cc65ecd6d4188ae8edc58c"
checksum = "4edc8853320c2a0dab800fbda86253c8938f6ea88510dc92c5f1ed20e794afc1"
dependencies = [
"atty",
"backtrace",
"cfg-if",
"miette-derive",
"once_cell",
"owo-colors",
"supports-color",
"supports-hyperlinks",
"supports-unicode",
"terminal_size 0.1.17",
"textwrap 0.15.2",
"textwrap",
"thiserror",
"unicode-width",
]
[[package]]
name = "miette-derive"
version = "4.7.1"
version = "7.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6b5bc45b761bcf1b5e6e6c4128cd93b84c218721a8d9b894aa0aff4ed180174c"
checksum = "dcf09caffaac8068c346b6df2a7fc27a177fd20b39421a39ce0a211bde679a6c"
dependencies = [
"proc-macro2",
"quote",
"syn 1.0.109",
"syn 2.0.48",
]
[[package]]
@ -1976,9 +1964,9 @@ checksum = "b15813163c1d831bf4a13c3610c05c0d03b39feb07f7e09fa234dac9b15aaf39"
[[package]]
name = "owo-colors"
version = "3.5.0"
version = "4.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c1b04fb49957986fdce4d6ee7a65027d55d4b6d2265e5848bbb507b58ccfdb6f"
checksum = "caff54706df99d2a78a5a4e3455ff45448d81ef1bb63c22cd14052ca0e993a3f"
[[package]]
name = "parking_lot"
@ -2893,34 +2881,6 @@ version = "2.4.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6bdef32e8150c2a081110b42772ffe7d7c9032b606bc226c8260fd97e0976601"
[[package]]
name = "supports-color"
version = "1.3.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8ba6faf2ca7ee42fdd458f4347ae0a9bd6bcc445ad7cb57ad82b383f18870d6f"
dependencies = [
"atty",
"is_ci",
]
[[package]]
name = "supports-hyperlinks"
version = "1.2.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "590b34f7c5f01ecc9d78dba4b3f445f31df750a67621cf31626f3b7441ce6406"
dependencies = [
"atty",
]
[[package]]
name = "supports-unicode"
version = "1.0.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "a8b945e45b417b125a8ec51f1b7df2f8df7920367700d1f98aedd21e5735f8b2"
dependencies = [
"atty",
]
[[package]]
name = "swc"
version = "0.279.0"
@ -3170,7 +3130,6 @@ dependencies = [
"swc_ecma_minifier",
"swc_ecma_parser",
"swc_ecma_transforms_base",
"swc_ecma_transforms_typescript",
"swc_ecma_visit",
"swc_malloc",
"swc_node_bundler",
@ -3850,9 +3809,9 @@ dependencies = [
[[package]]
name = "swc_error_reporters"
version = "0.18.0"
version = "0.18.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9bd8f9a90efb59dc5d918b4470e5d152f34cac2f8733bfba141a96440cab3eff"
checksum = "4689d9bb6092b5e6a0b79c0152336a8bd7f0acaf70dcf4133f86deb01775baa0"
dependencies = [
"anyhow",
"miette",
@ -3873,6 +3832,17 @@ dependencies = [
"swc_common",
]
[[package]]
name = "swc_fast_ts_strip"
version = "0.1.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8b318927847fba58352647cf78ee30edfe25d160b5ee9b43c1456b27a181d575"
dependencies = [
"anyhow",
"serde",
"swc_core",
]
[[package]]
name = "swc_graph_analyzer"
version = "0.23.0"
@ -4145,16 +4115,6 @@ dependencies = [
"winapi-util",
]
[[package]]
name = "terminal_size"
version = "0.1.17"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "633c1a546cee861a1a6d0dc69ebeca693bf4296661ba7852b9d21d159e0506df"
dependencies = [
"libc",
"winapi",
]
[[package]]
name = "terminal_size"
version = "0.2.6"
@ -4176,38 +4136,30 @@ dependencies = [
[[package]]
name = "textwrap"
version = "0.15.2"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "b7b3e525a49ec206798b40326a44121291b530c963cfb01018f63e135bac543d"
checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d"
dependencies = [
"smawk",
"terminal_size",
"unicode-linebreak",
"unicode-width",
]
[[package]]
name = "textwrap"
version = "0.16.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "222a222a5bfe1bba4a77b45ec488a741b3cb8872e5e499451fd7d0129c9c7c3d"
dependencies = [
"terminal_size 0.2.6",
]
[[package]]
name = "thiserror"
version = "1.0.40"
version = "1.0.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "978c9a314bd8dc99be594bc3c175faaa9794be04a5a5e153caba6915336cebac"
checksum = "c546c80d6be4bc6a00c0f01730c08df82eaa7a7a61f11d656526506112cc1709"
dependencies = [
"thiserror-impl",
]
[[package]]
name = "thiserror-impl"
version = "1.0.40"
version = "1.0.61"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f9456a42c5b0d803c8cd86e73dd7cc9edd429499f37a3550d286d5e86720569f"
checksum = "46c3384250002a6d5af4d114f2845d37b57521033f30d5c3f46c4d70e1197533"
dependencies = [
"proc-macro2",
"quote",
@ -4538,9 +4490,9 @@ checksum = "1dd624098567895118886609431a7c3b8f516e41d30e0643f03d94592a147e36"
[[package]]
name = "unicode-width"
version = "0.1.10"
version = "0.1.13"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c0edd1e5b14653f783770bce4a4dabb4a5108a5370a5f5d8cfe8710c361f6c8b"
checksum = "0336d538f7abc86d282a4189614dfaa90810dfc2c6f6427eaf88e16311dd225d"
[[package]]
name = "unicode-xid"

View File

@ -0,0 +1,7 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`minify should work 1`] = `
Object {
"code": "console.log(1);",
}
`;

View File

@ -0,0 +1,15 @@
const swc = require("../pkg");
describe("minify", () => {
it("should work", async () => {
const output = await swc.minify(
`{
const a = 1;
console.log(a);
}`,
{}
);
expect(output).toMatchSnapshot();
});
});

View File

@ -0,0 +1,6 @@
#!/usr/bin/env bash
set -eu
wasm-pack build --out-name wasm --release --scope=swc --target nodejs
npx jest $@

View File

@ -20,18 +20,9 @@ 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.96.3", features = [
"common",
"common_sourcemap",
"ecma_ast",
"ecma_codegen",
"ecma_parser",
"ecma_transforms",
"ecma_transforms_typescript",
"ecma_visit",
] }
swc_ecma_codegen = { version = "0.151.1", features = ["serde-impl"] }
swc_common = "0.34.3"
swc_error_reporters = "0.18.0"
swc_fast_ts_strip = "0.1.1"
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" }

View File

@ -1,8 +1,66 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
exports[`transform in strip-only mode should throw an error when it encounters an enum 1`] = `
exports[`transform in strip-only mode should remove declare enum 1`] = `""`;
exports[`transform in strip-only mode should remove declare enum 2`] = `""`;
exports[`transform in strip-only mode should remove declare enum 3`] = `""`;
exports[`transform in strip-only mode should strip complex expressions 1`] = `
"const foo = {
foo: 1,
bar: "bar",
};
const bar = "bar";"
`;
exports[`transform in strip-only mode should strip nonnull assertions 1`] = `
"const foo = 1;
const bar = "bar";"
`;
exports[`transform in strip-only mode should strip satisfies 1`] = `
"const foo = 1;
const bar = "bar";"
`;
exports[`transform in strip-only mode should strip type annotations 1`] = `
"const foo = 1;
const bar = "bar";"
`;
exports[`transform in strip-only mode should strip type assertions 1`] = `
"const foo = 1;
const bar = "bar";"
`;
exports[`transform in strip-only mode should strip type declarations 1`] = `
"const foo = 1;
const bar = "bar";"
`;
exports[`transform in strip-only mode should throw an error when it encounters a module 1`] = `
" x TypeScript namespace declaration is not supported in strip-only mode
,----
1 | module 'foo' {}
: ^^^^^^^^^^^^^^^
\`----
"
x TypeScript enum is not supported in strip-only mode
`;
exports[`transform in strip-only mode should throw an error when it encounters a namespace 1`] = `
" x TypeScript namespace declaration is not supported in strip-only mode
,----
1 | namespace Foo {}
: ^^^^^^^^^^^^^^^^
\`----
"
`;
exports[`transform in strip-only mode should throw an error when it encounters an enum 1`] = `
" x TypeScript enum is not supported in strip-only mode
,----
1 | enum Foo {}
: ^^^^^^^^^^^
@ -11,8 +69,7 @@ exports[`transform in strip-only mode should throw an error when it encounters a
`;
exports[`transform in strip-only mode should throw an error with a descriptive message when it encounters a decorator 1`] = `
"
x Decorators are not supported
" x Decorators are not supported
,----
1 | class Foo { @decorator foo() {} }
: ^^^^^^^^^^
@ -20,13 +77,9 @@ exports[`transform in strip-only mode should throw an error with a descriptive m
"
`;
exports[`transform in transform mode should transpile enum 1`] = `
"var Foo;
(function(Foo) {})(Foo || (Foo = {}));
"
`;
exports[`transform should strip types 1`] = `
"export const foo = 1;
"
export const foo = 1;
"
`;

View File

@ -8,7 +8,7 @@ it("properly reports error", function () {
describe("transform", () => {
it("should strip types", async () => {
const { code } = await swc.transform(
const code = await swc.transform(
`
export const foo: number = 1;
type Foo = number;
@ -19,6 +19,94 @@ describe("transform", () => {
});
describe("in strip-only mode", () => {
it("should remove declare enum", async () => {
await expect(
swc.transform(`declare enum Foo {}`, {})
).resolves.toMatchSnapshot();
await expect(
swc.transform(
`declare enum Foo {
A
}`,
{}
)
).resolves.toMatchSnapshot();
expect(
swc.transform(
`declare enum Foo {
a = 2,
b,
}`,
{}
)
).resolves.toMatchSnapshot();
});
it("should strip type declarations", async () => {
await expect(
swc.transform(
`const foo = 1;
type Foo = number;
type Bar = string;
const bar: Bar = "bar";`,
{}
)
).resolves.toMatchSnapshot();
});
it("should strip type annotations", async () => {
await expect(
swc.transform(
`const foo = 1;
const bar: Bar = "bar";`,
{}
)
).resolves.toMatchSnapshot();
});
it("should strip type assertions", async () => {
await expect(
swc.transform(
`const foo = 1 as number;
const bar = "bar";`,
{}
)
).resolves.toMatchSnapshot();
});
it("should strip nonnull assertions", async () => {
await expect(
swc.transform(
`const foo = 1!;
const bar = "bar";`,
{}
)
).resolves.toMatchSnapshot();
});
it("should strip satisfies", async () => {
await expect(
swc.transform(
`const foo = 1 satisfies number;
const bar = "bar";`,
{}
)
).resolves.toMatchSnapshot();
});
it("should strip complex expressions", async () => {
await expect(
swc.transform(
`const foo = {
foo: 1 as number,
bar: "bar" as any as number,
} satisfies number;
const bar = "bar";`,
{}
)
).resolves.toMatchSnapshot();
});
it("should throw an error when it encounters an enum", async () => {
await expect(
swc.transform("enum Foo {}", {
@ -27,24 +115,28 @@ describe("transform", () => {
).rejects.toMatchSnapshot();
});
it('should throw an error with a descriptive message when it encounters a decorator', async () => {
it("should throw an error with a descriptive message when it encounters a decorator", async () => {
await expect(
swc.transform("class Foo { @decorator foo() {} }", {
mode: "strip-only",
})
).rejects.toMatchSnapshot();
})
});
describe("in transform mode", () => {
it("should transpile enum", async () => {
const { code } = await swc.transform("enum Foo {}", {
mode: "transform",
});
expect(code).toMatchSnapshot();
});
it("should throw an error when it encounters a namespace", async () => {
await expect(
swc.transform("namespace Foo {}", {
mode: "strip-only",
})
).rejects.toMatchSnapshot();
});
it("should throw an error when it encounters a module", async () => {
await expect(
swc.transform("module 'foo' {}", {
mode: "strip-only",
})
).rejects.toMatchSnapshot();
});
});
});

View File

@ -1,32 +1,7 @@
use anyhow::{Context, Error};
use serde::{Deserialize, Serialize};
use swc_core::{
common::{
comments::SingleThreadedComments,
errors::{ColorConfig, HANDLER},
source_map::SourceMapGenConfig,
sync::Lrc,
FileName, Mark, SourceMap, Spanned, GLOBALS,
},
ecma::{
ast::{Decorator, EsVersion, Program, TsEnumDecl, TsParamPropParam},
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,
},
typescript::{strip_type, typescript},
},
visit::{Visit, VisitMutWith, VisitWith},
},
};
use anyhow::Error;
use swc_common::{errors::ColorConfig, sync::Lrc, SourceMap, GLOBALS};
use swc_error_reporters::handler::{try_with_handler, HandlerOpts};
use swc_fast_ts_strip::Options;
use wasm_bindgen::prelude::*;
use wasm_bindgen_futures::{
future_to_promise,
@ -41,54 +16,6 @@ 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 = "default_ts_syntax")]
pub parser: TsSyntax,
#[serde(default)]
pub external_helpers: bool,
#[serde(default)]
pub source_maps: bool,
#[serde(default)]
pub mode: Mode,
#[serde(default)]
pub transform: Option<swc_core::ecma::transforms::typescript::Config>,
#[serde(default)]
pub codegen: swc_core::ecma::codegen::Config,
}
fn default_ts_syntax() -> TsSyntax {
TsSyntax {
decorators: true,
..Default::default()
}
}
#[derive(Clone, Copy, Default, Deserialize)]
#[serde(rename_all = "kebab-case")]
pub enum Mode {
#[default]
StripOnly,
Transform,
}
#[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) })
@ -107,7 +34,7 @@ pub fn transform_sync(input: JsString, options: JsValue) -> Result<JsValue, JsVa
Ok(serde_wasm_bindgen::to_value(&result)?)
}
fn operate(input: String, options: Options) -> Result<TransformOutput, Error> {
fn operate(input: String, options: Options) -> Result<String, Error> {
let cm = Lrc::new(SourceMap::default());
try_with_handler(
@ -116,177 +43,10 @@ fn operate(input: String, options: Options) -> Result<TransformOutput, Error> {
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_with(&mut Validator { mode: options.mode });
match options.mode {
Mode::StripOnly => {
program.visit_mut_with(&mut strip_type());
}
Mode::Transform => {
program.visit_mut_with(&mut typescript(
options.transform.unwrap_or_default(),
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 })
},
|handler| swc_fast_ts_strip::operate(&cm, handler, input, options),
)
}
pub fn convert_err(err: Error) -> wasm_bindgen::prelude::JsValue {
format!("{:?}", err).into()
}
struct TsSourceMapGenConfig;
impl SourceMapGenConfig for TsSourceMapGenConfig {
fn file_name_to_source(&self, f: &FileName) -> String {
f.to_string()
}
}
struct Validator {
mode: Mode,
}
impl Visit for Validator {
fn visit_decorator(&mut self, n: &Decorator) {
HANDLER.with(|handler| {
handler.span_err(n.span, "Decorators are not supported");
});
}
fn visit_ts_enum_decl(&mut self, e: &TsEnumDecl) {
if matches!(self.mode, Mode::StripOnly) {
HANDLER.with(|handler| {
handler.span_err(
e.span,
"TypeScript enum is not supported in strip-only mode",
);
});
return;
}
e.visit_children_with(self);
}
fn visit_ts_param_prop_param(&mut self, n: &TsParamPropParam) {
if matches!(self.mode, Mode::StripOnly) {
HANDLER.with(|handler| {
handler.span_err(
n.span(),
"TypeScript parameter property is not supported in strip-only mode",
);
});
return;
}
n.visit_children_with(self);
}
}

@ -1 +1 @@
Subproject commit 8e9c0b0fb3d548f378420aabbd351087efb5d5e5
Subproject commit a03c69227413dfcac8a1e9f89d93554fa3b8b7fe

View File

@ -0,0 +1,23 @@
[package]
authors = ["강동윤 <kdy1997.dev@gmail.com>"]
description = "Super-fast TypeScript stripper based on SWC"
documentation = "https://rustdoc.swc.rs/swc_fast_type_strip/"
edition = "2021"
include = ["Cargo.toml", "src/**/*.rs"]
license = "Apache-2.0"
name = "swc_fast_ts_strip"
repository = { workspace = true }
version = "0.1.1"
[dependencies]
anyhow = "1.0.66"
serde = { version = "1", features = ["derive"] }
swc_common = { version = "0.34.3", path = "../swc_common" }
swc_ecma_ast = { version = "0.115.1", path = "../swc_ecma_ast" }
swc_ecma_parser = { version = "0.146.9", path = "../swc_ecma_parser" }
swc_ecma_visit = { version = "0.101.0", path = "../swc_ecma_visit" }
[dev-dependencies]
testing = { version = "0.36.0", path = "../testing" }

View File

@ -0,0 +1,245 @@
use anyhow::Error;
use serde::Deserialize;
use swc_common::{
comments::SingleThreadedComments,
errors::{Handler, HANDLER},
sync::Lrc,
BytePos, FileName, SourceMap, Span, Spanned,
};
use swc_ecma_ast::{
BindingIdent, Decorator, EsVersion, Ident, Param, Pat, Program, TsAsExpr, TsConstAssertion,
TsEnumDecl, TsInstantiation, TsModuleDecl, TsNamespaceDecl, TsNonNullExpr, TsParamPropParam,
TsSatisfiesExpr, TsTypeAliasDecl, TsTypeAnn,
};
use swc_ecma_parser::{
parse_file_as_module, parse_file_as_program, parse_file_as_script, Syntax, TsSyntax,
};
use swc_ecma_visit::{Visit, VisitWith};
#[derive(Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct Options {
#[serde(default)]
pub module: Option<bool>,
#[serde(default)]
pub filename: Option<String>,
#[serde(default = "default_ts_syntax")]
pub parser: TsSyntax,
}
fn default_ts_syntax() -> TsSyntax {
TsSyntax {
decorators: true,
..Default::default()
}
}
pub fn operate(
cm: &Lrc<SourceMap>,
handler: &Handler,
input: String,
options: Options,
) -> Result<String, Error> {
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 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"));
}
// Strip typescript types
let mut ts_strip = TsStrip::default();
program.visit_with(&mut ts_strip);
let replacements = ts_strip.replacements;
if replacements.is_empty() {
return Ok(fm.src.to_string());
}
let mut code = <std::string::String as Clone>::clone(&fm.src).into_bytes();
for r in replacements {
code[(r.0 .0 - 1) as usize..(r.1 .0 - 1) as usize]
.iter_mut()
.for_each(|b| *b = b' ');
}
String::from_utf8(code).map_err(|_| anyhow::anyhow!("failed to convert to utf-8"))
}
#[derive(Default)]
struct TsStrip {
replacements: Vec<(BytePos, BytePos)>,
}
impl TsStrip {
fn add_replacement(&mut self, span: Span) {
self.replacements.push((span.lo, span.hi));
}
}
impl Visit for TsStrip {
fn visit_decorator(&mut self, n: &Decorator) {
HANDLER.with(|handler| {
handler.span_err(n.span, "Decorators are not supported");
});
}
fn visit_ts_as_expr(&mut self, n: &TsAsExpr) {
self.add_replacement(span(n.expr.span().hi, n.span.hi));
n.expr.visit_children_with(self);
}
fn visit_ts_const_assertion(&mut self, n: &TsConstAssertion) {
self.add_replacement(span(n.expr.span().hi, n.span.hi));
n.expr.visit_children_with(self);
}
fn visit_ts_enum_decl(&mut self, e: &TsEnumDecl) {
if e.declare {
self.add_replacement(e.span);
return;
}
HANDLER.with(|handler| {
handler.span_err(
e.span,
"TypeScript enum is not supported in strip-only mode",
);
});
}
fn visit_ts_instantiation(&mut self, n: &TsInstantiation) {
self.add_replacement(span(n.expr.span().hi, n.span.hi));
n.expr.visit_children_with(self);
}
fn visit_ts_module_decl(&mut self, n: &TsModuleDecl) {
if n.declare {
self.add_replacement(n.span);
return;
}
HANDLER.with(|handler| {
handler.span_err(
n.span(),
"TypeScript namespace declaration is not supported in strip-only mode",
);
});
}
fn visit_ts_namespace_decl(&mut self, n: &TsNamespaceDecl) {
if n.declare {
self.add_replacement(n.span);
return;
}
HANDLER.with(|handler| {
handler.span_err(
n.span(),
"TypeScript module declaration is not supported in strip-only mode",
);
});
}
fn visit_ts_non_null_expr(&mut self, n: &TsNonNullExpr) {
self.add_replacement(span(n.span.hi - BytePos(1), n.span.hi));
n.expr.visit_children_with(self);
}
fn visit_ts_param_prop_param(&mut self, n: &TsParamPropParam) {
HANDLER.with(|handler| {
handler.span_err(
n.span(),
"TypeScript parameter property is not supported in strip-only mode",
);
});
}
fn visit_ts_satisfies_expr(&mut self, n: &TsSatisfiesExpr) {
self.add_replacement(span(n.expr.span().hi, n.span.hi));
n.expr.visit_children_with(self);
}
fn visit_ts_type_alias_decl(&mut self, n: &TsTypeAliasDecl) {
self.add_replacement(n.span);
}
fn visit_ts_type_ann(&mut self, n: &TsTypeAnn) {
self.add_replacement(n.span);
}
fn visit_binding_ident(&mut self, n: &BindingIdent) {
n.visit_children_with(self);
if n.optional {
self.add_replacement(span(n.id.span.hi, n.id.span.hi + BytePos(1)));
}
}
fn visit_params(&mut self, n: &[Param]) {
if let Some(p) = n.first().filter(|param| {
matches!(
&param.pat,
Pat::Ident(BindingIdent {
id: Ident { sym, .. },
..
}) if &**sym == "this"
)
}) {
let lo = p.span.lo;
let hi = n.get(1).map(|x| x.span.lo).unwrap_or(p.span.hi);
self.add_replacement(span(lo, hi));
n[1..].visit_children_with(self);
return;
}
n.visit_children_with(self);
}
}
fn span(lo: BytePos, hi: BytePos) -> Span {
Span::new(lo, hi, Default::default())
}

View File

@ -0,0 +1,5 @@
x Decorators are not supported
,----
1 | class Foo { @decorator foo() { } }
: ^^^^^^^^^^
`----

View File

@ -0,0 +1 @@
class Foo { @decorator foo() { } }

View File

@ -0,0 +1,5 @@
x TypeScript enum is not supported in strip-only mode
,----
1 | enum Foo { }
: ^^^^^^^^^^^^
`----

View File

@ -0,0 +1 @@
enum Foo { }

View File

@ -0,0 +1,5 @@
x TypeScript namespace declaration is not supported in strip-only mode
,----
1 | module 'foo' { }
: ^^^^^^^^^^^^^^^^
`----

View File

@ -0,0 +1 @@
module 'foo' { }

View File

@ -0,0 +1,5 @@
x TypeScript namespace declaration is not supported in strip-only mode
,----
1 | namespace Foo { }
: ^^^^^^^^^^^^^^^^^
`----

View File

@ -0,0 +1 @@
namespace Foo { }

View File

@ -0,0 +1,48 @@
use std::path::PathBuf;
use swc_ecma_parser::TsSyntax;
use swc_fast_ts_strip::{operate, Options};
use testing::NormalizedOutput;
#[testing::fixture("tests/fixture/**/*.ts")]
fn test(input: PathBuf) {
let input_code = std::fs::read_to_string(&input).unwrap();
let output_file = input.with_extension("js");
testing::run_test(false, |cm, handler| {
let code = operate(&cm, handler, input_code, opts()).expect("should not return Err()");
NormalizedOutput::new_raw(code)
.compare_to_file(output_file)
.unwrap();
Ok(())
})
.expect("should not fail");
}
#[testing::fixture("tests/errors/**/*.ts")]
fn error(input: PathBuf) {
let input_code = std::fs::read_to_string(&input).unwrap();
let output_file = input.with_extension("swc-stderr");
testing::run_test(false, |cm, handler| {
operate(&cm, handler, input_code, opts()).expect("should not return Err()");
Err::<(), _>(())
})
.expect_err("should fail")
.compare_to_file(output_file)
.unwrap();
}
fn opts() -> Options {
Options {
module: None,
filename: None,
parser: TsSyntax {
decorators: true,
..Default::default()
},
}
}

View File

@ -0,0 +1,3 @@
const foo = [1, 3, 5] ;

View File

@ -0,0 +1,3 @@
const foo = [1, 3, 5] as const;

View File

@ -0,0 +1 @@

View File

@ -0,0 +1 @@
declare enum Foo { }

View File

@ -0,0 +1,3 @@
declare enum Foo {
A
}

View File

@ -0,0 +1,4 @@
declare enum Foo {
a = 2,
b,
}

View File

@ -0,0 +1,3 @@
const foo = call

View File

@ -0,0 +1,3 @@
const foo = call<string>

View File

@ -0,0 +1,6 @@
const foo = {
foo: 1 ,
bar: "bar" ,
baz: foo ,
} ;
const bar = "bar";

View File

@ -0,0 +1,6 @@
const foo = {
foo: 1 as number,
bar: "bar" as any as number,
baz: foo<string> as const,
} satisfies number;
const bar = "bar";

View File

@ -0,0 +1,2 @@
const foo = 1 ;
const bar = "bar";

View File

@ -0,0 +1,2 @@
const foo = 1!;
const bar = "bar";

View File

@ -0,0 +1,9 @@
export function typeAnn({ a, b, c } ) {
console.log(a, b, c);
}
export function optional(a ) {
console.log(a, b, c);
}

View File

@ -0,0 +1,9 @@
export function typeAnn({ a, b, c }: { a: number; b: number; c?: number }) {
console.log(a, b, c);
}
export function optional(a?: string) {
console.log(a, b, c);
}

View File

@ -0,0 +1,2 @@
const foo = 1 ;
const bar = "bar";

View File

@ -0,0 +1,2 @@
const foo = 1 satisfies number;
const bar = "bar";

View File

@ -0,0 +1,2 @@
export function foo( ) {}
export function bar( x ) {}

View File

@ -0,0 +1,2 @@
export function foo(this: number): void {}
export function bar(this: number, x: string): void {}

View File

@ -0,0 +1,2 @@
export const foo = 1;

View File

@ -0,0 +1,2 @@
export const foo: number = 1;
type Foo = number;

View File

@ -0,0 +1,4 @@
const foo = 1;
const bar = "bar";

View File

@ -0,0 +1,4 @@
const foo = 1;
type Foo = number;
type Bar = string;
const bar: Bar = "bar";

View File

@ -0,0 +1,2 @@
const foo = 1;
const bar = "bar";

View File

@ -0,0 +1,2 @@
const foo = 1;
const bar: Bar = "bar";

View File

@ -0,0 +1,2 @@
const foo = 1 ;
const bar = "bar";

View File

@ -0,0 +1,2 @@
const foo = 1 as number;
const bar = "bar";