macroforge_ts/host/
config.rs

1//! # Configuration for the Macro Host
2//!
3//! This module handles loading and managing configuration for the macro system.
4//! Configuration is provided via a `macroforge.config.js` (or `.ts`, `.mjs`, `.cjs`) file
5//! in the project root.
6//!
7//! ## Configuration File Locations
8//!
9//! The system searches for configuration files in this order:
10//! 1. `macroforge.config.ts` (preferred)
11//! 2. `macroforge.config.mts`
12//! 3. `macroforge.config.js`
13//! 4. `macroforge.config.mjs`
14//! 5. `macroforge.config.cjs`
15//!
16//! The search starts from the current directory and walks up to the nearest
17//! `package.json` (project root).
18//!
19//! ## Example Configuration
20//!
21//! ```javascript
22//! import { DateTime } from "effect";
23//!
24//! export default {
25//!   keepDecorators: false,
26//!   generateConvenienceConst: true,
27//!   foreignTypes: {
28//!     "DateTime.DateTime": {
29//!       from: ["effect"],
30//!       aliases: [
31//!         { name: "DateTime", from: "effect/DateTime" }
32//!       ],
33//!       serialize: (v) => DateTime.formatIso(v),
34//!       deserialize: (raw) => DateTime.unsafeFromDate(new Date(raw)),
35//!       default: () => DateTime.unsafeNow()
36//!     }
37//!   }
38//! }
39//! ```
40//!
41//! ## Configuration Caching
42//!
43//! Configurations are parsed once and cached globally by file path. When using
44//! [`expand_sync`](crate::expand_sync) or [`NativePlugin::process_file`](crate::NativePlugin::process_file),
45//! you can pass `config_path` in the options to use a previously loaded configuration.
46//! This is particularly useful for accessing foreign type handlers during expansion.
47//!
48//! ## Foreign Types
49//!
50//! Foreign types allow global registration of handlers for external types.
51//! When a field has a type that matches a configured foreign type, the appropriate
52//! handler function is used automatically without per-field annotations.
53//!
54//! ### Foreign Type Options
55//!
56//! | Option | Description |
57//! |--------|-------------|
58//! | `from` | Array of module paths this type can be imported from |
59//! | `aliases` | Array of `{ name, from }` objects for alternative type-package pairs |
60//! | `serialize` | Function `(value) => unknown` for serialization |
61//! | `deserialize` | Function `(raw) => T` for deserialization |
62//! | `default` | Function `() => T` for default value generation |
63//!
64//! ### Import Source Validation
65//!
66//! Foreign types are only matched when the type is imported from one of the configured
67//! sources (in `from` or `aliases`). Types imported from other packages with the same
68//! name are ignored, falling back to generic handling.
69
70use 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
86/// Global cache for parsed configurations.
87/// Maps config file path to the parsed configuration.
88pub static CONFIG_CACHE: LazyLock<DashMap<String, MacroforgeConfig>> = LazyLock::new(DashMap::new);
89
90/// Clear the configuration cache.
91///
92/// This is useful for testing to ensure each test starts with a clean state.
93/// In production, clearing the cache will force configs to be re-parsed on next access.
94pub fn clear_config_cache() {
95    CONFIG_CACHE.clear();
96}
97
98/// Supported config file names in order of precedence.
99const 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/// Information about an imported function.
108#[derive(Debug, Clone, Serialize, Deserialize)]
109pub struct ImportInfo {
110    /// The imported name (or "default" for default imports).
111    pub name: String,
112    /// The module specifier.
113    pub source: String,
114}
115
116/// An alias for a foreign type that allows matching different name-package pairs.
117///
118/// This is useful when a type can be imported from different paths or with different names.
119///
120/// ## Example
121///
122/// ```javascript
123/// foreignTypes: {
124///   "DateTime.DateTime": {
125///     from: ["effect"],
126///     aliases: [
127///       { name: "DateTime", from: "effect/DateTime" }
128///     ],
129///     serialize: (v) => DateTime.formatIso(v),
130///     // ...
131///   }
132/// }
133/// ```
134#[derive(Debug, Clone, Default, Serialize, Deserialize)]
135pub struct ForeignTypeAlias {
136    /// The type name to match (e.g., "DateTime" or "DateTime.DateTime").
137    pub name: String,
138    /// The import source to match (e.g., "effect/DateTime").
139    pub from: String,
140}
141
142/// Configuration for a single foreign type.
143///
144/// Foreign types allow global registration of handlers for external types
145/// (like Effect's `DateTime`) so they work like primitives without per-field annotations.
146///
147/// ## Key Format
148///
149/// The key in `foreignTypes` should be the fully qualified type name as used in code:
150/// - Simple type name: `"DateTime"` - matches `DateTime` in code
151/// - Fully qualified: `"DateTime.DateTime"` - matches `DateTime.DateTime` (namespace.type pattern)
152///
153/// ## Import Source Validation
154///
155/// Foreign types are only matched when the type is imported from a source listed in
156/// `from` or one of the `aliases`. Types with the same name from different packages
157/// are ignored (fall back to generic handling).
158///
159/// ## Example
160///
161/// ```javascript
162/// foreignTypes: {
163///   // For Effect's DateTime where you import { DateTime } and use DateTime.DateTime
164///   "DateTime.DateTime": {
165///     from: ["effect"],
166///     aliases: [
167///       { name: "DateTime", from: "effect/DateTime" },
168///       { name: "MyDateTime", from: "my-effect-wrapper" }
169///     ],
170///     serialize: (v) => DateTime.formatIso(v),
171///     deserialize: (raw) => DateTime.unsafeFromDate(new Date(raw)),
172///     default: () => DateTime.unsafeNow()
173///   }
174/// }
175/// ```
176///
177/// This configuration matches:
178/// - `import { DateTime } from 'effect'` with type `DateTime.DateTime`
179/// - `import { DateTime } from 'effect/DateTime'` with type `DateTime`
180/// - `import { MyDateTime } from 'my-effect-wrapper'` with type `MyDateTime`
181#[derive(Debug, Clone, Default, Serialize, Deserialize)]
182pub struct ForeignTypeConfig {
183    /// The full type key as specified in config (e.g., "DateTime" or "DateTime.DateTime").
184    /// This is the key from the foreignTypes object.
185    pub name: String,
186
187    /// Optional namespace for the type (e.g., "DateTime" for DateTime.DateTime).
188    /// If specified, the type is accessed as `namespace.typeName`.
189    /// If not specified, defaults to the first segment of the name if it contains a dot.
190    pub namespace: Option<String>,
191
192    /// Import sources where this type can come from (e.g., ["effect", "effect/DateTime"]).
193    /// Used to validate that the type is imported from the correct module.
194    pub from: Vec<String>,
195
196    /// Serialization function expression (e.g., "(v, ctx) => v.toJSON()").
197    pub serialize_expr: Option<String>,
198
199    /// Import info if serialize is a named function from another module.
200    pub serialize_import: Option<ImportInfo>,
201
202    /// Deserialization function expression.
203    pub deserialize_expr: Option<String>,
204
205    /// Import info if deserialize is a named function from another module.
206    pub deserialize_import: Option<ImportInfo>,
207
208    /// Default value function expression (e.g., "() => DateTime.now()").
209    pub default_expr: Option<String>,
210
211    /// Import info if default is a named function from another module.
212    pub default_import: Option<ImportInfo>,
213
214    /// Aliases for this foreign type, allowing different name-package pairs to use the same config.
215    ///
216    /// Useful when a type can be imported from different paths or with different names.
217    ///
218    /// ## Example
219    ///
220    /// ```javascript
221    /// foreignTypes: {
222    ///   "DateTime.DateTime": {
223    ///     from: ["effect"],
224    ///     aliases: [
225    ///       { name: "DateTime", from: "effect/DateTime" }
226    ///     ],
227    ///     // ...
228    ///   }
229    /// }
230    /// ```
231    #[serde(default)]
232    pub aliases: Vec<ForeignTypeAlias>,
233
234    /// Namespaces referenced in expressions (serialize_expr, deserialize_expr, default_expr).
235    ///
236    /// This is auto-extracted during config parsing by analyzing the expression ASTs.
237    /// For example, if `serialize: (v) => DateTime.formatIso(v)`, this would contain `["DateTime"]`.
238    ///
239    /// Used to determine which namespaces need to be imported for the generated code to work.
240    #[serde(default, skip_serializing)]
241    pub expression_namespaces: Vec<String>,
242}
243
244impl ForeignTypeConfig {
245    /// Returns the namespace for this type.
246    /// If `namespace` is explicitly set, returns that.
247    /// Otherwise, if the name contains a dot (e.g., "Deep.A.B.Type"), returns everything before the last dot.
248    /// Otherwise, returns None.
249    pub fn get_namespace(&self) -> Option<&str> {
250        if let Some(ref ns) = self.namespace {
251            return Some(ns);
252        }
253        // If name contains a dot, extract namespace (everything before the last dot)
254        if let Some(dot_idx) = self.name.rfind('.') {
255            return Some(&self.name[..dot_idx]);
256        }
257        None
258    }
259
260    /// Returns the simple type name (last segment after dots).
261    /// For "DateTime.DateTime", returns "DateTime".
262    /// For "DateTime", returns "DateTime".
263    pub fn get_type_name(&self) -> &str {
264        self.name.rsplit('.').next().unwrap_or(&self.name)
265    }
266
267    /// Returns the full qualified name to match against.
268    /// If namespace is set: "namespace.typeName"
269    /// Otherwise: the name as-is
270    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/// Configuration for the macro host system.
282///
283/// This struct represents the contents of a `macroforge.config.js` file.
284/// It controls macro loading, execution, and foreign type handling.
285#[derive(Debug, Clone, Serialize, Deserialize)]
286#[serde(rename_all = "camelCase")]
287pub struct MacroforgeConfig {
288    /// Whether to preserve `@derive` decorators in the output code.
289    ///
290    /// When `false` (default), decorators are stripped after expansion.
291    /// When `true`, decorators remain in the output (useful for debugging).
292    #[serde(default)]
293    pub keep_decorators: bool,
294
295    /// Whether to generate a convenience const for non-class types.
296    ///
297    /// When `true` (default), generates an `export const TypeName = { ... } as const;`
298    /// that groups all generated functions for a type into a single namespace-like object.
299    #[serde(default = "default_generate_convenience_const")]
300    pub generate_convenience_const: bool,
301
302    /// Foreign type configurations.
303    ///
304    /// Maps type names to their handlers for serialization, deserialization, and defaults.
305    #[serde(default)]
306    pub foreign_types: Vec<ForeignTypeConfig>,
307
308    /// Import sources from the config file itself.
309    ///
310    /// Maps imported names (e.g., "DateTime", "Option") to their import info
311    /// (module source). This is used to determine the correct import source
312    /// when generating namespace imports for foreign type expressions.
313    #[serde(default, skip_serializing)]
314    pub config_imports: HashMap<String, ImportInfo>,
315}
316
317/// Returns the default for generate_convenience_const (true).
318fn 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, // Default to true
327            foreign_types: Vec::new(),
328            config_imports: HashMap::new(),
329        }
330    }
331}
332
333impl MacroforgeConfig {
334    /// Parse a macroforge.config.js/ts file and extract configuration.
335    ///
336    /// Uses SWC to parse the JavaScript/TypeScript config file and extract
337    /// the configuration object from the default export.
338    ///
339    /// # Arguments
340    ///
341    /// * `content` - The raw file content
342    /// * `filepath` - Path to the config file (used to determine syntax)
343    ///
344    /// # Returns
345    ///
346    /// The parsed configuration.
347    ///
348    /// # Errors
349    ///
350    /// Returns an error if parsing fails.
351    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        // Extract imports map for resolving function references
381        let imports = extract_imports(&module);
382
383        // Find default export and extract config
384        let config = extract_default_export(&module, &imports, &cm)?;
385
386        Ok(config)
387    }
388
389    /// Load configuration from cache or parse from file content.
390    ///
391    /// Caches the parsed configuration for reuse during macro expansion.
392    ///
393    /// # Arguments
394    ///
395    /// * `content` - The raw file content
396    /// * `filepath` - Path to the config file
397    ///
398    /// # Returns
399    ///
400    /// The parsed configuration.
401    pub fn load_and_cache(content: &str, filepath: &str) -> Result<Self> {
402        // Check cache first
403        if let Some(cached) = CONFIG_CACHE.get(filepath) {
404            return Ok(cached.clone());
405        }
406
407        // Parse and cache
408        let config = Self::from_config_file(content, filepath)?;
409        CONFIG_CACHE.insert(filepath.to_string(), config.clone());
410
411        Ok(config)
412    }
413
414    /// Get cached configuration by file path.
415    pub fn get_cached(filepath: &str) -> Option<Self> {
416        CONFIG_CACHE.get(filepath).map(|c| c.clone())
417    }
418
419    /// Find and load a configuration file from the filesystem.
420    ///
421    /// Searches for config files starting from `start_dir` and walking up
422    /// to the nearest `package.json`.
423    ///
424    /// # Returns
425    ///
426    /// - `Ok(Some((config, dir)))` - Found and loaded configuration
427    /// - `Ok(None)` - No configuration file found
428    /// - `Err(_)` - Error reading or parsing configuration
429    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(&current_dir)
432    }
433
434    /// Finds configuration starting from a specific path.
435    ///
436    /// This is useful when expanding files in different directories,
437    /// as it allows finding the config relative to each file rather
438    /// than from the current working directory.
439    ///
440    /// # Arguments
441    ///
442    /// * `start_path` - The file or directory to start searching from.
443    ///   If a file is provided, the search starts from its parent directory.
444    ///
445    /// # Returns
446    ///
447    /// - `Ok(Some((config, dir)))` - Found and loaded configuration
448    /// - `Ok(None)` - No configuration file found
449    /// - `Err(_)` - Error reading or parsing configuration
450    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    /// Convenience wrapper that finds and loads config from a specific path.
465    ///
466    /// # Arguments
467    ///
468    /// * `start_path` - The file or directory to start searching from.
469    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    /// Find configuration in ancestors, returning config and root dir.
474    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            // Try each config file name in order
479            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            // Check for package.json as a stop condition
490            if current.join("package.json").exists() {
491                break;
492            }
493
494            // Move to parent directory
495            if !current.pop() {
496                break;
497            }
498        }
499
500        Ok(None)
501    }
502
503    /// Convenience wrapper around [`find_with_root`](Self::find_with_root)
504    /// for callers that don't need the project root path.
505    pub fn find_and_load() -> Result<Option<Self>> {
506        Ok(Self::find_with_root()?.map(|(cfg, _)| cfg))
507    }
508}
509
510/// Helper to convert a Wtf8Atom (string value) to a String.
511fn atom_to_string(atom: &swc_core::ecma::utils::swc_atoms::Wtf8Atom) -> String {
512    String::from_utf8_lossy(atom.as_bytes()).to_string()
513}
514
515/// Extract import statements into a lookup map.
516fn 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
569/// Extract the default export and parse it as configuration.
570fn 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                // export default { ... }
579                Expr::Object(obj) => {
580                    return parse_config_object(obj, imports, cm);
581                }
582                // export default defineConfig({ ... }) or similar
583                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    // No default export found - return defaults
596    Ok(MacroforgeConfig::default())
597}
598
599/// Parse the config object to extract configuration values.
600fn 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    // Store the config file's imports for use when generating namespace imports
631    config.config_imports = imports.clone();
632
633    Ok(config)
634}
635
636/// Parse the foreignTypes object.
637fn 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
659/// Parse a single foreign type configuration.
660fn 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    // Extract namespace references from all expressions
705    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
726/// Parse an array of aliases: [{ name: "DateTime", from: "effect/DateTime" }, ...]
727fn 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
743/// Parse a single alias object: { name: "DateTime", from: "effect/DateTime" }
744fn 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    // Both name and from are required
771    match (name, from) {
772        (Some(name), Some(from)) => Some(ForeignTypeAlias { name, from }),
773        _ => None,
774    }
775}
776
777/// Get property key as string.
778fn 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
794/// Get boolean value from expression.
795fn get_bool_value(expr: &Expr) -> Option<bool> {
796    match expr {
797        Expr::Lit(Lit::Bool(b)) => Some(b.value),
798        _ => None,
799    }
800}
801
802/// Extract string or array of strings from expression.
803fn 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
823/// Extract function expression - either inline arrow or reference to imported/declared function.
824fn extract_function_expr(
825    expr: &Expr,
826    imports: &HashMap<String, ImportInfo>,
827    cm: &Lrc<SourceMap>,
828) -> (Option<String>, Option<ImportInfo>) {
829    match expr {
830        // Inline arrow function: (v, ctx) => v.toJSON()
831        Expr::Arrow(_) => {
832            let source = codegen_expr(expr, cm);
833            (Some(source), None)
834        }
835        // Function expression: function(v, ctx) { return v.toJSON(); }
836        Expr::Fn(_) => {
837            let source = codegen_expr(expr, cm);
838            (Some(source), None)
839        }
840        // Reference to a variable: serializeDateTime
841        Expr::Ident(ident) => {
842            let name = ident.sym.to_string();
843            if let Some(import_info) = imports.get(&name) {
844                // It's an imported function
845                (Some(name.clone()), Some(import_info.clone()))
846            } else {
847                // It's a locally declared function - just use the name
848                (Some(name), None)
849            }
850        }
851        // Member expression: DateTime.fromJSON
852        Expr::Member(_) => {
853            let source = codegen_expr(expr, cm);
854            (Some(source), None)
855        }
856        _ => (None, None),
857    }
858}
859
860/// Convert expression AST back to source code using SWC's codegen.
861fn 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        // Use the Node trait's emit_with method
874        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
882/// Extract namespace identifiers referenced in an expression string.
883///
884/// Parses the expression and finds all member expression roots that could be namespaces.
885/// For example, `(v) => DateTime.formatIso(v)` would return `["DateTime"]`.
886///
887/// This is used to determine which namespaces need to be imported for foreign type
888/// expressions to work at runtime.
889pub 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
920/// Recursively collect root identifiers from member expressions.
921///
922/// For `DateTime.formatIso(v)`, this extracts `DateTime`.
923/// For `A.B.c()`, this extracts `A`.
924fn collect_member_expression_roots(
925    expr: &Expr,
926    namespaces: &mut std::collections::HashSet<String>,
927) {
928    match expr {
929        // Member expression: DateTime.formatIso
930        Expr::Member(member) => {
931            // Get the root of the member expression chain
932            if let Some(root) = get_member_root(&member.obj) {
933                namespaces.insert(root);
934            }
935            // Also check the object recursively for nested member expressions
936            collect_member_expression_roots(&member.obj, namespaces);
937        }
938        // Call expression: DateTime.formatIso(v)
939        Expr::Call(call) => {
940            if let Callee::Expr(callee) = &call.callee {
941                collect_member_expression_roots(callee, namespaces);
942            }
943            // Also check arguments
944            for arg in &call.args {
945                collect_member_expression_roots(&arg.expr, namespaces);
946            }
947        }
948        // Arrow function: (v) => DateTime.formatIso(v)
949        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        // Function expression: function(v) { return DateTime.formatIso(v); }
958        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        // Parenthesized expression
966        Expr::Paren(paren) => {
967            collect_member_expression_roots(&paren.expr, namespaces);
968        }
969        // Binary expression
970        Expr::Bin(bin) => {
971            collect_member_expression_roots(&bin.left, namespaces);
972            collect_member_expression_roots(&bin.right, namespaces);
973        }
974        // Conditional expression
975        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        // New expression: new DateTime()
981        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        // Array expression
990        Expr::Array(arr) => {
991            for elem in arr.elems.iter().flatten() {
992                collect_member_expression_roots(&elem.expr, namespaces);
993            }
994        }
995        // Object expression
996        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        // Template literal
1006        Expr::Tpl(tpl) => {
1007            for expr in &tpl.exprs {
1008                collect_member_expression_roots(expr, namespaces);
1009            }
1010        }
1011        // Sequence expression
1012        Expr::Seq(seq) => {
1013            for expr in &seq.exprs {
1014                collect_member_expression_roots(expr, namespaces);
1015            }
1016        }
1017        _ => {}
1018    }
1019}
1020
1021/// Collect namespaces from statements.
1022fn 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
1055/// Get the root identifier of a member expression chain.
1056///
1057/// For `DateTime.formatIso`, returns `Some("DateTime")`.
1058/// For `a.b.c`, returns `Some("a")`.
1059fn 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// ============================================================================
1068// Legacy MacroConfig for backwards compatibility during transition
1069// ============================================================================
1070
1071/// Legacy configuration struct for backwards compatibility.
1072///
1073/// This is used internally when the MacroExpander needs a simpler config format.
1074/// New code should use [`MacroforgeConfig`] instead.
1075#[derive(Debug, Clone, Serialize, Deserialize)]
1076#[serde(rename_all = "camelCase")]
1077pub struct MacroConfig {
1078    /// List of macro packages to load.
1079    #[serde(default)]
1080    pub macro_packages: Vec<String>,
1081
1082    /// Whether to allow native (non-WASM) macros.
1083    #[serde(default)]
1084    pub allow_native_macros: bool,
1085
1086    /// Per-package runtime mode overrides.
1087    #[serde(default)]
1088    pub macro_runtime_overrides: std::collections::HashMap<String, RuntimeMode>,
1089
1090    /// Resource limits for macro execution.
1091    #[serde(default)]
1092    pub limits: ResourceLimits,
1093
1094    /// Whether to preserve `@derive` decorators in the output code.
1095    #[serde(default)]
1096    pub keep_decorators: bool,
1097
1098    /// Whether to generate a convenience const for non-class types.
1099    #[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    /// Finds and loads a configuration file, returning both the config and its directory.
1128    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    /// Finds and loads a configuration file, returning just the config.
1136    pub fn find_and_load() -> Result<Option<Self>> {
1137        Ok(Self::find_with_root()?.map(|(cfg, _)| cfg))
1138    }
1139}
1140
1141/// Runtime mode for macro execution.
1142#[derive(Debug, Clone, Copy, Serialize, Deserialize)]
1143#[serde(rename_all = "lowercase")]
1144pub enum RuntimeMode {
1145    /// Execute in a WebAssembly sandbox.
1146    Wasm,
1147    /// Execute as native Rust code.
1148    Native,
1149}
1150
1151/// Resource limits for macro execution.
1152#[derive(Debug, Clone, Serialize, Deserialize)]
1153#[serde(rename_all = "camelCase")]
1154pub struct ResourceLimits {
1155    /// Maximum execution time per macro invocation in milliseconds.
1156    #[serde(default = "default_max_execution_time")]
1157    pub max_execution_time_ms: u64,
1158
1159    /// Maximum memory usage in bytes.
1160    #[serde(default = "default_max_memory")]
1161    pub max_memory_bytes: usize,
1162
1163    /// Maximum size of generated output in bytes.
1164    #[serde(default = "default_max_output_size")]
1165    pub max_output_size: usize,
1166
1167    /// Maximum number of diagnostics a single macro can emit.
1168    #[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}