use napi::bindgen_prelude::*;
use napi_derive::napi;
#[cfg(feature = "swc")]
use swc_core::common::{GLOBALS, Globals};
use crate::api_types::{ExpandOptions, ExpandResult, JsDiagnostic, ProcessFileOptions};
use crate::expand_core::expand_inner;
use crate::position_mapper::{NativeMapper, NativePositionMapper};
#[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,
}
pub(crate) 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,
declarative_registry_json: o.declarative_registry_json,
build_mode: o.build_mode,
})
}
#[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 work = move || {
std::panic::catch_unwind(std::panic::AssertUnwindSafe(|| {
expand_inner(&code, &filepath_for_thread, opts_clone)
}))
};
#[cfg(feature = "swc")]
{
let globals = Globals::default();
GLOBALS.set(&globals, work)
}
#[cfg(all(not(feature = "swc"), feature = "oxc"))]
{
work()
}
})
.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?)".to_string(),
)
})?
.map_err(|_| {
Error::new(
Status::GenericFailure,
"Macro expansion panicked inside worker".to_string(),
)
})?
.map_err(|e| {
Error::new(
Status::GenericFailure,
format!("Macro expansion failed: {}", e),
)
})?;
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()
}
}