use anyhow::{Result, anyhow};
#[cfg(feature = "swc")]
use swc_core::{
common::{FileName, SourceMap, errors::Handler, sync::Lrc},
ecma::{
ast::{EsVersion, Program},
codegen::{Emitter, text_writer::JsWriter},
parser::{Parser, StringInput, Syntax, TsSyntax, lexer::Lexer},
},
};
#[cfg(feature = "oxc")]
use oxc::allocator::Allocator;
#[cfg(feature = "oxc")]
use oxc::codegen::Codegen as OxcCodegen;
#[cfg(feature = "oxc")]
use oxc::parser::Parser as OxcParser;
#[cfg(feature = "oxc")]
use oxc::span::SourceType;
use std::collections::hash_map::DefaultHasher;
use std::hash::{Hash, Hasher};
use std::sync::LazyLock;
use crate::api_types::{
ExpandOptions, ExpandResult, GeneratedRegionResult, MacroDiagnostic, MappingSegmentResult,
SourceMappingResult, TransformResult,
};
use crate::host::CONFIG_CACHE;
use crate::host::MacroExpander;
#[cfg(all(not(feature = "swc"), feature = "oxc"))]
use crate::host::expand::LoweredItems;
use crate::ts_syn::abi::ir::type_registry::TypeRegistry;
#[cfg(feature = "swc")]
use crate::ts_syn::{Diagnostic, DiagnosticLevel};
#[cfg(not(target_arch = "wasm32"))]
static DISCOVERED_CONFIG: LazyLock<
std::sync::Mutex<Option<(crate::host::config::MacroConfig, std::path::PathBuf)>>,
> = LazyLock::new(|| std::sync::Mutex::new(None));
fn create_expander() -> Result<MacroExpander> {
#[cfg(not(target_arch = "wasm32"))]
{
use crate::host::MacroConfig;
let mut guard = DISCOVERED_CONFIG
.lock()
.map_err(|e| anyhow!("Lock poisoned: {e}"))?;
let (config, root) = if let Some((c, r)) = guard.as_ref() {
(c.clone(), r.clone())
} else {
let discovered = MacroConfig::find_with_root()
.map_err(|e| anyhow!("Config discovery failed: {e}"))?
.unwrap_or_else(|| {
(
MacroConfig::default(),
std::env::current_dir().unwrap_or_else(|_| std::path::PathBuf::from(".")),
)
});
*guard = Some(discovered.clone());
discovered
};
MacroExpander::with_config(config, root)
.map_err(|err| anyhow!("Failed to initialize macro host: {err:?}"))
}
#[cfg(target_arch = "wasm32")]
{
MacroExpander::new().map_err(|err| anyhow!("Failed to initialize macro host: {err:?}"))
}
}
pub(crate) static REGISTRY_CACHE: LazyLock<dashmap::DashMap<u64, TypeRegistry>> =
LazyLock::new(dashmap::DashMap::new);
pub(crate) 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,
}
}
pub(crate) trait CompilerBackend {
fn expand(
&self,
code: &str,
filepath: &str,
options: &Option<ExpandOptions>,
) -> Result<ExpandResult>;
fn transform(&self, code: &str, filepath: &str) -> Result<TransformResult>;
}
#[cfg(feature = "swc")]
pub(crate) struct SwcBackend;
#[cfg(feature = "swc")]
impl CompilerBackend for SwcBackend {
fn expand(
&self,
code: &str,
filepath: &str,
options: &Option<ExpandOptions>,
) -> Result<ExpandResult> {
let mut macro_host = create_expander()?;
apply_options(&mut macro_host, options);
let (program, _) = match parse_program(code, filepath) {
Ok(p) => p,
Err(e) => return Ok(make_syntax_error_result(code, &e.to_string())),
};
let expansion_result = macro_host.expand(code, &program, filepath);
crate::host::import_registry::clear_registry();
let expansion =
expansion_result.map_err(|err| anyhow!("Macro expansion failed: {err:?}"))?;
Ok(finalize_expansion(expansion))
}
fn transform(&self, code: &str, filepath: &str) -> Result<TransformResult> {
let macro_host = create_expander()?;
let (program, cm) = parse_program(code, filepath)?;
let expansion = macro_host
.expand(code, &program, filepath)
.map_err(|err| anyhow!("Expansion failed: {err:?}"))?;
handle_macro_diagnostics(&expansion.diagnostics, filepath).map_err(|e| anyhow!(e))?;
let generated = if expansion.changed {
expansion.code
} else {
emit_program(&program, &cm)?
};
Ok(TransformResult {
code: generated,
map: None,
types: expansion.type_output,
metadata: serialize_metadata(&expansion.classes),
})
}
}
#[cfg(feature = "oxc")]
pub(crate) struct OxcBackend;
#[cfg(all(not(feature = "swc"), feature = "oxc"))]
fn run_buildtime_prepass_oxc(
code: &str,
filepath: &str,
source_type: oxc::span::SourceType,
) -> Result<(
Option<String>,
Vec<std::path::PathBuf>,
Vec<crate::ts_syn::Diagnostic>,
)> {
if !code.contains("@buildtime") {
return Ok((None, Vec::new(), Vec::new()));
}
let Some(sandbox) = crate::host::buildtime::default_backend() else {
return Ok((None, Vec::new(), Vec::new()));
};
let allocator = Allocator::default();
let ret = OxcParser::new(&allocator, code, source_type).parse();
if !ret.errors.is_empty() {
return Err(anyhow!(
"Oxc parse errors (buildtime pre-pass): {:?}",
ret.errors
));
}
let origin_path = std::path::PathBuf::from(filepath);
let mut options = crate::host::buildtime::SandboxOptions::new(origin_path.clone());
options.capabilities = crate::host::buildtime::CapabilitySet {
fs_read: vec![crate::host::buildtime::PathPattern::new("**").expect("valid glob")],
fs_write: vec![],
env_allow: vec![],
network: false,
};
let out = crate::host::buildtime::run_prepass(
&ret.program,
code,
&origin_path,
sandbox.as_ref(),
&options,
);
Ok((out.rewritten, out.dependencies, out.diagnostics))
}
#[cfg(feature = "oxc")]
impl CompilerBackend for OxcBackend {
fn expand(
&self,
code: &str,
filepath: &str,
options: &Option<ExpandOptions>,
) -> Result<ExpandResult> {
let source_type = SourceType::ts().with_jsx(filepath.ends_with(".tsx"));
#[cfg(not(feature = "swc"))]
let (buildtime_rewritten, buildtime_deps, buildtime_diagnostics) =
run_buildtime_prepass_oxc(code, filepath, source_type)?;
#[cfg(not(feature = "swc"))]
let code_after_buildtime: &str = buildtime_rewritten.as_deref().unwrap_or(code);
#[cfg(not(feature = "swc"))]
let (rewritten, decl_diagnostics) = {
use std::path::PathBuf;
let code = code_after_buildtime;
let allocator = Allocator::default();
let ret = OxcParser::new(&allocator, code, source_type).parse();
if !ret.errors.is_empty() {
return Err(anyhow!("Oxc parse errors: {:?}", ret.errors));
}
let discovered = crate::host::declarative::discover(&ret.program, code)
.map_err(|e| anyhow!("Declarative macro error: {}", e))?;
let project_registry = options
.as_ref()
.and_then(|o| o.declarative_registry_json.as_ref())
.and_then(|json| {
crate::host::declarative::ProjectDeclarativeRegistry::from_json(json).ok()
});
let file_path = PathBuf::from(filepath);
let resolved_imports = if let Some(ref pr) = project_registry {
crate::host::declarative::resolve_cross_file_imports(code, &file_path, pr)
} else {
crate::host::declarative::ResolvedImports::default()
};
let has_dollar_calls = code.contains('$') && {
let bytes = code.as_bytes();
bytes
.windows(2)
.any(|w| w[0] == b'$' && w[1].is_ascii_alphabetic())
};
if discovered.is_empty() && resolved_imports.imported.is_empty() && !has_dollar_calls {
(None::<String>, resolved_imports.diagnostics)
} else {
let mut registry = crate::host::declarative::DeclarativeMacroRegistry::new();
for dm in &discovered {
registry
.register(dm.def.clone())
.map_err(|e| anyhow!("Declarative macro error: {}", e))?;
}
for imported in &resolved_imports.imported {
registry.register(imported.def.clone()).map_err(|e| {
anyhow!(
"Declarative macro error (imported from {}): {}",
imported.source_file.display(),
e
)
})?;
}
let build_mode = crate::host::declarative::BuildMode::from_option(
options.as_ref().and_then(|o| o.build_mode.as_deref()),
);
let type_registry = options
.as_ref()
.and_then(|o| o.type_registry_json.as_ref())
.and_then(|json| get_or_parse_registry(json));
let early_expander = create_expander()?;
let macro_import_sources =
crate::ts_syn::import_registry::collect_macro_import_comments_pub(code);
let proc_fallback = crate::host::declarative::ProcMacroFallback {
dispatcher: &early_expander.dispatcher,
import_sources: ¯o_import_sources,
external_loader: early_expander.external_loader_ref(),
};
let rewrite_out = crate::host::declarative::rewrite(
&ret.program,
code,
®istry,
&discovered,
build_mode,
type_registry.as_ref(),
Some(proc_fallback),
);
let mut diagnostics = resolved_imports.diagnostics;
diagnostics.extend(rewrite_out.diagnostics);
let source = if rewrite_out.patches.is_empty() {
None
} else {
let applicator = crate::host::patch_applicator::PatchApplicator::new(
code,
rewrite_out.patches,
);
Some(
applicator
.apply()
.map_err(|e| anyhow!("Patch apply failed: {}", e))?,
)
};
(source, diagnostics)
}
};
#[cfg(not(feature = "swc"))]
let code_owned = rewritten;
#[cfg(not(feature = "swc"))]
let code: &str = code_owned.as_deref().unwrap_or(code_after_buildtime);
#[cfg(not(feature = "swc"))]
let changed_by_decl = code_owned.is_some();
let allocator = Allocator::default();
let ret = OxcParser::new(&allocator, code, source_type).parse();
if !ret.errors.is_empty() {
#[cfg(not(feature = "swc"))]
let ctx = if changed_by_decl {
"Oxc parse errors after declarative macro expansion"
} else {
"Oxc parse errors"
};
#[cfg(feature = "swc")]
let ctx = "Oxc parse errors";
return Err(anyhow!("{}: {:?}", ctx, ret.errors));
}
#[cfg(feature = "swc")]
{
SwcBackend.expand(code, filepath, options)
}
#[cfg(not(feature = "swc"))]
{
let mut macro_host = create_expander()?;
apply_options(&mut macro_host, options);
let classes = crate::ts_syn::lower_classes_oxc(&ret.program, code, None)?;
let interfaces = crate::ts_syn::lower_interfaces_oxc(&ret.program, code, None)?;
let enums = crate::ts_syn::lower_enums_oxc(&ret.program, code, None)?;
let type_aliases = crate::ts_syn::lower_type_aliases_oxc(&ret.program, code, None)?;
let imports = crate::ts_syn::ImportRegistry::from_oxc_program(&ret.program, code);
let functions = crate::ts_syn::lower_functions_oxc(&ret.program, code, None)?;
let items = LoweredItems {
classes,
interfaces,
enums,
type_aliases,
functions,
imports,
};
if items.is_empty() {
let changed_by_buildtime = buildtime_rewritten.is_some();
let has_buildtime_work = changed_by_buildtime || !buildtime_diagnostics.is_empty();
if !changed_by_decl && decl_diagnostics.is_empty() && !has_buildtime_work {
return Ok(ExpandResult::unchanged(code));
}
let mut diagnostics: Vec<MacroDiagnostic> = buildtime_diagnostics
.iter()
.map(|d| MacroDiagnostic {
level: format!("{:?}", d.level).to_lowercase(),
message: d.message.clone(),
start: d.span.map(|s| s.start),
end: d.span.map(|s| s.end),
})
.collect();
diagnostics.extend(decl_diagnostics.iter().map(|d| MacroDiagnostic {
level: format!("{:?}", d.level).to_lowercase(),
message: d.message.clone(),
start: d.span.map(|s| s.start),
end: d.span.map(|s| s.end),
}));
let mut result = ExpandResult {
code: code.to_string(),
types: None,
metadata: None,
diagnostics,
source_mapping: None,
buildtime_dependencies: buildtime_deps
.iter()
.map(|p| p.to_string_lossy().into_owned())
.collect(),
};
inject_log_comments(&mut result);
return Ok(result);
}
let items_clone = items.clone();
let (mut collector, mut diagnostics) =
macro_host.collect_macro_patches_oxc(items, filepath, code);
let expansion = macro_host
.apply_and_finalize_expansion(code, &mut collector, &mut diagnostics, items_clone)
.map_err(anyhow::Error::from)?;
let mut result_diagnostics: Vec<MacroDiagnostic> = buildtime_diagnostics
.iter()
.map(|d| MacroDiagnostic {
level: format!("{:?}", d.level).to_lowercase(),
message: d.message.clone(),
start: d.span.map(|s| s.start),
end: d.span.map(|s| s.end),
})
.collect();
result_diagnostics.extend(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),
}));
for d in &decl_diagnostics {
result_diagnostics.push(MacroDiagnostic {
level: format!("{:?}", d.level).to_lowercase(),
message: d.message.clone(),
start: d.span.map(|s| s.start),
end: d.span.map(|s| s.end),
});
}
let mut result = ExpandResult {
code: expansion.code,
types: expansion.type_output,
metadata: serialize_metadata(&expansion.classes),
diagnostics: result_diagnostics,
source_mapping: expansion.source_mapping.map(|mapping| SourceMappingResult {
segments: mapping
.segments
.into_iter()
.map(|segment| MappingSegmentResult {
original_start: segment.original_start,
original_end: segment.original_end,
expanded_start: segment.expanded_start,
expanded_end: segment.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(),
}),
buildtime_dependencies: buildtime_deps
.iter()
.map(|p| p.to_string_lossy().into_owned())
.collect(),
};
inject_log_comments(&mut result);
Ok(result)
}
}
fn transform(&self, code: &str, filepath: &str) -> Result<TransformResult> {
let allocator = Allocator::default();
let source_type = SourceType::ts().with_jsx(filepath.ends_with(".tsx"));
let ret = OxcParser::new(&allocator, code, source_type).parse();
if !ret.errors.is_empty() {
return Err(anyhow!("Oxc parse errors: {:?}", ret.errors));
}
let generated = OxcCodegen::new().build(&ret.program).code;
Ok(TransformResult {
code: generated,
map: None,
types: None,
metadata: None,
})
}
}
fn apply_options(macro_host: &mut MacroExpander, options: &Option<ExpandOptions>) {
if let Some(opts) = options {
if let Some(keep) = opts.keep_decorators {
macro_host.set_keep_decorators(keep);
}
if let Some(modules) = &opts.external_decorator_modules {
macro_host.set_external_decorator_modules(modules.clone());
}
if let Some(json) = &opts.type_registry_json {
let registry = get_or_parse_registry(json);
if let Some(ref reg) = registry {
eprintln!(
"[macroforge:expand] Found type registry with {} types",
reg.len()
);
} else {
eprintln!("[macroforge:expand] Failed to parse type registry JSON");
}
macro_host.set_type_registry(registry);
}
if let Some(path) = opts.config_path.as_ref() {
if let Some(config) = CONFIG_CACHE.get(path) {
eprintln!("[macroforge:expand] Applying config from cache: {}", 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();
});
} else {
eprintln!(
"[macroforge:expand] Config path provided but NOT FOUND in cache: {}",
path
);
}
}
}
}
#[cfg(feature = "swc")]
fn make_syntax_error_result(code: &str, error_msg: &str) -> ExpandResult {
crate::host::import_registry::clear_registry();
crate::host::import_registry::clear_foreign_types();
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,
buildtime_dependencies: vec![],
}
}
fn serialize_metadata(classes: &Vec<crate::ts_syn::abi::ir::ClassIR>) -> Option<String> {
if classes.is_empty() {
None
} else {
serde_json::to_string(classes).ok()
}
}
#[cfg(feature = "swc")]
fn finalize_expansion(expansion: crate::host::expand::MacroExpansion) -> ExpandResult {
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(")
&& let Some(insert_at) = types.rfind('}')
{
let types: &mut String = types;
types.insert_str(insert_at, " toJSON(): Record<string, unknown>;\n");
}
let mut result = ExpandResult {
code: expansion.code,
types: types_output,
metadata: serialize_metadata(&expansion.classes),
diagnostics,
source_mapping,
buildtime_dependencies: vec![],
};
inject_log_comments(&mut result);
result
}
pub(crate) fn get_backend() -> Box<dyn CompilerBackend> {
#[cfg(feature = "oxc")]
{
Box::new(OxcBackend)
}
#[cfg(all(not(feature = "oxc"), feature = "swc"))]
{
Box::new(SwcBackend)
}
}
pub(crate) fn expand_inner(
code: &str,
filepath: &str,
options: Option<ExpandOptions>,
) -> Result<ExpandResult> {
if !has_macro_annotations(code) {
return Ok(ExpandResult::unchanged(code));
}
get_backend().expand(code, filepath, &options)
}
pub(crate) fn transform_inner(code: &str, filepath: &str) -> Result<TransformResult> {
get_backend().transform(code, filepath)
}
#[derive(PartialEq, Eq, PartialOrd, Ord, Clone, Copy)]
enum LogLevel {
Off = 0,
Error = 1,
Warn = 2,
Info = 3,
Debug = 4,
Trace = 5,
}
fn parse_log_level() -> LogLevel {
match std::env::var("MF_LOG").ok().as_deref() {
Some("error") => LogLevel::Error,
Some("warn") => LogLevel::Warn,
Some("info") => LogLevel::Info,
Some("debug") => LogLevel::Debug,
Some("trace") => LogLevel::Trace,
Some("1" | "true") => LogLevel::Info,
_ => LogLevel::Off,
}
}
fn inject_log_comments(result: &mut ExpandResult) {
let level = parse_log_level();
if level == LogLevel::Off {
return;
}
let trace_diags: Vec<_> = result
.diagnostics
.iter()
.filter(|d| {
d.message.starts_with("[trace]") && level >= LogLevel::Trace
|| d.level == "error" && level >= LogLevel::Error
|| d.level == "warning" && level >= LogLevel::Warn
|| d.level == "info" && level >= LogLevel::Info
})
.collect();
if trace_diags.is_empty() {
return;
}
let mut positioned: Vec<(u32, &str)> = Vec::new();
let mut top_lines: Vec<String> = Vec::new();
for d in &trace_diags {
if let Some(start) = d.start {
positioned.push((start, &d.message));
} else {
top_lines.push(format!("// {}", d.message));
}
}
let mut header = String::new();
if !top_lines.is_empty() {
header.push_str("/*\n * MF_LOG output\n");
for line in &top_lines {
header.push_str(" * ");
header.push_str(line.trim_start_matches("// "));
header.push('\n');
}
header.push_str(" */\n");
}
let mut code = result.code.clone();
positioned.sort_by(|a, b| b.0.cmp(&a.0));
for (offset, msg) in &positioned {
let offset = *offset as usize;
if offset <= code.len() {
let line_start = code[..offset].rfind('\n').map(|i| i + 1).unwrap_or(0);
let indent = &code[line_start..offset]
.chars()
.take_while(|c| c.is_whitespace())
.collect::<String>();
let comment = format!("{}// {}\n", indent, msg);
code.insert_str(line_start, &comment);
}
}
if !header.is_empty() {
code.insert_str(0, &header);
}
result.code = code;
}
pub(crate) fn has_macro_annotations(source: &str) -> bool {
if source.contains("@buildtime") {
return true;
}
if source.contains("macroforge/rules") {
return true;
}
if source.contains("import macro") {
return true;
}
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
}
#[cfg(feature = "swc")]
pub(crate) 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,
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(anyhow!(msg))
}
}
}
#[cfg(feature = "swc")]
pub(crate) 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| anyhow!("{:?}", e))?;
Ok(String::from_utf8_lossy(&buf).to_string())
}
#[cfg(feature = "swc")]
pub(crate) fn handle_macro_diagnostics(diags: &[Diagnostic], file: &str) -> Result<(), String> {
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(format!("Macro error at {}: {}", loc, diag.message));
}
}
Ok(())
}