use super::error::Result;
use dashmap::DashMap;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::path::Path;
use std::sync::LazyLock;
use swc_core::{
common::{FileName, SourceMap, sync::Lrc},
ecma::{
ast::*,
codegen::{Config, Emitter, Node, text_writer::JsWriter},
parser::{EsSyntax, Parser, StringInput, Syntax, TsSyntax, lexer::Lexer},
},
};
pub static CONFIG_CACHE: LazyLock<DashMap<String, MacroforgeConfig>> = LazyLock::new(DashMap::new);
pub fn clear_config_cache() {
CONFIG_CACHE.clear();
}
const CONFIG_FILES: &[&str] = &[
"macroforge.config.ts",
"macroforge.config.mts",
"macroforge.config.js",
"macroforge.config.mjs",
"macroforge.config.cjs",
];
pub use macroforge_ts_syn::config::{
ForeignTypeAlias, ForeignTypeConfig, ImportInfo, MacroforgeConfig,
};
pub struct MacroforgeConfigLoader;
impl MacroforgeConfigLoader {
pub fn from_config_file(content: &str, filepath: &str) -> Result<MacroforgeConfig> {
let is_typescript = filepath.ends_with(".ts") || filepath.ends_with(".mts");
let cm: Lrc<SourceMap> = Default::default();
let fm = cm.new_source_file(
FileName::Custom(filepath.to_string()).into(),
content.to_string(),
);
let syntax = if is_typescript {
Syntax::Typescript(TsSyntax {
tsx: false,
decorators: true,
..Default::default()
})
} else {
Syntax::Es(EsSyntax {
decorators: true,
..Default::default()
})
};
let lexer = Lexer::new(syntax, EsVersion::latest(), StringInput::from(&*fm), None);
let mut parser = Parser::new_from(lexer);
let module = parser
.parse_module()
.map_err(|e| super::MacroError::InvalidConfig(format!("Parse error: {:?}", e)))?;
let imports = extract_imports(&module);
let config = extract_default_export(&module, &imports, &cm)?;
Ok(config)
}
pub fn load_and_cache(content: &str, filepath: &str) -> Result<MacroforgeConfig> {
if let Some(cached) = CONFIG_CACHE.get(filepath) {
return Ok(cached.clone());
}
let config = Self::from_config_file(content, filepath)?;
CONFIG_CACHE.insert(filepath.to_string(), config.clone());
Ok(config)
}
pub fn get_cached(filepath: &str) -> Option<MacroforgeConfig> {
CONFIG_CACHE.get(filepath).map(|c| c.clone())
}
pub fn find_with_root() -> Result<Option<(MacroforgeConfig, std::path::PathBuf)>> {
let current_dir = std::env::current_dir()?;
Self::find_config_in_ancestors(¤t_dir)
}
pub fn find_with_root_from_path(
start_path: &Path,
) -> Result<Option<(MacroforgeConfig, std::path::PathBuf)>> {
let start_dir = if start_path.is_file() {
start_path
.parent()
.map(|p| p.to_path_buf())
.unwrap_or_else(|| start_path.to_path_buf())
} else {
start_path.to_path_buf()
};
Self::find_config_in_ancestors(&start_dir)
}
pub fn find_from_path(start_path: &Path) -> Result<Option<MacroforgeConfig>> {
Ok(Self::find_with_root_from_path(start_path)?.map(|(cfg, _)| cfg))
}
fn find_config_in_ancestors(
start_dir: &Path,
) -> Result<Option<(MacroforgeConfig, std::path::PathBuf)>> {
let mut current = start_dir.to_path_buf();
loop {
for config_name in CONFIG_FILES {
let config_path = current.join(config_name);
if config_path.exists() {
let content = std::fs::read_to_string(&config_path)?;
let config =
Self::from_config_file(&content, config_path.to_string_lossy().as_ref())?;
return Ok(Some((config, current.clone())));
}
}
if current.join("package.json").exists() {
break;
}
if !current.pop() {
break;
}
}
Ok(None)
}
pub fn find_and_load() -> Result<Option<MacroforgeConfig>> {
Ok(Self::find_with_root()?.map(|(cfg, _)| cfg))
}
}
fn atom_to_string(atom: &swc_core::ecma::utils::swc_atoms::Wtf8Atom) -> String {
String::from_utf8_lossy(atom.as_bytes()).to_string()
}
fn extract_imports(module: &Module) -> HashMap<String, ImportInfo> {
let mut imports = HashMap::new();
for item in &module.body {
if let ModuleItem::ModuleDecl(ModuleDecl::Import(import)) = item {
let source = atom_to_string(&import.src.value);
for specifier in &import.specifiers {
match specifier {
ImportSpecifier::Named(named) => {
let local = named.local.sym.to_string();
let imported = named
.imported
.as_ref()
.map(|i| match i {
ModuleExportName::Ident(id) => id.sym.to_string(),
ModuleExportName::Str(s) => atom_to_string(&s.value),
})
.unwrap_or_else(|| local.clone());
imports.insert(
local,
ImportInfo {
name: imported,
source: source.clone(),
},
);
}
ImportSpecifier::Default(default) => {
imports.insert(
default.local.sym.to_string(),
ImportInfo {
name: "default".to_string(),
source: source.clone(),
},
);
}
ImportSpecifier::Namespace(ns) => {
imports.insert(
ns.local.sym.to_string(),
ImportInfo {
name: "*".to_string(),
source: source.clone(),
},
);
}
}
}
}
}
imports
}
fn extract_default_export(
module: &Module,
imports: &HashMap<String, ImportInfo>,
cm: &Lrc<SourceMap>,
) -> Result<MacroforgeConfig> {
for item in &module.body {
if let ModuleItem::ModuleDecl(ModuleDecl::ExportDefaultExpr(export)) = item {
match &*export.expr {
Expr::Object(obj) => {
return parse_config_object(obj, imports, cm);
}
Expr::Call(call) => {
if let Some(first_arg) = call.args.first()
&& let Expr::Object(obj) = &*first_arg.expr
{
return parse_config_object(obj, imports, cm);
}
}
_ => {}
}
}
}
Ok(MacroforgeConfig::default())
}
fn parse_config_object(
obj: &ObjectLit,
imports: &HashMap<String, ImportInfo>,
cm: &Lrc<SourceMap>,
) -> Result<MacroforgeConfig> {
let mut config = MacroforgeConfig::default();
for prop in &obj.props {
if let PropOrSpread::Prop(prop) = prop
&& let Prop::KeyValue(kv) = &**prop
{
let key = get_prop_key(&kv.key);
match key.as_str() {
"keepDecorators" => {
config.keep_decorators = get_bool_value(&kv.value).unwrap_or(false);
}
"generateConvenienceConst" => {
config.generate_convenience_const = get_bool_value(&kv.value).unwrap_or(true);
}
"foreignTypes" => {
if let Expr::Object(ft_obj) = &*kv.value {
config.foreign_types = parse_foreign_types(ft_obj, imports, cm)?;
}
}
_ => {}
}
}
}
config.config_imports = imports.clone();
Ok(config)
}
fn parse_foreign_types(
obj: &ObjectLit,
imports: &HashMap<String, ImportInfo>,
cm: &Lrc<SourceMap>,
) -> Result<Vec<ForeignTypeConfig>> {
let mut foreign_types = vec![];
for prop in &obj.props {
if let PropOrSpread::Prop(prop) = prop
&& let Prop::KeyValue(kv) = &**prop
{
let type_name = get_prop_key(&kv.key);
if let Expr::Object(type_obj) = &*kv.value {
let ft = parse_single_foreign_type(&type_name, type_obj, imports, cm)?;
foreign_types.push(ft);
}
}
}
Ok(foreign_types)
}
fn parse_single_foreign_type(
name: &str,
obj: &ObjectLit,
imports: &HashMap<String, ImportInfo>,
cm: &Lrc<SourceMap>,
) -> Result<ForeignTypeConfig> {
let mut ft = ForeignTypeConfig {
name: name.to_string(),
..Default::default()
};
for prop in &obj.props {
if let PropOrSpread::Prop(prop) = prop
&& let Prop::KeyValue(kv) = &**prop
{
let key = get_prop_key(&kv.key);
match key.as_str() {
"from" => {
ft.from = extract_string_or_array(&kv.value);
}
"serialize" => {
let (expr, import) = extract_function_expr(&kv.value, imports, cm);
ft.serialize_expr = expr;
ft.serialize_import = import;
}
"deserialize" => {
let (expr, import) = extract_function_expr(&kv.value, imports, cm);
ft.deserialize_expr = expr;
ft.deserialize_import = import;
}
"default" => {
let (expr, import) = extract_function_expr(&kv.value, imports, cm);
ft.default_expr = expr;
ft.default_import = import;
}
"hasShape" => {
let (expr, import) = extract_function_expr(&kv.value, imports, cm);
ft.has_shape_expr = expr;
ft.has_shape_import = import;
}
"aliases" => {
ft.aliases = parse_aliases_array(&kv.value);
}
_ => {}
}
}
}
let mut all_namespaces = std::collections::HashSet::new();
if let Some(ref expr) = ft.serialize_expr {
for ns in extract_expression_namespaces(expr) {
all_namespaces.insert(ns);
}
}
if let Some(ref expr) = ft.deserialize_expr {
for ns in extract_expression_namespaces(expr) {
all_namespaces.insert(ns);
}
}
if let Some(ref expr) = ft.default_expr {
for ns in extract_expression_namespaces(expr) {
all_namespaces.insert(ns);
}
}
if let Some(ref expr) = ft.has_shape_expr {
for ns in extract_expression_namespaces(expr) {
all_namespaces.insert(ns);
}
}
ft.expression_namespaces = all_namespaces.into_iter().collect();
Ok(ft)
}
fn parse_aliases_array(expr: &Expr) -> Vec<ForeignTypeAlias> {
let mut aliases = Vec::new();
if let Expr::Array(arr) = expr {
for elem in arr.elems.iter().flatten() {
if let Expr::Object(obj) = &*elem.expr
&& let Some(alias) = parse_single_alias(obj)
{
aliases.push(alias);
}
}
}
aliases
}
fn parse_single_alias(obj: &ObjectLit) -> Option<ForeignTypeAlias> {
let mut name = None;
let mut from = None;
for prop in &obj.props {
if let PropOrSpread::Prop(prop) = prop
&& let Prop::KeyValue(kv) = &**prop
{
let key = get_prop_key(&kv.key);
match key.as_str() {
"name" => {
if let Expr::Lit(Lit::Str(s)) = &*kv.value {
name = Some(atom_to_string(&s.value));
}
}
"from" => {
if let Expr::Lit(Lit::Str(s)) = &*kv.value {
from = Some(atom_to_string(&s.value));
}
}
_ => {}
}
}
}
match (name, from) {
(Some(name), Some(from)) => Some(ForeignTypeAlias { name, from }),
_ => None,
}
}
fn get_prop_key(key: &PropName) -> String {
match key {
PropName::Ident(id) => id.sym.to_string(),
PropName::Str(s) => atom_to_string(&s.value),
PropName::Num(n) => n.value.to_string(),
PropName::BigInt(b) => b.value.to_string(),
PropName::Computed(c) => {
if let Expr::Lit(Lit::Str(s)) = &*c.expr {
atom_to_string(&s.value)
} else {
"[computed]".to_string()
}
}
}
}
fn get_bool_value(expr: &Expr) -> Option<bool> {
match expr {
Expr::Lit(Lit::Bool(b)) => Some(b.value),
_ => None,
}
}
fn extract_string_or_array(expr: &Expr) -> Vec<String> {
match expr {
Expr::Lit(Lit::Str(s)) => vec![atom_to_string(&s.value)],
Expr::Array(arr) => arr
.elems
.iter()
.filter_map(|elem| {
elem.as_ref().and_then(|e| {
if let Expr::Lit(Lit::Str(s)) = &*e.expr {
Some(atom_to_string(&s.value))
} else {
None
}
})
})
.collect(),
_ => vec![],
}
}
fn extract_function_expr(
expr: &Expr,
imports: &HashMap<String, ImportInfo>,
cm: &Lrc<SourceMap>,
) -> (Option<String>, Option<ImportInfo>) {
match expr {
Expr::Arrow(_) => {
let source = codegen_expr(expr, cm);
(Some(source), None)
}
Expr::Fn(_) => {
let source = codegen_expr(expr, cm);
(Some(source), None)
}
Expr::Ident(ident) => {
let name = ident.sym.to_string();
if let Some(import_info) = imports.get(&name) {
(Some(name.clone()), Some(import_info.clone()))
} else {
(Some(name), None)
}
}
Expr::Member(_) => {
let source = codegen_expr(expr, cm);
(Some(source), None)
}
_ => (None, None),
}
}
fn codegen_expr(expr: &Expr, cm: &Lrc<SourceMap>) -> String {
let mut buf = Vec::new();
{
let writer = JsWriter::new(cm.clone(), "\n", &mut buf, None);
let mut emitter = Emitter {
cfg: Config::default(),
cm: cm.clone(),
comments: None,
wr: writer,
};
if expr.emit_with(&mut emitter).is_err() {
return String::new();
}
}
String::from_utf8(buf).unwrap_or_default()
}
pub fn extract_expression_namespaces(expr_str: &str) -> Vec<String> {
use std::collections::HashSet;
let cm: Lrc<SourceMap> = Default::default();
let fm = cm.new_source_file(
FileName::Custom("expr.ts".to_string()).into(),
expr_str.to_string(),
);
let lexer = Lexer::new(
Syntax::Typescript(TsSyntax {
tsx: false,
decorators: false,
..Default::default()
}),
EsVersion::latest(),
StringInput::from(&*fm),
None,
);
let mut parser = Parser::new_from(lexer);
let expr = match parser.parse_expr() {
Ok(e) => e,
Err(_) => return Vec::new(),
};
let mut namespaces = HashSet::new();
collect_member_expression_roots(&expr, &mut namespaces);
namespaces.into_iter().collect()
}
fn collect_member_expression_roots(
expr: &Expr,
namespaces: &mut std::collections::HashSet<String>,
) {
match expr {
Expr::Member(member) => {
if let Some(root) = get_member_root(&member.obj) {
namespaces.insert(root);
}
collect_member_expression_roots(&member.obj, namespaces);
}
Expr::Call(call) => {
if let Callee::Expr(callee) = &call.callee {
collect_member_expression_roots(callee, namespaces);
}
for arg in &call.args {
collect_member_expression_roots(&arg.expr, namespaces);
}
}
Expr::Arrow(arrow) => match &*arrow.body {
BlockStmtOrExpr::Expr(e) => collect_member_expression_roots(e, namespaces),
BlockStmtOrExpr::BlockStmt(block) => {
for stmt in &block.stmts {
collect_statement_namespaces(stmt, namespaces);
}
}
},
Expr::Fn(fn_expr) => {
if let Some(body) = &fn_expr.function.body {
for stmt in &body.stmts {
collect_statement_namespaces(stmt, namespaces);
}
}
}
Expr::Paren(paren) => {
collect_member_expression_roots(&paren.expr, namespaces);
}
Expr::Bin(bin) => {
collect_member_expression_roots(&bin.left, namespaces);
collect_member_expression_roots(&bin.right, namespaces);
}
Expr::Cond(cond) => {
collect_member_expression_roots(&cond.test, namespaces);
collect_member_expression_roots(&cond.cons, namespaces);
collect_member_expression_roots(&cond.alt, namespaces);
}
Expr::New(new) => {
collect_member_expression_roots(&new.callee, namespaces);
if let Some(args) = &new.args {
for arg in args {
collect_member_expression_roots(&arg.expr, namespaces);
}
}
}
Expr::Array(arr) => {
for elem in arr.elems.iter().flatten() {
collect_member_expression_roots(&elem.expr, namespaces);
}
}
Expr::Object(obj) => {
for prop in &obj.props {
if let PropOrSpread::Prop(p) = prop
&& let Prop::KeyValue(kv) = &**p
{
collect_member_expression_roots(&kv.value, namespaces);
}
}
}
Expr::Tpl(tpl) => {
for expr in &tpl.exprs {
collect_member_expression_roots(expr, namespaces);
}
}
Expr::Seq(seq) => {
for expr in &seq.exprs {
collect_member_expression_roots(expr, namespaces);
}
}
_ => {}
}
}
fn collect_statement_namespaces(stmt: &Stmt, namespaces: &mut std::collections::HashSet<String>) {
match stmt {
Stmt::Return(ret) => {
if let Some(arg) = &ret.arg {
collect_member_expression_roots(arg, namespaces);
}
}
Stmt::Expr(expr) => {
collect_member_expression_roots(&expr.expr, namespaces);
}
Stmt::If(if_stmt) => {
collect_member_expression_roots(&if_stmt.test, namespaces);
collect_statement_namespaces(&if_stmt.cons, namespaces);
if let Some(alt) = &if_stmt.alt {
collect_statement_namespaces(alt, namespaces);
}
}
Stmt::Block(block) => {
for s in &block.stmts {
collect_statement_namespaces(s, namespaces);
}
}
Stmt::Decl(Decl::Var(var)) => {
for decl in &var.decls {
if let Some(init) = &decl.init {
collect_member_expression_roots(init, namespaces);
}
}
}
_ => {}
}
}
fn get_member_root(expr: &Expr) -> Option<String> {
match expr {
Expr::Ident(ident) => Some(ident.sym.to_string()),
Expr::Member(member) => get_member_root(&member.obj),
_ => None,
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct MacroConfig {
#[serde(default)]
pub macro_packages: Vec<String>,
#[serde(default)]
pub allow_native_macros: bool,
#[serde(default)]
pub macro_runtime_overrides: std::collections::HashMap<String, RuntimeMode>,
#[serde(default)]
pub limits: ResourceLimits,
#[serde(default)]
pub keep_decorators: bool,
#[serde(default = "macroforge_ts_syn::config::default_generate_convenience_const")]
pub generate_convenience_const: bool,
}
impl Default for MacroConfig {
fn default() -> Self {
Self {
macro_packages: Vec::new(),
allow_native_macros: false,
macro_runtime_overrides: Default::default(),
limits: Default::default(),
keep_decorators: false,
generate_convenience_const: true,
}
}
}
impl From<MacroforgeConfig> for MacroConfig {
fn from(cfg: MacroforgeConfig) -> Self {
MacroConfig {
keep_decorators: cfg.keep_decorators,
generate_convenience_const: cfg.generate_convenience_const,
..Default::default()
}
}
}
impl MacroConfig {
pub fn find_with_root() -> Result<Option<(Self, std::path::PathBuf)>> {
match MacroforgeConfigLoader::find_with_root()? {
Some((cfg, path)) => Ok(Some((cfg.into(), path))),
None => Ok(None),
}
}
pub fn find_and_load() -> Result<Option<Self>> {
Ok(Self::find_with_root()?.map(|(cfg, _)| cfg))
}
}
#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum RuntimeMode {
Wasm,
Native,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct ResourceLimits {
#[serde(default = "default_max_execution_time")]
pub max_execution_time_ms: u64,
#[serde(default = "default_max_memory")]
pub max_memory_bytes: usize,
#[serde(default = "default_max_output_size")]
pub max_output_size: usize,
#[serde(default = "default_max_diagnostics")]
pub max_diagnostics: usize,
}
impl Default for ResourceLimits {
fn default() -> Self {
Self {
max_execution_time_ms: default_max_execution_time(),
max_memory_bytes: default_max_memory(),
max_output_size: default_max_output_size(),
max_diagnostics: default_max_diagnostics(),
}
}
}
fn default_max_execution_time() -> u64 {
5000
}
fn default_max_memory() -> usize {
100 * 1024 * 1024
}
fn default_max_output_size() -> usize {
10 * 1024 * 1024
}
fn default_max_diagnostics() -> usize {
100
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_simple_config() {
let content = r#"
export default {
keepDecorators: true,
generateConvenienceConst: false
}
"#;
let config =
MacroforgeConfigLoader::from_config_file(content, "macroforge.config.js").unwrap();
assert!(config.keep_decorators);
assert!(!config.generate_convenience_const);
}
#[test]
fn test_parse_config_with_foreign_types() {
let content = r#"
export default {
foreignTypes: {
DateTime: {
from: ["effect"],
serialize: (v, ctx) => v.toJSON(),
deserialize: (raw, ctx) => DateTime.fromJSON(raw)
}
}
}
"#;
let config =
MacroforgeConfigLoader::from_config_file(content, "macroforge.config.js").unwrap();
assert_eq!(config.foreign_types.len(), 1);
let dt = &config.foreign_types[0];
assert_eq!(dt.name, "DateTime");
assert_eq!(dt.from, vec!["effect"]);
assert!(dt.serialize_expr.is_some());
assert!(dt.deserialize_expr.is_some());
}
#[test]
fn test_parse_config_with_multiple_sources() {
let content = r#"
export default {
foreignTypes: {
DateTime: {
from: ["effect", "@effect/schema"]
}
}
}
"#;
let config =
MacroforgeConfigLoader::from_config_file(content, "macroforge.config.js").unwrap();
let dt = &config.foreign_types[0];
assert_eq!(dt.from, vec!["effect", "@effect/schema"]);
}
#[test]
fn test_parse_typescript_config() {
let content = r#"
import { DateTime } from "effect";
export default {
foreignTypes: {
DateTime: {
from: ["effect"],
serialize: (v: DateTime, ctx: unknown) => v.toJSON(),
}
}
}
"#;
let config =
MacroforgeConfigLoader::from_config_file(content, "macroforge.config.ts").unwrap();
assert_eq!(config.foreign_types.len(), 1);
}
#[test]
fn test_default_values() {
let content = "export default {}";
let config =
MacroforgeConfigLoader::from_config_file(content, "macroforge.config.js").unwrap();
assert!(!config.keep_decorators);
assert!(config.generate_convenience_const);
assert!(config.foreign_types.is_empty());
}
#[test]
fn test_legacy_macro_config_conversion() {
let mf_config = MacroforgeConfig {
keep_decorators: true,
generate_convenience_const: false,
foreign_types: vec![],
config_imports: HashMap::new(),
};
let legacy: MacroConfig = mf_config.into();
assert!(legacy.keep_decorators);
assert!(!legacy.generate_convenience_const);
}
}