diff --git a/libjrpc/src/compile.rs b/libjrpc/src/compile.rs index e6b2967..5a935b7 100644 --- a/libjrpc/src/compile.rs +++ b/libjrpc/src/compile.rs @@ -137,9 +137,9 @@ impl FileGenerator { self.a(6, content); } - pub fn add_line(&mut self, line: &str) { - self.content.push(line.to_string()); - } + // pub fn add_line(&mut self, line: &str) { + // self.content.push(line.to_string()); + // } pub fn get_content(&self) -> String { self.content.join("\n") diff --git a/libjrpc/src/ir.rs b/libjrpc/src/ir.rs index fb5bc28..b35f2f3 100644 --- a/libjrpc/src/ir.rs +++ b/libjrpc/src/ir.rs @@ -1,7 +1,7 @@ use std::{ collections::{HashMap, HashSet}, error::Error, - fmt::Display, + fmt::{Debug, Display}, hash::{Hash, Hasher}, }; @@ -13,8 +13,9 @@ use crate::parser::{ static BUILT_INS: [&str; 6] = ["int", "float", "string", "boolean", "bytes", "void"]; -pub trait Definition { +pub trait Definition: Debug { fn get_position(&self) -> ParserPosition; + fn get_name(&self) -> String; } #[derive(Debug, Clone)] @@ -186,6 +187,9 @@ impl Definition for TypeDefinition { fn get_position(&self) -> ParserPosition { self.position.clone() } + fn get_name(&self) -> String { + self.name.clone() + } } #[derive(Debug, Clone)] @@ -205,6 +209,9 @@ impl Definition for EnumDefinition { fn get_position(&self) -> ParserPosition { self.position.clone() } + fn get_name(&self) -> String { + self.name.clone() + } } #[derive(Debug, Clone)] @@ -225,6 +232,9 @@ impl Definition for ServiceDefinition { fn get_position(&self) -> ParserPosition { self.position.clone() } + fn get_name(&self) -> String { + self.name.clone() + } } #[derive(Debug, Clone)] diff --git a/libjrpc/src/lib.rs b/libjrpc/src/lib.rs index c8c40da..615eeed 100644 --- a/libjrpc/src/lib.rs +++ b/libjrpc/src/lib.rs @@ -10,31 +10,3 @@ pub use ir::IR; pub use parser::{Parser, RootNode}; pub use process::FileProcessor; pub use tokenizer::{tokenize, Token, TokenError, TokenPosition, TokenType}; - -#[cfg(test)] -mod test { - use crate::targets::{self, rust::RustCompiler}; - - #[cfg(test)] - #[ctor::ctor] - fn init() { - env_logger::init(); - } - - #[test] - pub fn parse_jrpc() { - let mut fp = crate::process::FileProcessor::new(); - // let ir = fp.start_compile("./test.jrpc").unwrap(); - let ir = fp.start_compile("http://127.0.0.1:7878/test.jrpc").unwrap(); - - println!("{:?}", ir); - } - - #[test] - pub fn generate_rust() { - let mut fp = crate::process::FileProcessor::new(); - let ir = fp.start_compile("./test.jrpc").unwrap(); - - targets::compile::(ir, "./output/rust").unwrap(); - } -} diff --git a/libjrpc/src/parser.rs b/libjrpc/src/parser.rs index aaca716..598ba35 100644 --- a/libjrpc/src/parser.rs +++ b/libjrpc/src/parser.rs @@ -667,8 +667,8 @@ pub struct ParserError { } impl ParserError { - fn new(msg: &str, token: &Token) -> ParserError { - ParserError { + fn new(msg: &str, token: &Token) -> Self { + Self { message: format!("{}: {}", msg, token.1), token: token.clone(), } diff --git a/libjrpc/src/targets/mod.rs b/libjrpc/src/targets/mod.rs index b3d114a..111ac16 100644 --- a/libjrpc/src/targets/mod.rs +++ b/libjrpc/src/targets/mod.rs @@ -1,13 +1,47 @@ +use std::{ + error::Error, + fmt::{Debug, Display}, +}; + use anyhow::Result; use crate::{ compile::{Compile, CompileContext}, + ir::Definition, IR, }; pub mod rust; pub mod typescript; +#[derive(Debug, Clone)] +pub struct CompilerError { + pub message: String, + pub definition: D, +} + +impl CompilerError { + fn new(msg: &str, definition: D) -> Self { + Self { + message: format!("{}: {}", msg, definition.get_name()), + definition, + } + } +} + +impl Error for CompilerError {} + +impl Display for CompilerError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "CompilerError: {} at {:?}", + self.message, + self.definition.get_position() + ) + } +} + pub fn compile(ir: IR, output: &str) -> Result<()> { let mut ctx = CompileContext::new(output); let mut compiler = T::new(&ir.options)?; @@ -15,10 +49,29 @@ pub fn compile(ir: IR, output: &str) -> Result<()> { for step in ir.steps.iter() { match step { - crate::ir::Step::Type(definition) => compiler.generate_type(&mut ctx, &definition)?, - crate::ir::Step::Enum(definition) => compiler.generate_enum(&mut ctx, &definition)?, + crate::ir::Step::Type(definition) => { + match compiler.generate_type(&mut ctx, &definition) { + Ok(_) => (), + Err(err) => { + return Err(CompilerError::new(&err.to_string(), definition.clone()).into()) + } + } + } + crate::ir::Step::Enum(definition) => { + match compiler.generate_enum(&mut ctx, &definition) { + Ok(_) => (), + Err(err) => { + return Err(CompilerError::new(&err.to_string(), definition.clone()).into()) + } + } + } crate::ir::Step::Service(definition) => { - compiler.generate_service(&mut ctx, &definition)? + match compiler.generate_service(&mut ctx, &definition) { + Ok(_) => (), + Err(err) => { + return Err(CompilerError::new(&err.to_string(), definition.clone()).into()) + } + } } } } diff --git a/libjrpc/src/targets/rust.rs b/libjrpc/src/targets/rust.rs index 240be8e..4b13963 100644 --- a/libjrpc/src/targets/rust.rs +++ b/libjrpc/src/targets/rust.rs @@ -3,9 +3,7 @@ use log::warn; use std::collections::{HashMap, HashSet}; use crate::compile::{Compile, CompileContext, FileGenerator}; -use crate::ir::{ - BaseType, EnumDefinition, ServiceDefinition, Step, Type, TypeDefinition, TypeModifier, -}; +use crate::ir::{BaseType, EnumDefinition, ServiceDefinition, Step, Type, TypeDefinition}; use crate::shared::Keywords; use crate::IR; diff --git a/libjrpc/src/targets/typescript.rs b/libjrpc/src/targets/typescript.rs index c04dd52..8f2e209 100644 --- a/libjrpc/src/targets/typescript.rs +++ b/libjrpc/src/targets/typescript.rs @@ -1,78 +1,105 @@ -use anyhow::Result; -use log::{info, warn}; +use anyhow::{anyhow, Result}; +use log::info; use std::collections::{HashMap, HashSet}; use crate::compile::{Compile, CompileContext, FileGenerator}; use crate::ir::{BaseType, EnumDefinition, ServiceDefinition, Step, Type, TypeDefinition}; -use crate::shared::Keywords; + use crate::IR; -#[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] -pub enum Flavour { - ESM, - Node, +// #[derive(Debug, PartialEq, Eq, PartialOrd, Ord)] +// pub enum Flavour { +// ESM, +// Node, +// } + +pub trait Flavour { + fn ext() -> &'static str; + fn name() -> &'static str; } -pub struct TypeScriptCompiler { - flavour: Flavour, +pub struct Node; +impl Flavour for Node { + fn ext() -> &'static str { + "" + } + fn name() -> &'static str { + "ts-node" + } } -static TS_KEYWORDS: [&'static str; 52] = [ - "abstract", - "arguments", - "await", - "boolean", - "break", - "byte", - "case", - "catch", - "class", - "const", - "continue", - "debugger", - "default", - "delete", - "do", - "else", - "enum", - "export", - "extends", - "false", - "final", - "finally", - "for", - "function", - "goto", - "if", - "implements", - "import", - "in", - "instanceof", - "interface", - "let", - "new", - "null", - "package", - "private", - "protected", - "public", - "return", - "super", - "switch", - "this", - "throw", - "true", - "try", - "typeof", - "var", - "void", - "while", - "with", - "yield", - "static", -]; +pub struct ESM; +impl Flavour for ESM { + fn ext() -> &'static str { + ".js" + } -impl TypeScriptCompiler { + fn name() -> &'static str { + "ts-esm" + } +} + +pub struct TypeScriptCompiler { + // flavour: Flavour, + flavour: std::marker::PhantomData, +} + +// static TS_KEYWORDS: [&'static str; 52] = [ +// "abstract", +// "arguments", +// "await", +// "boolean", +// "break", +// "byte", +// "case", +// "catch", +// "class", +// "const", +// "continue", +// "debugger", +// "default", +// "delete", +// "do", +// "else", +// "enum", +// "export", +// "extends", +// "false", +// "final", +// "finally", +// "for", +// "function", +// "goto", +// "if", +// "implements", +// "import", +// "in", +// "instanceof", +// "interface", +// "let", +// "new", +// "null", +// "package", +// "private", +// "protected", +// "public", +// "return", +// "super", +// "switch", +// "this", +// "throw", +// "true", +// "try", +// "typeof", +// "var", +// "void", +// "while", +// "with", +// "yield", +// "static", +// ]; + +impl TypeScriptCompiler { fn type_to_typescript(typ: &BaseType) -> String { match typ { BaseType::String => "string".to_string(), @@ -109,13 +136,9 @@ impl TypeScriptCompiler { file: &mut FileGenerator, depends: &HashSet, ) -> Result<()> { - let esm = if self.flavour == Flavour::ESM { - ".js" - } else { - "" - }; + let esm = F::ext(); file.a0(format!( - "import {{ VerificationError, apply_int, apply_float, apply_string, apply_boolean, apply_void }} from \"./ts_base{esm}\"")); + "import {{ VerificationError, apply_int, apply_float, apply_string, apply_boolean, apply_void, apply_array, apply_required, apply_optional, apply_map }} from \"./ts_base{esm}\"")); for dep in depends { match dep { BaseType::Custom(name) => { @@ -132,17 +155,76 @@ impl TypeScriptCompiler { } fn fix_keyword_name(name: &str) -> String { - if TS_KEYWORDS.contains(&name) { - format!("{}_", name) - } else { - name.to_string() + // if TS_KEYWORDS.contains(&name) { + // format!("{}_", name) + // } else { + // name.to_string() + // } + // TODO: Check if this is something that can be implemented. There is the issue of JSON generation... + name.to_string() + } + + fn get_type_apply(typ: &Type, value: &str) -> String { + let apply_field_value = format!("apply_{}", typ.0.to_string()); + match &typ.1 { + crate::ir::TypeModifier::None => format!("{}({})", apply_field_value, value), + crate::ir::TypeModifier::Optional => format!("apply_optional({}, {})", value, apply_field_value), + crate::ir::TypeModifier::Array => format!("apply_array({}, {})", value, apply_field_value), + crate::ir::TypeModifier::OptionalArray => format!("apply_optional({}, data => apply_array(data, {}))", value, apply_field_value), + crate::ir::TypeModifier::Map(map) => format!("apply_map({}, apply_{}, {})", value, map.to_string(), apply_field_value), + crate::ir::TypeModifier::OptionalMap(map) => format!("apply_optional({}, data => apply_map(data, apply_{}, {}))", value, map.to_string(), apply_field_value), + crate::ir::TypeModifier::MapArray(map) => format!("apply_map({}, apply_{}, data => apply_array(data, {}))", value, map.to_string(), apply_field_value), + crate::ir::TypeModifier::OptionalMapArray(map) => format!("apply_optional({}, data => apply_map(data, apply_{}, (data) => apply_array(data, {})))", value, map.to_string(), apply_field_value), } } - fn generate_service_lib(ctx: &CompileContext, ir: &IR) -> Result<()> { + fn generate_service_lib(&mut self, ctx: &CompileContext, ir: &IR) -> Result<()> { + let esm = F::ext(); + let mut f = FileGenerator::new(); + let mut fc = FileGenerator::new(); + fc.a0(format!("export * from \"./ts_service_client{}\";", esm)); + fc.a0(""); + let mut fs = FileGenerator::new(); + fs.a0(format!("export * from \"./ts_service_server{}\";", esm)); + fs.a0(""); + + for step in &ir.steps { + match step { + Step::Enum(def) => { + f.a0(format!( + "import {}, {{ apply_{} }} from \"./{}{}\";", + def.name, def.name, def.name, esm + )); + f.a0(format!("export {{ {}, apply_{} }};", def.name, def.name)); + f.a0(""); + } + Step::Type(def) => { + f.a0(format!( + "import {}, {{ apply_{} }} from \"./{}{}\";", + def.name, def.name, def.name, esm + )); + f.a0(format!("export {{ {}, apply_{} }};", def.name, def.name)); + f.a0(""); + } + Step::Service(def) => { + fc.a0(format!( + "export {{ {} }} from \"./{}_client{}\"", + def.name, def.name, esm + )); + fs.a0(format!( + "export {{ {} }} from \"./{}_server{}\"", + def.name, def.name, esm + )); + } + }; + } + + ctx.write_file("index.ts", &f.into_content())?; + ctx.write_file("index_client.ts", &fc.into_content())?; + ctx.write_file("index_server.ts", &fs.into_content())?; Ok(()) } @@ -152,11 +234,7 @@ impl TypeScriptCompiler { ctx: &mut CompileContext, definition: &ServiceDefinition, ) -> anyhow::Result<()> { - let esm = if self.flavour == Flavour::ESM { - ".js" - } else { - "" - }; + let esm = F::ext(); ctx.write_file("ts_service_server.ts", &format!(" import {{ type RequestObject, type ResponseObject, ErrorCodes, Logging }} from \"./ts_service_base{esm}\"; @@ -190,7 +268,13 @@ import {{ VerificationError }} from \"./ts_base{esm}\"; let mut params = func .inputs .iter() - .map(|p| format!("{}: {}", p.name, Self::type_to_typescript_ext(&p.typ))) + .map(|p| { + format!( + "{}: {}", + Self::fix_keyword_name(&p.name), + Self::type_to_typescript_ext(&p.typ) + ) + }) .collect::>(); params.push("ctx: T".to_string()); let params = params.join(", "); @@ -207,61 +291,48 @@ import {{ VerificationError }} from \"./ts_base{esm}\"; func.name, Self::type_to_typescript_ext(&output.typ) )); - let params_str_arr = func - .inputs - .iter() - .map(|e| format!("\"{}\"", e.name)) - .collect::>() - .join(", "); - f.a2(format!( - "let p = this._into_parameters([{}], params)", - params_str_arr - )); - - for (index, input) in func.inputs.iter().enumerate() { - f.a2(format!( - "if(p[{}] !== null && p[{}] !== undefined) {{", - index, index - )); - if input.typ.array { - f.a2(format!("for (const elm of p[{}]) {{", index)); - f.a3(format!("apply_{}(elm);", input.name)); - f.a2(format!("}}")); - } else if let Some(_map) = &input.typ.map { - // TODO: Implement map type handling - panic!("Map in arguments is not allowed!"); - } else { - f.a3(format!( - "apply_{}(p[{}]);", - input.typ.base.to_string(), - index - )); - } - f.a2("}"); - if !input.typ.optional { - f.a2(format!( - "else throw new Error(`Missing required parameter ${}`);", - input.name - )); - } - f.a0(""); - } - - f.a2("p.push(ctx);"); - - let ret_apply = String::new(); - if output.typ. - - f.a2("//@ts-ignore This will cause a typescript error when strict checking, since p is not a tuple"); - f.a2(format!( - "return this.{}.call(this, ...p).then(res=>{});", - func.name, ret_apply - )); - - f.a1("}"); } else { f.a1(format!("public abstract {}({}): void;", func.name, params)); + f.a1(format!( + "_{}(params: any[] | any, ctx: T): void {{", + func.name, + )); } + let params_str_arr = func + .inputs + .iter() + .map(|e| format!("\"{}\"", e.name)) + .collect::>() + .join(", "); + f.a2(format!( + "let p = this._into_parameters([{}], params);", + params_str_arr + )); + + for (index, input) in func.inputs.iter().enumerate() { + f.a2(format!( + "p[{}] = {};", + index, + Self::get_type_apply(&input.typ, &format!("p[{}]", index)) + )); + } + + f.a2("p.push(ctx);"); + + let ret_value = if let Some(output) = &func.output { + let ret_apply = Self::get_type_apply(&output.typ, "res"); + format!(".then(res => {})", ret_apply) + } else { + String::new() + }; + + f.a2("//@ts-ignore This will cause a typescript error when strict checking, since p is not a tuple"); + f.a2(format!( + "return this.{}.call(this, ...p){};", + func.name, ret_value + )); + + f.a1("}"); f.a0(""); } @@ -279,11 +350,7 @@ import {{ VerificationError }} from \"./ts_base{esm}\"; ctx: &mut CompileContext, definition: &ServiceDefinition, ) -> anyhow::Result<()> { - let esm = if self.flavour == Flavour::ESM { - ".js" - } else { - "" - }; + let esm = F::ext(); ctx.write_file( "ts_service_base.ts", @@ -364,7 +431,7 @@ import {{ VerificationError }} from \"./ts_base{esm}\"; } } -impl Compile for TypeScriptCompiler { +impl Compile for TypeScriptCompiler { fn new(options: &HashMap) -> Result { let flavour = options .get("flavour") @@ -372,15 +439,12 @@ impl Compile for TypeScriptCompiler { .unwrap_or_else(|| "node".to_string()); info!("TypeScript target initialized with flavour: {}", flavour); Ok(TypeScriptCompiler { - flavour: Flavour::ESM, + flavour: std::marker::PhantomData, }) } fn name(&self) -> String { - match self.flavour { - Flavour::ESM => "ts-esm".to_string(), - Flavour::Node => "ts-node".to_string(), - } + F::name().to_string() } fn start(&mut self, ctx: &mut CompileContext) -> anyhow::Result<()> { @@ -418,8 +482,12 @@ impl Compile for TypeScriptCompiler { )); f.a2("if(init) {"); for field in definition.fields.iter() { - f.a3(format!("if(init.{})", field.name)); - f.a4(format!("this.{} = init[\"{}\"]", field.name, field.name)); + f.a3(format!("if(init.{})", Self::fix_keyword_name(&field.name))); + f.a4(format!( + "this.{} = init[\"{}\"]", + Self::fix_keyword_name(&field.name), + Self::fix_keyword_name(&field.name) + )); } f.a2("}"); f.a1("}"); @@ -443,68 +511,12 @@ impl Compile for TypeScriptCompiler { f.a1(format!("let res = new {}() as any;", definition.name)); for field in definition.fields.iter() { - if field.typ.is_optional() { - f.a1(format!( - "if (data.{} !== null && data.{} !== undefined ) {{", - field.name, field.name - )); - } else { - f.a1(format!( - "if (data.{} === null || data.{} === undefined ) throw new VerificationError(\"{}\", \"{}\", data.{});", - field.name, field.name, - definition.name, - field.name, - field.name - )); - f.a1("else {"); - } - - if field.typ.is_map() { - f.a2(format!( - "if (typeof data.{} != \"object\") throw new VerificationError(\"map\", \"{}\", data.{}); ", - field.name, - definition.name, - field.name - )); - f.a2(format!("res.{} = {{}}", field.name)); - f.a2(format!( - "Object.entries(data.{}).forEach(([key, val]) => res.{}[key] = apply_{}(val));", - field.name, - field.name, - Self::type_to_typescript(&field.typ.base), - )); - } - - - if field.typ.is_array() { - f.a2(format!( - "if (!Array.isArray(data.{})) throw new VerificationError(\"array\", \"{}\", data.{});", - field.name, - definition.name, - field.name - )); - f.a2(format!( - "res.{} = data.{}.map(elm => apply_{}(elm));", - field.name, - field.name, - Self::type_to_typescript(&field.typ.base), - )); - } - - if field.typ.array { - - } else if let Some(_map) = &field.typ.map { - - - } else { - f.a2(format!( - "res.{} = apply_{}(data.{})", - field.name, - Self::type_to_typescript(&field.typ.base), - field.name - )); - } - f.a1("}"); + let value_name = format!("data.{}", Self::fix_keyword_name(&field.name)); + f.a1(format!( + "res.{} = {};", + Self::fix_keyword_name(&field.name), + Self::get_type_apply(&field.typ, &value_name) + )); } f.a1("return res;"); @@ -560,7 +572,7 @@ impl Compile for TypeScriptCompiler { } fn finalize(&mut self, ctx: &mut CompileContext, ir: &IR) -> anyhow::Result<()> { - Self::generate_service_lib(ctx, ir)?; + self.generate_service_lib(ctx, ir)?; Ok(()) } } diff --git a/libjrpc/templates/TypeScript/ts_base.ts b/libjrpc/templates/TypeScript/ts_base.ts index d296810..cfaf70e 100644 --- a/libjrpc/templates/TypeScript/ts_base.ts +++ b/libjrpc/templates/TypeScript/ts_base.ts @@ -1,44 +1,87 @@ function form_verficiation_error_message(type?: string, field?: string) { - let msg = "Parameter verification failed! "; - if (type && field) { - msg += `At ${type}.${field}! `; - } else if (type) { - msg += `At type ${type}! `; - } else if (field) { - msg += `At field ${field}! `; - } - return msg; + let msg = "Parameter verification failed! "; + if (type && field) { + msg += `At ${type}.${field}! `; + } else if (type) { + msg += `At type ${type}! `; + } else if (field) { + msg += `At field ${field}! `; + } + return msg; } export class VerificationError extends Error { - constructor( - public readonly type?: string, - public readonly field?: string, - public readonly value?: any - ) { - super(form_verficiation_error_message(type, field)); - } + constructor( + public readonly type?: string, + public readonly field?: string, + public readonly value?: any, + ) { + super(form_verficiation_error_message(type, field)); + } } export function apply_int(data: any) { - data = Math.floor(Number(data)); - if (Number.isNaN(data)) throw new VerificationError("int", undefined, data); - return data; + data = Math.floor(Number(data)); + if (Number.isNaN(data)) throw new VerificationError("int", undefined, data); + return data; } export function apply_float(data: any) { - data = Number(data); - if (Number.isNaN(data)) - throw new VerificationError("float", undefined, data); - return data; + data = Number(data); + if (Number.isNaN(data)) throw new VerificationError("float", undefined, data); + return data; } export function apply_string(data: any) { - return String(data); + return String(data); } export function apply_boolean(data: any) { - return Boolean(data); + return Boolean(data); } -export function apply_void(data: any) { } +export function apply_map( + data: any, + apply_key: (data: any) => any, + apply_value: (data: any) => any, +) { + if (typeof data !== "object") + throw new VerificationError("map", undefined, data); + + let res = {}; + for (const key in data) { + let key_ = apply_key(key); + let value_ = apply_value(data[key]); + res[key_] = value_; + } + return res; +} + +export function apply_array(data: any, apply_value: (data: any) => any) { + if (!Array.isArray(data)) + throw new VerificationError("array", undefined, data); + return data.map((item) => apply_value(item)); +} + +export function apply_required( + data: any, + apply_value: (data: any) => any, +): any { + if (typeof data === "undefined" || data === null) { + throw new VerificationError("required", undefined, data); + } + return apply_value(data); +} + +export function apply_optional( + data: any, + apply_value: (data: any) => any, +): any { + if (typeof data === "undefined" || data === null) { + return data; + } else { + return apply_value(data); + } +} + +export function apply_void(data: any) {} diff --git a/src/main.rs b/src/main.rs index 2144cf3..dbf4f50 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,7 +1,10 @@ use anyhow::Result; use clap::{Parser, Subcommand}; use libjrpc::{ - targets::{rust::RustCompiler, typescript::TypeScriptCompiler}, + targets::{ + rust::RustCompiler, + typescript::{Node, TypeScriptCompiler}, + }, FileProcessor, }; @@ -55,17 +58,28 @@ pub fn main() -> Result<()> { match output_target { "rust" => libjrpc::targets::compile::(ir, output_dir)?, - "ts-node" => libjrpc::targets::compile::(ir, output_dir)?, + "ts-node" => { + libjrpc::targets::compile::>(ir, output_dir)? + } + "ts-esm" => { + libjrpc::targets::compile::>(ir, output_dir)? + } _ => { println!("Unsupported target: {}", output_target); } } } + if let Some(_def) = definition { + panic!("Definition output is not yet implemented!"); + } + //TODO: Implement definition output! } Commands::Targets => { - panic!("Not yet implemented!") + println!("rust"); + println!("ts-node"); + println!("ts-esm"); } }