Skip to content

Commit 241b881

Browse files
authored
feat(ts/fast-strip): Throw an object instead of string (#10162)
**Description:** Throw an object from `@swc/wasm-typescript`. **Related issue:** - Closes #10150
1 parent b23d133 commit 241b881

File tree

4 files changed

+209
-41
lines changed

4 files changed

+209
-41
lines changed

bindings/Cargo.lock

-1
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

bindings/binding_typescript_wasm/Cargo.toml

-1
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,6 @@ serde = { workspace = true, features = ["derive"] }
2222
serde-wasm-bindgen = { workspace = true }
2323
serde_json = { workspace = true }
2424
swc_common = { workspace = true }
25-
swc_error_reporters = { workspace = true }
2625
swc_fast_ts_strip = { workspace = true, features = ["wasm-bindgen"] }
2726
tracing = { workspace = true, features = ["max_level_off"] }
2827
wasm-bindgen = { workspace = true, features = ["enable-interning"] }

bindings/binding_typescript_wasm/__tests__/__snapshots__/transform.js.snap

+82-20
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,14 @@
11
// Jest Snapshot v1, https://goo.gl/fbAQLP
22

33
exports[`transform in strip-only mode should not emit 'Caused by: failed to parse' 1`] = `
4-
"{"code":"InvalidSyntax","message":"await isn't allowed in non-async function","snippet":"Promise","filename":"test.ts","line":1,"column":23}
5-
"
4+
Map {
5+
"code" => "InvalidSyntax",
6+
"column" => 23,
7+
"filename" => "test.ts",
8+
"line" => 1,
9+
"message" => "await isn't allowed in non-async function",
10+
"snippet" => "Promise",
11+
}
612
`;
713

814
exports[`transform in strip-only mode should remove declare enum 1`] = `
@@ -32,13 +38,27 @@ exports[`transform in strip-only mode should remove declare enum 3`] = `
3238
`;
3339

3440
exports[`transform in strip-only mode should report correct error for syntax error 1`] = `
35-
"{"code":"InvalidSyntax","message":"Expected ';', '}' or <eof>","snippet":"syntax","filename":"test.ts","line":1,"column":25}
36-
"
41+
Map {
42+
"code" => "InvalidSyntax",
43+
"column" => 25,
44+
"filename" => "test.ts",
45+
"line" => 1,
46+
"message" => "Expected ';', '}' or <eof>",
47+
"snippet" => "syntax",
48+
}
3749
`;
3850

3951
exports[`transform in strip-only mode should report correct error for unsupported syntax 1`] = `
40-
"{"code":"UnsupportedSyntax","message":"TypeScript enum is not supported in strip-only mode","snippet":"enum Foo {\\n a, b \\n }","filename":"test.ts","line":1,"column":0}
41-
"
52+
Map {
53+
"code" => "UnsupportedSyntax",
54+
"column" => 0,
55+
"filename" => "test.ts",
56+
"line" => 1,
57+
"message" => "TypeScript enum is not supported in strip-only mode",
58+
"snippet" => "enum Foo {
59+
a, b
60+
}",
61+
}
4262
`;
4363

4464
exports[`transform in strip-only mode should strip complex expressions 1`] = `
@@ -95,38 +115,80 @@ exports[`transform in strip-only mode should strip type declarations 1`] = `
95115
`;
96116

97117
exports[`transform in strip-only mode should throw an error when it encounters a module 1`] = `
98-
"{"code":"UnsupportedSyntax","message":"\`module\` keyword is not supported. Use \`namespace\` instead.","snippet":"module foo","filename":"test.ts","line":1,"column":0}
99-
"
118+
Map {
119+
"code" => "UnsupportedSyntax",
120+
"column" => 0,
121+
"filename" => "test.ts",
122+
"line" => 1,
123+
"message" => "\`module\` keyword is not supported. Use \`namespace\` instead.",
124+
"snippet" => "module foo",
125+
}
100126
`;
101127

102128
exports[`transform in strip-only mode should throw an error when it encounters a module 2`] = `
103-
"{"code":"UnsupportedSyntax","message":"\`module\` keyword is not supported. Use \`namespace\` instead.","snippet":"module foo","filename":"test.ts","line":1,"column":8}
104-
"
129+
Map {
130+
"code" => "UnsupportedSyntax",
131+
"column" => 8,
132+
"filename" => "test.ts",
133+
"line" => 1,
134+
"message" => "\`module\` keyword is not supported. Use \`namespace\` instead.",
135+
"snippet" => "module foo",
136+
}
105137
`;
106138

