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        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            // todo see https://github.com/swc-project/swc/discussions/4126
185            // for better example
186
187            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    // we store this in a thread local inb the worker thread so they are dropped when the runtimefacade is dropped
224    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
228// fix stacktrace method
229pub(crate) fn transpile_serverside(
230    _rt: &QuickJsRuntimeAdapter,
231    script: &mut Script,
232) -> Result<(), JsError> {
233    // transpile and store map in qjsrt
234
235    // transpile
236    TRANSPILER.with(|rc| {
237        let transpiler: &TypeScriptTranspiler = &rc.borrow();
238        transpiler.transpile_script(script)
239    })?;
240
241    // register in source_maps so fix_stack can use it later
242    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        // remove 'at '
264        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    // 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?
329    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                // Now you have the original filename and line number
371                // You can use them as needed
372            }
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        // far from perfect test, also line numbers don't yet realy match..
432        // check again after https://github.com/HiRoFa/quickjs_es_runtime/issues/77
433        println!("stack:{}", res.get_stack());
434
435        // bellard is actually better as it points at the actual issue, not the start of the function
436
437        #[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        // just to init logging;
445        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}