1use super::error::Result;
71use dashmap::DashMap;
72use serde::{Deserialize, Serialize};
73use std::collections::HashMap;
74use std::path::Path;
75use std::sync::LazyLock;
76
77use swc_core::{
78 common::{FileName, SourceMap, sync::Lrc},
79 ecma::{
80 ast::*,
81 codegen::{Config, Emitter, Node, text_writer::JsWriter},
82 parser::{EsSyntax, Parser, StringInput, Syntax, TsSyntax, lexer::Lexer},
83 },
84};
85
86pub static CONFIG_CACHE: LazyLock<DashMap<String, MacroforgeConfig>> = LazyLock::new(DashMap::new);
89
90pub fn clear_config_cache() {
95 CONFIG_CACHE.clear();
96}
97
98const CONFIG_FILES: &[&str] = &[
100 "macroforge.config.ts",
101 "macroforge.config.mts",
102 "macroforge.config.js",
103 "macroforge.config.mjs",
104 "macroforge.config.cjs",
105];
106
107#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct ImportInfo {
110 pub name: String,
112 pub source: String,
114}
115
116#[derive(Debug, Clone, Default, Serialize, Deserialize)]
135pub struct ForeignTypeAlias {
136 pub name: String,
138 pub from: String,
140}
141
142#[derive(Debug, Clone, Default, Serialize, Deserialize)]
182pub struct ForeignTypeConfig {
183 pub name: String,
186
187 pub namespace: Option<String>,
191
192 pub from: Vec<String>,
195
196 pub serialize_expr: Option<String>,
198
199 pub serialize_import: Option<ImportInfo>,
201
202 pub deserialize_expr: Option<String>,
204
205 pub deserialize_import: Option<ImportInfo>,
207
208 pub default_expr: Option<String>,
210
211 pub default_import: Option<ImportInfo>,
213
214 #[serde(default)]
232 pub aliases: Vec<ForeignTypeAlias>,
233
234 #[serde(default, skip_serializing)]
241 pub expression_namespaces: Vec<String>,
242}
243
244impl ForeignTypeConfig {
245 pub fn get_namespace(&self) -> Option<&str> {
250 if let Some(ref ns) = self.namespace {
251 return Some(ns);
252 }
253 if let Some(dot_idx) = self.name.rfind('.') {
255 return Some(&self.name[..dot_idx]);
256 }
257 None
258 }
259
260 pub fn get_type_name(&self) -> &str {
264 self.name.rsplit('.').next().unwrap_or(&self.name)
265 }
266
267 pub fn get_qualified_name(&self) -> String {
271 if let Some(ns) = self.get_namespace() {
272 let type_name = self.get_type_name();
273 if ns != type_name {
274 return format!("{}.{}", ns, type_name);
275 }
276 }
277 self.name.clone()
278 }
279}
280
281#[derive(Debug, Clone, Serialize, Deserialize)]
286#[serde(rename_all = "camelCase")]
287pub struct MacroforgeConfig {
288 #[serde(default)]
293 pub keep_decorators: bool,
294
295 #[serde(default = "default_generate_convenience_const")]
300 pub generate_convenience_const: bool,
301
302 #[serde(default)]
306 pub foreign_types: Vec<ForeignTypeConfig>,
307
308 #[serde(default, skip_serializing)]
314 pub config_imports: HashMap<String, ImportInfo>,
315}
316
317fn default_generate_convenience_const() -> bool {
319 true
320}
321
322impl Default for MacroforgeConfig {
323 fn default() -> Self {
324 Self {
325 keep_decorators: false,
326 generate_convenience_const: true, foreign_types: Vec::new(),
328 config_imports: HashMap::new(),
329 }
330 }
331}
332
333impl MacroforgeConfig {
334 pub fn from_config_file(content: &str, filepath: &str) -> Result<Self> {
352 let is_typescript = filepath.ends_with(".ts") || filepath.ends_with(".mts");
353
354 let cm: Lrc<SourceMap> = Default::default();
355 let fm = cm.new_source_file(
356 FileName::Custom(filepath.to_string()).into(),
357 content.to_string(),
358 );
359
360 let syntax = if is_typescript {
361 Syntax::Typescript(TsSyntax {
362 tsx: false,
363 decorators: true,
364 ..Default::default()
365 })
366 } else {
367 Syntax::Es(EsSyntax {
368 decorators: true,
369 ..Default::default()
370 })
371 };
372
373 let lexer = Lexer::new(syntax, EsVersion::latest(), StringInput::from(&*fm), None);
374 let mut parser = Parser::new_from(lexer);
375
376 let module = parser
377 .parse_module()
378 .map_err(|e| super::MacroError::InvalidConfig(format!("Parse error: {:?}", e)))?;
379
380 let imports = extract_imports(&module);
382
383 let config = extract_default_export(&module, &imports, &cm)?;
385
386 Ok(config)
387 }
388
389 pub fn load_and_cache(content: &str, filepath: &str) -> Result<Self> {
402 if let Some(cached) = CONFIG_CACHE.get(filepath) {
404 return Ok(cached.clone());
405 }
406
407 let config = Self::from_config_file(content, filepath)?;
409 CONFIG_CACHE.insert(filepath.to_string(), config.clone());
410
411 Ok(config)
412 }
413
414 pub fn get_cached(filepath: &str) -> Option<Self> {
416 CONFIG_CACHE.get(filepath).map(|c| c.clone())
417 }
418
419 pub fn find_with_root() -> Result<Option<(Self, std::path::PathBuf)>> {
430 let current_dir = std::env::current_dir()?;
431 Self::find_config_in_ancestors(¤t_dir)
432 }
433
434 pub fn find_with_root_from_path(
451 start_path: &Path,
452 ) -> Result<Option<(Self, std::path::PathBuf)>> {
453 let start_dir = if start_path.is_file() {
454 start_path
455 .parent()
456 .map(|p| p.to_path_buf())
457 .unwrap_or_else(|| start_path.to_path_buf())
458 } else {
459 start_path.to_path_buf()
460 };
461 Self::find_config_in_ancestors(&start_dir)
462 }
463
464 pub fn find_from_path(start_path: &Path) -> Result<Option<Self>> {
470 Ok(Self::find_with_root_from_path(start_path)?.map(|(cfg, _)| cfg))
471 }
472
473 fn find_config_in_ancestors(start_dir: &Path) -> Result<Option<(Self, std::path::PathBuf)>> {
475 let mut current = start_dir.to_path_buf();
476
477 loop {
478 for config_name in CONFIG_FILES {
480 let config_path = current.join(config_name);
481 if config_path.exists() {
482 let content = std::fs::read_to_string(&config_path)?;
483 let config =
484 Self::from_config_file(&content, config_path.to_string_lossy().as_ref())?;
485 return Ok(Some((config, current.clone())));
486 }
487 }
488
489 if current.join("package.json").exists() {
491 break;
492 }
493
494 if !current.pop() {
496 break;
497 }
498 }
499
500 Ok(None)
501 }
502
503 pub fn find_and_load() -> Result<Option<Self>> {
506 Ok(Self::find_with_root()?.map(|(cfg, _)| cfg))
507 }
508}
509
510fn atom_to_string(atom: &swc_core::ecma::utils::swc_atoms::Wtf8Atom) -> String {
512 String::from_utf8_lossy(atom.as_bytes()).to_string()
513}
514
515fn extract_imports(module: &Module) -> HashMap<String, ImportInfo> {
517 let mut imports = HashMap::new();
518
519 for item in &module.body {
520 if let ModuleItem::ModuleDecl(ModuleDecl::Import(import)) = item {
521 let source = atom_to_string(&import.src.value);
522
523 for specifier in &import.specifiers {
524 match specifier {
525 ImportSpecifier::Named(named) => {
526 let local = named.local.sym.to_string();
527 let imported = named
528 .imported
529 .as_ref()
530 .map(|i| match i {
531 ModuleExportName::Ident(id) => id.sym.to_string(),
532 ModuleExportName::Str(s) => atom_to_string(&s.value),
533 })
534 .unwrap_or_else(|| local.clone());
535 imports.insert(
536 local,
537 ImportInfo {
538 name: imported,
539 source: source.clone(),
540 },
541 );
542 }
543 ImportSpecifier::Default(default) => {
544 imports.insert(
545 default.local.sym.to_string(),
546 ImportInfo {
547 name: "default".to_string(),
548 source: source.clone(),
549 },
550 );
551 }
552 ImportSpecifier::Namespace(ns) => {
553 imports.insert(
554 ns.local.sym.to_string(),
555 ImportInfo {
556 name: "*".to_string(),
557 source: source.clone(),
558 },
559 );
560 }
561 }
562 }
563 }
564 }
565
566 imports
567}
568
569fn extract_default_export(
571 module: &Module,
572 imports: &HashMap<String, ImportInfo>,
573 cm: &Lrc<SourceMap>,
574) -> Result<MacroforgeConfig> {
575 for item in &module.body {
576 if let ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(export)) = item {
577 match &*export.expr {
578 Expr::Object(obj) => {
580 return parse_config_object(obj, imports, cm);
581 }
582 Expr::Call(call) => {
584 if let Some(first_arg) = call.args.first()
585 && let Expr::Object(obj) = &*first_arg.expr
586 {
587 return parse_config_object(obj, imports, cm);
588 }
589 }
590 _ => {}
591 }
592 }
593 }
594
595 Ok(MacroforgeConfig::default())
597}
598
599fn parse_config_object(
601 obj: &ObjectLit,
602 imports: &HashMap<String, ImportInfo>,
603 cm: &Lrc<SourceMap>,
604) -> Result<MacroforgeConfig> {
605 let mut config = MacroforgeConfig::default();
606
607 for prop in &obj.props {
608 if let PropOrSpread::Prop(prop) = prop
609 && let Prop::KeyValue(kv) = &**prop
610 {
611 let key = get_prop_key(&kv.key);
612
613 match key.as_str() {
614 "keepDecorators" => {
615 config.keep_decorators = get_bool_value(&kv.value).unwrap_or(false);
616 }
617 "generateConvenienceConst" => {
618 config.generate_convenience_const = get_bool_value(&kv.value).unwrap_or(true);
619 }
620 "foreignTypes" => {
621 if let Expr::Object(ft_obj) = &*kv.value {
622 config.foreign_types = parse_foreign_types(ft_obj, imports, cm)?;
623 }
624 }
625 _ => {}
626 }
627 }
628 }
629
630 config.config_imports = imports.clone();
632
633 Ok(config)
634}
635
636fn parse_foreign_types(
638 obj: &ObjectLit,
639 imports: &HashMap<String, ImportInfo>,
640 cm: &Lrc<SourceMap>,
641) -> Result<Vec<ForeignTypeConfig>> {
642 let mut foreign_types = vec![];
643
644 for prop in &obj.props {
645 if let PropOrSpread::Prop(prop) = prop
646 && let Prop::KeyValue(kv) = &**prop
647 {
648 let type_name = get_prop_key(&kv.key);
649 if let Expr::Object(type_obj) = &*kv.value {
650 let ft = parse_single_foreign_type(&type_name, type_obj, imports, cm)?;
651 foreign_types.push(ft);
652 }
653 }
654 }
655
656 Ok(foreign_types)
657}
658
659fn parse_single_foreign_type(
661 name: &str,
662 obj: &ObjectLit,
663 imports: &HashMap<String, ImportInfo>,
664 cm: &Lrc<SourceMap>,
665) -> Result<ForeignTypeConfig> {
666 let mut ft = ForeignTypeConfig {
667 name: name.to_string(),
668 ..Default::default()
669 };
670
671 for prop in &obj.props {
672 if let PropOrSpread::Prop(prop) = prop
673 && let Prop::KeyValue(kv) = &**prop
674 {
675 let key = get_prop_key(&kv.key);
676
677 match key.as_str() {
678 "from" => {
679 ft.from = extract_string_or_array(&kv.value);
680 }
681 "serialize" => {
682 let (expr, import) = extract_function_expr(&kv.value, imports, cm);
683 ft.serialize_expr = expr;
684 ft.serialize_import = import;
685 }
686 "deserialize" => {
687 let (expr, import) = extract_function_expr(&kv.value, imports, cm);
688 ft.deserialize_expr = expr;
689 ft.deserialize_import = import;
690 }
691 "default" => {
692 let (expr, import) = extract_function_expr(&kv.value, imports, cm);
693 ft.default_expr = expr;
694 ft.default_import = import;
695 }
696 "aliases" => {
697 ft.aliases = parse_aliases_array(&kv.value);
698 }
699 _ => {}
700 }
701 }
702 }
703
704 let mut all_namespaces = std::collections::HashSet::new();
706 if let Some(ref expr) = ft.serialize_expr {
707 for ns in extract_expression_namespaces(expr) {
708 all_namespaces.insert(ns);
709 }
710 }
711 if let Some(ref expr) = ft.deserialize_expr {
712 for ns in extract_expression_namespaces(expr) {
713 all_namespaces.insert(ns);
714 }
715 }
716 if let Some(ref expr) = ft.default_expr {
717 for ns in extract_expression_namespaces(expr) {
718 all_namespaces.insert(ns);
719 }
720 }
721 ft.expression_namespaces = all_namespaces.into_iter().collect();
722
723 Ok(ft)
724}
725
726fn parse_aliases_array(expr: &Expr) -> Vec<ForeignTypeAlias> {
728 let mut aliases = Vec::new();
729
730 if let Expr::Array(arr) = expr {
731 for elem in arr.elems.iter().flatten() {
732 if let Expr::Object(obj) = &*elem.expr
733 && let Some(alias) = parse_single_alias(obj)
734 {
735 aliases.push(alias);
736 }
737 }
738 }
739
740 aliases
741}
742
743fn parse_single_alias(obj: &ObjectLit) -> Option<ForeignTypeAlias> {
745 let mut name = None;
746 let mut from = None;
747
748 for prop in &obj.props {
749 if let PropOrSpread::Prop(prop) = prop
750 && let Prop::KeyValue(kv) = &**prop
751 {
752 let key = get_prop_key(&kv.key);
753
754 match key.as_str() {
755 "name" => {
756 if let Expr::Lit(Lit::Str(s)) = &*kv.value {
757 name = Some(atom_to_string(&s.value));
758 }
759 }
760 "from" => {
761 if let Expr::Lit(Lit::Str(s)) = &*kv.value {
762 from = Some(atom_to_string(&s.value));
763 }
764 }
765 _ => {}
766 }
767 }
768 }
769
770 match (name, from) {
772 (Some(name), Some(from)) => Some(ForeignTypeAlias { name, from }),
773 _ => None,
774 }
775}
776
777fn get_prop_key(key: &PropName) -> String {
779 match key {
780 PropName::Ident(id) => id.sym.to_string(),
781 PropName::Str(s) => atom_to_string(&s.value),
782 PropName::Num(n) => n.value.to_string(),
783 PropName::BigInt(b) => b.value.to_string(),
784 PropName::Computed(c) => {
785 if let Expr::Lit(Lit::Str(s)) = &*c.expr {
786 atom_to_string(&s.value)
787 } else {
788 "[computed]".to_string()
789 }
790 }
791 }
792}
793
794fn get_bool_value(expr: &Expr) -> Option<bool> {
796 match expr {
797 Expr::Lit(Lit::Bool(b)) => Some(b.value),
798 _ => None,
799 }
800}
801
802fn extract_string_or_array(expr: &Expr) -> Vec<String> {
804 match expr {
805 Expr::Lit(Lit::Str(s)) => vec![atom_to_string(&s.value)],
806 Expr::Array(arr) => arr
807 .elems
808 .iter()
809 .filter_map(|elem| {
810 elem.as_ref().and_then(|e| {
811 if let Expr::Lit(Lit::Str(s)) = &*e.expr {
812 Some(atom_to_string(&s.value))
813 } else {
814 None
815 }
816 })
817 })
818 .collect(),
819 _ => vec![],
820 }
821}
822
823fn extract_function_expr(
825 expr: &Expr,
826 imports: &HashMap<String, ImportInfo>,
827 cm: &Lrc<SourceMap>,
828) -> (Option<String>, Option<ImportInfo>) {
829 match expr {
830 Expr::Arrow(_) => {
832 let source = codegen_expr(expr, cm);
833 (Some(source), None)
834 }
835 Expr::Fn(_) => {
837 let source = codegen_expr(expr, cm);
838 (Some(source), None)
839 }
840 Expr::Ident(ident) => {
842 let name = ident.sym.to_string();
843 if let Some(import_info) = imports.get(&name) {
844 (Some(name.clone()), Some(import_info.clone()))
846 } else {
847 (Some(name), None)
849 }
850 }
851 Expr::Member(_) => {
853 let source = codegen_expr(expr, cm);
854 (Some(source), None)
855 }
856 _ => (None, None),
857 }
858}
859
860fn codegen_expr(expr: &Expr, cm: &Lrc<SourceMap>) -> String {
862 let mut buf = Vec::new();
863
864 {
865 let writer = JsWriter::new(cm.clone(), "\n", &mut buf, None);
866 let mut emitter = Emitter {
867 cfg: Config::default(),
868 cm: cm.clone(),
869 comments: None,
870 wr: writer,
871 };
872
873 if expr.emit_with(&mut emitter).is_err() {
875 return String::new();
876 }
877 }
878
879 String::from_utf8(buf).unwrap_or_default()
880}
881
882pub fn extract_expression_namespaces(expr_str: &str) -> Vec<String> {
890 use std::collections::HashSet;
891
892 let cm: Lrc<SourceMap> = Default::default();
893 let fm = cm.new_source_file(
894 FileName::Custom("expr.ts".to_string()).into(),
895 expr_str.to_string(),
896 );
897
898 let lexer = Lexer::new(
899 Syntax::Typescript(TsSyntax {
900 tsx: false,
901 decorators: false,
902 ..Default::default()
903 }),
904 EsVersion::latest(),
905 StringInput::from(&*fm),
906 None,
907 );
908
909 let mut parser = Parser::new_from(lexer);
910 let expr = match parser.parse_expr() {
911 Ok(e) => e,
912 Err(_) => return Vec::new(),
913 };
914
915 let mut namespaces = HashSet::new();
916 collect_member_expression_roots(&expr, &mut namespaces);
917 namespaces.into_iter().collect()
918}
919
920fn collect_member_expression_roots(
925 expr: &Expr,
926 namespaces: &mut std::collections::HashSet<String>,
927) {
928 match expr {
929 Expr::Member(member) => {
931 if let Some(root) = get_member_root(&member.obj) {
933 namespaces.insert(root);
934 }
935 collect_member_expression_roots(&member.obj, namespaces);
937 }
938 Expr::Call(call) => {
940 if let Callee::Expr(callee) = &call.callee {
941 collect_member_expression_roots(callee, namespaces);
942 }
943 for arg in &call.args {
945 collect_member_expression_roots(&arg.expr, namespaces);
946 }
947 }
948 Expr::Arrow(arrow) => match &*arrow.body {
950 BlockStmtOrExpr::Expr(e) => collect_member_expression_roots(e, namespaces),
951 BlockStmtOrExpr::BlockStmt(block) => {
952 for stmt in &block.stmts {
953 collect_statement_namespaces(stmt, namespaces);
954 }
955 }
956 },
957 Expr::Fn(fn_expr) => {
959 if let Some(body) = &fn_expr.function.body {
960 for stmt in &body.stmts {
961 collect_statement_namespaces(stmt, namespaces);
962 }
963 }
964 }
965 Expr::Paren(paren) => {
967 collect_member_expression_roots(&paren.expr, namespaces);
968 }
969 Expr::Bin(bin) => {
971 collect_member_expression_roots(&bin.left, namespaces);
972 collect_member_expression_roots(&bin.right, namespaces);
973 }
974 Expr::Cond(cond) => {
976 collect_member_expression_roots(&cond.test, namespaces);
977 collect_member_expression_roots(&cond.cons, namespaces);
978 collect_member_expression_roots(&cond.alt, namespaces);
979 }
980 Expr::New(new) => {
982 collect_member_expression_roots(&new.callee, namespaces);
983 if let Some(args) = &new.args {
984 for arg in args {
985 collect_member_expression_roots(&arg.expr, namespaces);
986 }
987 }
988 }
989 Expr::Array(arr) => {
991 for elem in arr.elems.iter().flatten() {
992 collect_member_expression_roots(&elem.expr, namespaces);
993 }
994 }
995 Expr::Object(obj) => {
997 for prop in &obj.props {
998 if let PropOrSpread::Prop(p) = prop
999 && let Prop::KeyValue(kv) = &**p
1000 {
1001 collect_member_expression_roots(&kv.value, namespaces);
1002 }
1003 }
1004 }
1005 Expr::Tpl(tpl) => {
1007 for expr in &tpl.exprs {
1008 collect_member_expression_roots(expr, namespaces);
1009 }
1010 }
1011 Expr::Seq(seq) => {
1013 for expr in &seq.exprs {
1014 collect_member_expression_roots(expr, namespaces);
1015 }
1016 }
1017 _ => {}
1018 }
1019}
1020
1021fn collect_statement_namespaces(stmt: &Stmt, namespaces: &mut std::collections::HashSet<String>) {
1023 match stmt {
1024 Stmt::Return(ret) => {
1025 if let Some(arg) = &ret.arg {
1026 collect_member_expression_roots(arg, namespaces);
1027 }
1028 }
1029 Stmt::Expr(expr) => {
1030 collect_member_expression_roots(&expr.expr, namespaces);
1031 }
1032 Stmt::If(if_stmt) => {
1033 collect_member_expression_roots(&if_stmt.test, namespaces);
1034 collect_statement_namespaces(&if_stmt.cons, namespaces);
1035 if let Some(alt) = &if_stmt.alt {
1036 collect_statement_namespaces(alt, namespaces);
1037 }
1038 }
1039 Stmt::Block(block) => {
1040 for s in &block.stmts {
1041 collect_statement_namespaces(s, namespaces);
1042 }
1043 }
1044 Stmt::Decl(Decl::Var(var)) => {
1045 for decl in &var.decls {
1046 if let Some(init) = &decl.init {
1047 collect_member_expression_roots(init, namespaces);
1048 }
1049 }
1050 }
1051 _ => {}
1052 }
1053}
1054
1055fn get_member_root(expr: &Expr) -> Option<String> {
1060 match expr {
1061 Expr::Ident(ident) => Some(ident.sym.to_string()),
1062 Expr::Member(member) => get_member_root(&member.obj),
1063 _ => None,
1064 }
1065}
1066
1067#[derive(Debug, Clone, Serialize, Deserialize)]
1076#[serde(rename_all = "camelCase")]
1077pub struct MacroConfig {
1078 #[serde(default)]
1080 pub macro_packages: Vec<String>,
1081
1082 #[serde(default)]
1084 pub allow_native_macros: bool,
1085
1086 #[serde(default)]
1088 pub macro_runtime_overrides: std::collections::HashMap<String, RuntimeMode>,
1089
1090 #[serde(default)]
1092 pub limits: ResourceLimits,
1093
1094 #[serde(default)]
1096 pub keep_decorators: bool,
1097
1098 #[serde(default = "default_generate_convenience_const")]
1100 pub generate_convenience_const: bool,
1101}
1102
1103impl Default for MacroConfig {
1104 fn default() -> Self {
1105 Self {
1106 macro_packages: Vec::new(),
1107 allow_native_macros: false,
1108 macro_runtime_overrides: Default::default(),
1109 limits: Default::default(),
1110 keep_decorators: false,
1111 generate_convenience_const: default_generate_convenience_const(),
1112 }
1113 }
1114}
1115
1116impl From<MacroforgeConfig> for MacroConfig {
1117 fn from(cfg: MacroforgeConfig) -> Self {
1118 MacroConfig {
1119 keep_decorators: cfg.keep_decorators,
1120 generate_convenience_const: cfg.generate_convenience_const,
1121 ..Default::default()
1122 }
1123 }
1124}
1125
1126impl MacroConfig {
1127 pub fn find_with_root() -> Result<Option<(Self, std::path::PathBuf)>> {
1129 match MacroforgeConfig::find_with_root()? {
1130 Some((cfg, path)) => Ok(Some((cfg.into(), path))),
1131 None => Ok(None),
1132 }
1133 }
1134
1135 pub fn find_and_load() -> Result<Option<Self>> {
1137 Ok(Self::find_with_root()?.map(|(cfg, _)| cfg))
1138 }
1139}
1140
1141#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
1143#[serde(rename_all = "lowercase")]
1144pub enum RuntimeMode {
1145 Wasm,
1147 Native,
1149}
1150
1151#[derive(Debug, Clone, Serialize, Deserialize)]
1153#[serde(rename_all = "camelCase")]
1154pub struct ResourceLimits {
1155 #[serde(default = "default_max_execution_time")]
1157 pub max_execution_time_ms: u64,
1158
1159 #[serde(default = "default_max_memory")]
1161 pub max_memory_bytes: usize,
1162
1163 #[serde(default = "default_max_output_size")]
1165 pub max_output_size: usize,
1166
1167 #[serde(default = "default_max_diagnostics")]
1169 pub max_diagnostics: usize,
1170}
1171
1172impl Default for ResourceLimits {
1173 fn default() -> Self {
1174 Self {
1175 max_execution_time_ms: default_max_execution_time(),
1176 max_memory_bytes: default_max_memory(),
1177 max_output_size: default_max_output_size(),
1178 max_diagnostics: default_max_diagnostics(),
1179 }
1180 }
1181}
1182
1183fn default_max_execution_time() -> u64 {
1184 5000
1185}
1186
1187fn default_max_memory() -> usize {
1188 100 * 1024 * 1024
1189}
1190
1191fn default_max_output_size() -> usize {
1192 10 * 1024 * 1024
1193}
1194
1195fn default_max_diagnostics() -> usize {
1196 100
1197}
1198
1199#[cfg(test)]
1200mod tests {
1201 use super::*;
1202
1203 #[test]
1204 fn test_parse_simple_config() {
1205 let content = r#"
1206 export default {
1207 keepDecorators: true,
1208 generateConvenienceConst: false
1209 }
1210 "#;
1211
1212 let config = MacroforgeConfig::from_config_file(content, "macroforge.config.js").unwrap();
1213 assert!(config.keep_decorators);
1214 assert!(!config.generate_convenience_const);
1215 }
1216
1217 #[test]
1218 fn test_parse_config_with_foreign_types() {
1219 let content = r#"
1220 export default {
1221 foreignTypes: {
1222 DateTime: {
1223 from: ["effect"],
1224 serialize: (v, ctx) => v.toJSON(),
1225 deserialize: (raw, ctx) => DateTime.fromJSON(raw)
1226 }
1227 }
1228 }
1229 "#;
1230
1231 let config = MacroforgeConfig::from_config_file(content, "macroforge.config.js").unwrap();
1232 assert_eq!(config.foreign_types.len(), 1);
1233
1234 let dt = &config.foreign_types[0];
1235 assert_eq!(dt.name, "DateTime");
1236 assert_eq!(dt.from, vec!["effect"]);
1237 assert!(dt.serialize_expr.is_some());
1238 assert!(dt.deserialize_expr.is_some());
1239 }
1240
1241 #[test]
1242 fn test_parse_config_with_multiple_sources() {
1243 let content = r#"
1244 export default {
1245 foreignTypes: {
1246 DateTime: {
1247 from: ["effect", "@effect/schema"]
1248 }
1249 }
1250 }
1251 "#;
1252
1253 let config = MacroforgeConfig::from_config_file(content, "macroforge.config.js").unwrap();
1254 let dt = &config.foreign_types[0];
1255 assert_eq!(dt.from, vec!["effect", "@effect/schema"]);
1256 }
1257
1258 #[test]
1259 fn test_parse_typescript_config() {
1260 let content = r#"
1261 import { DateTime } from "effect";
1262
1263 export default {
1264 foreignTypes: {
1265 DateTime: {
1266 from: ["effect"],
1267 serialize: (v: DateTime, ctx: unknown) => v.toJSON(),
1268 }
1269 }
1270 }
1271 "#;
1272
1273 let config = MacroforgeConfig::from_config_file(content, "macroforge.config.ts").unwrap();
1274 assert_eq!(config.foreign_types.len(), 1);
1275 }
1276
1277 #[test]
1278 fn test_default_values() {
1279 let content = "export default {}";
1280 let config = MacroforgeConfig::from_config_file(content, "macroforge.config.js").unwrap();
1281
1282 assert!(!config.keep_decorators);
1283 assert!(config.generate_convenience_const);
1284 assert!(config.foreign_types.is_empty());
1285 }
1286
1287 #[test]
1288 fn test_legacy_macro_config_conversion() {
1289 let mf_config = MacroforgeConfig {
1290 keep_decorators: true,
1291 generate_convenience_const: false,
1292 foreign_types: vec![],
1293 config_imports: HashMap::new(),
1294 };
1295
1296 let legacy: MacroConfig = mf_config.into();
1297 assert!(legacy.keep_decorators);
1298 assert!(!legacy.generate_convenience_const);
1299 }
1300}