107139
exports[`transform in strip-only mode should throw an error when it encounters a namespace 1`] = `
108-
"{"code":"UnsupportedSyntax","message":"TypeScript namespace declaration is not supported in strip-only mode","snippet":"namespace Foo { export const m = 1; }","filename":"test.ts","line":1,"column":0}
109-
"
140+
Map {
141+
"code" => "UnsupportedSyntax",
142+
"column" => 0,
143+
"filename" => "test.ts",
144+
"line" => 1,
145+
"message" => "TypeScript namespace declaration is not supported in strip-only mode",
146+
"snippet" => "namespace Foo { export const m = 1; }",
147+
}
110148
`;
111149

112150
exports[`transform in strip-only mode should throw an error when it encounters an enum 1`] = `
113-
"{"code":"UnsupportedSyntax","message":"TypeScript enum is not supported in strip-only mode","snippet":"enum Foo {}","filename":"test.ts","line":1,"column":0}
114-
"
151+
Map {
152+
"code" => "UnsupportedSyntax",
153+
"column" => 0,
154+
"filename" => "test.ts",
155+
"line" => 1,
156+
"message" => "TypeScript enum is not supported in strip-only mode",
157+
"snippet" => "enum Foo {}",
158+
}
115159
`;
116160

117161
exports[`transform in transform mode shoud throw an object even with deprecatedTsModuleAsError = true 1`] = `
118-
"{"code":"UnsupportedSyntax","message":"\`module\` keyword is not supported. Use \`namespace\` instead.","snippet":"module F","filename":"<anon>","line":1,"column":0}
119-
"
162+
Map {
163+
"code" => "UnsupportedSyntax",
164+
"column" => 0,
165+
"filename" => "<anon>",
166+
"line" => 1,
167+
"message" => "\`module\` keyword is not supported. Use \`namespace\` instead.",
168+
"snippet" => "module F",
169+
}
120170
`;
121171

122172
exports[`transform in transform mode should throw an error when it encounters a declared module 1`] = `
123-
"{"code":"UnsupportedSyntax","message":"\`module\` keyword is not supported. Use \`namespace\` instead.","snippet":"module foo","filename":"test.ts","line":1,"column":8}
124-
"
173+
Map {
174+
"code" => "UnsupportedSyntax",
175+
"column" => 8,
176+
"filename" => "test.ts",
177+
"line" => 1,
178+
"message" => "\`module\` keyword is not supported. Use \`namespace\` instead.",
179+
"snippet" => "module foo",
180+
}
125181
`;
126182

127183
exports[`transform in transform mode should throw an error when it encounters a module 1`] = `
128-
"{"code":"UnsupportedSyntax","message":"\`module\` keyword is not supported. Use \`namespace\` instead.","snippet":"module foo","filename":"test.ts","line":1,"column":0}
129-
"
184+
Map {
185+
"code" => "UnsupportedSyntax",
186+
"column" => 0,
187+
"filename" => "test.ts",
188+
"line" => 1,
189+
"message" => "\`module\` keyword is not supported. Use \`namespace\` instead.",
190+
"snippet" => "module foo",
191+
}
130192
`;
131193

