quickjs_runtime/typescript/
mod.rs1use crate::jsutils::JsError;
4use crate::jsutils::Script;
5use crate::quickjs_utils::modules::detect_module;
6use crate::quickjsruntimeadapter::QuickJsRuntimeAdapter;
7use std::cell::RefCell;
8use std::collections::HashMap;
9use std::io;
10use std::str::FromStr;
11use std::sync::Arc;
12use swc::Compiler;
13use swc_common::errors::{ColorConfig, Handler};
14use swc_common::{FileName, SourceMap};
15
16pub enum TargetVersion {
17 Es3,
18 Es5,
19 Es2016,
20 Es2020,
21 Es2021,
22 Es2022,
23}
24
25impl TargetVersion {
26 fn as_str(&self) -> &str {
27 match self {
28 TargetVersion::Es3 => "es3",
29 TargetVersion::Es5 => "es5",
30 TargetVersion::Es2016 => "es2016",
31 TargetVersion::Es2020 => "es2020",
32 TargetVersion::Es2021 => "es2021",
33 TargetVersion::Es2022 => "es2022",
34 }
35 }
36}
37
38pub struct TypeScriptTranspiler {
39 minify: bool,
40 mangle: bool,
41 external_helpers: bool,
42 target: TargetVersion,
43 compiler: Compiler,
44 source_map: Arc<SourceMap>,
45}
46
47impl TypeScriptTranspiler {
48 pub fn new(target: TargetVersion, minify: bool, external_helpers: bool, mangle: bool) -> Self {
49 let source_map = Arc::<SourceMap>::default();
50 let compiler = swc::Compiler::new(source_map.clone());
51
52 Self {
53 minify,
54 mangle,
55 external_helpers,
56 target,
57 source_map,
58 compiler,
59 }
60 }
61 pub fn transpile(
63 &self,
64 code: &str,
65 file_name: &str,
66 is_module: bool,
67 ) -> Result<(String, Option<String>), JsError> {
68 let globals = swc_common::Globals::new();
69 let code = code.to_string();
70 swc_common::GLOBALS.set(&globals, || {
71 let handler = Handler::with_tty_emitter(
72 ColorConfig::Auto,
73 true,
74 false,
75 Some(self.source_map.clone()),
76 );
77
78 let fm = self
79 .source_map
80 .new_source_file(Arc::new(FileName::Custom(file_name.into())), code);
81
82 let mangle_config = if self.mangle {
83 r#"
84 {
85 "topLevel": false,
86 "keepClassNames": true
87 }
88 "#
89 } else {
90 "false"
91 };
92
93 let minify_options = if self.minify {
94 format!(
95 r#"
96 "minify": {{
97 "compress": {{
98 "unused": true
99 }},
100 "format": {{
101 "comments": false
102 }},
103 "mangle": {mangle_config}
104 }},
105 "#
106 )
107 } else {
108 r#"
109 "minify": {
110 "format": {
111 "comments": false
112 }
113 },
114 "#
115 .to_string()
116 };
117
118 let module = if is_module {
119 r#"
120 "module": {
121 "type": "es6",
122 "strict": true,
123 "strictMode": true,
124 "lazy": false,
125 "noInterop": false,
126 "ignoreDynamic": true
127 },
128 "#
129 } else {
130 ""
131 };
132
133 let cfg_json = format!(
134 r#"
135
136 {{
137 "minify": {},
138 "sourceMaps": true,
139 {}
140 "jsc": {{
141 {}
142 "externalHelpers": {},
143 "parser": {{
144 "syntax": "typescript",
145 "jsx": true,
146 "tsx": true,
147 "decorators": true,
148 "decoratorsBeforeExport": true,
149 "dynamicImport": true,
150 "preserveAllComments": false
151 }},
152 "transform": {{
153 "legacyDecorator": true,
154 "decoratorMetadata": true,
155 "react": {{
156 "runtime": "classic",
157 "useBuiltins": true,
158 "refresh": true
159 }}
160 }},
161 "target": "{}",
162 "keepClassNames": true
163 }}
164 }}
165
166 "#,
167 self.minify,
168 module,
169 minify_options,
170 self.external_helpers,
171 self.target.as_str()
172 );
173
174 log::trace!("using config {}", cfg_json);
175
176 let cfg = serde_json::from_str(cfg_json.as_str())
177 .map_err(|e| JsError::new_string(format!("{e}")))?;
178
179 let ops = swc::config::Options {
180 config: cfg,
181 ..Default::default()
182 };
183
184 let res = self.compiler.process_js_file(fm, &handler, &ops);
188
189 match res {
190 Ok(to) => Ok((to.code, to.map)),
191 Err(e) => Err(JsError::new_string(format!("transpile failed: {e}"))),
192 }
193 })
194 }
195
196 pub fn transpile_script(&self, script: &mut Script) -> Result<(), JsError> {
197 if script.get_path().ends_with(".ts") {
198 let code = script.get_code();
199
200 let is_module = detect_module(code);
201
202 let js = self.transpile(code, script.get_path(), is_module)?;
203 log::debug!("map: {:?}", js.1);
204 script.set_transpiled_code(js.0, js.1);
205 }
206 log::debug!(
207 "TypeScriptPreProcessor:process file={} result = {}",
208 script.get_path(),
209 script.get_runnable_code()
210 );
211
212 Ok(())
213 }
214}
215
216impl Default for TypeScriptTranspiler {
217 fn default() -> Self {
218 Self::new(TargetVersion::Es2020, false, false, false)
219 }
220}
221
222thread_local! {
223 static SOURCE_MAPS: RefCell<HashMap<String, String>> = RefCell::new(HashMap::new());
225 static TRANSPILER: RefCell<TypeScriptTranspiler> = RefCell::new(TypeScriptTranspiler::new(TargetVersion::Es2020, false, false, false));
226}
227
228pub(crate) fn transpile_serverside(
230 _rt: &QuickJsRuntimeAdapter,
231 script: &mut Script,
232) -> Result<(), JsError> {
233 TRANSPILER.with(|rc| {
237 let transpiler: &TypeScriptTranspiler = &rc.borrow();
238 transpiler.transpile_script(script)
239 })?;
240
241 if let Some(map_str) = script.get_map() {
243 SOURCE_MAPS.with(|rc| {
244 let maps = &mut *rc.borrow_mut();
245 maps.insert(script.get_path().to_string(), map_str.to_string());
246 })
247 }
248 Ok(())
249}
250
251#[derive(Debug)]
252struct StackEntry {
253 function_name: String,
254 file_name: String,
255 line_number: Option<u32>,
256 column_number: Option<u32>,
257}
258
259impl FromStr for StackEntry {
260 type Err = String;
261
262 fn from_str(s: &str) -> Result<Self, Self::Err> {
263 let s = &s[3..];
265
266 let mut parts = s.splitn(2, ' ');
267 let function_name = parts.next().unwrap_or("unnamed").to_string();
268 let mut file_name = parts.next().unwrap_or("(unknown)").to_string();
269 if file_name.starts_with('(') {
270 file_name = file_name.as_str()[1..].to_string();
271 }
272 if file_name.ends_with(')') {
273 file_name = file_name.as_str()[..file_name.len() - 1].to_string();
274 }
275 file_name = file_name.replace("://", "_double_point_placeholder_//");
276
277 let parts: Vec<&str> = file_name.split(':').collect();
278
279 let file_name = parts[0]
280 .to_string()
281 .replace("_double_point_placeholder_//", "://");
282 let line_number = parts.get(1).and_then(|s| s.parse::<u32>().ok());
283 let column_number = parts.get(2).and_then(|s| s.parse::<u32>().ok());
284
285 Ok(StackEntry {
286 function_name,
287 file_name,
288 column_number,
289 line_number,
290 })
291 }
292}
293
294fn parse_stack_trace(stack_trace: &str) -> Result<Vec<StackEntry>, String> {
295 let entries: Vec<StackEntry> = stack_trace
296 .lines()
297 .map(|line| line.trim())
298 .filter(|line| !line.is_empty())
299 .map(|line| line.parse::<StackEntry>())
300 .collect::<Result<Vec<_>, _>>()?;
301
302 Ok(entries)
303}
304
305fn serialize_stack(entries: &[StackEntry]) -> String {
306 let mut result = String::new();
307
308 for entry in entries {
309 let fname_lnum = if let Some(line_number) = entry.line_number {
310 if let Some(column_number) = entry.column_number {
311 format!("{}:{line_number}:{column_number}", entry.file_name)
312 } else {
313 format!("{}:{line_number}", entry.file_name)
314 }
315 } else {
316 entry.file_name.clone()
317 };
318
319 result.push_str(&format!(" at {} ({fname_lnum})", entry.function_name));
320
321 result.push('\n');
322 }
323
324 result
325}
326
327pub(crate) fn unmap_stack_trace(stack_trace: &str) -> String {
328 SOURCE_MAPS.with(|rc| fix_stack_trace(stack_trace, &rc.borrow()))
330}
331
332pub fn fix_stack_trace(stack_trace: &str, maps: &HashMap<String, String>) -> String {
333 log::trace!("fix_stack_trace:\n{stack_trace}");
334
335 match parse_stack_trace(stack_trace) {
336 Ok(mut parsed_stack) => {
337 for stack_trace_entry in parsed_stack.iter_mut() {
338 if let Some(map_str) = maps.get(stack_trace_entry.file_name.as_str()) {
339 log::trace!(
340 "fix_stack_trace:found map for file {}:\n{map_str}",
341 stack_trace_entry.file_name.as_str()
342 );
343 if let Some(line_number) = stack_trace_entry.line_number {
344 log::trace!("lookup line number:{line_number}");
345 match swc::sourcemap::SourceMap::from_reader(io::Cursor::new(map_str)) {
346 Ok(source_map) => {
347 if let Some(original_location) = source_map.lookup_token(
348 line_number - 1,
349 stack_trace_entry.column_number.unwrap_or(1) - 1,
350 ) {
351 let original_line = original_location.get_src_line();
352 let original_column = original_location.get_src_col();
353 log::trace!("lookup original_line:{original_line}");
354 stack_trace_entry.line_number = Some(original_line + 1);
355 stack_trace_entry.column_number = Some(original_column + 1);
356 }
357 }
358 Err(_) => {
359 log::trace!(
360 "could not parse source_map for {}",
361 stack_trace_entry.file_name.as_str()
362 );
363 }
364 }
365 }
366 } else {
367 log::trace!("no map found for {}", stack_trace_entry.file_name.as_str());
368 }
369
370 }
373
374 let ret = serialize_stack(&parsed_stack);
375 log::trace!("fix_stack_trace ret:\n{ret}");
376 ret
377 }
378 Err(_) => {
379 log::error!("could not parse stack: \n{}", stack_trace);
380 stack_trace.to_string()
381 }
382 }
383}
384
385#[cfg(test)]
386pub mod tests {
387 use crate::facades::tests::init_test_rt;
388 use crate::jsutils::{JsValueType, Script};
389 use crate::typescript::{parse_stack_trace, serialize_stack};
390
391 #[test]
392 fn test_ts() {
393 let rt = init_test_rt();
394 println!("testing ts");
395 let script = Script::new(
396 "test.ts",
397 r#"
398 // hi
399 // ho
400 function t_ts(a: string, b: num): boolean {
401 return true;
402 }
403 t_ts("hello", 1337);
404 "#,
405 );
406 let res = rt.eval_sync(None, script).expect("script failed");
407 assert!(res.get_value_type() == JsValueType::Boolean);
408 }
409 #[test]
410 fn test_stack_map() {
411 let rt = init_test_rt();
412 println!("testing ts");
413 let script = Script::new(
414 "test.ts",
415 r#"
416
417 type Nonsense = {
418 hello: string
419 };
420
421 function t_ts(a: string, b: num): boolean {
422 return a.a.a === "hi";
423 }
424
425 t_ts("hello", 1337);
426"#,
427 );
428 let res = rt
429 .eval_sync(None, script)
430 .expect_err("script passed.. which it shouldnt");
431 println!("stack:{}", res.get_stack());
434
435 #[cfg(feature = "bellard")]
438 assert!(res.get_stack().contains("t_ts (test.ts:8"));
439 #[cfg(feature = "quickjs-ng")]
440 assert!(res.get_stack().contains("t_ts (test.ts:7"));
441 }
442 #[test]
443 fn test_stack_parse() {
444 let _rt = init_test_rt();
446
447 let stack = r#"
448 at func (file.ts:88:12)
449 at doWriteTransactioned (gcsproject:///gcs_objectstore/ObjectStore.ts:170)
450 "#;
451 match parse_stack_trace(stack) {
452 Ok(a) => {
453 assert_eq!(a[0].file_name, "file.ts");
454 assert_eq!(a[0].line_number, Some(88));
455 assert_eq!(a[0].column_number, Some(12));
456 assert_eq!(a[0].function_name, "func");
457
458 assert_eq!(
459 a[1].file_name,
460 "gcsproject:///gcs_objectstore/ObjectStore.ts"
461 );
462 assert_eq!(a[1].line_number, Some(170));
463 assert_eq!(a[1].column_number, None);
464 assert_eq!(a[1].function_name, "doWriteTransactioned");
465
466 assert_eq!(
467 serialize_stack(&a).as_str(),
468 r#" at func (file.ts:88:12)
469 at doWriteTransactioned (gcsproject:///gcs_objectstore/ObjectStore.ts:170)
470"#
471 );
472 }
473 Err(e) => {
474 panic!("{}", e);
475 }
476 }
477 }
478}