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}