1use napi::bindgen_prelude::*;
2use napi_derive::napi;
3use swc_core::{
4 common::{FileName, GLOBALS, Globals, SourceMap, errors::Handler, sync::Lrc},
5 ecma::{
6 ast::{EsVersion, Program},
7 codegen::{Emitter, text_writer::JsWriter},
8 parser::{Parser, StringInput, Syntax, TsSyntax, lexer::Lexer},
9 },
10};
11
12extern crate self as macroforge_ts;
15
16pub extern crate macroforge_ts_syn;
24pub extern crate macroforge_ts_quote;
25pub extern crate macroforge_ts_macros;
26pub extern crate inventory;
27pub extern crate serde_json;
28pub extern crate napi;
29pub extern crate napi_derive;
30
31pub use macroforge_ts_syn as ts_syn;
34
35pub mod macros {
38 pub use macroforge_ts_macros::ts_macro_derive;
40
41 pub use macroforge_ts_quote::{ts_template, body, above, below, signature};
43}
44
45pub use macroforge_ts_syn::swc_core;
47pub use macroforge_ts_syn::swc_common;
48pub use macroforge_ts_syn::swc_ecma_ast;
49
50pub mod host;
54
55pub use ts_syn::abi;
57
58use host::derived;
59use ts_syn::{Diagnostic, DiagnosticLevel};
60
61mod builtin;
62
63#[cfg(test)]
64mod test;
65
66use crate::host::MacroExpander;
67
68#[napi(object)]
73#[derive(Clone)]
74pub struct TransformResult {
75 pub code: String,
76 pub map: Option<String>,
77 pub types: Option<String>,
78 pub metadata: Option<String>,
79}
80
81#[napi(object)]
82#[derive(Clone)]
83pub struct MacroDiagnostic {
84 pub level: String,
85 pub message: String,
86 pub start: Option<u32>,
87 pub end: Option<u32>,
88}
89
90#[napi(object)]
91#[derive(Clone)]
92pub struct MappingSegmentResult {
93 pub original_start: u32,
94 pub original_end: u32,
95 pub expanded_start: u32,
96 pub expanded_end: u32,
97}
98
99#[napi(object)]
100#[derive(Clone)]
101pub struct GeneratedRegionResult {
102 pub start: u32,
103 pub end: u32,
104 pub source_macro: String,
105}
106
107#[napi(object)]
108#[derive(Clone)]
109pub struct SourceMappingResult {
110 pub segments: Vec<MappingSegmentResult>,
111 pub generated_regions: Vec<GeneratedRegionResult>,
112}
113
114#[napi(object)]
115#[derive(Clone)]
116pub struct ExpandResult {
117 pub code: String,
118 pub types: Option<String>,
119 pub metadata: Option<String>,
120 pub diagnostics: Vec<MacroDiagnostic>,
121 pub source_mapping: Option<SourceMappingResult>,
122}
123
124#[napi(object)]
125#[derive(Clone)]
126pub struct ImportSourceResult {
127 pub local: String,
129 pub module: String,
131}
132
133#[napi(object)]
134#[derive(Clone)]
135pub struct SyntaxCheckResult {
136 pub ok: bool,
137 pub error: Option<String>,
138}
139
140#[napi(object)]
141#[derive(Clone)]
142pub struct SpanResult {
143 pub start: u32,
144 pub length: u32,
145}
146
147#[napi(object)]
148#[derive(Clone)]
149pub struct JsDiagnostic {
150 pub start: Option<u32>,
151 pub length: Option<u32>,
152 pub message: Option<String>,
153 pub code: Option<u32>,
154 pub category: Option<String>,
155}
156
157#[napi(js_name = "PositionMapper")]
162pub struct NativePositionMapper {
163 segments: Vec<MappingSegmentResult>,
164 generated_regions: Vec<GeneratedRegionResult>,
165}
166
167#[napi(js_name = "NativeMapper")]
168pub struct NativeMapper {
169 inner: NativePositionMapper,
170}
171
172#[napi]
173impl NativePositionMapper {
174 #[napi(constructor)]
175 pub fn new(mapping: SourceMappingResult) -> Self {
176 Self {
177 segments: mapping.segments,
178 generated_regions: mapping.generated_regions,
179 }
180 }
181
182 #[napi(js_name = "isEmpty")]
183 pub fn is_empty(&self) -> bool {
184 self.segments.is_empty() && self.generated_regions.is_empty()
185 }
186
187 #[napi]
188 pub fn original_to_expanded(&self, pos: u32) -> u32 {
189 let idx = self.segments.partition_point(|seg| seg.original_end <= pos);
191
192 if let Some(seg) = self.segments.get(idx) {
193 if pos >= seg.original_start && pos < seg.original_end {
195 let offset = pos - seg.original_start;
196 return seg.expanded_start + offset;
197 }
198 }
199
200 if let Some(last) = self.segments.last()
202 && pos >= last.original_end
203 {
204 let delta = pos - last.original_end;
205 return last.expanded_end + delta;
206 }
207
208 pos
210 }
211
212 #[napi]
213 pub fn expanded_to_original(&self, pos: u32) -> Option<u32> {
214 if self.is_in_generated(pos) {
215 return None;
216 }
217
218 let idx = self.segments.partition_point(|seg| seg.expanded_end <= pos);
220
221 if let Some(seg) = self.segments.get(idx)
222 && pos >= seg.expanded_start
223 && pos < seg.expanded_end
224 {
225 let offset = pos - seg.expanded_start;
226 return Some(seg.original_start + offset);
227 }
228
229 if let Some(last) = self.segments.last()
230 && pos >= last.expanded_end
231 {
232 let delta = pos - last.expanded_end;
233 return Some(last.original_end + delta);
234 }
235
236 None
237 }
238
239 #[napi]
240 pub fn generated_by(&self, pos: u32) -> Option<String> {
241 self.generated_regions
243 .iter()
244 .find(|r| pos >= r.start && pos < r.end)
245 .map(|r| r.source_macro.clone())
246 }
247
248 #[napi]
249 pub fn map_span_to_original(&self, start: u32, length: u32) -> Option<SpanResult> {
250 let end = start.saturating_add(length);
251 let original_start = self.expanded_to_original(start)?;
252 let original_end = self.expanded_to_original(end)?;
253
254 Some(SpanResult {
255 start: original_start,
256 length: original_end.saturating_sub(original_start),
257 })
258 }
259
260 #[napi]
261 pub fn map_span_to_expanded(&self, start: u32, length: u32) -> SpanResult {
262 let end = start.saturating_add(length);
263 let expanded_start = self.original_to_expanded(start);
264 let expanded_end = self.original_to_expanded(end);
265
266 SpanResult {
267 start: expanded_start,
268 length: expanded_end.saturating_sub(expanded_start),
269 }
270 }
271
272 #[napi]
273 pub fn is_in_generated(&self, pos: u32) -> bool {
274 self.generated_regions
275 .iter()
276 .any(|r| pos >= r.start && pos < r.end)
277 }
278}
279
280#[napi]
281impl NativeMapper {
282 #[napi(constructor)]
283 pub fn new(mapping: SourceMappingResult) -> Self {
284 Self {
285 inner: NativePositionMapper::new(mapping),
286 }
287 }
288 #[napi(js_name = "isEmpty")]
290 pub fn is_empty(&self) -> bool {
291 self.inner.is_empty()
292 }
293 #[napi]
294 pub fn original_to_expanded(&self, pos: u32) -> u32 {
295 self.inner.original_to_expanded(pos)
296 }
297 #[napi]
298 pub fn expanded_to_original(&self, pos: u32) -> Option<u32> {
299 self.inner.expanded_to_original(pos)
300 }
301 #[napi]
302 pub fn generated_by(&self, pos: u32) -> Option<String> {
303 self.inner.generated_by(pos)
304 }
305 #[napi]
306 pub fn map_span_to_original(&self, start: u32, length: u32) -> Option<SpanResult> {
307 self.inner.map_span_to_original(start, length)
308 }
309 #[napi]
310 pub fn map_span_to_expanded(&self, start: u32, length: u32) -> SpanResult {
311 self.inner.map_span_to_expanded(start, length)
312 }
313 #[napi]
314 pub fn is_in_generated(&self, pos: u32) -> bool {
315 self.inner.is_in_generated(pos)
316 }
317}
318
319#[napi]
320pub fn check_syntax(code: String, filepath: String) -> SyntaxCheckResult {
321 match parse_program(&code, &filepath) {
322 Ok(_) => SyntaxCheckResult {
323 ok: true,
324 error: None,
325 },
326 Err(err) => SyntaxCheckResult {
327 ok: false,
328 error: Some(err.to_string()),
329 },
330 }
331}
332
333#[napi(object)]
338pub struct ProcessFileOptions {
339 pub keep_decorators: Option<bool>,
340 pub version: Option<String>,
341}
342
343#[napi(object)]
344pub struct ExpandOptions {
345 pub keep_decorators: Option<bool>,
346}
347
348#[napi]
349pub struct NativePlugin {
350 cache: std::sync::Mutex<std::collections::HashMap<String, CachedResult>>,
351 log_file: std::sync::Mutex<Option<std::path::PathBuf>>,
352}
353
354impl Default for NativePlugin {
355 fn default() -> Self {
356 Self::new()
357 }
358}
359
360#[derive(Clone)]
361struct CachedResult {
362 version: Option<String>,
363 result: ExpandResult,
364}
365
366fn option_expand_options(opts: Option<ProcessFileOptions>) -> Option<ExpandOptions> {
367 opts.map(|o| ExpandOptions {
368 keep_decorators: o.keep_decorators,
369 })
370}
371
372#[napi]
373impl NativePlugin {
374 #[napi(constructor)]
375 pub fn new() -> Self {
376 let plugin = Self {
377 cache: std::sync::Mutex::new(std::collections::HashMap::new()),
378 log_file: std::sync::Mutex::new(None),
379 };
380
381 if let Ok(mut log_guard) = plugin.log_file.lock() {
383 let log_path = std::path::PathBuf::from("/tmp/macroforge-plugin.log");
384
385 if let Err(e) = std::fs::write(&log_path, "=== macroforge plugin loaded ===\n") {
387 eprintln!("[macroforge] Failed to initialize log file: {}", e);
388 } else {
389 *log_guard = Some(log_path);
390 }
391 }
392
393 plugin
394 }
395
396 #[napi]
397 pub fn log(&self, message: String) {
398 if let Ok(log_guard) = self.log_file.lock()
399 && let Some(log_path) = log_guard.as_ref()
400 {
401 use std::io::Write;
402 if let Ok(mut file) = std::fs::OpenOptions::new()
403 .append(true)
404 .create(true)
405 .open(log_path)
406 {
407 let _ = writeln!(file, "{}", message);
408 }
409 }
410 }
411
412 #[napi]
413 pub fn set_log_file(&self, path: String) {
414 if let Ok(mut log_guard) = self.log_file.lock() {
415 *log_guard = Some(std::path::PathBuf::from(path));
416 }
417 }
418
419 #[napi]
420 pub fn process_file(
421 &self,
422 _env: Env,
423 filepath: String,
424 code: String,
425 options: Option<ProcessFileOptions>,
426 ) -> Result<ExpandResult> {
427 let version = options.as_ref().and_then(|o| o.version.clone());
428
429 if let (Some(ver), Ok(guard)) = (version.as_ref(), self.cache.lock())
431 && let Some(cached) = guard.get(&filepath)
432 && cached.version.as_ref() == Some(ver)
433 {
434 return Ok(cached.result.clone());
435 }
436
437 let opts_clone = option_expand_options(options);
441 let filepath_for_thread = filepath.clone();
442
443 let builder = std::thread::Builder::new().stack_size(32 * 1024 * 1024);
444 let handle = builder
445 .spawn(move || {
446 let globals = Globals::default();
447 GLOBALS.set(&globals, || {
448 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
449 expand_inner(&code, &filepath_for_thread, opts_clone)
456 }))
457 })
458 })
459 .map_err(|e| {
460 Error::new(
461 Status::GenericFailure,
462 format!("Failed to spawn worker thread: {}", e),
463 )
464 })?;
465
466 let expand_result = handle
467 .join()
468 .map_err(|_| {
469 Error::new(
470 Status::GenericFailure,
471 "Macro expansion worker thread panicked (Stack Overflow?)",
472 )
473 })?
474 .map_err(|_| {
475 Error::new(
476 Status::GenericFailure,
477 "Macro expansion panicked inside worker",
478 )
479 })??;
480
481 if let Ok(mut guard) = self.cache.lock() {
483 guard.insert(
484 filepath.clone(),
485 CachedResult {
486 version,
487 result: expand_result.clone(),
488 },
489 );
490 }
491
492 Ok(expand_result)
493 }
494
495 #[napi]
496 pub fn get_mapper(&self, filepath: String) -> Option<NativeMapper> {
497 let mapping = match self.cache.lock() {
498 Ok(guard) => guard
499 .get(&filepath)
500 .cloned()
501 .and_then(|c| c.result.source_mapping),
502 Err(_) => None,
503 };
504
505 mapping.map(|m| NativeMapper {
506 inner: NativePositionMapper::new(m),
507 })
508 }
509
510 #[napi]
511 pub fn map_diagnostics(&self, filepath: String, diags: Vec<JsDiagnostic>) -> Vec<JsDiagnostic> {
512 let Some(mapper) = self.get_mapper(filepath) else {
513 return diags;
514 };
515
516 diags
517 .into_iter()
518 .map(|mut d| {
519 if let (Some(start), Some(length)) = (d.start, d.length)
520 && let Some(mapped) = mapper.map_span_to_original(start, length)
521 {
522 d.start = Some(mapped.start);
523 d.length = Some(mapped.length);
524 }
525 d
526 })
527 .collect()
528 }
529}
530
531#[napi]
536pub fn parse_import_sources(code: String, filepath: String) -> Result<Vec<ImportSourceResult>> {
537 let (program, _cm) = parse_program(&code, &filepath)?;
538 let module = match program {
539 Program::Module(module) => module,
540 Program::Script(_) => return Ok(vec![]),
541 };
542
543 let import_map = crate::host::collect_import_sources(&module, &code);
544 let mut imports = Vec::with_capacity(import_map.len());
545 for (local, module) in import_map {
546 imports.push(ImportSourceResult { local, module });
547 }
548 Ok(imports)
549}
550
551#[napi(
552 js_name = "Derive",
553 ts_return_type = "ClassDecorator",
554 ts_args_type = "...features: any[]"
555)]
556pub fn derive_decorator() {}
557
558#[napi]
559pub fn transform_sync(_env: Env, code: String, filepath: String) -> Result<TransformResult> {
560 let builder = std::thread::Builder::new().stack_size(32 * 1024 * 1024);
562 let handle = builder
563 .spawn(move || {
564 let globals = Globals::default();
565 GLOBALS.set(&globals, || {
566 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
567 transform_inner(&code, &filepath)
568 }))
569 })
570 })
571 .map_err(|e| {
572 Error::new(
573 Status::GenericFailure,
574 format!("Failed to spawn transform thread: {}", e),
575 )
576 })?;
577
578 handle
579 .join()
580 .map_err(|_| Error::new(Status::GenericFailure, "Transform worker crashed"))?
581 .map_err(|_| Error::new(Status::GenericFailure, "Transform panicked"))?
582}
583
584#[napi]
586pub fn expand_sync(
587 _env: Env,
588 code: String,
589 filepath: String,
590 options: Option<ExpandOptions>,
591) -> Result<ExpandResult> {
592 let builder = std::thread::Builder::new().stack_size(32 * 1024 * 1024);
594 let handle = builder
595 .spawn(move || {
596 let globals = Globals::default();
597 GLOBALS.set(&globals, || {
598 std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
599 expand_inner(&code, &filepath, options)
600 }))
601 })
602 })
603 .map_err(|e| {
604 Error::new(
605 Status::GenericFailure,
606 format!("Failed to spawn expand thread: {}", e),
607 )
608 })?;
609
610 handle
611 .join()
612 .map_err(|_| Error::new(Status::GenericFailure, "Expand worker crashed"))?
613 .map_err(|_| Error::new(Status::GenericFailure, "Expand panicked"))?
614}
615
616fn expand_inner(
622 code: &str,
623 filepath: &str,
624 options: Option<ExpandOptions>,
625) -> Result<ExpandResult> {
626 let mut macro_host = MacroExpander::new().map_err(|err| {
630 Error::new(
631 Status::GenericFailure,
632 format!("Failed to initialize macro host: {err:?}"),
633 )
634 })?;
635
636 if let Some(opts) = options
637 && let Some(keep) = opts.keep_decorators
638 {
639 macro_host.set_keep_decorators(keep);
640 }
641
642 let (program, _) = match parse_program(code, filepath) {
643 Ok(p) => p,
644 Err(e) => {
645 let error_msg = e.to_string();
650
651 return Ok(ExpandResult {
654 code: code.to_string(),
655 types: None,
656 metadata: None,
657 diagnostics: vec![MacroDiagnostic {
658 level: "info".to_string(),
659 message: format!("Macro expansion skipped due to syntax error: {}", error_msg),
660 start: None,
661 end: None,
662 }],
663 source_mapping: None,
664 });
665 }
666 };
667
668 let expansion = macro_host.expand(code, &program, filepath).map_err(|err| {
669 Error::new(
670 Status::GenericFailure,
671 format!("Macro expansion failed: {err:?}"),
672 )
673 })?;
674
675 let diagnostics = expansion
676 .diagnostics
677 .into_iter()
678 .map(|d| MacroDiagnostic {
679 level: format!("{:?}", d.level).to_lowercase(),
680 message: d.message,
681 start: d.span.map(|s| s.start),
682 end: d.span.map(|s| s.end),
683 })
684 .collect();
685
686 let source_mapping = expansion.source_mapping.map(|mapping| SourceMappingResult {
687 segments: mapping
688 .segments
689 .into_iter()
690 .map(|seg| MappingSegmentResult {
691 original_start: seg.original_start,
692 original_end: seg.original_end,
693 expanded_start: seg.expanded_start,
694 expanded_end: seg.expanded_end,
695 })
696 .collect(),
697 generated_regions: mapping
698 .generated_regions
699 .into_iter()
700 .map(|region| GeneratedRegionResult {
701 start: region.start,
702 end: region.end,
703 source_macro: region.source_macro,
704 })
705 .collect(),
706 });
707
708 let mut types_output = expansion.type_output;
709 if let Some(types) = &mut types_output
711 && expansion.code.contains("toJSON(")
712 && !types.contains("toJSON(")
713 {
714 if let Some(insert_at) = types.rfind('}') {
716 types.insert_str(insert_at, " toJSON(): Record<string, unknown>;\n");
717 }
718 }
719
720 Ok(ExpandResult {
721 code: expansion.code,
722 types: types_output,
723 metadata: if expansion.classes.is_empty() {
724 None
725 } else {
726 serde_json::to_string(&expansion.classes).ok()
727 },
728 diagnostics,
729 source_mapping,
730 })
731}
732
733fn transform_inner(code: &str, filepath: &str) -> Result<TransformResult> {
734 let macro_host = MacroExpander::new().map_err(|err| {
735 Error::new(
736 Status::GenericFailure,
737 format!("Failed to init host: {err:?}"),
738 )
739 })?;
740
741 let (program, cm) = parse_program(code, filepath)?;
742
743 let expansion = macro_host
744 .expand(code, &program, filepath)
745 .map_err(|err| Error::new(Status::GenericFailure, format!("Expansion failed: {err:?}")))?;
746
747 handle_macro_diagnostics(&expansion.diagnostics, filepath)?;
748
749 let generated = if expansion.changed {
753 expansion.code
754 } else {
755 emit_program(&program, &cm)?
757 };
758
759 let metadata = if expansion.classes.is_empty() {
760 None
761 } else {
762 serde_json::to_string(&expansion.classes).ok()
763 };
764
765 Ok(TransformResult {
766 code: generated,
767 map: None,
768 types: expansion.type_output,
769 metadata,
770 })
771}
772
773fn parse_program(code: &str, filepath: &str) -> Result<(Program, Lrc<SourceMap>)> {
774 let cm: Lrc<SourceMap> = Lrc::new(SourceMap::default());
775 let fm = cm.new_source_file(
776 FileName::Custom(filepath.to_string()).into(),
777 code.to_string(),
778 );
779 let handler =
780 Handler::with_emitter_writer(Box::new(std::io::Cursor::new(Vec::new())), Some(cm.clone()));
781
782 let lexer = Lexer::new(
783 Syntax::Typescript(TsSyntax {
784 tsx: filepath.ends_with(".tsx"),
785 decorators: true,
786 dts: false,
787 no_early_errors: true,
788 ..Default::default()
789 }),
790 EsVersion::latest(),
791 StringInput::from(&*fm),
792 None,
793 );
794
795 let mut parser = Parser::new_from(lexer);
796 match parser.parse_program() {
797 Ok(program) => Ok((program, cm)),
798 Err(error) => {
799 let msg = format!("Failed to parse TypeScript: {:?}", error);
800 error.into_diagnostic(&handler).emit();
801 Err(Error::new(Status::GenericFailure, msg))
802 }
803 }
804}
805
806fn emit_program(program: &Program, cm: &Lrc<SourceMap>) -> Result<String> {
807 let mut buf = vec![];
808 let mut emitter = Emitter {
809 cfg: swc_core::ecma::codegen::Config::default(),
810 cm: cm.clone(),
811 comments: None,
812 wr: Box::new(JsWriter::new(cm.clone(), "\n", &mut buf, None)),
813 };
814 emitter
815 .emit_program(program)
816 .map_err(|e| Error::new(Status::GenericFailure, format!("{:?}", e)))?;
817 Ok(String::from_utf8_lossy(&buf).to_string())
818}
819
820fn handle_macro_diagnostics(diags: &[Diagnostic], file: &str) -> Result<()> {
821 for diag in diags {
822 if matches!(diag.level, DiagnosticLevel::Error) {
823 let loc = diag
824 .span
825 .map(|s| format!("{}:{}-{}", file, s.start, s.end))
826 .unwrap_or_else(|| file.to_string());
827 return Err(Error::new(
828 Status::GenericFailure,
829 format!("Macro error at {}: {}", loc, diag.message),
830 ));
831 }
832 }
833 Ok(())
834}
835
836#[napi(object)]
841pub struct MacroManifestEntry {
842 pub name: String,
843 pub kind: String,
844 pub description: String,
845 pub package: String,
846}
847#[napi(object)]
848pub struct DecoratorManifestEntry {
849 pub module: String,
850 pub export: String,
851 pub kind: String,
852 pub docs: String,
853}
854#[napi(object)]
855pub struct MacroManifest {
856 pub version: u32,
857 pub macros: Vec<MacroManifestEntry>,
858 pub decorators: Vec<DecoratorManifestEntry>,
859}
860
861#[napi(js_name = "__macroforgeGetManifest")]
862pub fn get_macro_manifest() -> MacroManifest {
863 let manifest = derived::get_manifest();
864 MacroManifest {
865 version: manifest.version,
866 macros: manifest
867 .macros
868 .into_iter()
869 .map(|m| MacroManifestEntry {
870 name: m.name.to_string(),
871 kind: format!("{:?}", m.kind).to_lowercase(),
872 description: m.description.to_string(),
873 package: m.package.to_string(),
874 })
875 .collect(),
876 decorators: manifest
877 .decorators
878 .into_iter()
879 .map(|d| DecoratorManifestEntry {
880 module: d.module.to_string(),
881 export: d.export.to_string(),
882 kind: format!("{:?}", d.kind).to_lowercase(),
883 docs: d.docs.to_string(),
884 })
885 .collect(),
886 }
887}
888
889#[napi(js_name = "__macroforgeIsMacroPackage")]
890pub fn is_macro_package() -> bool {
891 !derived::macro_names().is_empty()
892}
893#[napi(js_name = "__macroforgeGetMacroNames")]
894pub fn get_macro_names() -> Vec<String> {
895 derived::macro_names()
896 .into_iter()
897 .map(|s| s.to_string())
898 .collect()
899}
900#[napi(js_name = "__macroforgeDebugGetModules")]
901pub fn debug_get_modules() -> Vec<String> {
902 crate::host::derived::modules()
903 .into_iter()
904 .map(|s| s.to_string())
905 .collect()
906}
907
908#[napi(js_name = "__macroforgeDebugLookup")]
909pub fn debug_lookup(module: String, name: String) -> String {
910 match MacroExpander::new() {
911 Ok(host) => match host.dispatcher.registry().lookup(&module, &name) {
912 Ok(_) => format!("Found: ({}, {})", module, name),
913 Err(_) => format!("Not found: ({}, {})", module, name),
914 },
915 Err(e) => format!("Host init failed: {}", e),
916 }
917}
918
919#[napi(js_name = "__macroforgeDebugDescriptors")]
920pub fn debug_descriptors() -> Vec<String> {
921 inventory::iter::<crate::host::derived::DerivedMacroRegistration>()
922 .map(|entry| {
923 format!(
924 "name={}, module={}, package={}",
925 entry.descriptor.name, entry.descriptor.module, entry.descriptor.package
926 )
927 })
928 .collect()
929}