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