132194
exports[`transform should strip types 1`] = `

bindings/binding_typescript_wasm/src/lib.rs

+127-19
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,16 @@
1+
use std::{
2+
mem::take,
3+
sync::{Arc, Mutex},
4+
};
5+
16
use anyhow::Error;
27
use js_sys::Uint8Array;
3-
use swc_common::{errors::ColorConfig, sync::Lrc, SourceMap, GLOBALS};
4-
use swc_error_reporters::handler::{try_with_json_handler, HandlerOpts};
8+
use serde::Serialize;
9+
use swc_common::{
10+
errors::{DiagnosticBuilder, DiagnosticId, Emitter, Handler, HANDLER},
11+
sync::Lrc,
12+
SourceMap, SourceMapper, GLOBALS,
13+
};
514
use swc_fast_ts_strip::{Options, TransformOutput};
615
use wasm_bindgen::prelude::*;
716
use wasm_bindgen_futures::{future_to_promise, js_sys::Promise};
@@ -43,28 +52,127 @@ pub fn transform_sync(input: JsValue, options: JsValue) -> Result<JsValue, JsVal
4352
}
4453
};
4554

46-
let result = GLOBALS
47-
.set(&Default::default(), || operate(input, options))
48-
.map_err(convert_err)?;
55+
let result = GLOBALS.set(&Default::default(), || operate(input, options));
4956

50-
Ok(serde_wasm_bindgen::to_value(&result)?)
57+
match result {
58+
Ok(v) => Ok(serde_wasm_bindgen::to_value(&v)?),
59+
Err(v) => Err(serde_wasm_bindgen::to_value(&v[0])?),
60+
}
5161
}
5262

53-
fn operate(input: String, options: Options) -> Result<TransformOutput, Error> {
63+
fn operate(input: String, options: Options) -> Result<TransformOutput, Vec<serde_json::Value>> {
5464
let cm = Lrc::new(SourceMap::default());
5565

56-
try_with_json_handler(
57-
cm.clone(),
58-
HandlerOpts {
59-
color: ColorConfig::Never,
60-
skip_filename: false,
61-
},
62-
|handler| {
63-
swc_fast_ts_strip::operate(&cm, handler, input, options).map_err(anyhow::Error::new)
64-
},
65-
)
66+
try_with_json_handler(cm.clone(), |handler| {
67+
swc_fast_ts_strip::operate(&cm, handler, input, options).map_err(anyhow::Error::new)
68+
})
69+
}
70+
71+
#[derive(Clone)]
72+
struct LockedWriter {
73+
errors: Arc<Mutex<Vec<serde_json::Value>>>,
74+
cm: Lrc<SourceMap>,
75+
}
76+
77+
fn try_with_json_handler<F, Ret>(cm: Lrc<SourceMap>, op: F) -> Result<Ret, Vec<serde_json::Value>>
78+
where
79+
F: FnOnce(&Handler) -> Result<Ret, Error>,
80+
{
81+
let wr = LockedWriter {
82+
errors: Default::default(),
83+
cm,
84+
};
85+
let emitter: Box<dyn Emitter> = Box::new(wr.clone());
86+
87+
let handler = Handler::with_emitter(true, false, emitter);
88+
89+
let ret = HANDLER.set(&handler, || op(&handler));
90+
91+
if handler.has_errors() {
92+
let mut lock = wr.errors.lock().unwrap();
93+
let error = take(&mut *lock);
94+
95+
Err(error)
96+
} else {
97+
Ok(ret.expect("it should not fail without emitting errors to handler"))
98+
}
99+
}
100+
101+
impl Emitter for LockedWriter {
102+
fn emit(&mut self, db: &DiagnosticBuilder) {
103+
let d = &**db;
104+
105+
let children = d
106+
.children
107+
.iter()
108+
.map(|d| todo!("json subdiagnostic: {d:?}"))
109+
.collect::<Vec<_>>();
110+
111+
let error_code = match &d.code {
112+
Some(DiagnosticId::Error(s)) => Some(&**s),
113+
Some(DiagnosticId::Lint(s)) => Some(&**s),
114+
None => None,
115+
};
116+
117+
let loc = d
118+
.span
119+
.primary_span()
120+
.and_then(|span| self.cm.try_lookup_char_pos(span.lo()).ok());
121+
122+
let snippet = d
123+
.span
124+
.primary_span()
125+
.and_then(|span| self.cm.span_to_snippet(span).ok());
126+
127+
let filename = loc.as_ref().map(|loc| loc.file.name.to_string());
128+
129+
let error = JsonDiagnostic {
130+
code: error_code,
131+
message: &d.message[0].0,
132+
snippet: snippet.as_deref(),
133+
filename: filename.as_deref(),
134+
line: loc.as_ref().map(|loc| loc.line),
135+
column: loc.as_ref().map(|loc| loc.col_display),
136+
children,
137+
};
138+
139+
self.errors
140+
.lock()
141+
.unwrap()
142+
.push(serde_json::to_value(&error).unwrap());
143+
}
144+
145+
fn take_diagnostics(&mut self) -> Vec<String> {
146+
Default::default()
147+
}
148+
}
149+
150+
#[derive(Serialize)]
151+
struct JsonDiagnostic<'a> {
152+
/// Error code
153+
#[serde(skip_serializing_if = "Option::is_none")]
154+
code: Option<&'a str>,
155+
message: &'a str,
156+
157+
#[serde(skip_serializing_if = "Option::is_none")]
158+
snippet: Option<&'a str>,
159+
160+
#[serde(skip_serializing_if = "Option::is_none")]
161+
filename: Option<&'a str>,
162+
163+
#[serde(skip_serializing_if = "Option::is_none")]
164+
line: Option<usize>,
165+
#[serde(skip_serializing_if = "Option::is_none")]
166+
column: Option<usize>,
167+
168+
#[serde(skip_serializing_if = "Vec::is_empty")]
169+
children: Vec<JsonSubdiagnostic<'a>>,
66170
}
67171

68-
pub fn convert_err(err: Error) -> wasm_bindgen::prelude::JsValue {
69-
err.to_string().into()
172+
#[derive(Serialize)]
173+
struct JsonSubdiagnostic<'a> {
174+
message: &'a str,
175+
snippet: Option<&'a str>,
176+
filename: &'a str,
177+
line: usize,
70178
}

0 commit comments

Comments
 (0)