Skip to main content

macroforge_ts/
lib.rs

1//! # Macroforge TypeScript Macro Engine
2//!
3//! This crate provides a TypeScript macro expansion engine that brings Rust-like derive macros
4//! to TypeScript. It is designed to be used via NAPI bindings from Node.js, enabling compile-time
5//! code generation for TypeScript projects.
6//!
7//! ## Overview
8//!
9//! Macroforge processes TypeScript source files containing `@derive` decorators and expands them
10//! into concrete implementations. For example, a class decorated with `@derive(Debug, Clone)`
11//! will have `toString()` and `clone()` methods automatically generated.
12//!
13//! ## Architecture
14//!
15//! The crate is organized into several key components:
16//!
17//! - **NAPI Bindings** (`NativePlugin`, `expand_sync`, `transform_sync`): Entry points for Node.js
18//! - **Position Mapping** (`NativePositionMapper`, `NativeMapper`): Bidirectional source mapping
19//!   for IDE integration
20//! - **Macro Host** (`host` module): Core expansion engine with registry and dispatcher
21//! - **Built-in Macros** (`builtin` module): Standard derive macros (Debug, Clone, Serialize, etc.)
22//!
23//! ## Performance Considerations
24//!
25//! - Uses a 32MB thread stack to prevent stack overflow during deep SWC AST recursion
26//! - Implements early bailout for files without `@derive` decorators
27//! - Caches expansion results keyed by filepath and version
28//! - Uses binary search for O(log n) position mapping lookups
29//!
30//! ## Usage from Node.js
31//!
32//! ```javascript
33//! const { NativePlugin, expand_sync } = require('macroforge-ts');
34//!
35//! // Create a plugin instance with caching
36//! const plugin = new NativePlugin();
37//!
38//! // Process a file (uses cache if version matches)
39//! const result = plugin.process_file(filepath, code, { version: '1.0' });
40//!
41//! // Or use the sync function directly
42//! const result = expand_sync(code, filepath, { keep_decorators: false });
43//! ```
44//!
45//! ## Re-exports for Macro Authors
46//!
47//! This crate re-exports several dependencies for convenience when writing custom macros:
48//! - `ts_syn`: TypeScript syntax types for AST manipulation
49//! - `macros`: Macro attributes and quote templates
50//! - `swc_core`, `swc_common`, `swc_ecma_ast`: SWC compiler infrastructure
51
52use napi::bindgen_prelude::*;
53use napi_derive::napi;
54use swc_core::{
55    common::{FileName, GLOBALS, Globals, SourceMap, errors::Handler, sync::Lrc},
56    ecma::{
57        ast::{EsVersion, Program},
58        codegen::{Emitter, text_writer::JsWriter},
59        parser::{Parser, StringInput, Syntax, TsSyntax, lexer::Lexer},
60    },
61};
62
63// Allow the crate to reference itself as `macroforge_ts`.
64// This self-reference is required for the macroforge_ts_macros generated code
65// to correctly resolve paths when the macro expansion happens within this crate.
66extern crate self as macroforge_ts;
67
68// ============================================================================
69// Re-exports for Macro Authors
70// ============================================================================
71// These re-exports allow users to only depend on `macroforge_ts` in their
72// Cargo.toml instead of needing to add multiple dependencies.
73
74// Re-export internal crates (needed for generated code)
75pub extern crate inventory;
76pub extern crate macroforge_ts_macros;
77pub extern crate macroforge_ts_quote;
78pub extern crate macroforge_ts_syn;
79pub extern crate napi;
80pub extern crate napi_derive;
81pub extern crate serde_json;
82
83/// Debug logging for external macros.
84/// Writes to `.macroforge/debug.log` relative to the project root.
85/// Use: `macroforge_ts::debug::log("tag", "message")` or `macroforge_ts::debug_log!("tag", "...")`
86pub mod debug;
87
88/// TypeScript syntax types for macro development
89/// Use: `use macroforge_ts::ts_syn::*;`
90pub use macroforge_ts_syn as ts_syn;
91
92/// Macro attributes and quote templates
93/// Use: `use macroforge_ts::macros::*;`
94pub mod macros {
95    // Re-export the ts_macro_derive attribute
96    pub use macroforge_ts_macros::ts_macro_derive;
97
98    // Re-export all quote macros
99    pub use macroforge_ts_quote::{ts_quote, ts_template};
100}
101
102// ============================================================================
103// Wrapper macros for proper $crate resolution
104// ============================================================================
105// These wrapper macros ensure that $crate resolves to macroforge_ts (this crate)
106// rather than macroforge_ts_syn, allowing users to only depend on macroforge_ts.
107
108/// Creates an SWC [`Ident`](swc_core::ecma::ast::Ident) from a string or format expression.
109///
110/// This is a re-export wrapper that ensures `$crate` resolves correctly when
111/// used from crates that only depend on `macroforge_ts`.
112///
113/// # Examples
114///
115/// ```rust,ignore
116/// use macroforge_ts::ident;
117///
118/// let simple = ident!("foo");
119/// let formatted = ident!("{}Bar", "foo");
120/// ```
121#[macro_export]
122macro_rules! ident {
123    ($name:expr) => {
124        $crate::swc_core::ecma::ast::Ident::new_no_ctxt(
125            AsRef::<str>::as_ref(&$name).into(),
126            $crate::swc_core::common::DUMMY_SP,
127        )
128    };
129    ($fmt:expr, $($args:expr),+ $(,)?) => {
130        $crate::swc_core::ecma::ast::Ident::new_no_ctxt(
131            format!($fmt, $($args),+).into(),
132            $crate::swc_core::common::DUMMY_SP,
133        )
134    };
135}
136
137/// Creates a private (marked) SWC [`Ident`](swc_core::ecma::ast::Ident).
138///
139/// Unlike [`ident!`], this macro creates an identifier with a fresh hygiene mark,
140/// making it unique and preventing name collisions with user code.
141#[macro_export]
142macro_rules! private_ident {
143    ($name:expr) => {{
144        let mark = $crate::swc_core::common::Mark::fresh($crate::swc_core::common::Mark::root());
145        $crate::swc_core::ecma::ast::Ident::new(
146            $name.into(),
147            $crate::swc_core::common::DUMMY_SP,
148            $crate::swc_core::common::SyntaxContext::empty().apply_mark(mark),
149        )
150    }};
151}
152
153// Re-export swc_core and common modules (via ts_syn for version consistency)
154pub use macroforge_ts_syn::swc_common;
155pub use macroforge_ts_syn::swc_core;
156pub use macroforge_ts_syn::swc_ecma_ast;
157
158// ============================================================================
159// Internal modules
160// ============================================================================
161pub mod host;
162
163// Build script utilities (enabled with "build" feature)
164#[cfg(feature = "build")]
165pub mod build;
166
167// Re-export abi types from ts_syn
168pub use ts_syn::abi;
169
170use host::CONFIG_CACHE;
171use host::derived;
172use ts_syn::{Diagnostic, DiagnosticLevel};
173
174pub mod builtin;
175
176#[cfg(test)]
177mod test;
178
179use crate::host::MacroExpander;
180
181// ============================================================================
182// Type Registry Cache
183// ============================================================================
184// Caches deserialized TypeRegistry by hash of the JSON string to avoid
185// re-parsing the registry on every expand_sync call.
186
187use std::collections::hash_map::DefaultHasher;
188use std::hash::{Hash, Hasher};
189use std::sync::LazyLock;
190
191use crate::ts_syn::abi::ir::type_registry::TypeRegistry;
192
193static REGISTRY_CACHE: LazyLock<dashmap::DashMap<u64, TypeRegistry>> =
194    LazyLock::new(dashmap::DashMap::new);
195
196fn get_or_parse_registry(json: &str) -> Option<TypeRegistry> {
197    let mut hasher = DefaultHasher::new();
198    json.hash(&mut hasher);
199    let key = hasher.finish();
200
201    if let Some(entry) = REGISTRY_CACHE.get(&key) {
202        return Some(entry.clone());
203    }
204
205    match serde_json::from_str::<TypeRegistry>(json) {
206        Ok(registry) => {
207            REGISTRY_CACHE.insert(key, registry.clone());
208            Some(registry)
209        }
210        Err(_) => None,
211    }
212}
213
214// ============================================================================
215// Data Structures
216// ============================================================================
217
218/// Result of transforming TypeScript code through the macro system.
219///
220/// This struct is returned by [`transform_sync`] and contains the transformed code
221/// along with optional source maps, type declarations, and metadata about processed classes.
222///
223/// # Fields
224///
225/// * `code` - The transformed TypeScript/JavaScript code with macros expanded
226/// * `map` - Optional source map for debugging (currently not implemented)
227/// * `types` - Optional TypeScript type declarations for generated methods
228/// * `metadata` - Optional JSON metadata about processed classes
229#[napi(object)]
230#[derive(Clone)]
231pub struct TransformResult {
232    /// The transformed TypeScript/JavaScript code with all macros expanded.
233    pub code: String,
234    /// Source map for mapping transformed positions back to original.
235    /// Currently always `None` - source mapping is handled separately via `SourceMappingResult`.
236    pub map: Option<String>,
237    /// TypeScript type declarations (`.d.ts` content) for generated methods.
238    /// Used by IDEs to provide type information for macro-generated code.
239    pub types: Option<String>,
240    /// JSON-serialized metadata about processed classes.
241    /// Contains information about which classes were processed and what was generated.
242    pub metadata: Option<String>,
243}
244
245/// A diagnostic message produced during macro expansion.
246///
247/// Diagnostics can represent errors, warnings, or informational messages
248/// that occurred during the macro expansion process.
249///
250/// # Fields
251///
252/// * `level` - Severity level: "error", "warning", or "info"
253/// * `message` - Human-readable description of the issue
254/// * `start` - Optional byte offset where the issue starts in the source
255/// * `end` - Optional byte offset where the issue ends in the source
256///
257/// # Example
258///
259/// ```rust
260/// use macroforge_ts::MacroDiagnostic;
261///
262/// let _diag = MacroDiagnostic {
263///     level: "error".to_string(),
264///     message: "Unknown macro 'Foo'".to_string(),
265///     start: Some(42),
266///     end: Some(45),
267/// };
268/// ```
269#[napi(object)]
270#[derive(Clone)]
271pub struct MacroDiagnostic {
272    /// Severity level of the diagnostic.
273    /// One of: "error", "warning", "info".
274    pub level: String,
275    /// Human-readable message describing the diagnostic.
276    pub message: String,
277    /// Byte offset in the original source where the issue starts.
278    /// `None` if the diagnostic is not associated with a specific location.
279    pub start: Option<u32>,
280    /// Byte offset in the original source where the issue ends.
281    /// `None` if the diagnostic is not associated with a specific location.
282    pub end: Option<u32>,
283}
284
285/// A segment mapping a range in the original source to a range in the expanded source.
286///
287/// These segments form the core of the bidirectional source mapping system,
288/// enabling IDE features like "go to definition" and error reporting to work
289/// correctly with macro-expanded code.
290///
291/// # Invariants
292///
293/// - `original_start < original_end`
294/// - `expanded_start < expanded_end`
295/// - Segments are non-overlapping and sorted by position
296#[napi(object)]
297#[derive(Clone)]
298pub struct MappingSegmentResult {
299    /// Byte offset where this segment starts in the original source.
300    pub original_start: u32,
301    /// Byte offset where this segment ends in the original source.
302    pub original_end: u32,
303    /// Byte offset where this segment starts in the expanded source.
304    pub expanded_start: u32,
305    /// Byte offset where this segment ends in the expanded source.
306    pub expanded_end: u32,
307}
308
309/// A region in the expanded source that was generated by a macro.
310///
311/// These regions identify code that has no corresponding location in the
312/// original source because it was synthesized by a macro.
313///
314/// # Example
315///
316/// For a `@derive(Debug)` macro that generates a `toString()` method,
317/// a `GeneratedRegionResult` would mark the entire method body as generated
318/// with `source_macro = "Debug"`.
319#[napi(object)]
320#[derive(Clone)]
321pub struct GeneratedRegionResult {
322    /// Byte offset where the generated region starts in the expanded source.
323    pub start: u32,
324    /// Byte offset where the generated region ends in the expanded source.
325    pub end: u32,
326    /// Name of the macro that generated this region (e.g., "Debug", "Clone").
327    pub source_macro: String,
328}
329
330/// Complete source mapping information for a macro expansion.
331///
332/// Contains both preserved segments (original code that wasn't modified)
333/// and generated regions (new code synthesized by macros).
334///
335/// # Usage
336///
337/// This mapping enables:
338/// - Converting positions from original source to expanded source and vice versa
339/// - Identifying which macro generated a given piece of code
340/// - Mapping IDE diagnostics from expanded code back to original source
341#[napi(object)]
342#[derive(Clone)]
343pub struct SourceMappingResult {
344    /// Segments mapping preserved regions between original and expanded source.
345    /// Sorted by position for efficient binary search lookups.
346    pub segments: Vec<MappingSegmentResult>,
347    /// Regions in the expanded source that were generated by macros.
348    /// Used to identify synthetic code with no original source location.
349    pub generated_regions: Vec<GeneratedRegionResult>,
350}
351
352/// Result of expanding macros in TypeScript source code.
353///
354/// This is the primary return type for macro expansion operations,
355/// containing the expanded code, diagnostics, and source mapping.
356///
357/// # Example
358///
359/// ```rust
360/// use macroforge_ts::{ExpandResult, MacroDiagnostic};
361///
362/// // Create an ExpandResult programmatically
363/// let result = ExpandResult {
364///     code: "class User {}".to_string(),
365///     types: None,
366///     metadata: None,
367///     diagnostics: vec![],
368///     source_mapping: None,
369/// };
370///
371/// // Check for errors
372/// if result.diagnostics.iter().any(|d| d.level == "error") {
373///     // Handle errors
374/// }
375/// ```
376#[napi(object)]
377#[derive(Clone)]
378pub struct ExpandResult {
379    /// The expanded TypeScript code with all macros processed.
380    pub code: String,
381    /// Optional TypeScript type declarations for generated methods.
382    pub types: Option<String>,
383    /// Optional JSON metadata about processed classes.
384    pub metadata: Option<String>,
385    /// Diagnostics (errors, warnings, info) from the expansion process.
386    pub diagnostics: Vec<MacroDiagnostic>,
387    /// Source mapping for position translation between original and expanded code.
388    pub source_mapping: Option<SourceMappingResult>,
389}
390
391impl ExpandResult {
392    /// Creates a no-op result with the original code unchanged.
393    ///
394    /// Used for early bailout when no macros need processing, avoiding
395    /// unnecessary parsing and expansion overhead.
396    ///
397    /// # Arguments
398    ///
399    /// * `code` - The original source code to return unchanged
400    ///
401    /// # Returns
402    ///
403    /// An `ExpandResult` with:
404    /// - `code` set to the input
405    /// - All other fields empty/None
406    /// - No source mapping (identity mapping implied)
407    pub fn unchanged(code: &str) -> Self {
408        Self {
409            code: code.to_string(),
410            types: None,
411            metadata: None,
412            diagnostics: vec![],
413            source_mapping: None,
414        }
415    }
416}
417
418/// Information about an imported identifier from a TypeScript module.
419///
420/// Used to track where decorators and macro-related imports come from.
421#[napi(object)]
422#[derive(Clone)]
423pub struct ImportSourceResult {
424    /// Local identifier name in the import statement (e.g., `Derive` in `import { Derive }`).
425    pub local: String,
426    /// Module specifier this identifier was imported from (e.g., `"macroforge-ts"`).
427    pub module: String,
428}
429
430/// Result of checking TypeScript syntax validity.
431///
432/// Returned by [`check_syntax`] to indicate whether code parses successfully.
433#[napi(object)]
434#[derive(Clone)]
435pub struct SyntaxCheckResult {
436    /// `true` if the code parsed without errors, `false` otherwise.
437    pub ok: bool,
438    /// Error message if parsing failed, `None` if successful.
439    pub error: Option<String>,
440}
441
442/// A span (range) in source code, represented as start position and length.
443///
444/// Used for mapping diagnostics and other positional information.
445#[napi(object)]
446#[derive(Clone)]
447pub struct SpanResult {
448    /// Byte offset where the span starts.
449    pub start: u32,
450    /// Length of the span in bytes.
451    pub length: u32,
452}
453
454/// A diagnostic from the TypeScript/JavaScript compiler or IDE.
455///
456/// This structure mirrors TypeScript's diagnostic format for interoperability
457/// with language servers and IDEs.
458#[napi(object)]
459#[derive(Clone)]
460pub struct JsDiagnostic {
461    /// Byte offset where the diagnostic starts. `None` for global diagnostics.
462    pub start: Option<u32>,
463    /// Length of the diagnostic span in bytes.
464    pub length: Option<u32>,
465    /// Human-readable diagnostic message.
466    pub message: Option<String>,
467    /// TypeScript diagnostic code (e.g., 2304 for "Cannot find name").
468    pub code: Option<u32>,
469    /// Diagnostic category: "error", "warning", "suggestion", "message".
470    pub category: Option<String>,
471}
472
473// ============================================================================
474// Position Mapper (Optimized with Binary Search)
475// ============================================================================
476
477/// Bidirectional position mapper for translating between original and expanded source positions.
478///
479/// This mapper enables IDE features like error reporting, go-to-definition, and hover
480/// to work correctly with macro-expanded code by translating positions between the
481/// original source (what the user wrote) and the expanded source (what the compiler sees).
482///
483/// # Performance
484///
485/// Position lookups use binary search for O(log n) complexity, where n is the number
486/// of mapping segments. This is critical for responsive IDE interactions.
487///
488/// # Example
489///
490/// ```javascript
491/// const mapper = new PositionMapper(sourceMapping);
492///
493/// // Convert original position to expanded
494/// const expandedPos = mapper.original_to_expanded(42);
495///
496/// // Convert expanded position back to original (if not in generated code)
497/// const originalPos = mapper.expanded_to_original(100);
498///
499/// // Check if a position is in macro-generated code
500/// if (mapper.is_in_generated(pos)) {
501///     const macro = mapper.generated_by(pos); // e.g., "Debug"
502/// }
503/// ```
504#[napi(js_name = "PositionMapper")]
505pub struct NativePositionMapper {
506    /// Mapping segments sorted by position for binary search.
507    segments: Vec<MappingSegmentResult>,
508    /// Regions marking code generated by macros.
509    generated_regions: Vec<GeneratedRegionResult>,
510}
511
512/// Wrapper around `NativePositionMapper` for NAPI compatibility.
513///
514/// This provides the same functionality as `NativePositionMapper` but with a
515/// different JavaScript class name. Used internally by [`NativePlugin::get_mapper`].
516#[napi(js_name = "NativeMapper")]
517pub struct NativeMapper {
518    /// The underlying position mapper implementation.
519    inner: NativePositionMapper,
520}
521
522#[napi]
523impl NativePositionMapper {
524    /// Creates a new position mapper from source mapping data.
525    ///
526    /// # Arguments
527    ///
528    /// * `mapping` - The source mapping result from macro expansion
529    ///
530    /// # Returns
531    ///
532    /// A new `NativePositionMapper` ready for position translation.
533    #[napi(constructor)]
534    pub fn new(mapping: SourceMappingResult) -> Self {
535        Self {
536            segments: mapping.segments,
537            generated_regions: mapping.generated_regions,
538        }
539    }
540
541    /// Checks if this mapper has no mapping data.
542    ///
543    /// An empty mapper indicates no transformations occurred, so position
544    /// translation is an identity operation.
545    ///
546    /// # Returns
547    ///
548    /// `true` if there are no segments and no generated regions.
549    #[napi(js_name = "isEmpty")]
550    pub fn is_empty(&self) -> bool {
551        self.segments.is_empty() && self.generated_regions.is_empty()
552    }
553
554    /// Converts a position in the original source to the corresponding position in expanded source.
555    ///
556    /// Uses binary search for O(log n) lookup performance.
557    ///
558    /// # Arguments
559    ///
560    /// * `pos` - Byte offset in the original source
561    ///
562    /// # Returns
563    ///
564    /// The corresponding byte offset in the expanded source. If the position falls
565    /// in a gap between segments, returns the position unchanged. If after the last
566    /// segment, extrapolates based on the delta.
567    ///
568    /// # Algorithm
569    ///
570    /// 1. Binary search to find the segment containing or after `pos`
571    /// 2. If inside a segment, compute offset within segment and translate
572    /// 3. If after all segments, extrapolate from the last segment
573    /// 4. Otherwise, return position unchanged (gap or before first segment)
574    #[napi]
575    pub fn original_to_expanded(&self, pos: u32) -> u32 {
576        // Binary search to find the first segment where original_end > pos.
577        // This gives us the segment that might contain pos, or the one after it.
578        let idx = self.segments.partition_point(|seg| seg.original_end <= pos);
579
580        if let Some(seg) = self.segments.get(idx) {
581            // Check if pos is actually inside this segment (it might be in a gap)
582            if pos >= seg.original_start && pos < seg.original_end {
583                // Position is within this segment - calculate the offset and translate
584                let offset = pos - seg.original_start;
585                return seg.expanded_start + offset;
586            }
587        }
588
589        // Handle case where position is after the last segment.
590        // Extrapolate by adding the delta from the end of the last segment.
591        if let Some(last) = self.segments.last()
592            && pos >= last.original_end
593        {
594            let delta = pos - last.original_end;
595            return last.expanded_end + delta;
596        }
597
598        // Fallback for positions before first segment or in gaps between segments.
599        // Return unchanged as an identity mapping.
600        pos
601    }
602
603    /// Converts a position in the expanded source back to the original source position.
604    ///
605    /// Returns `None` if the position is inside macro-generated code that has no
606    /// corresponding location in the original source.
607    ///
608    /// # Arguments
609    ///
610    /// * `pos` - Byte offset in the expanded source
611    ///
612    /// # Returns
613    ///
614    /// `Some(original_pos)` if the position maps to original code,
615    /// `None` if the position is in macro-generated code.
616    #[napi]
617    pub fn expanded_to_original(&self, pos: u32) -> Option<u32> {
618        // First check if the position is in a generated region (no original mapping)
619        if self.is_in_generated(pos) {
620            return None;
621        }
622
623        // Binary search to find the segment containing or after this expanded position
624        let idx = self.segments.partition_point(|seg| seg.expanded_end <= pos);
625
626        if let Some(seg) = self.segments.get(idx)
627            && pos >= seg.expanded_start
628            && pos < seg.expanded_end
629        {
630            // Position is within this segment - translate back to original
631            let offset = pos - seg.expanded_start;
632            return Some(seg.original_start + offset);
633        }
634
635        // Handle extrapolation after the last segment
636        if let Some(last) = self.segments.last()
637            && pos >= last.expanded_end
638        {
639            let delta = pos - last.expanded_end;
640            return Some(last.original_end + delta);
641        }
642
643        // Position doesn't map to any segment
644        None
645    }
646
647    /// Returns the name of the macro that generated code at the given position.
648    ///
649    /// # Arguments
650    ///
651    /// * `pos` - Byte offset in the expanded source
652    ///
653    /// # Returns
654    ///
655    /// `Some(macro_name)` if the position is inside generated code (e.g., "Debug"),
656    /// `None` if the position is in original (non-generated) code.
657    #[napi]
658    pub fn generated_by(&self, pos: u32) -> Option<String> {
659        // Generated regions are typically small in number, so linear scan is acceptable.
660        // If this becomes a bottleneck with many macros, could be optimized with binary search.
661        self.generated_regions
662            .iter()
663            .find(|r| pos >= r.start && pos < r.end)
664            .map(|r| r.source_macro.clone())
665    }
666
667    /// Maps a span (start + length) from expanded source to original source.
668    ///
669    /// # Arguments
670    ///
671    /// * `start` - Start byte offset in expanded source
672    /// * `length` - Length of the span in bytes
673    ///
674    /// # Returns
675    ///
676    /// `Some(SpanResult)` with the mapped span in original source,
677    /// `None` if either endpoint is in generated code.
678    #[napi]
679    pub fn map_span_to_original(&self, start: u32, length: u32) -> Option<SpanResult> {
680        let end = start.saturating_add(length);
681        // Both start and end must successfully map for the span to be valid
682        let original_start = self.expanded_to_original(start)?;
683        let original_end = self.expanded_to_original(end)?;
684
685        Some(SpanResult {
686            start: original_start,
687            length: original_end.saturating_sub(original_start),
688        })
689    }
690
691    /// Maps a span (start + length) from original source to expanded source.
692    ///
693    /// This always succeeds since every original position has an expanded equivalent.
694    ///
695    /// # Arguments
696    ///
697    /// * `start` - Start byte offset in original source
698    /// * `length` - Length of the span in bytes
699    ///
700    /// # Returns
701    ///
702    /// A `SpanResult` with the mapped span in expanded source.
703    #[napi]
704    pub fn map_span_to_expanded(&self, start: u32, length: u32) -> SpanResult {
705        let end = start.saturating_add(length);
706        let expanded_start = self.original_to_expanded(start);
707        let expanded_end = self.original_to_expanded(end);
708
709        SpanResult {
710            start: expanded_start,
711            length: expanded_end.saturating_sub(expanded_start),
712        }
713    }
714
715    /// Checks if a position is inside macro-generated code.
716    ///
717    /// # Arguments
718    ///
719    /// * `pos` - Byte offset in the expanded source
720    ///
721    /// # Returns
722    ///
723    /// `true` if the position is inside a generated region, `false` otherwise.
724    #[napi]
725    pub fn is_in_generated(&self, pos: u32) -> bool {
726        self.generated_regions
727            .iter()
728            .any(|r| pos >= r.start && pos < r.end)
729    }
730}
731
732#[napi]
733impl NativeMapper {
734    /// Creates a new mapper wrapping the given source mapping.
735    ///
736    /// # Arguments
737    ///
738    /// * `mapping` - The source mapping result from macro expansion
739    #[napi(constructor)]
740    pub fn new(mapping: SourceMappingResult) -> Self {
741        Self {
742            inner: NativePositionMapper::new(mapping),
743        }
744    }
745
746    /// Checks if this mapper has no mapping data.
747    #[napi(js_name = "isEmpty")]
748    pub fn is_empty(&self) -> bool {
749        self.inner.is_empty()
750    }
751
752    /// Converts a position in the original source to expanded source.
753    /// See [`NativePositionMapper::original_to_expanded`] for details.
754    #[napi]
755    pub fn original_to_expanded(&self, pos: u32) -> u32 {
756        self.inner.original_to_expanded(pos)
757    }
758
759    /// Converts a position in the expanded source back to original.
760    /// See [`NativePositionMapper::expanded_to_original`] for details.
761    #[napi]
762    pub fn expanded_to_original(&self, pos: u32) -> Option<u32> {
763        self.inner.expanded_to_original(pos)
764    }
765
766    /// Returns the name of the macro that generated code at the given position.
767    /// See [`NativePositionMapper::generated_by`] for details.
768    #[napi]
769    pub fn generated_by(&self, pos: u32) -> Option<String> {
770        self.inner.generated_by(pos)
771    }
772
773    /// Maps a span from expanded source to original source.
774    /// See [`NativePositionMapper::map_span_to_original`] for details.
775    #[napi]
776    pub fn map_span_to_original(&self, start: u32, length: u32) -> Option<SpanResult> {
777        self.inner.map_span_to_original(start, length)
778    }
779
780    /// Maps a span from original source to expanded source.
781    /// See [`NativePositionMapper::map_span_to_expanded`] for details.
782    #[napi]
783    pub fn map_span_to_expanded(&self, start: u32, length: u32) -> SpanResult {
784        self.inner.map_span_to_expanded(start, length)
785    }
786
787    /// Checks if a position is inside macro-generated code.
788    /// See [`NativePositionMapper::is_in_generated`] for details.
789    #[napi]
790    pub fn is_in_generated(&self, pos: u32) -> bool {
791        self.inner.is_in_generated(pos)
792    }
793}
794
795/// Checks if the given TypeScript code has valid syntax.
796///
797/// This function attempts to parse the code using SWC's TypeScript parser
798/// without performing any macro expansion.
799///
800/// # Arguments
801///
802/// * `code` - The TypeScript source code to check
803/// * `filepath` - The file path (used to determine if it's TSX based on extension)
804///
805/// # Returns
806///
807/// A [`SyntaxCheckResult`] indicating success or containing the parse error.
808///
809/// # Example
810///
811/// ```javascript
812/// const result = check_syntax("const x: number = 42;", "test.ts");
813/// if (!result.ok) {
814///     console.error("Syntax error:", result.error);
815/// }
816/// ```
817#[napi]
818pub fn check_syntax(code: String, filepath: String) -> SyntaxCheckResult {
819    match parse_program(&code, &filepath) {
820        Ok(_) => SyntaxCheckResult {
821            ok: true,
822            error: None,
823        },
824        Err(err) => SyntaxCheckResult {
825            ok: false,
826            error: Some(err.to_string()),
827        },
828    }
829}
830
831// ============================================================================
832// Core Plugin Logic
833// ============================================================================
834
835/// Options for processing a file through the macro system.
836///
837/// Used by [`NativePlugin::process_file`] to configure expansion behavior
838/// and caching.
839#[napi(object)]
840pub struct ProcessFileOptions {
841    /// If `true`, preserves `@derive` decorators in the output.
842    /// If `false` (default), decorators are stripped after expansion.
843    pub keep_decorators: Option<bool>,
844    /// Version string for cache invalidation.
845    /// When provided, cached results are only reused if versions match.
846    pub version: Option<String>,
847    /// Additional decorator module names from external macros.
848    /// See [`ExpandOptions::external_decorator_modules`] for details.
849    pub external_decorator_modules: Option<Vec<String>>,
850    /// Path to a previously loaded config file (for foreign types lookup).
851    /// See [`ExpandOptions::config_path`] for details.
852    pub config_path: Option<String>,
853    /// Pre-built type registry JSON from [`scan_project_sync`].
854    /// See [`ExpandOptions::type_registry_json`] for details.
855    pub type_registry_json: Option<String>,
856}
857
858/// Options for macro expansion.
859///
860/// Used by [`expand_sync`] to configure expansion behavior.
861#[napi(object)]
862pub struct ExpandOptions {
863    /// If `true`, preserves `@derive` decorators in the output.
864    /// If `false` (default), decorators are stripped after expansion.
865    pub keep_decorators: Option<bool>,
866
867    /// Additional decorator module names from external macros.
868    ///
869    /// These are used during decorator stripping to identify Macroforge-specific
870    /// decorators that should be removed from the output. Built-in decorator modules
871    /// (like "serde", "debug") are automatically included.
872    ///
873    /// External macro packages should export their decorator module names, which
874    /// plugins can collect and pass here.
875    ///
876    /// # Example
877    ///
878    /// ```javascript
879    /// expandSync(code, filepath, {
880    ///   keepDecorators: false,
881    ///   externalDecoratorModules: ["myMacro", "customValidator"]
882    /// });
883    /// ```
884    pub external_decorator_modules: Option<Vec<String>>,
885
886    /// Path to a previously loaded config file.
887    ///
888    /// When provided, the expansion will use the cached configuration
889    /// (including foreign types) from this path. The config must have been
890    /// previously loaded via [`load_config`].
891    ///
892    /// # Example
893    ///
894    /// ```javascript
895    /// // First, load the config
896    /// const configResult = loadConfig(configContent, configPath);
897    ///
898    /// // Then use it during expansion
899    /// expandSync(code, filepath, { configPath });
900    /// ```
901    pub config_path: Option<String>,
902
903    /// Pre-built type registry JSON from [`scan_project_sync`].
904    ///
905    /// When provided, macros receive project-wide type awareness through
906    /// the `type_registry` and `resolved_fields` fields on `MacroContextIR`.
907    ///
908    /// # Example
909    ///
910    /// ```javascript
911    /// const scan = scanProjectSync(projectRoot);
912    /// expandSync(code, filepath, { typeRegistryJson: scan.registryJson });
913    /// ```
914    pub type_registry_json: Option<String>,
915}
916
917/// The main plugin class for macro expansion with caching support.
918///
919/// `NativePlugin` is designed to be instantiated once and reused across multiple
920/// file processing operations. It maintains a cache of expansion results keyed
921/// by filepath and version, enabling efficient incremental processing.
922///
923/// # Thread Safety
924///
925/// The plugin is thread-safe through the use of `Mutex` for internal state.
926/// However, macro expansion itself runs in a separate thread with a 32MB stack
927/// to prevent stack overflow during deep AST recursion.
928///
929/// # Example
930///
931/// ```javascript
932/// // Create a single plugin instance (typically at startup)
933/// const plugin = new NativePlugin();
934///
935/// // Process files with caching
936/// const result1 = plugin.process_file("src/foo.ts", code1, { version: "1" });
937/// const result2 = plugin.process_file("src/foo.ts", code2, { version: "1" }); // Cache hit!
938/// const result3 = plugin.process_file("src/foo.ts", code3, { version: "2" }); // Cache miss
939///
940/// // Get a mapper for position translation
941/// const mapper = plugin.get_mapper("src/foo.ts");
942/// ```
943#[napi]
944pub struct NativePlugin {
945    /// Cache of expansion results, keyed by filepath.
946    /// Protected by a mutex for thread-safe access.
947    cache: std::sync::Mutex<std::collections::HashMap<String, CachedResult>>,
948    /// Optional path to a log file for debugging.
949    /// Protected by a mutex for thread-safe access.
950    log_file: std::sync::Mutex<Option<std::path::PathBuf>>,
951}
952
953impl Default for NativePlugin {
954    fn default() -> Self {
955        Self::new()
956    }
957}
958
959/// Internal structure for cached expansion results.
960#[derive(Clone)]
961struct CachedResult {
962    /// Version string at the time of caching.
963    /// Used for cache invalidation when version changes.
964    version: Option<String>,
965    /// The cached expansion result.
966    result: ExpandResult,
967}
968
969/// Converts `ProcessFileOptions` to `ExpandOptions`.
970///
971/// Extracts only the options relevant for expansion, discarding cache-related
972/// options like `version`.
973fn option_expand_options(opts: Option<ProcessFileOptions>) -> Option<ExpandOptions> {
974    opts.map(|o| ExpandOptions {
975        keep_decorators: o.keep_decorators,
976        external_decorator_modules: o.external_decorator_modules,
977        config_path: o.config_path,
978        type_registry_json: o.type_registry_json,
979    })
980}
981
982#[napi]
983impl NativePlugin {
984    /// Creates a new `NativePlugin` instance.
985    ///
986    /// Initializes the plugin with an empty cache and sets up a default log file
987    /// at `/tmp/macroforge-plugin.log` for debugging purposes.
988    ///
989    /// # Returns
990    ///
991    /// A new `NativePlugin` ready for processing files.
992    ///
993    /// # Side Effects
994    ///
995    /// Creates or clears the log file at `/tmp/macroforge-plugin.log`.
996    #[napi(constructor)]
997    pub fn new() -> Self {
998        let plugin = Self {
999            cache: std::sync::Mutex::new(std::collections::HashMap::new()),
1000            log_file: std::sync::Mutex::new(None),
1001        };
1002
1003        // Initialize log file with default path for debugging.
1004        // This is useful for diagnosing issues in production environments
1005        // where console output might not be easily accessible.
1006        if let Ok(mut log_guard) = plugin.log_file.lock() {
1007            let log_path = std::path::PathBuf::from("/tmp/macroforge-plugin.log");
1008
1009            // Clear/create log file to start fresh on each plugin instantiation
1010            if let Err(e) = std::fs::write(&log_path, "=== macroforge plugin loaded ===\n") {
1011                eprintln!("[macroforge] Failed to initialize log file: {}", e);
1012            } else {
1013                *log_guard = Some(log_path);
1014            }
1015        }
1016
1017        plugin
1018    }
1019
1020    /// Writes a message to the plugin's log file.
1021    ///
1022    /// Useful for debugging macro expansion issues in production environments.
1023    ///
1024    /// # Arguments
1025    ///
1026    /// * `message` - The message to log
1027    ///
1028    /// # Note
1029    ///
1030    /// Messages are appended to the log file. If the log file hasn't been
1031    /// configured or cannot be written to, the message is silently dropped.
1032    #[napi]
1033    pub fn log(&self, message: String) {
1034        if let Ok(log_guard) = self.log_file.lock()
1035            && let Some(log_path) = log_guard.as_ref()
1036        {
1037            use std::io::Write;
1038            if let Ok(mut file) = std::fs::OpenOptions::new()
1039                .append(true)
1040                .create(true)
1041                .open(log_path)
1042            {
1043                let _ = writeln!(file, "{}", message);
1044            }
1045        }
1046    }
1047
1048    /// Sets the path for the plugin's log file.
1049    ///
1050    /// # Arguments
1051    ///
1052    /// * `path` - The file path to use for logging
1053    ///
1054    /// # Note
1055    ///
1056    /// This does not create the file; it will be created when the first
1057    /// message is logged.
1058    #[napi]
1059    pub fn set_log_file(&self, path: String) {
1060        if let Ok(mut log_guard) = self.log_file.lock() {
1061            *log_guard = Some(std::path::PathBuf::from(path));
1062        }
1063    }
1064
1065    /// Processes a TypeScript file through the macro expansion system.
1066    ///
1067    /// This is the main entry point for file processing. It handles caching,
1068    /// thread isolation (to prevent stack overflow), and error recovery.
1069    ///
1070    /// # Arguments
1071    ///
1072    /// * `_env` - NAPI environment (unused but required by NAPI)
1073    /// * `filepath` - Path to the file (used for TSX detection and caching)
1074    /// * `code` - The TypeScript source code to process
1075    /// * `options` - Optional configuration for expansion and caching
1076    ///
1077    /// # Returns
1078    ///
1079    /// An [`ExpandResult`] containing the expanded code, diagnostics, and source mapping.
1080    ///
1081    /// # Errors
1082    ///
1083    /// Returns an error if:
1084    /// - Thread spawning fails
1085    /// - The worker thread panics (often due to stack overflow)
1086    /// - Macro expansion fails internally
1087    ///
1088    /// # Performance
1089    ///
1090    /// - Uses a 32MB thread stack to prevent stack overflow during deep AST recursion
1091    /// - Caches results by filepath and version for efficient incremental processing
1092    /// - Early bailout for files without `@derive` decorators
1093    ///
1094    /// # Thread Safety
1095    ///
1096    /// Macro expansion runs in a separate thread because:
1097    /// 1. SWC AST operations can be deeply recursive, exceeding default stack limits
1098    /// 2. Node.js thread stack is typically only 2MB
1099    /// 3. Panics in the worker thread are caught and reported gracefully
1100    #[napi]
1101    pub fn process_file(
1102        &self,
1103        _env: Env,
1104        filepath: String,
1105        code: String,
1106        options: Option<ProcessFileOptions>,
1107    ) -> Result<ExpandResult> {
1108        let version = options.as_ref().and_then(|o| o.version.clone());
1109
1110        // Cache Check: Return cached result if version matches.
1111        // This enables efficient incremental processing when files haven't changed.
1112        if let (Some(ver), Ok(guard)) = (version.as_ref(), self.cache.lock())
1113            && let Some(cached) = guard.get(&filepath)
1114            && cached.version.as_ref() == Some(ver)
1115        {
1116            return Ok(cached.result.clone());
1117        }
1118
1119        // Run expansion in a separate thread with a LARGE stack (32MB).
1120        // Standard threads (and Node threads) often have 2MB stacks, which causes
1121        // "Broken pipe" / SEGFAULTS when SWC recurses deeply in macros.
1122        let opts_clone = option_expand_options(options);
1123        let filepath_for_thread = filepath.clone();
1124
1125        let builder = std::thread::Builder::new().stack_size(32 * 1024 * 1024);
1126        let handle = builder
1127            .spawn(move || {
1128                // Set up SWC globals for this thread.
1129                // SWC uses thread-local storage for some operations.
1130                let globals = Globals::default();
1131                GLOBALS.set(&globals, || {
1132                    // Catch panics to report them gracefully instead of crashing.
1133                    // Common cause: stack overflow from deeply nested AST.
1134                    std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1135                        // Note: NAPI Env is NOT thread safe - we cannot pass it.
1136                        // The expand_inner function uses pure Rust AST operations.
1137                        expand_inner(&code, &filepath_for_thread, opts_clone)
1138                    }))
1139                })
1140            })
1141            .map_err(|e| {
1142                Error::new(
1143                    Status::GenericFailure,
1144                    format!("Failed to spawn worker thread: {}", e),
1145                )
1146            })?;
1147
1148        // Wait for the worker thread and unwrap nested Results.
1149        // Result structure: join() -> panic catch -> expand_inner -> final result
1150        let expand_result = handle
1151            .join()
1152            .map_err(|_| {
1153                Error::new(
1154                    Status::GenericFailure,
1155                    "Macro expansion worker thread panicked (Stack Overflow?)",
1156                )
1157            })?
1158            .map_err(|_| {
1159                Error::new(
1160                    Status::GenericFailure,
1161                    "Macro expansion panicked inside worker",
1162                )
1163            })??;
1164
1165        // Update Cache: Store the result for future requests with the same version.
1166        if let Ok(mut guard) = self.cache.lock() {
1167            guard.insert(
1168                filepath.clone(),
1169                CachedResult {
1170                    version,
1171                    result: expand_result.clone(),
1172                },
1173            );
1174        }
1175
1176        Ok(expand_result)
1177    }
1178
1179    /// Retrieves a position mapper for a previously processed file.
1180    ///
1181    /// The mapper enables translation between original and expanded source positions,
1182    /// which is essential for IDE features like error reporting and navigation.
1183    ///
1184    /// # Arguments
1185    ///
1186    /// * `filepath` - Path to the file (must have been previously processed)
1187    ///
1188    /// # Returns
1189    ///
1190    /// `Some(NativeMapper)` if the file has been processed and has source mapping data,
1191    /// `None` if the file hasn't been processed or has no mapping (no macros expanded).
1192    #[napi]
1193    pub fn get_mapper(&self, filepath: String) -> Option<NativeMapper> {
1194        let mapping = match self.cache.lock() {
1195            Ok(guard) => guard
1196                .get(&filepath)
1197                .cloned()
1198                .and_then(|c| c.result.source_mapping),
1199            Err(_) => None,
1200        };
1201
1202        mapping.map(|m| NativeMapper {
1203            inner: NativePositionMapper::new(m),
1204        })
1205    }
1206
1207    /// Maps diagnostics from expanded source positions back to original source positions.
1208    ///
1209    /// This is used by IDE integrations to show errors at the correct locations
1210    /// in the user's original code, rather than in the macro-expanded output.
1211    ///
1212    /// # Arguments
1213    ///
1214    /// * `filepath` - Path to the file the diagnostics are for
1215    /// * `diags` - Diagnostics with positions in the expanded source
1216    ///
1217    /// # Returns
1218    ///
1219    /// Diagnostics with positions mapped back to the original source.
1220    /// If no mapper is available for the file, returns diagnostics unchanged.
1221    #[napi]
1222    pub fn map_diagnostics(&self, filepath: String, diags: Vec<JsDiagnostic>) -> Vec<JsDiagnostic> {
1223        let Some(mapper) = self.get_mapper(filepath) else {
1224            // No mapper available - return diagnostics unchanged
1225            return diags;
1226        };
1227
1228        diags
1229            .into_iter()
1230            .map(|mut d| {
1231                // Attempt to map the diagnostic span back to original source
1232                if let (Some(start), Some(length)) = (d.start, d.length)
1233                    && let Some(mapped) = mapper.map_span_to_original(start, length)
1234                {
1235                    d.start = Some(mapped.start);
1236                    d.length = Some(mapped.length);
1237                }
1238                // Note: Diagnostics in generated code cannot be mapped and keep
1239                // their original (expanded) positions
1240                d
1241            })
1242            .collect()
1243    }
1244}
1245
1246// ============================================================================
1247// Sync Functions (Refactored for Thread Safety & Performance)
1248// ============================================================================
1249
1250/// Parses import statements from TypeScript code and returns their sources.
1251///
1252/// This function extracts information about all import statements in the code,
1253/// mapping each imported identifier to its source module. Useful for analyzing
1254/// dependencies and understanding where decorators come from.
1255///
1256/// # Arguments
1257///
1258/// * `code` - The TypeScript source code to parse
1259/// * `filepath` - The file path (used for TSX detection)
1260///
1261/// # Returns
1262///
1263/// A vector of [`ImportSourceResult`] entries, one for each imported identifier.
1264///
1265/// # Errors
1266///
1267/// Returns an error if the code cannot be parsed.
1268///
1269/// # Example
1270///
1271/// ```javascript
1272/// // For code: import { Derive, Clone } from "macroforge-ts";
1273/// const imports = parse_import_sources(code, "test.ts");
1274/// // Returns: [
1275/// //   { local: "Derive", module: "macroforge-ts" },
1276/// //   { local: "Clone", module: "macroforge-ts" }
1277/// // ]
1278/// ```
1279#[napi]
1280pub fn parse_import_sources(code: String, filepath: String) -> Result<Vec<ImportSourceResult>> {
1281    let (program, _cm) = parse_program(&code, &filepath)?;
1282    let module = match program {
1283        Program::Module(module) => module,
1284        // Scripts don't have import statements
1285        Program::Script(_) => return Ok(vec![]),
1286    };
1287
1288    let import_result = crate::host::collect_import_sources(&module, &code);
1289    let mut imports = Vec::with_capacity(import_result.sources.len());
1290    for (local, module) in import_result.sources {
1291        imports.push(ImportSourceResult { local, module });
1292    }
1293    Ok(imports)
1294}
1295
1296/// The `@Derive` decorator function exported to JavaScript/TypeScript.
1297///
1298/// This is a no-op function that exists purely for TypeScript type checking.
1299/// The actual decorator processing happens during macro expansion, where
1300/// `@derive(...)` decorators are recognized and transformed.
1301///
1302/// # TypeScript Usage
1303///
1304/// ```typescript
1305/// import { Derive } from "macroforge-ts";
1306///
1307/// @Derive(Debug, Clone, Serialize)
1308/// class User {
1309///     name: string;
1310///     email: string;
1311/// }
1312/// ```
1313#[napi(
1314    js_name = "Derive",
1315    ts_return_type = "ClassDecorator",
1316    ts_args_type = "...features: any[]"
1317)]
1318pub fn derive_decorator() {}
1319
1320/// Result of loading a macroforge configuration file.
1321///
1322/// Returned by [`load_config`] after parsing a `macroforge.config.js/ts` file.
1323#[napi(object)]
1324pub struct LoadConfigResult {
1325    /// Whether to preserve `@derive` decorators in the output code.
1326    pub keep_decorators: bool,
1327    /// Whether to generate a convenience const for non-class types.
1328    pub generate_convenience_const: bool,
1329    /// Whether the config has any foreign type handlers defined.
1330    pub has_foreign_types: bool,
1331    /// Number of foreign types configured.
1332    pub foreign_type_count: u32,
1333}
1334
1335/// Load and parse a macroforge configuration file.
1336///
1337/// Parses a `macroforge.config.js/ts` file and caches the result for use
1338/// during macro expansion. The configuration includes both simple settings
1339/// (like `keepDecorators`) and foreign type handlers.
1340///
1341/// # Arguments
1342///
1343/// * `content` - The raw content of the configuration file
1344/// * `filepath` - Path to the configuration file (used to determine syntax and as cache key)
1345///
1346/// # Returns
1347///
1348/// A [`LoadConfigResult`] containing the parsed configuration summary.
1349///
1350/// # Example
1351///
1352/// ```javascript
1353/// import { loadConfig, expandSync } from 'macroforge';
1354/// import fs from 'fs';
1355///
1356/// const configPath = 'macroforge.config.js';
1357/// const configContent = fs.readFileSync(configPath, 'utf-8');
1358///
1359/// // Load and cache the configuration
1360/// const result = loadConfig(configContent, configPath);
1361/// console.log(`Loaded config with ${result.foreignTypeCount} foreign types`);
1362///
1363/// // The config is now cached and will be used by expandSync
1364/// const expanded = expandSync(code, filepath, { configPath });
1365/// ```
1366#[napi]
1367pub fn load_config(content: String, filepath: String) -> Result<LoadConfigResult> {
1368    use crate::host::MacroforgeConfigLoader;
1369
1370    let config = MacroforgeConfigLoader::load_and_cache(&content, &filepath).map_err(|e| {
1371        Error::new(
1372            Status::GenericFailure,
1373            format!("Failed to parse config: {}", e),
1374        )
1375    })?;
1376
1377    Ok(LoadConfigResult {
1378        keep_decorators: config.keep_decorators,
1379        generate_convenience_const: config.generate_convenience_const,
1380        has_foreign_types: !config.foreign_types.is_empty(),
1381        foreign_type_count: config.foreign_types.len() as u32,
1382    })
1383}
1384
1385/// Clears the configuration cache.
1386///
1387/// This is useful for testing to ensure each test starts with a clean state.
1388/// In production, clearing the cache will force configs to be re-parsed on next access.
1389///
1390/// # Example
1391///
1392/// ```javascript
1393/// const { clearConfigCache, loadConfig } = require('macroforge-ts');
1394///
1395/// // Clear cache before each test
1396/// clearConfigCache();
1397///
1398/// // Now load a fresh config
1399/// const result = loadConfig(configContent, configPath);
1400/// ```
1401#[napi]
1402pub fn clear_config_cache() {
1403    crate::host::clear_config_cache();
1404}
1405
1406// ============================================================================
1407// Project Scanning for Type Awareness
1408// ============================================================================
1409
1410/// Options for scanning a TypeScript project for type information.
1411#[napi(object)]
1412pub struct ScanOptions {
1413    /// File extensions to scan (default: `[".ts", ".tsx"]`).
1414    pub extensions: Option<Vec<String>>,
1415    /// Whether to only collect exported types (default: `false`).
1416    pub exported_only: Option<bool>,
1417}
1418
1419/// Result of scanning a project for type information.
1420#[napi(object)]
1421#[derive(Clone)]
1422pub struct ScanResult {
1423    /// JSON-serialized [`TypeRegistry`].
1424    /// Pass this to `expand_sync` via `ExpandOptions.type_registry_json`.
1425    pub registry_json: String,
1426    /// Number of files scanned.
1427    pub files_scanned: u32,
1428    /// Number of types found.
1429    pub types_found: u32,
1430    /// Diagnostics from scanning (e.g., files that failed to parse).
1431    pub diagnostics: Vec<MacroDiagnostic>,
1432}
1433
1434/// Scan a TypeScript project and build a type registry.
1435///
1436/// This should be called once at build start (e.g., in Vite's `buildStart` hook)
1437/// and the resulting `registry_json` should be passed to [`expand_sync`] via
1438/// `ExpandOptions.type_registry_json`.
1439///
1440/// # Arguments
1441///
1442/// * `root_dir` - The project root directory to scan
1443/// * `options` - Optional scan configuration
1444///
1445/// # Returns
1446///
1447/// A [`ScanResult`] with the serialized registry and scan statistics.
1448///
1449/// # Example
1450///
1451/// ```javascript
1452/// const scan = scanProjectSync(projectRoot);
1453/// console.log(`Found ${scan.typesFound} types in ${scan.filesScanned} files`);
1454///
1455/// // Pass to expand_sync for each file
1456/// const result = expandSync(code, filepath, {
1457///   typeRegistryJson: scan.registryJson,
1458/// });
1459/// ```
1460#[napi]
1461pub fn scan_project_sync(root_dir: String, options: Option<ScanOptions>) -> Result<ScanResult> {
1462    let builder = std::thread::Builder::new().stack_size(32 * 1024 * 1024);
1463    let handle = builder
1464        .spawn(move || scan_project_inner(&root_dir, options))
1465        .map_err(|e| {
1466            Error::new(
1467                Status::GenericFailure,
1468                format!("Failed to spawn scan thread: {}", e),
1469            )
1470        })?;
1471
1472    handle
1473        .join()
1474        .map_err(|_| Error::new(Status::GenericFailure, "Scan worker panicked"))?
1475}
1476
1477fn scan_project_inner(root_dir: &str, options: Option<ScanOptions>) -> Result<ScanResult> {
1478    use crate::host::scanner::{ProjectScanner, ScanConfig};
1479
1480    let mut config = ScanConfig {
1481        root_dir: std::path::PathBuf::from(root_dir),
1482        ..ScanConfig::default()
1483    };
1484
1485    if let Some(opts) = options {
1486        if let Some(exts) = opts.extensions {
1487            config.extensions = exts;
1488        }
1489        if let Some(exported) = opts.exported_only {
1490            config.exported_only = exported;
1491        }
1492    }
1493
1494    let scanner = ProjectScanner::new(config);
1495    let output = scanner.scan().map_err(|e| {
1496        Error::new(
1497            Status::GenericFailure,
1498            format!("Project scan failed: {}", e),
1499        )
1500    })?;
1501
1502    let types_found = output.registry.len() as u32;
1503    let registry_json = serde_json::to_string(&output.registry).map_err(|e| {
1504        Error::new(
1505            Status::GenericFailure,
1506            format!("Failed to serialize registry: {}", e),
1507        )
1508    })?;
1509
1510    let diagnostics = output
1511        .warnings
1512        .into_iter()
1513        .map(|msg| MacroDiagnostic {
1514            level: "warning".to_string(),
1515            message: msg,
1516            start: None,
1517            end: None,
1518        })
1519        .collect();
1520
1521    Ok(ScanResult {
1522        registry_json,
1523        files_scanned: output.files_scanned,
1524        types_found,
1525        diagnostics,
1526    })
1527}
1528
1529/// Synchronously transforms TypeScript code through the macro expansion system.
1530///
1531/// This is similar to [`expand_sync`] but returns a [`TransformResult`] which
1532/// includes source map information (when available).
1533///
1534/// # Arguments
1535///
1536/// * `_env` - NAPI environment (unused but required by NAPI)
1537/// * `code` - The TypeScript source code to transform
1538/// * `filepath` - The file path (used for TSX detection)
1539///
1540/// # Returns
1541///
1542/// A [`TransformResult`] containing the transformed code and metadata.
1543///
1544/// # Errors
1545///
1546/// Returns an error if:
1547/// - Thread spawning fails
1548/// - The worker thread panics
1549/// - Macro expansion fails
1550///
1551/// # Thread Safety
1552///
1553/// Uses a 32MB thread stack to prevent stack overflow during deep AST recursion.
1554#[napi]
1555pub fn transform_sync(_env: Env, code: String, filepath: String) -> Result<TransformResult> {
1556    // Run in a separate thread with large stack for deep AST recursion
1557    let builder = std::thread::Builder::new().stack_size(32 * 1024 * 1024);
1558    let handle = builder
1559        .spawn(move || {
1560            let globals = Globals::default();
1561            GLOBALS.set(&globals, || {
1562                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1563                    transform_inner(&code, &filepath)
1564                }))
1565            })
1566        })
1567        .map_err(|e| {
1568            Error::new(
1569                Status::GenericFailure,
1570                format!("Failed to spawn transform thread: {}", e),
1571            )
1572        })?;
1573
1574    handle
1575        .join()
1576        .map_err(|_| Error::new(Status::GenericFailure, "Transform worker crashed"))?
1577        .map_err(|_| Error::new(Status::GenericFailure, "Transform panicked"))?
1578}
1579
1580/// Synchronously expands macros in TypeScript code.
1581///
1582/// This is the standalone macro expansion function that doesn't use caching.
1583/// For cached expansion, use [`NativePlugin::process_file`] instead.
1584///
1585/// # Arguments
1586///
1587/// * `_env` - NAPI environment (unused but required by NAPI)
1588/// * `code` - The TypeScript source code to expand
1589/// * `filepath` - The file path (used for TSX detection)
1590/// * `options` - Optional configuration (e.g., `keep_decorators`)
1591///
1592/// # Returns
1593///
1594/// An [`ExpandResult`] containing the expanded code, diagnostics, and source mapping.
1595///
1596/// # Errors
1597///
1598/// Returns an error if:
1599/// - Thread spawning fails
1600/// - The worker thread panics
1601/// - Macro host initialization fails
1602///
1603/// # Performance
1604///
1605/// - Uses a 32MB thread stack to prevent stack overflow
1606/// - Performs early bailout for files without `@derive` decorators
1607///
1608/// # Example
1609///
1610/// ```javascript
1611/// const result = expand_sync(env, code, "user.ts", { keep_decorators: false });
1612/// console.log(result.code); // Expanded TypeScript code
1613/// console.log(result.diagnostics); // Any warnings or errors
1614/// ```
1615#[napi]
1616pub fn expand_sync(
1617    _env: Env,
1618    code: String,
1619    filepath: String,
1620    options: Option<ExpandOptions>,
1621) -> Result<ExpandResult> {
1622    // Run in a separate thread with large stack for deep AST recursion
1623    let builder = std::thread::Builder::new().stack_size(32 * 1024 * 1024);
1624    let handle = builder
1625        .spawn(move || {
1626            let globals = Globals::default();
1627            GLOBALS.set(&globals, || {
1628                std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
1629                    expand_inner(&code, &filepath, options)
1630                }))
1631            })
1632        })
1633        .map_err(|e| {
1634            Error::new(
1635                Status::GenericFailure,
1636                format!("Failed to spawn expand thread: {}", e),
1637            )
1638        })?;
1639
1640    handle
1641        .join()
1642        .map_err(|_| Error::new(Status::GenericFailure, "Expand worker crashed"))?
1643        .map_err(|_| Error::new(Status::GenericFailure, "Expand panicked"))?
1644}
1645
1646// ============================================================================
1647// Inner Logic (Optimized)
1648// ============================================================================
1649
1650/// Core macro expansion logic, decoupled from NAPI Env to allow threading.
1651///
1652/// This function contains the actual expansion implementation and is called
1653/// from a separate thread with a large stack size to prevent stack overflow.
1654///
1655/// # Arguments
1656///
1657/// * `code` - The TypeScript source code to expand
1658/// * `filepath` - The file path (used for TSX detection and error reporting)
1659/// * `options` - Optional expansion configuration
1660///
1661/// # Returns
1662///
1663/// An [`ExpandResult`] containing the expanded code and metadata.
1664///
1665/// # Errors
1666///
1667/// Returns an error if:
1668/// - The macro host fails to initialize
1669/// - Macro expansion fails internally
1670///
1671/// # Algorithm
1672///
1673/// Check if source code contains `@derive(` as a standalone JSDoc directive.
1674///
1675/// Only matches `@derive(` when it appears at the start of a JSDoc line (after
1676/// stripping `/**`, `*/`, `*`, and whitespace). Skips `@derive` embedded in prose
1677/// (e.g., `"result from @derive(Deserialize)"`) and inside fenced code blocks.
1678fn has_macro_annotations(source: &str) -> bool {
1679    if !source.contains("@derive") {
1680        return false;
1681    }
1682    let mut in_code_block = false;
1683    for line in source.lines() {
1684        // Strip JSDoc comment syntax: /**, */, leading *, and whitespace
1685        let trimmed = line
1686            .trim()
1687            .trim_start_matches('/')
1688            .trim_start_matches('*')
1689            .trim_end_matches('/')
1690            .trim_end_matches('*')
1691            .trim();
1692        if trimmed.starts_with("```") {
1693            in_code_block = !in_code_block;
1694            continue;
1695        }
1696        if in_code_block {
1697            continue;
1698        }
1699        // A line must START with @derive( to be a real directive.
1700        // This rejects prose like "result from @derive(Deserialize)".
1701        if trimmed.starts_with("@derive(") {
1702            return true;
1703        }
1704    }
1705    false
1706}
1707
1708/// 1. **Early bailout**: If code doesn't contain `@derive`, return unchanged
1709/// 2. **Parse**: Convert code to SWC AST
1710/// 3. **Expand**: Run all registered macros on decorated classes
1711/// 4. **Collect**: Gather diagnostics and source mapping
1712/// 5. **Post-process**: Inject type declarations for generated methods
1713fn expand_inner(
1714    code: &str,
1715    filepath: &str,
1716    options: Option<ExpandOptions>,
1717) -> Result<ExpandResult> {
1718    // Early bailout: Skip files without @derive decorator.
1719    // This optimization avoids expensive parsing for files that don't use macros
1720    // and prevents issues with Svelte runes ($state, $derived, etc.) that use
1721    // similar syntax but aren't macroforge decorators.
1722    // Uses has_macro_annotations to skip @derive inside fenced code blocks in docs.
1723    if !has_macro_annotations(code) {
1724        return Ok(ExpandResult::unchanged(code));
1725    }
1726
1727    // Create a new macro host for this thread.
1728    // Each thread needs its own MacroExpander because the expansion process
1729    // is stateful and cannot be safely shared across threads.
1730    let mut macro_host = MacroExpander::new().map_err(|err| {
1731        Error::new(
1732            Status::GenericFailure,
1733            format!("Failed to initialize macro host: {err:?}"),
1734        )
1735    })?;
1736
1737    // Apply options if provided
1738    if let Some(ref opts) = options {
1739        if let Some(keep) = opts.keep_decorators {
1740            macro_host.set_keep_decorators(keep);
1741        }
1742        if let Some(ref modules) = opts.external_decorator_modules {
1743            macro_host.set_external_decorator_modules(modules.clone());
1744        }
1745        // Deserialize and attach the type registry for type awareness
1746        if let Some(ref json) = opts.type_registry_json {
1747            let registry = get_or_parse_registry(json);
1748            macro_host.set_type_registry(registry);
1749        }
1750    }
1751
1752    // Set up foreign types and config imports on the registry (from config file).
1753    // The registry's source imports will be built from the AST later in prepare_expansion_context.
1754    let config_path = options.as_ref().and_then(|o| o.config_path.as_ref());
1755    if let Some(path) = config_path
1756        && let Some(config) = CONFIG_CACHE.get(path)
1757    {
1758        crate::host::import_registry::set_foreign_types(config.foreign_types.clone());
1759        crate::host::import_registry::with_registry_mut(|r| {
1760            r.config_imports = config
1761                .config_imports
1762                .iter()
1763                .map(|(name, info)| (name.clone(), info.source.clone()))
1764                .collect();
1765        });
1766    }
1767
1768    // Parse the code into an AST.
1769    // On parse errors, we return a graceful "no-op" result instead of failing,
1770    // because parse errors can happen frequently during typing in an IDE.
1771    let (program, _) = match parse_program(code, filepath) {
1772        Ok(p) => p,
1773        Err(e) => {
1774            let error_msg = e.to_string();
1775
1776            // Clean up registry before returning
1777            crate::host::import_registry::clear_registry();
1778            crate::host::import_registry::clear_foreign_types();
1779
1780            return Ok(ExpandResult {
1781                code: code.to_string(),
1782                types: None,
1783                metadata: None,
1784                diagnostics: vec![MacroDiagnostic {
1785                    level: "info".to_string(),
1786                    message: format!("Macro expansion skipped due to syntax error: {}", error_msg),
1787                    start: None,
1788                    end: None,
1789                }],
1790                source_mapping: None,
1791            });
1792        }
1793    };
1794
1795    // Run macro expansion on the parsed AST.
1796    // expand() internally calls prepare_expansion_context which builds the full registry from the AST.
1797    let expansion_result = macro_host.expand(code, &program, filepath);
1798
1799    // Single cleanup — replaces 7 separate clear_* calls
1800    crate::host::import_registry::clear_registry();
1801
1802    // Now propagate any error
1803    let expansion = expansion_result.map_err(|err| {
1804        Error::new(
1805            Status::GenericFailure,
1806            format!("Macro expansion failed: {err:?}"),
1807        )
1808    })?;
1809
1810    // Convert internal diagnostics to NAPI-compatible format
1811    let diagnostics = expansion
1812        .diagnostics
1813        .into_iter()
1814        .map(|d| MacroDiagnostic {
1815            level: format!("{:?}", d.level).to_lowercase(),
1816            message: d.message,
1817            start: d.span.map(|s| s.start),
1818            end: d.span.map(|s| s.end),
1819        })
1820        .collect();
1821
1822    // Convert internal source mapping to NAPI-compatible format
1823    let source_mapping = expansion.source_mapping.map(|mapping| SourceMappingResult {
1824        segments: mapping
1825            .segments
1826            .into_iter()
1827            .map(|seg| MappingSegmentResult {
1828                original_start: seg.original_start,
1829                original_end: seg.original_end,
1830                expanded_start: seg.expanded_start,
1831                expanded_end: seg.expanded_end,
1832            })
1833            .collect(),
1834        generated_regions: mapping
1835            .generated_regions
1836            .into_iter()
1837            .map(|region| GeneratedRegionResult {
1838                start: region.start,
1839                end: region.end,
1840                source_macro: region.source_macro,
1841            })
1842            .collect(),
1843    });
1844
1845    // Post-process type declarations.
1846    // Heuristic fix: If the expanded code contains toJSON() but the type
1847    // declarations don't, inject the type signature. This ensures IDEs
1848    // provide proper type information for serialized objects.
1849    let mut types_output = expansion.type_output;
1850    if let Some(types) = &mut types_output
1851        && expansion.code.contains("toJSON(")
1852        && !types.contains("toJSON(")
1853    {
1854        // Find the last closing brace and insert before it.
1855        // This is a heuristic that works for simple cases.
1856        if let Some(insert_at) = types.rfind('}') {
1857            types.insert_str(insert_at, "  toJSON(): Record<string, unknown>;\n");
1858        }
1859    }
1860
1861    Ok(ExpandResult {
1862        code: expansion.code,
1863        types: types_output,
1864        // Only include metadata if there were classes processed
1865        metadata: if expansion.classes.is_empty() {
1866            None
1867        } else {
1868            serde_json::to_string(&expansion.classes).ok()
1869        },
1870        diagnostics,
1871        source_mapping,
1872    })
1873}
1874
1875/// Core transform logic, decoupled from NAPI Env to allow threading.
1876///
1877/// Similar to [`expand_inner`] but returns a [`TransformResult`] and fails
1878/// on any error-level diagnostics.
1879///
1880/// # Arguments
1881///
1882/// * `code` - The TypeScript source code to transform
1883/// * `filepath` - The file path (used for TSX detection)
1884///
1885/// # Returns
1886///
1887/// A [`TransformResult`] containing the transformed code.
1888///
1889/// # Errors
1890///
1891/// Returns an error if:
1892/// - The macro host fails to initialize
1893/// - Parsing fails
1894/// - Expansion fails
1895/// - Any error-level diagnostic is emitted
1896fn transform_inner(code: &str, filepath: &str) -> Result<TransformResult> {
1897    let macro_host = MacroExpander::new().map_err(|err| {
1898        Error::new(
1899            Status::GenericFailure,
1900            format!("Failed to init host: {err:?}"),
1901        )
1902    })?;
1903
1904    let (program, cm) = parse_program(code, filepath)?;
1905
1906    let expansion = macro_host
1907        .expand(code, &program, filepath)
1908        .map_err(|err| Error::new(Status::GenericFailure, format!("Expansion failed: {err:?}")))?;
1909
1910    // Unlike expand_inner, transform_inner treats errors as fatal
1911    handle_macro_diagnostics(&expansion.diagnostics, filepath)?;
1912
1913    // Optimization: Only re-emit if we didn't change anything.
1914    // If expansion.changed is true, we already have the string from the expander.
1915    // Otherwise, emit the original AST to string (no changes made).
1916    let generated = if expansion.changed {
1917        expansion.code
1918    } else {
1919        emit_program(&program, &cm)?
1920    };
1921
1922    let metadata = if expansion.classes.is_empty() {
1923        None
1924    } else {
1925        serde_json::to_string(&expansion.classes).ok()
1926    };
1927
1928    Ok(TransformResult {
1929        code: generated,
1930        map: None, // Source mapping handled separately via SourceMappingResult
1931        types: expansion.type_output,
1932        metadata,
1933    })
1934}
1935
1936/// Parses TypeScript source code into an SWC AST.
1937///
1938/// # Arguments
1939///
1940/// * `code` - The TypeScript source code
1941/// * `filepath` - The file path (used to determine TSX mode from extension)
1942///
1943/// # Returns
1944///
1945/// A tuple of `(Program, SourceMap)` on success.
1946///
1947/// # Errors
1948///
1949/// Returns an error if the code contains syntax errors.
1950///
1951/// # Configuration
1952///
1953/// - TSX mode is enabled for `.tsx` files
1954/// - Decorators are always enabled
1955/// - Uses latest ES version
1956/// - `no_early_errors` is enabled for better error recovery
1957fn parse_program(code: &str, filepath: &str) -> Result<(Program, Lrc<SourceMap>)> {
1958    let cm: Lrc<SourceMap> = Lrc::new(SourceMap::default());
1959    let fm = cm.new_source_file(
1960        FileName::Custom(filepath.to_string()).into(),
1961        code.to_string(),
1962    );
1963    // Create a handler that captures errors to a buffer (we don't use its output directly)
1964    let handler =
1965        Handler::with_emitter_writer(Box::new(std::io::Cursor::new(Vec::new())), Some(cm.clone()));
1966
1967    // Configure the lexer for TypeScript with decorator support
1968    let lexer = Lexer::new(
1969        Syntax::Typescript(TsSyntax {
1970            tsx: filepath.ends_with(".tsx"), // Enable TSX for .tsx files
1971            decorators: true,                // Required for @derive decorators
1972            dts: false,                      // Not parsing .d.ts files
1973            no_early_errors: true,           // Better error recovery during typing
1974            ..Default::default()
1975        }),
1976        EsVersion::latest(),
1977        StringInput::from(&*fm),
1978        None, // No comments collection
1979    );
1980
1981    let mut parser = Parser::new_from(lexer);
1982    match parser.parse_program() {
1983        Ok(program) => Ok((program, cm)),
1984        Err(error) => {
1985            // Format and emit the error for debugging purposes
1986            let msg = format!("Failed to parse TypeScript: {:?}", error);
1987            error.into_diagnostic(&handler).emit();
1988            Err(Error::new(Status::GenericFailure, msg))
1989        }
1990    }
1991}
1992
1993/// Emits an SWC AST back to JavaScript/TypeScript source code.
1994///
1995/// # Arguments
1996///
1997/// * `program` - The AST to emit
1998/// * `cm` - The source map (used for line/column tracking)
1999///
2000/// # Returns
2001///
2002/// The generated source code as a string.
2003///
2004/// # Errors
2005///
2006/// Returns an error if code generation fails (rare).
2007fn emit_program(program: &Program, cm: &Lrc<SourceMap>) -> Result<String> {
2008    let mut buf = vec![];
2009    let mut emitter = Emitter {
2010        cfg: swc_core::ecma::codegen::Config::default(),
2011        cm: cm.clone(),
2012        comments: None,
2013        wr: Box::new(JsWriter::new(cm.clone(), "\n", &mut buf, None)),
2014    };
2015    emitter
2016        .emit_program(program)
2017        .map_err(|e| Error::new(Status::GenericFailure, format!("{:?}", e)))?;
2018    Ok(String::from_utf8_lossy(&buf).to_string())
2019}
2020
2021/// Checks diagnostics for errors and returns the first error as a Result.
2022///
2023/// This is used by [`transform_inner`] which treats errors as fatal,
2024/// unlike [`expand_inner`] which allows non-fatal errors in diagnostics.
2025///
2026/// # Arguments
2027///
2028/// * `diags` - The diagnostics to check
2029/// * `file` - The file path for error location reporting
2030///
2031/// # Returns
2032///
2033/// `Ok(())` if no errors, `Err` with the first error message otherwise.
2034fn handle_macro_diagnostics(diags: &[Diagnostic], file: &str) -> Result<()> {
2035    for diag in diags {
2036        if matches!(diag.level, DiagnosticLevel::Error) {
2037            // Format error location for helpful error messages
2038            let loc = diag
2039                .span
2040                .map(|s| format!("{}:{}-{}", file, s.start, s.end))
2041                .unwrap_or_else(|| file.to_string());
2042            return Err(Error::new(
2043                Status::GenericFailure,
2044                format!("Macro error at {}: {}", loc, diag.message),
2045            ));
2046        }
2047    }
2048    Ok(())
2049}
2050
2051// ============================================================================
2052// Manifest / Debug API
2053// ============================================================================
2054
2055/// Entry for a registered macro in the manifest.
2056///
2057/// Used by [`MacroManifest`] to describe available macros to tooling
2058/// such as IDE extensions and documentation generators.
2059#[napi(object)]
2060pub struct MacroManifestEntry {
2061    /// The macro name (e.g., "Debug", "Clone", "Serialize").
2062    pub name: String,
2063    /// The macro kind: "derive", "attribute", or "function".
2064    pub kind: String,
2065    /// Human-readable description of what the macro does.
2066    pub description: String,
2067    /// The package that provides this macro (e.g., "macroforge-ts").
2068    pub package: String,
2069}
2070
2071/// Entry for a registered decorator in the manifest.
2072///
2073/// Used by [`MacroManifest`] to describe field-level decorators
2074/// that can be used with macros.
2075#[napi(object)]
2076pub struct DecoratorManifestEntry {
2077    /// The module this decorator belongs to (e.g., "serde").
2078    pub module: String,
2079    /// The exported name of the decorator (e.g., "skip", "rename").
2080    pub export: String,
2081    /// The decorator kind: "class", "property", "method", "accessor", "parameter".
2082    pub kind: String,
2083    /// Documentation string for the decorator.
2084    pub docs: String,
2085}
2086
2087/// Complete manifest of all available macros and decorators.
2088///
2089/// This is returned by [`get_macro_manifest`] and is useful for:
2090/// - IDE autocompletion
2091/// - Documentation generation
2092/// - Tooling integration
2093#[napi(object)]
2094pub struct MacroManifest {
2095    /// ABI version for compatibility checking.
2096    pub version: u32,
2097    /// All registered macros (derive, attribute, function).
2098    pub macros: Vec<MacroManifestEntry>,
2099    /// All registered field/class decorators.
2100    pub decorators: Vec<DecoratorManifestEntry>,
2101}
2102
2103/// Returns the complete manifest of all registered macros and decorators.
2104///
2105/// This is a debug/introspection API that allows tooling to discover
2106/// what macros are available at runtime.
2107///
2108/// # Returns
2109///
2110/// A [`MacroManifest`] containing all registered macros and decorators.
2111///
2112/// # Example (JavaScript)
2113///
2114/// ```javascript
2115/// const manifest = __macroforgeGetManifest();
2116/// console.log("Available macros:", manifest.macros.map(m => m.name));
2117/// // ["Debug", "Clone", "PartialEq", "Hash", "Serialize", "Deserialize", ...]
2118/// ```
2119#[napi(js_name = "__macroforgeGetManifest")]
2120pub fn get_macro_manifest() -> MacroManifest {
2121    let manifest = derived::get_manifest();
2122    MacroManifest {
2123        version: manifest.version,
2124        macros: manifest
2125            .macros
2126            .into_iter()
2127            .map(|m| MacroManifestEntry {
2128                name: m.name.to_string(),
2129                kind: format!("{:?}", m.kind).to_lowercase(),
2130                description: m.description.to_string(),
2131                package: m.package.to_string(),
2132            })
2133            .collect(),
2134        decorators: manifest
2135            .decorators
2136            .into_iter()
2137            .map(|d| DecoratorManifestEntry {
2138                module: d.module.to_string(),
2139                export: d.export.to_string(),
2140                kind: format!("{:?}", d.kind).to_lowercase(),
2141                docs: d.docs.to_string(),
2142            })
2143            .collect(),
2144    }
2145}
2146
2147/// Checks if any macros are registered in this package.
2148///
2149/// Useful for build tools to determine if macro expansion is needed.
2150///
2151/// # Returns
2152///
2153/// `true` if at least one macro is registered, `false` otherwise.
2154#[napi(js_name = "__macroforgeIsMacroPackage")]
2155pub fn is_macro_package() -> bool {
2156    !derived::macro_names().is_empty()
2157}
2158
2159/// Returns the names of all registered macros.
2160///
2161/// # Returns
2162///
2163/// A vector of macro names (e.g., `["Debug", "Clone", "Serialize"]`).
2164#[napi(js_name = "__macroforgeGetMacroNames")]
2165pub fn get_macro_names() -> Vec<String> {
2166    derived::macro_names()
2167        .into_iter()
2168        .map(|s| s.to_string())
2169        .collect()
2170}
2171
2172/// Returns all registered macro module names (debug API).
2173///
2174/// Modules group related macros together (e.g., "builtin", "serde").
2175///
2176/// # Returns
2177///
2178/// A vector of module names.
2179#[napi(js_name = "__macroforgeDebugGetModules")]
2180pub fn debug_get_modules() -> Vec<String> {
2181    crate::host::derived::modules()
2182        .into_iter()
2183        .map(|s| s.to_string())
2184        .collect()
2185}
2186
2187/// Looks up a macro by module and name (debug API).
2188///
2189/// Useful for testing macro registration and debugging lookup issues.
2190///
2191/// # Arguments
2192///
2193/// * `module` - The module name (e.g., "builtin")
2194/// * `name` - The macro name (e.g., "Debug")
2195///
2196/// # Returns
2197///
2198/// A string describing whether the macro was found or not.
2199#[napi(js_name = "__macroforgeDebugLookup")]
2200pub fn debug_lookup(module: String, name: String) -> String {
2201    match MacroExpander::new() {
2202        Ok(host) => match host.dispatcher.registry().lookup(&module, &name) {
2203            Ok(_) => format!("Found: ({}, {})", module, name),
2204            Err(_) => format!("Not found: ({}, {})", module, name),
2205        },
2206        Err(e) => format!("Host init failed: {}", e),
2207    }
2208}
2209
2210/// Returns debug information about all registered macro descriptors (debug API).
2211///
2212/// This provides low-level access to the inventory-based macro registration
2213/// system for debugging purposes.
2214///
2215/// # Returns
2216///
2217/// A vector of strings describing each registered macro descriptor.
2218#[napi(js_name = "__macroforgeDebugDescriptors")]
2219pub fn debug_descriptors() -> Vec<String> {
2220    inventory::iter::<crate::host::derived::DerivedMacroRegistration>()
2221        .map(|entry| {
2222            format!(
2223                "name={}, module={}, package={}",
2224                entry.descriptor.name, entry.descriptor.module, entry.descriptor.package
2225            )
2226        })
2227        .collect()
2228}