quickjs_runtime/typescript/
mod.rs

1// public transpile function which can also be used by gcs to transpile clientside ts
2
3use 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    // todo custom target
62    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        swc_common::GLOBALS.set(&globals, || {
70            let handler = Handler::with_tty_emitter(
71                ColorConfig::Auto,
72                true,
73                false,
74                Some(self.source_map.clone()),
75            );
76
77            let fm = self
78                .source_map
79                .new_source_file(Arc::new(FileName::Custom(file_name.into())), code.into());
80
81            let mangle_config = if self.mangle {
82                r#"
83                    {
84                        "topLevel": false,
85                        "keepClassNames": true
86                    }
87                "#
88            } else {
89                "false"
90            };
91
92            let minify_options = if self.minify {
93                format!(
94                    r#"
95                "minify": {{
96                  "compress": {{
97                    "unused": true
98                  }},
99                  "format": {{
100                    "comments": false
101                  }},
102                  "mangle": {mangle_config}
103                }},
104            "#
105                )
106            } else {
107                r#"
108                "minify": {
109                  "format": {
110                    "comments": false
111                  }
112                },
113                "#
114                .to_string()
115            };
116
117            let module = if is_module {
118                r#"
119                "module": {
120                    "type": "es6",
121                    "strict": true,
122                    "strictMode": true,
123                    "lazy": false,
124                    "noInterop": false,
125                    "ignoreDynamic": true
126                },
127                "#
128            } else {
129                ""
130            };
131
132            let cfg_json = format!(
133                r#"
134
135            {{
136              "minify": {},
137              "sourceMaps": true,
138              {}
139              "jsc": {{
140                {}
141                "externalHelpers": {},
142                "parser": {{
143                  "syntax": "typescript",
144                  "jsx": true,
145                  "tsx": true,
146                  "decorators": true,
147                  "decoratorsBeforeExport": true,
148                  "dynamicImport": true,
149                  "preserveAllComments": false
150                }},
151                "transform": {{
152                  "legacyDecorator": true,
153                  "decoratorMetadata": true,
154                  "react": {{
155                      "runtime": "classic",
156                      "useBuiltins": true,
157                      "refresh": true
158                  }}
159                }},
160                "target": "{}",
161                "keepClassNames": true
162              }}
163            }}
164
165        "#,
166                self.minify,
167                module,
168                minify_options,
169                self.external_helpers,
170                self.target.as_str()
171            );
172
173            log::trace!("using config {}", cfg_json);
174
175            let cfg = serde_json::from_str(cfg_json.as_str())
176                .map_err(|e| JsError::new_string(format!("{e}")))?;
177
178            let ops = swc::config::Options {
179                config: cfg,
180                ..Default::default()
181            };
182
183            // todo see https://github.com/swc-project/swc/discussions/4126
184            // for better example
185
186            let res = self.compiler.process_js_file(fm, &handler, &ops);
187
188            match res {
189                Ok(to) => Ok((to.code, to.map)),
190                Err(e) => Err(JsError::new_string(format!("transpile failed: {e}"))),
191            }
192        })
193    }
194
195    pub fn transpile_script(&self, script: &mut Script) -> Result<(), JsError> {
196        if script.get_path().ends_with(".ts") {
197            let code = script.get_code();
198
199            let is_module = detect_module(code);
200
201            let js = self.transpile(code, script.get_path(), is_module)?;
202            log::debug!("map: {:?}", js.1);
203            script.set_transpiled_code(js.0, js.1);
204        }
205        log::debug!(
206            "TypeScriptPreProcessor:process file={} result = {}",
207            script.get_path(),
208            script.get_runnable_code()
209        );
210
211        Ok(())
212    }
213}
214
215impl Default for TypeScriptTranspiler {
216    fn default() -> Self {
217        Self::new(TargetVersion::Es2020, false, false, false)
218    }
219}
220
221thread_local! {
222    // we store this in a thread local inb the worker thread so they are dropped when the runtimefacade is dropped
223    static SOURCE_MAPS: RefCell<HashMap<String, String>> = RefCell::new(HashMap::new());
224    static TRANSPILER: RefCell<TypeScriptTranspiler> = RefCell::new(TypeScriptTranspiler::new(TargetVersion::Es2020, false, false, false));
225}
226
227// fix stacktrace method
228pub(crate) fn transpile_serverside(
229    _rt: &QuickJsRuntimeAdapter,
230    script: &mut Script,
231) -> Result<(), JsError> {
232    // transpile and store map in qjsrt
233
234    // transpile
235    TRANSPILER.with(|rc| {
236        let transpiler: &TypeScriptTranspiler = &rc.borrow();
237        transpiler.transpile_script(script)
238    })?;
239
240    // register in source_maps so fix_stack can use it later
241    if let Some(map_str) = script.get_map() {
242        SOURCE_MAPS.with(|rc| {
243            let maps = &mut *rc.borrow_mut();
244            maps.insert(script.get_path().to_string(), map_str.to_string());
245        })
246    }
247    Ok(())
248}
249
250#[derive(Debug)]
251struct StackEntry {
252    function_name: String,
253    file_name: String,
254    line_number: Option<u32>,
255    column_number: Option<u32>,
256}
257
258impl FromStr for StackEntry {
259    type Err = String;
260
261    fn from_str(s: &str) -> Result<Self, Self::Err> {
262        // remove 'at '
263        let s = &s[3..];
264
265        let mut parts = s.splitn(2, ' ');
266        let function_name = parts.next().unwrap_or("unnamed").to_string();
267        let mut file_name = parts.next().unwrap_or("(unknown)").to_string();
268        if file_name.starts_with('(') {
269            file_name = file_name.as_str()[1..].to_string();
270        }
271        if file_name.ends_with(')') {
272            file_name = file_name.as_str()[..file_name.len() - 1].to_string();
273        }
274        file_name = file_name.replace("://", "_double_point_placeholder_//");
275
276        let parts: Vec<&str> = file_name.split(':').collect();
277
278        let file_name = parts[0]
279            .to_string()
280            .replace("_double_point_placeholder_//", "://");
281        let line_number = parts.get(1).and_then(|s| s.parse::<u32>().ok());
282        let column_number = parts.get(2).and_then(|s| s.parse::<u32>().ok());
283
284        Ok(StackEntry {
285            function_name,
286            file_name,
287            column_number,
288            line_number,
289        })
290    }
291}
292
293fn parse_stack_trace(stack_trace: &str) -> Result<Vec<StackEntry>, String> {
294    let entries: Vec<StackEntry> = stack_trace
295        .lines()
296        .map(|line| line.trim())
297        .filter(|line| !line.is_empty())
298        .map(|line| line.parse::<StackEntry>())
299        .collect::<Result<Vec<_>, _>>()?;
300
301    Ok(entries)
302}
303
304fn serialize_stack(entries: &[StackEntry]) -> String {
305    let mut result = String::new();
306
307    for entry in entries {
308        let fname_lnum = if let Some(line_number) = entry.line_number {
309            if let Some(column_number) = entry.column_number {
310                format!("{}:{line_number}:{column_number}", entry.file_name)
311            } else {
312                format!("{}:{line_number}", entry.file_name)
313            }
314        } else {
315            entry.file_name.clone()
316        };
317
318        result.push_str(&format!("    at {} ({fname_lnum})", entry.function_name));
319
320        result.push('\n');
321    }
322
323    result
324}
325
326pub(crate) fn unmap_stack_trace(stack_trace: &str) -> String {
327    // todo: not the fastest way to impl this.. should I keep instances of source map instead of string? what does that do to mem consumtion?
328    SOURCE_MAPS.with(|rc| fix_stack_trace(stack_trace, &rc.borrow()))
329}
330
331pub fn fix_stack_trace(stack_trace: &str, maps: &HashMap<String, String>) -> String {
332    log::trace!("fix_stack_trace:\n{stack_trace}");
333
334    match parse_stack_trace(stack_trace) {
335        Ok(mut parsed_stack) => {
336            for stack_trace_entry in parsed_stack.iter_mut() {
337                if let Some(map_str) = maps.get(stack_trace_entry.file_name.as_str()) {
338                    log::trace!(
339                        "fix_stack_trace:found map for file {}:\n{map_str}",
340                        stack_trace_entry.file_name.as_str()
341                    );
342                    if let Some(line_number) = stack_trace_entry.line_number {
343                        log::trace!("lookup line number:{line_number}");
344                        match swc::sourcemap::SourceMap::from_reader(io::Cursor::new(map_str)) {
345                            Ok(source_map) => {
346                                if let Some(original_location) = source_map.lookup_token(
347                                    line_number - 1,
348                                    stack_trace_entry.column_number.unwrap_or(1) - 1,
349                                ) {
350                                    let original_line = original_location.get_src_line();
351                                    let original_column = original_location.get_src_col();
352                                    log::trace!("lookup original_line:{original_line}");
353                                    stack_trace_entry.line_number = Some(original_line + 1);
354                                    stack_trace_entry.column_number = Some(original_column + 1);
355                                }
356                            }
357                            Err(_) => {
358                                log::trace!(
359                                    "could not parse source_map for {}",
360                                    stack_trace_entry.file_name.as_str()
361                                );
362                            }
363                        }
364                    }
365                } else {
366                    log::trace!("no map found for {}", stack_trace_entry.file_name.as_str());
367                }
368
369                // Now you have the original filename and line number
370                // You can use them as needed
371            }
372
373            let ret = serialize_stack(&parsed_stack);
374            log::trace!("fix_stack_trace ret:\n{ret}");
375            ret
376        }
377        Err(_) => {
378            log::error!("could not parse stack: \n{}", stack_trace);
379            stack_trace.to_string()
380        }
381    }
382}
383
384#[cfg(test)]
385pub mod tests {
386    use crate::facades::tests::init_test_rt;
387    use crate::jsutils::{JsValueType, Script};
388    use crate::typescript::{parse_stack_trace, serialize_stack};
389
390    #[test]
391    fn test_ts() {
392        let rt = init_test_rt();
393        println!("testing ts");
394        let script = Script::new(
395            "test.ts",
396            r#"
397            // hi
398            // ho
399            function t_ts(a: string, b: num): boolean {
400                return true;
401            }
402            t_ts("hello", 1337);
403        "#,
404        );
405        let res = rt.eval_sync(None, script).expect("script failed");
406        assert!(res.get_value_type() == JsValueType::Boolean);
407    }
408    #[test]
409    fn test_stack_map() {
410        let rt = init_test_rt();
411        println!("testing ts");
412        let script = Script::new(
413            "test.ts",
414            r#"
415            
416            type Nonsense = {
417                hello: string
418            };
419            
420            function t_ts(a: string, b: num): boolean {
421                return a.a.a === "hi";
422            }
423
424            t_ts("hello", 1337);
425"#,
426        );
427        let res = rt
428            .eval_sync(None, script)
429            .expect_err("script passed.. which it shouldnt");
430        // far from perfect test, also line numbers don't yet realy match..
431        // check again after https://github.com/HiRoFa/quickjs_es_runtime/issues/77
432        println!("stack:{}", res.get_stack());
433
434        // bellard is actually better as it points at the actual issue, not the start of the function
435
436        #[cfg(feature = "bellard")]
437        assert!(res.get_stack().contains("t_ts (test.ts:8"));
438        #[cfg(feature = "quickjs-ng")]
439        assert!(res.get_stack().contains("t_ts (test.ts:7"));
440    }
441    #[test]
442    fn test_stack_parse() {
443        // just to init logging;
444        let _rt = init_test_rt();
445
446        let stack = r#"
447            at func (file.ts:88:12)
448            at doWriteTransactioned (gcsproject:///gcs_objectstore/ObjectStore.ts:170)
449        "#;
450        match parse_stack_trace(stack) {
451            Ok(a) => {
452                assert_eq!(a[0].file_name, "file.ts");
453                assert_eq!(a[0].line_number, Some(88));
454                assert_eq!(a[0].column_number, Some(12));
455                assert_eq!(a[0].function_name, "func");
456
457                assert_eq!(
458                    a[1].file_name,
459                    "gcsproject:///gcs_objectstore/ObjectStore.ts"
460                );
461                assert_eq!(a[1].line_number, Some(170));
462                assert_eq!(a[1].column_number, None);
463                assert_eq!(a[1].function_name, "doWriteTransactioned");
464
465                assert_eq!(
466                    serialize_stack(&a).as_str(),
467                    r#"    at func (file.ts:88:12)
468    at doWriteTransactioned (gcsproject:///gcs_objectstore/ObjectStore.ts:170)
469"#
470                );
471            }
472            Err(e) => {
473                panic!("{}", e);
474            }
475        }
476    }
477}