feat(bindings/ts): Add transform/strip-only mode (#9138)

**Description:**

This PR adds `strip-only`/`transform` mode to `@swc/wasm-typescript`.

 - Both mode errors on decorator usages.
 - In `strip-only` mode, `enum` and TypeScript parameter properties are treated as an invalid syntax.
 - In `transform` mode, those are transpiled.
This commit is contained in:
Donny/강동윤 2024-07-04 17:51:01 +09:00 committed by GitHub
parent db5c25309b
commit a08bb46ebd
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 147 additions and 35 deletions

View File

@ -13,9 +13,6 @@ bench = false
crate-type = ["cdylib"] crate-type = ["cdylib"]
[features] [features]
default = ["swc_v1"]
swc_v1 = []
swc_v2 = []
[dependencies] [dependencies]
anyhow = "1.0.66" anyhow = "1.0.66"

View File

@ -0,0 +1,32 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
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 {}
: ^^^^^^^^^^^
\`----
"
`;
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
,----
1 | class Foo { @decorator foo() {} }
: ^^^^^^^^^^
\`----
"
`;
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;
"
`;

View File

@ -6,7 +6,7 @@ it("properly reports error", function () {
}).toThrow(); }).toThrow();
}); });
describe("trannsform", () => { describe("transform", () => {
it("should strip types", async () => { it("should strip types", async () => {
const { code } = await swc.transform( const { code } = await swc.transform(
` `
@ -15,26 +15,36 @@ describe("trannsform", () => {
`, `,
{} {}
); );
expect(code).toMatchInlineSnapshot(` expect(code).toMatchSnapshot();
"export const foo = 1;
"
`);
}); });
it("should preserve enum", async () => { describe("in strip-only mode", () => {
const { code } = await swc.transform( it("should throw an error when it encounters an enum", async () => {
` await expect(
enum Foo { swc.transform("enum Foo {}", {
Bar mode: "strip-only",
} })
`, ).rejects.toMatchSnapshot();
{} });
);
await expect(code).toMatchInlineSnapshot(` it('should throw an error with a descriptive message when it encounters a decorator', async () => {
"enum Foo { await expect(
Bar 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();
});
}); });
}); });

View File

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

View File

@ -2,11 +2,14 @@ use anyhow::{Context, Error};
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use swc_core::{ use swc_core::{
common::{ common::{
comments::SingleThreadedComments, errors::ColorConfig, source_map::SourceMapGenConfig, comments::SingleThreadedComments,
sync::Lrc, FileName, Mark, SourceMap, GLOBALS, errors::{ColorConfig, HANDLER},
source_map::SourceMapGenConfig,
sync::Lrc,
FileName, Mark, SourceMap, Spanned, GLOBALS,
}, },
ecma::{ ecma::{
ast::{EsVersion, Program}, ast::{Decorator, EsVersion, Program, TsEnumDecl, TsParamPropParam},
codegen::text_writer::JsWriter, codegen::text_writer::JsWriter,
parser::{ parser::{
parse_file_as_module, parse_file_as_program, parse_file_as_script, Syntax, TsSyntax, parse_file_as_module, parse_file_as_program, parse_file_as_script, Syntax, TsSyntax,
@ -18,9 +21,9 @@ use swc_core::{
hygiene::hygiene, hygiene::hygiene,
resolver, resolver,
}, },
typescript::strip_type, typescript::{strip_type, typescript},
}, },
visit::VisitMutWith, visit::{Visit, VisitMutWith, VisitWith},
}, },
}; };
use swc_error_reporters::handler::{try_with_handler, HandlerOpts}; use swc_error_reporters::handler::{try_with_handler, HandlerOpts};
@ -46,7 +49,7 @@ pub struct Options {
#[serde(default)] #[serde(default)]
pub filename: Option<String>, pub filename: Option<String>,
#[serde(default)] #[serde(default = "default_ts_syntax")]
pub parser: TsSyntax, pub parser: TsSyntax,
#[serde(default)] #[serde(default)]
@ -55,12 +58,31 @@ pub struct Options {
#[serde(default)] #[serde(default)]
pub source_maps: bool, pub source_maps: bool,
// #[serde(default)] #[serde(default)]
// pub transform: swc_core::ecma::transforms::typescript::Config, pub mode: Mode,
#[serde(default)]
pub transform: Option<swc_core::ecma::transforms::typescript::Config>,
#[serde(default)] #[serde(default)]
pub codegen: swc_core::ecma::codegen::Config, 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)] #[derive(Serialize)]
pub struct TransformOutput { pub struct TransformOutput {
code: String, code: String,
@ -142,7 +164,6 @@ fn operate(input: String, options: Options) -> Result<TransformOutput, Error> {
let unresolved_mark = Mark::new(); let unresolved_mark = Mark::new();
let top_level_mark = Mark::new(); let top_level_mark = Mark::new();
HELPERS.set(&Helpers::new(options.external_helpers), || { HELPERS.set(&Helpers::new(options.external_helpers), || {
// Apply resolver // Apply resolver
@ -150,7 +171,19 @@ fn operate(input: String, options: Options) -> Result<TransformOutput, Error> {
// Strip typescript types // Strip typescript types
program.visit_mut_with(&mut strip_type()); 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 // Apply external helpers
@ -217,3 +250,43 @@ impl SourceMapGenConfig for TsSourceMapGenConfig {
f.to_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);
}
}

View File

@ -6327,7 +6327,7 @@ __metadata:
version: 0.0.0-use.local version: 0.0.0-use.local
resolution: "binding_typescript_wasm-2142ce@workspace:bindings/binding_typescript_wasm" resolution: "binding_typescript_wasm-2142ce@workspace:bindings/binding_typescript_wasm"
dependencies: dependencies:
jest: "npm:^25.1.0" jest: "npm:^29.7.0"
languageName: unknown languageName: unknown
linkType: soft linkType: soft