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