use napi::bindgen_prelude::*;
use napi_derive::napi;
use swc_core::{
common::{FileName, GLOBALS, Globals, SourceMap, errors::Handler, sync::Lrc},
ecma::{
ast::{EsVersion, Program},
codegen::{Emitter, text_writer::JsWriter},
parser::{Parser, StringInput, Syntax, TsSyntax, lexer::Lexer},
},
};
extern crate self as macroforge_ts;
pub extern crate inventory;
pub extern crate macroforge_ts_macros;
pub extern crate macroforge_ts_quote;
pub extern crate macroforge_ts_syn;
pub extern crate napi;
pub extern crate napi_derive;
pub extern crate serde_json;
pub mod debug;
pub use macroforge_ts_syn as ts_syn;
pub mod macros {
pub use macroforge_ts_macros::ts_macro_derive;
pub use macroforge_ts_quote::{ts_quote, ts_template};
}
#[macro_export]
macro_rules! ident {
($name:expr) => {
$crate::swc_core::ecma::ast::Ident::new_no_ctxt(
AsRef::<str>::as_ref(&$name).into(),
$crate::swc_core::common::DUMMY_SP,
)
};
($fmt:expr, $($args:expr),+ $(,)?) => {
$crate::swc_core::ecma::ast::Ident::new_no_ctxt(
format!($fmt, $($args),+).into(),
$crate::swc_core::common::DUMMY_SP,
)
};
}
#[macro_export]
macro_rules! private_ident {
($name:expr) => {{
let mark = $crate::swc_core::common::Mark::fresh($crate::swc_core::common::Mark::root());
$crate::swc_core::ecma::ast::Ident::new(
$name.into(),
$crate::swc_core::common::DUMMY_SP,
$crate::swc_core::common::SyntaxContext::empty().apply_mark(mark),
)
}};
}
pub use macroforge_ts_syn::swc_common;
pub use macroforge_ts_syn::swc_core;
pub use macroforge_ts_syn::swc_ecma_ast;
pub mod host;
#[cfg(feature = "build")]
pub mod build;
pub use ts_syn::abi;
use host::CONFIG_CACHE;
use host::derived;
use ts_syn::{Diagnostic, DiagnosticLevel};
pub mod builtin;
#[cfg(test)]
mod test;
use crate::host::MacroExpander;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::sync::LazyLock;
use crate::ts_syn::abi::ir::type_registry::TypeRegistry;
static REGISTRY_CACHE: LazyLock<dashmap::DashMap<u64, TypeRegistry>> =
LazyLock::new(dashmap::DashMap::new);
fn get_or_parse_registry(json: &str) -> Option<TypeRegistry> {
let mut hasher = DefaultHasher::new();
json.hash(&mut hasher);
let key = hasher.finish();
if let Some(entry) = REGISTRY_CACHE.get(&key) {
return Some(entry.clone());
}
match serde_json::from_str::<TypeRegistry>(json) {
Ok(registry) => {
REGISTRY_CACHE.insert(key, registry.clone());
Some(registry)
}
Err(_) => None,
}
}
#[napi(object)]
#[derive(Clone)]
pub struct TransformResult {
pub code: String,
pub map: Option<String>,
pub types: Option<String>,
pub metadata: Option<String>,
}
#[napi(object)]
#[derive(Clone)]
pub struct MacroDiagnostic {
pub level: String,
pub message: String,
pub start: Option<u32>,
pub end: Option<u32>,
}
#[napi(object)]
#[derive(Clone)]
pub struct MappingSegmentResult {
pub original_start: u32,
pub original_end: u32,
pub expanded_start: u32,
pub expanded_end: u32,
}
#[napi(object)]
#[derive(Clone)]
pub struct GeneratedRegionResult {
pub start: u32,
pub end: u32,
pub source_macro: String,
}
#[napi(object)]
#[derive(Clone)]
pub struct SourceMappingResult {
pub segments: Vec<MappingSegmentResult>,
pub generated_regions: Vec<GeneratedRegionResult>,
}
#[napi(object)]
#[derive(Clone)]
pub struct ExpandResult {
pub code: String,
pub types: Option<String>,
pub metadata: Option<String>,
pub diagnostics: Vec<MacroDiagnostic>,
pub source_mapping: Option<SourceMappingResult>,
}
impl ExpandResult {
pub fn unchanged(code: &str) -> Self {
Self {
code: code.to_string(),
types: None,
metadata: None,
diagnostics: vec![],
source_mapping: None,
}
}
}
#[napi(object)]
#[derive(Clone)]
pub struct ImportSourceResult {
pub local: String,
pub module: String,
}
#[napi(object)]
#[derive(Clone)]
pub struct SyntaxCheckResult {
pub ok: bool,
pub error: Option<String>,
}
#[napi(object)]
#[derive(Clone)]
pub struct SpanResult {
pub start: u32,
pub length: u32,
}
#[napi(object)]
#[derive(Clone)]
pub struct JsDiagnostic {
pub start: Option<u32>,
pub length: Option<u32>,
pub message: Option<String>,
pub code: Option<u32>,
pub category: Option<String>,
}
#[napi(js_name = "PositionMapper")]
pub struct NativePositionMapper {
segments: Vec<MappingSegmentResult>,
generated_regions: Vec<GeneratedRegionResult>,
}
#[napi(js_name = "NativeMapper")]
pub struct NativeMapper {
inner: NativePositionMapper,
}
#[napi]
impl NativePositionMapper {
#[napi(constructor)]
pub fn new(mapping: SourceMappingResult) -> Self {
Self {
segments: mapping.segments,
generated_regions: mapping.generated_regions,
}
}
#[napi(js_name = "isEmpty")]
pub fn is_empty(&self) -> bool {
self.segments.is_empty() && self.generated_regions.is_empty()
}
#[napi]
pub fn original_to_expanded(&self, pos: u32) -> u32 {
let idx = self.segments.partition_point(|seg| seg.original_end <= pos);
if let Some(seg) = self.segments.get(idx) {
if pos >= seg.original_start && pos < seg.original_end {
let offset = pos - seg.original_start;
return seg.expanded_start + offset;
}
}
if let Some(last) = self.segments.last()
&& pos >= last.original_end
{
let delta = pos - last.original_end;
return last.expanded_end + delta;
}
pos
}
#[napi]
pub fn expanded_to_original(&self, pos: u32) -> Option<u32> {
if self.is_in_generated(pos) {
return None;
}
let idx = self.segments.partition_point(|seg| seg.expanded_end <= pos);
if let Some(seg) = self.segments.get(idx)
&& pos >= seg.expanded_start
&& pos < seg.expanded_end
{
let offset = pos - seg.expanded_start;
return Some(seg.original_start + offset);
}
if let Some(last) = self.segments.last()
&& pos >= last.expanded_end
{
let delta = pos - last.expanded_end;
return Some(last.original_end + delta);
}
None
}
#[napi]
pub fn generated_by(&self, pos: u32) -> Option<String> {
self.generated_regions
.iter()
.find(|r| pos >= r.start && pos < r.end)
.map(|r| r.source_macro.clone())
}
#[napi]
pub fn map_span_to_original(&self, start: u32, length: u32) -> Option<SpanResult> {
let end = start.saturating_add(length);
let original_start = self.expanded_to_original(start)?;
let original_end = self.expanded_to_original(end)?;
Some(SpanResult {
start: original_start,
length: original_end.saturating_sub(original_start),
})
}
#[napi]
pub fn map_span_to_expanded(&self, start: u32, length: u32) -> SpanResult {
let end = start.saturating_add(length);
let expanded_start = self.original_to_expanded(start);
let expanded_end = self.original_to_expanded(end);
SpanResult {
start: expanded_start,
length: expanded_end.saturating_sub(expanded_start),
}
}
#[napi]
pub fn is_in_generated(&self, pos: u32) -> bool {
self.generated_regions
.iter()
.any(|r| pos >= r.start && pos < r.end)
}
}
#[napi]
impl NativeMapper {
#[napi(constructor)]
pub fn new(mapping: SourceMappingResult) -> Self {
Self {
inner: NativePositionMapper::new(mapping),
}
}
#[napi(js_name = "isEmpty")]
pub fn is_empty(&self) -> bool {
self.inner.is_empty()
}
#[napi]
pub fn original_to_expanded(&self, pos: u32) -> u32 {
self.inner.original_to_expanded(pos)
}
#[napi]
pub fn expanded_to_original(&self, pos: u32) -> Option<u32> {
self.inner.expanded_to_original(pos)
}
#[napi]
pub fn generated_by(&self, pos: u32) -> Option<String> {
self.inner.generated_by(pos)
}
#[napi]
pub fn map_span_to_original(&self, start: u32, length: u32) -> Option<SpanResult> {
self.inner.map_span_to_original(start, length)
}
#[napi]
pub fn map_span_to_expanded(&self, start: u32, length: u32) -> SpanResult {
self.inner.map_span_to_expanded(start, length)
}
#[napi]
pub fn is_in_generated(&self, pos: u32) -> bool {
self.inner.is_in_generated(pos)
}
}
#[napi]
pub fn check_syntax(code: String, filepath: String) -> SyntaxCheckResult {
match parse_program(&code, &filepath) {
Ok(_) => SyntaxCheckResult {
ok: true,
error: None,
},
Err(err) => SyntaxCheckResult {
ok: false,
error: Some(err.to_string()),
},
}
}
#[napi(object)]
pub struct ProcessFileOptions {
pub keep_decorators: Option<bool>,
pub version: Option<String>,
pub external_decorator_modules: Option<Vec<String>>,
pub config_path: Option<String>,
pub type_registry_json: Option<String>,
}
#[napi(object)]
pub struct ExpandOptions {
pub keep_decorators: Option<bool>,
pub external_decorator_modules: Option<Vec<String>>,
pub config_path: Option<String>,
pub type_registry_json: Option<String>,
}
#[napi]
pub struct NativePlugin {
cache: std::sync::Mutex<std::collections::HashMap<String, CachedResult>>,
log_file: std::sync::Mutex<Option<std::path::PathBuf>>,
}
impl Default for NativePlugin {
fn default() -> Self {
Self::new()
}
}
#[derive(Clone)]
struct CachedResult {
version: Option<String>,
result: ExpandResult,
}
fn option_expand_options(opts: Option<ProcessFileOptions>) -> Option<ExpandOptions> {
opts.map(|o| ExpandOptions {
keep_decorators: o.keep_decorators,
external_decorator_modules: o.external_decorator_modules,
config_path: o.config_path,
type_registry_json: o.type_registry_json,
})
}
#[napi]
impl NativePlugin {
#[napi(constructor)]
pub fn new() -> Self {
let plugin = Self {
cache: std::sync::Mutex::new(std::collections::HashMap::new()),
log_file: std::sync::Mutex::new(None),
};
if let Ok(mut log_guard) = plugin.log_file.lock() {
let log_path = std::path::PathBuf::from("/tmp/macroforge-plugin.log");
if let Err(e) = std::fs::write(&log_path, "=== macroforge plugin loaded ===\n") {
eprintln!("[macroforge] Failed to initialize log file: {}", e);
} else {
*log_guard = Some(log_path);
}
}
plugin
}
#[napi]
pub fn log(&self, message: String) {
if let Ok(log_guard) = self.log_file.lock()
&& let Some(log_path) = log_guard.as_ref()
{
use std::io::Write;
if let Ok(mut file) = std::fs::OpenOptions::new()
.append(true)
.create(true)
.open(log_path)
{
let _ = writeln!(file, "{}", message);
}
}
}
#[napi]
pub fn set_log_file(&self, path: String) {
if let Ok(mut log_guard) = self.log_file.lock() {
*log_guard = Some(std::path::PathBuf::from(path));
}
}
#[napi]
pub fn process_file(
&self,
_env: Env,
filepath: String,
code: String,
options: Option<ProcessFileOptions>,
) -> Result<ExpandResult> {
let version = options.as_ref().and_then(|o| o.version.clone());
if let (Some(ver), Ok(guard)) = (version.as_ref(), self.cache.lock())
&& let Some(cached) = guard.get(&filepath)
&& cached.version.as_ref() == Some(ver)
{
return Ok(cached.result.clone());
}
let opts_clone = option_expand_options(options);
let filepath_for_thread = filepath.clone();
let builder = std::thread::Builder::new().stack_size(32 * 1024 * 1024);
let handle = builder
.spawn(move || {
let globals = Globals::default();
GLOBALS.set(&globals, || {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
expand_inner(&code, &filepath_for_thread, opts_clone)
}))
})
})
.map_err(|e| {
Error::new(
Status::GenericFailure,
format!("Failed to spawn worker thread: {}", e),
)
})?;
let expand_result = handle
.join()
.map_err(|_| {
Error::new(
Status::GenericFailure,
"Macro expansion worker thread panicked (Stack Overflow?)",
)
})?
.map_err(|_| {
Error::new(
Status::GenericFailure,
"Macro expansion panicked inside worker",
)
})??;
if let Ok(mut guard) = self.cache.lock() {
guard.insert(
filepath.clone(),
CachedResult {
version,
result: expand_result.clone(),
},
);
}
Ok(expand_result)
}
#[napi]
pub fn get_mapper(&self, filepath: String) -> Option<NativeMapper> {
let mapping = match self.cache.lock() {
Ok(guard) => guard
.get(&filepath)
.cloned()
.and_then(|c| c.result.source_mapping),
Err(_) => None,
};
mapping.map(|m| NativeMapper {
inner: NativePositionMapper::new(m),
})
}
#[napi]
pub fn map_diagnostics(&self, filepath: String, diags: Vec<JsDiagnostic>) -> Vec<JsDiagnostic> {
let Some(mapper) = self.get_mapper(filepath) else {
return diags;
};
diags
.into_iter()
.map(|mut d| {
if let (Some(start), Some(length)) = (d.start, d.length)
&& let Some(mapped) = mapper.map_span_to_original(start, length)
{
d.start = Some(mapped.start);
d.length = Some(mapped.length);
}
d
})
.collect()
}
}
#[napi]
pub fn parse_import_sources(code: String, filepath: String) -> Result<Vec<ImportSourceResult>> {
let (program, _cm) = parse_program(&code, &filepath)?;
let module = match program {
Program::Module(module) => module,
Program::Script(_) => return Ok(vec![]),
};
let import_result = crate::host::collect_import_sources(&module, &code);
let mut imports = Vec::with_capacity(import_result.sources.len());
for (local, module) in import_result.sources {
imports.push(ImportSourceResult { local, module });
}
Ok(imports)
}
#[napi(
js_name = "Derive",
ts_return_type = "ClassDecorator",
ts_args_type = "...features: any[]"
)]
pub fn derive_decorator() {}
#[napi(object)]
pub struct LoadConfigResult {
pub keep_decorators: bool,
pub generate_convenience_const: bool,
pub has_foreign_types: bool,
pub foreign_type_count: u32,
}
#[napi]
pub fn load_config(content: String, filepath: String) -> Result<LoadConfigResult> {
use crate::host::MacroforgeConfigLoader;
let config = MacroforgeConfigLoader::load_and_cache(&content, &filepath).map_err(|e| {
Error::new(
Status::GenericFailure,
format!("Failed to parse config: {}", e),
)
})?;
Ok(LoadConfigResult {
keep_decorators: config.keep_decorators,
generate_convenience_const: config.generate_convenience_const,
has_foreign_types: !config.foreign_types.is_empty(),
foreign_type_count: config.foreign_types.len() as u32,
})
}
#[napi]
pub fn clear_config_cache() {
crate::host::clear_config_cache();
}
#[napi(object)]
pub struct ScanOptions {
pub extensions: Option<Vec<String>>,
pub exported_only: Option<bool>,
}
#[napi(object)]
#[derive(Clone)]
pub struct ScanResult {
pub registry_json: String,
pub files_scanned: u32,
pub types_found: u32,
pub diagnostics: Vec<MacroDiagnostic>,
}
#[napi]
pub fn scan_project_sync(root_dir: String, options: Option<ScanOptions>) -> Result<ScanResult> {
let builder = std::thread::Builder::new().stack_size(32 * 1024 * 1024);
let handle = builder
.spawn(move || scan_project_inner(&root_dir, options))
.map_err(|e| {
Error::new(
Status::GenericFailure,
format!("Failed to spawn scan thread: {}", e),
)
})?;
handle
.join()
.map_err(|_| Error::new(Status::GenericFailure, "Scan worker panicked"))?
}
fn scan_project_inner(root_dir: &str, options: Option<ScanOptions>) -> Result<ScanResult> {
use crate::host::scanner::{ProjectScanner, ScanConfig};
let mut config = ScanConfig {
root_dir: std::path::PathBuf::from(root_dir),
..ScanConfig::default()
};
if let Some(opts) = options {
if let Some(exts) = opts.extensions {
config.extensions = exts;
}
if let Some(exported) = opts.exported_only {
config.exported_only = exported;
}
}
let scanner = ProjectScanner::new(config);
let output = scanner.scan().map_err(|e| {
Error::new(
Status::GenericFailure,
format!("Project scan failed: {}", e),
)
})?;
let types_found = output.registry.len() as u32;
let registry_json = serde_json::to_string(&output.registry).map_err(|e| {
Error::new(
Status::GenericFailure,
format!("Failed to serialize registry: {}", e),
)
})?;
let diagnostics = output
.warnings
.into_iter()
.map(|msg| MacroDiagnostic {
level: "warning".to_string(),
message: msg,
start: None,
end: None,
})
.collect();
Ok(ScanResult {
registry_json,
files_scanned: output.files_scanned,
types_found,
diagnostics,
})
}
#[napi]
pub fn transform_sync(_env: Env, code: String, filepath: String) -> Result<TransformResult> {
let builder = std::thread::Builder::new().stack_size(32 * 1024 * 1024);
let handle = builder
.spawn(move || {
let globals = Globals::default();
GLOBALS.set(&globals, || {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
transform_inner(&code, &filepath)
}))
})
})
.map_err(|e| {
Error::new(
Status::GenericFailure,
format!("Failed to spawn transform thread: {}", e),
)
})?;
handle
.join()
.map_err(|_| Error::new(Status::GenericFailure, "Transform worker crashed"))?
.map_err(|_| Error::new(Status::GenericFailure, "Transform panicked"))?
}
#[napi]
pub fn expand_sync(
_env: Env,
code: String,
filepath: String,
options: Option<ExpandOptions>,
) -> Result<ExpandResult> {
let builder = std::thread::Builder::new().stack_size(32 * 1024 * 1024);
let handle = builder
.spawn(move || {
let globals = Globals::default();
GLOBALS.set(&globals, || {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
expand_inner(&code, &filepath, options)
}))
})
})
.map_err(|e| {
Error::new(
Status::GenericFailure,
format!("Failed to spawn expand thread: {}", e),
)
})?;
handle
.join()
.map_err(|_| Error::new(Status::GenericFailure, "Expand worker crashed"))?
.map_err(|_| Error::new(Status::GenericFailure, "Expand panicked"))?
}
fn has_macro_annotations(source: &str) -> bool {
if !source.contains("@derive") {
return false;
}
let mut in_code_block = false;
for line in source.lines() {
let trimmed = line
.trim()
.trim_start_matches('/')
.trim_start_matches('*')
.trim_end_matches('/')
.trim_end_matches('*')
.trim();
if trimmed.starts_with("```") {
in_code_block = !in_code_block;
continue;
}
if in_code_block {
continue;
}
if trimmed.starts_with("@derive(") {
return true;
}
}
false
}
fn expand_inner(
code: &str,
filepath: &str,
options: Option<ExpandOptions>,
) -> Result<ExpandResult> {
if !has_macro_annotations(code) {
return Ok(ExpandResult::unchanged(code));
}
let mut macro_host = MacroExpander::new().map_err(|err| {
Error::new(
Status::GenericFailure,
format!("Failed to initialize macro host: {err:?}"),
)
})?;
if let Some(ref opts) = options {
if let Some(keep) = opts.keep_decorators {
macro_host.set_keep_decorators(keep);
}
if let Some(ref modules) = opts.external_decorator_modules {
macro_host.set_external_decorator_modules(modules.clone());
}
if let Some(ref json) = opts.type_registry_json {
let registry = get_or_parse_registry(json);
macro_host.set_type_registry(registry);
}
}
let config_path = options.as_ref().and_then(|o| o.config_path.as_ref());
if let Some(path) = config_path
&& let Some(config) = CONFIG_CACHE.get(path)
{
crate::host::import_registry::set_foreign_types(config.foreign_types.clone());
crate::host::import_registry::with_registry_mut(|r| {
r.config_imports = config
.config_imports
.iter()
.map(|(name, info)| (name.clone(), info.source.clone()))
.collect();
});
}
let (program, _) = match parse_program(code, filepath) {
Ok(p) => p,
Err(e) => {
let error_msg = e.to_string();
crate::host::import_registry::clear_registry();
crate::host::import_registry::clear_foreign_types();
return Ok(ExpandResult {
code: code.to_string(),
types: None,
metadata: None,
diagnostics: vec![MacroDiagnostic {
level: "info".to_string(),
message: format!("Macro expansion skipped due to syntax error: {}", error_msg),
start: None,
end: None,
}],
source_mapping: None,
});
}
};
let expansion_result = macro_host.expand(code, &program, filepath);
crate::host::import_registry::clear_registry();
let expansion = expansion_result.map_err(|err| {
Error::new(
Status::GenericFailure,
format!("Macro expansion failed: {err:?}"),
)
})?;
let diagnostics = expansion
.diagnostics
.into_iter()
.map(|d| MacroDiagnostic {
level: format!("{:?}", d.level).to_lowercase(),
message: d.message,
start: d.span.map(|s| s.start),
end: d.span.map(|s| s.end),
})
.collect();
let source_mapping = expansion.source_mapping.map(|mapping| SourceMappingResult {
segments: mapping
.segments
.into_iter()
.map(|seg| MappingSegmentResult {
original_start: seg.original_start,
original_end: seg.original_end,
expanded_start: seg.expanded_start,
expanded_end: seg.expanded_end,
})
.collect(),
generated_regions: mapping
.generated_regions
.into_iter()
.map(|region| GeneratedRegionResult {
start: region.start,
end: region.end,
source_macro: region.source_macro,
})
.collect(),
});
let mut types_output = expansion.type_output;
if let Some(types) = &mut types_output
&& expansion.code.contains("toJSON(")
&& !types.contains("toJSON(")
{
if let Some(insert_at) = types.rfind('}') {
types.insert_str(insert_at, " toJSON(): Record<string, unknown>;\n");
}
}
Ok(ExpandResult {
code: expansion.code,
types: types_output,
metadata: if expansion.classes.is_empty() {
None
} else {
serde_json::to_string(&expansion.classes).ok()
},
diagnostics,
source_mapping,
})
}
fn transform_inner(code: &str, filepath: &str) -> Result<TransformResult> {
let macro_host = MacroExpander::new().map_err(|err| {
Error::new(
Status::GenericFailure,
format!("Failed to init host: {err:?}"),
)
})?;
let (program, cm) = parse_program(code, filepath)?;
let expansion = macro_host
.expand(code, &program, filepath)
.map_err(|err| Error::new(Status::GenericFailure, format!("Expansion failed: {err:?}")))?;
handle_macro_diagnostics(&expansion.diagnostics, filepath)?;
let generated = if expansion.changed {
expansion.code
} else {
emit_program(&program, &cm)?
};
let metadata = if expansion.classes.is_empty() {
None
} else {
serde_json::to_string(&expansion.classes).ok()
};
Ok(TransformResult {
code: generated,
map: None, types: expansion.type_output,
metadata,
})
}
fn parse_program(code: &str, filepath: &str) -> Result<(Program, Lrc<SourceMap>)> {
let cm: Lrc<SourceMap> = Lrc::new(SourceMap::default());
let fm = cm.new_source_file(
FileName::Custom(filepath.to_string()).into(),
code.to_string(),
);
let handler =
Handler::with_emitter_writer(Box::new(std::io::Cursor::new(Vec::new())), Some(cm.clone()));
let lexer = Lexer::new(
Syntax::Typescript(TsSyntax {
tsx: filepath.ends_with(".tsx"), decorators: true, dts: false, no_early_errors: true, ..Default::default()
}),
EsVersion::latest(),
StringInput::from(&*fm),
None, );
let mut parser = Parser::new_from(lexer);
match parser.parse_program() {
Ok(program) => Ok((program, cm)),
Err(error) => {
let msg = format!("Failed to parse TypeScript: {:?}", error);
error.into_diagnostic(&handler).emit();
Err(Error::new(Status::GenericFailure, msg))
}
}
}
fn emit_program(program: &Program, cm: &Lrc<SourceMap>) -> Result<String> {
let mut buf = vec![];
let mut emitter = Emitter {
cfg: swc_core::ecma::codegen::Config::default(),
cm: cm.clone(),
comments: None,
wr: Box::new(JsWriter::new(cm.clone(), "\n", &mut buf, None)),
};
emitter
.emit_program(program)
.map_err(|e| Error::new(Status::GenericFailure, format!("{:?}", e)))?;
Ok(String::from_utf8_lossy(&buf).to_string())
}
fn handle_macro_diagnostics(diags: &[Diagnostic], file: &str) -> Result<()> {
for diag in diags {
if matches!(diag.level, DiagnosticLevel::Error) {
let loc = diag
.span
.map(|s| format!("{}:{}-{}", file, s.start, s.end))
.unwrap_or_else(|| file.to_string());
return Err(Error::new(
Status::GenericFailure,
format!("Macro error at {}: {}", loc, diag.message),
));
}
}
Ok(())
}
#[napi(object)]
pub struct MacroManifestEntry {
pub name: String,
pub kind: String,
pub description: String,
pub package: String,
}
#[napi(object)]
pub struct DecoratorManifestEntry {
pub module: String,
pub export: String,
pub kind: String,
pub docs: String,
}
#[napi(object)]
pub struct MacroManifest {
pub version: u32,
pub macros: Vec<MacroManifestEntry>,
pub decorators: Vec<DecoratorManifestEntry>,
}
#[napi(js_name = "__macroforgeGetManifest")]
pub fn get_macro_manifest() -> MacroManifest {
let manifest = derived::get_manifest();
MacroManifest {
version: manifest.version,
macros: manifest
.macros
.into_iter()
.map(|m| MacroManifestEntry {
name: m.name.to_string(),
kind: format!("{:?}", m.kind).to_lowercase(),
description: m.description.to_string(),
package: m.package.to_string(),
})
.collect(),
decorators: manifest
.decorators
.into_iter()
.map(|d| DecoratorManifestEntry {
module: d.module.to_string(),
export: d.export.to_string(),
kind: format!("{:?}", d.kind).to_lowercase(),
docs: d.docs.to_string(),
})
.collect(),
}
}
#[napi(js_name = "__macroforgeIsMacroPackage")]
pub fn is_macro_package() -> bool {
!derived::macro_names().is_empty()
}
#[napi(js_name = "__macroforgeGetMacroNames")]
pub fn get_macro_names() -> Vec<String> {
derived::macro_names()
.into_iter()
.map(|s| s.to_string())
.collect()
}
#[napi(js_name = "__macroforgeDebugGetModules")]
pub fn debug_get_modules() -> Vec<String> {
crate::host::derived::modules()
.into_iter()
.map(|s| s.to_string())
.collect()
}
#[napi(js_name = "__macroforgeDebugLookup")]
pub fn debug_lookup(module: String, name: String) -> String {
match MacroExpander::new() {
Ok(host) => match host.dispatcher.registry().lookup(&module, &name) {
Ok(_) => format!("Found: ({}, {})", module, name),
Err(_) => format!("Not found: ({}, {})", module, name),
},
Err(e) => format!("Host init failed: {}", e),
}
}
#[napi(js_name = "__macroforgeDebugDescriptors")]
pub fn debug_descriptors() -> Vec<String> {
inventory::iter::<crate::host::derived::DerivedMacroRegistration>()
.map(|entry| {
format!(
"name={}, module={}, package={}",
entry.descriptor.name, entry.descriptor.module, entry.descriptor.package
)
})
.collect()
}