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 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 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 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
227pub(crate) fn transpile_serverside(
229 _rt: &QuickJsRuntimeAdapter,
230 script: &mut Script,
231) -> Result<(), JsError> {
232 TRANSPILER.with(|rc| {
236 let transpiler: &TypeScriptTranspiler = &rc.borrow();
237 transpiler.transpile_script(script)
238 })?;
239
240 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 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 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 }
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 println!("stack:{}", res.get_stack());
433
434 #[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 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}