use oxc::allocator::Allocator;
use oxc::ast::ast::{
AwaitExpression, BindingPattern, Declaration, ExportDefaultDeclarationKind,
ImportDeclarationSpecifier, ModuleExportName, Statement,
};
use oxc::ast_visit::Visit;
use oxc::parser::Parser;
use oxc::semantic::SemanticBuilder;
use oxc::span::{GetSpan, SourceType};
use std::collections::HashMap;
use std::path::Path;
pub fn rewrite_esm_to_cjs(source: &str, path: &Path) -> Result<String, String> {
let source_owned: String;
let source: &str = if source.contains("import.meta") {
source_owned = rewrite_import_meta(source);
&source_owned
} else {
source
};
let allocator = Allocator::default();
let source_type = SourceType::from_path(path).unwrap_or(SourceType::mjs());
let parsed = Parser::new(&allocator, source, source_type).parse();
if !parsed.errors.is_empty() {
return Err(parsed
.errors
.iter()
.map(|e| format!("{e:?}"))
.collect::<Vec<_>>()
.join("\n"));
}
let semantic = SemanticBuilder::new().build(&parsed.program).semantic;
let scoping = semantic.scoping();
let nodes = semantic.nodes();
let mut edits: Vec<(usize, usize, String)> = Vec::new();
let mut has_named_or_default_export = false;
let mut named_import_rewrites: HashMap<oxc::syntax::symbol::SymbolId, String> = HashMap::new();
for stmt in &parsed.program.body {
match stmt {
Statement::ImportDeclaration(decl) => {
let src_name = decl.source.value.as_str();
let start = decl.span.start as usize;
let end = decl.span.end as usize;
let temp = next_temp();
let (replacement, named_locals) = rewrite_import(
src_name,
decl.specifiers.as_ref().map(|v| v.as_slice()),
&temp,
);
edits.push((start, end, replacement));
if let Some(specs) = decl.specifiers.as_ref() {
for spec in specs {
if let ImportDeclarationSpecifier::ImportSpecifier(s) = spec {
let imported_name = mod_export_name(&s.imported);
if let Some(symbol_id) = s.local.symbol_id.get() {
named_import_rewrites
.insert(symbol_id, format!("{temp}.{imported_name}"));
}
}
}
}
let _ = named_locals;
}
Statement::ExportDefaultDeclaration(decl) => {
has_named_or_default_export = true;
let start = decl.span.start as usize;
let end = decl.span.end as usize;
let replacement = rewrite_export_default(&decl.declaration, source);
edits.push((start, end, replacement));
}
Statement::ExportNamedDeclaration(decl) => {
has_named_or_default_export = true;
if let Some(inner) = decl.declaration.as_ref() {
let outer_start = decl.span.start as usize;
let inner_span = inner.span();
let inner_start = inner_span.start as usize;
let inner_end = inner_span.end as usize;
edits.push((outer_start, inner_start, String::new()));
let names = decl_names(inner);
let mut tail = String::new();
for name in names {
tail.push_str(&format!("\nexports.{name} = {name};"));
}
if !tail.is_empty() {
edits.push((inner_end, inner_end, tail));
}
} else {
let start = decl.span.start as usize;
let end = decl.span.end as usize;
let replacement = rewrite_export_named(
decl.declaration.as_ref(),
&decl.specifiers,
decl.source.as_ref().map(|s| s.value.as_str()),
source,
);
edits.push((start, end, replacement));
}
}
Statement::ExportAllDeclaration(decl) => {
has_named_or_default_export = true;
let start = decl.span.start as usize;
let end = decl.span.end as usize;
let src_name = decl.source.value.as_str();
let exported_as = decl.exported.as_ref().map(mod_export_name);
edits.push((
start,
end,
rewrite_export_all(src_name, exported_as.as_deref()),
));
}
_ => {}
}
}
if !named_import_rewrites.is_empty() {
for (symbol_id, replacement) in &named_import_rewrites {
for ref_id in scoping.get_resolved_reference_ids(*symbol_id) {
let reference = scoping.get_reference(*ref_id);
let node = nodes.get_node(reference.node_id());
let span = node.span();
edits.push((span.start as usize, span.end as usize, replacement.clone()));
}
}
}
{
let mut collector = AwaitSpanCollector::default();
collector.visit_program(&parsed.program);
for (arg_start, arg_end) in collector.spans {
edits.push((arg_start, arg_start, "__ab_await_track(".to_string()));
edits.push((arg_end, arg_end, ")".to_string()));
}
}
if std::env::var("BURN_DEBUGGER_INSTRUMENT")
.map(|v| v == "1" || v.eq_ignore_ascii_case("true"))
.unwrap_or(false)
{
let path_lit = js_string_literal(&path.display().to_string());
let mut bcol = BreakpointCollector::default();
bcol.visit_program(&parsed.program);
for stmt_start in bcol.statement_starts {
let (line, col) = byte_to_line_col(source, stmt_start);
let probe = format!("__ab_brk({path_lit},{line},{col});", line = line, col = col);
edits.push((stmt_start, stmt_start, probe));
}
}
if edits.is_empty() {
return Ok(source.to_string());
}
edits.sort_by_key(|(start, _, _)| std::cmp::Reverse(*start));
let mut out = source.to_string();
for (start, end, text) in edits {
out.replace_range(start..end, &text);
}
if has_named_or_default_export {
let prologue = "Object.defineProperty(exports, '__esModule', { value: true });\n";
out.insert_str(0, prologue);
}
Ok(out)
}
fn rewrite_import(
src: &str,
specifiers: Option<&[ImportDeclarationSpecifier]>,
temp: &str,
) -> (String, Vec<(String, String)>) {
let src_lit = js_string_literal(src);
let Some(specs) = specifiers else {
return (format!("require({src_lit});"), Vec::new());
};
if specs.is_empty() {
return (format!("require({src_lit});"), Vec::new());
}
let mut default_local: Option<String> = None;
let mut namespace_local: Option<String> = None;
let mut named: Vec<(String, String)> = Vec::new();
for spec in specs {
match spec {
ImportDeclarationSpecifier::ImportDefaultSpecifier(s) => {
default_local = Some(s.local.name.to_string());
}
ImportDeclarationSpecifier::ImportNamespaceSpecifier(s) => {
namespace_local = Some(s.local.name.to_string());
}
ImportDeclarationSpecifier::ImportSpecifier(s) => {
let imported = mod_export_name(&s.imported);
let local = s.local.name.to_string();
named.push((local, imported));
}
}
}
let mut out = format!("const {temp} = require({src_lit});\n");
if let Some(local) = default_local {
out.push_str(&format!(
"const {local} = {temp} && {temp}.__esModule ? {temp}.default : {temp};\n"
));
}
if let Some(local) = namespace_local {
out.push_str(&format!("const {local} = {temp};\n"));
}
(out, named)
}
fn rewrite_export_default(kind: &ExportDefaultDeclarationKind, source: &str) -> String {
let span = kind.span();
let text = &source[span.start as usize..span.end as usize];
match kind {
ExportDefaultDeclarationKind::FunctionDeclaration(f) => {
if let Some(id) = f.id.as_ref() {
format!("{text}\nmodule.exports.default = {};\n", id.name)
} else {
format!("module.exports.default = ({text});\n")
}
}
ExportDefaultDeclarationKind::ClassDeclaration(c) => {
if let Some(id) = c.id.as_ref() {
format!("{text}\nmodule.exports.default = {};\n", id.name)
} else {
format!("module.exports.default = ({text});\n")
}
}
ExportDefaultDeclarationKind::TSInterfaceDeclaration(_) => String::new(),
_ => format!("module.exports.default = ({text});\n"),
}
}
fn rewrite_export_named(
declaration: Option<&Declaration>,
specifiers: &[oxc::ast::ast::ExportSpecifier],
source_module: Option<&str>,
source_text: &str,
) -> String {
if let Some(decl) = declaration {
let (span, names) = decl_span_and_names(decl);
let text = &source_text[span.start as usize..span.end as usize];
let mut out = text.to_string();
out.push('\n');
for name in names {
out.push_str(&format!("exports.{name} = {name};\n"));
}
return out;
}
let mut out = String::new();
if let Some(src) = source_module {
let temp = next_temp();
out.push_str(&format!(
"const {temp} = require({});\n",
js_string_literal(src)
));
for spec in specifiers {
let local = mod_export_name(&spec.local);
let exported = mod_export_name(&spec.exported);
out.push_str(&format!("exports.{exported} = {temp}.{local};\n"));
}
return out;
}
for spec in specifiers {
let local = mod_export_name(&spec.local);
let exported = mod_export_name(&spec.exported);
out.push_str(&format!("exports.{exported} = {local};\n"));
}
out
}
fn rewrite_export_all(src: &str, exported_as: Option<&str>) -> String {
let src_lit = js_string_literal(src);
match exported_as {
Some(name) => {
format!("exports.{name} = require({src_lit});\n")
}
None => {
format!(
"Object.keys(require({src_lit})).forEach(function(k) {{ \
if (k !== 'default' && k !== '__esModule') exports[k] = require({src_lit})[k]; \
}});\n"
)
}
}
}
fn decl_names(decl: &Declaration) -> Vec<String> {
let (_, names) = decl_span_and_names(decl);
names
}
fn decl_span_and_names(decl: &Declaration) -> (oxc::span::Span, Vec<String>) {
let span = decl.span();
let names = match decl {
Declaration::VariableDeclaration(v) => {
let mut names = Vec::new();
for d in &v.declarations {
collect_binding_names(&d.id, &mut names);
}
names
}
Declaration::FunctionDeclaration(f) => {
f.id.as_ref()
.map(|id| vec![id.name.to_string()])
.unwrap_or_default()
}
Declaration::ClassDeclaration(c) => {
c.id.as_ref()
.map(|id| vec![id.name.to_string()])
.unwrap_or_default()
}
_ => vec![],
};
(span, names)
}
fn collect_binding_names(pat: &BindingPattern<'_>, out: &mut Vec<String>) {
match pat {
BindingPattern::BindingIdentifier(id) => out.push(id.name.to_string()),
BindingPattern::ObjectPattern(obj) => {
for prop in &obj.properties {
collect_binding_names(&prop.value, out);
}
if let Some(rest) = obj.rest.as_ref() {
collect_binding_names(&rest.argument, out);
}
}
BindingPattern::ArrayPattern(arr) => {
for elem in arr.elements.iter() {
if let Some(e) = elem.as_ref() {
collect_binding_names(e, out);
}
}
if let Some(rest) = arr.rest.as_ref() {
collect_binding_names(&rest.argument, out);
}
}
BindingPattern::AssignmentPattern(ap) => collect_binding_names(&ap.left, out),
}
}
fn mod_export_name(name: &ModuleExportName) -> String {
match name {
ModuleExportName::IdentifierName(i) => i.name.to_string(),
ModuleExportName::IdentifierReference(i) => i.name.to_string(),
ModuleExportName::StringLiteral(s) => s.value.to_string(),
}
}
fn js_string_literal(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for ch in s.chars() {
match ch {
'\\' => out.push_str("\\\\"),
'"' => out.push_str("\\\""),
'\n' => out.push_str("\\n"),
'\r' => out.push_str("\\r"),
'\t' => out.push_str("\\t"),
ch if (ch as u32) < 0x20 => out.push_str(&format!("\\u{:04x}", ch as u32)),
ch => out.push(ch),
}
}
out.push('"');
out
}
fn next_temp() -> String {
use std::sync::atomic::{AtomicU64, Ordering};
static CTR: AtomicU64 = AtomicU64::new(0);
let n = CTR.fetch_add(1, Ordering::Relaxed);
format!("__ab_esm_{n}")
}
fn rewrite_import_meta(source: &str) -> String {
let bytes = source.as_bytes();
let len = bytes.len();
let mut out = String::with_capacity(len + 64);
let mut i = 0usize;
while i < len {
let b = bytes[i];
if b == b'/' && i + 1 < len && bytes[i + 1] == b'/' {
while i < len && bytes[i] != b'\n' {
out.push(bytes[i] as char);
i += 1;
}
continue;
}
if b == b'/' && i + 1 < len && bytes[i + 1] == b'*' {
out.push_str("/*");
i += 2;
while i + 1 < len && !(bytes[i] == b'*' && bytes[i + 1] == b'/') {
out.push(bytes[i] as char);
i += 1;
}
if i + 1 < len {
out.push_str("*/");
i += 2;
} else if i < len {
out.push(bytes[i] as char);
i += 1;
}
continue;
}
if b == b'\'' || b == b'"' || b == b'`' {
let quote = b;
out.push(b as char);
i += 1;
while i < len {
let c = bytes[i];
out.push(c as char);
i += 1;
if c == b'\\' && i < len {
out.push(bytes[i] as char);
i += 1;
continue;
}
if c == quote {
break;
}
}
continue;
}
if b == b'i' && starts_with(bytes, i, b"import.meta") {
let prev_ok = i == 0 || !is_ident_byte(bytes[i - 1]);
if prev_ok {
let after = i + b"import.meta".len();
if starts_with(bytes, after, b".dirname") {
out.push_str("__dirname");
i = after + b".dirname".len();
continue;
}
if starts_with(bytes, after, b".filename") {
out.push_str("__filename");
i = after + b".filename".len();
continue;
}
if starts_with(bytes, after, b".url") {
out.push_str("('file://' + __filename)");
i = after + b".url".len();
continue;
}
if starts_with(bytes, after, b".resolve") {
out.push_str("require.resolve");
i = after + b".resolve".len();
continue;
}
if after >= len || !is_ident_byte(bytes[after]) {
out.push_str(
"({ url: 'file://' + __filename, dirname: __dirname, \
filename: __filename, \
resolve: function(s){ return require.resolve(s); } })",
);
i = after;
continue;
}
}
}
out.push(b as char);
i += 1;
}
out
}
fn starts_with(bytes: &[u8], at: usize, needle: &[u8]) -> bool {
if at + needle.len() > bytes.len() {
return false;
}
&bytes[at..at + needle.len()] == needle
}
fn is_ident_byte(b: u8) -> bool {
b.is_ascii_alphanumeric() || b == b'_' || b == b'$'
}
#[derive(Default)]
struct AwaitSpanCollector {
spans: Vec<(usize, usize)>,
}
impl<'a> Visit<'a> for AwaitSpanCollector {
fn visit_await_expression(&mut self, expr: &AwaitExpression<'a>) {
let arg_span = expr.argument.span();
self.spans
.push((arg_span.start as usize, arg_span.end as usize));
oxc::ast_visit::walk::walk_await_expression(self, expr);
}
}
#[derive(Default)]
struct BreakpointCollector {
statement_starts: Vec<usize>,
}
impl<'a> Visit<'a> for BreakpointCollector {
fn visit_statement(&mut self, stmt: &Statement<'a>) {
match stmt {
Statement::FunctionDeclaration(_)
| Statement::ClassDeclaration(_)
| Statement::ImportDeclaration(_)
| Statement::ExportNamedDeclaration(_)
| Statement::ExportDefaultDeclaration(_)
| Statement::ExportAllDeclaration(_) => {}
_ => {
let span = stmt.span();
self.statement_starts.push(span.start as usize);
}
}
oxc::ast_visit::walk::walk_statement(self, stmt);
}
}
fn byte_to_line_col(source: &str, offset: usize) -> (u32, u32) {
let mut line: u32 = 1;
let mut last_nl: usize = 0;
let bytes = source.as_bytes();
let cap = offset.min(bytes.len());
for (i, &b) in bytes.iter().enumerate().take(cap) {
if b == b'\n' {
line += 1;
last_nl = i + 1;
}
}
let col = (cap - last_nl) as u32 + 1;
(line, col)
}
#[cfg(test)]
mod import_meta_tests {
use super::rewrite_import_meta;
#[test]
fn rewrites_dirname_and_filename() {
let s = "console.log(import.meta.dirname, import.meta.filename);";
let out = rewrite_import_meta(s);
assert_eq!(out, "console.log(__dirname, __filename);");
}
#[test]
fn rewrites_url_and_resolve() {
let s = "const u = import.meta.url; const p = import.meta.resolve('x');";
let out = rewrite_import_meta(s);
assert!(out.contains("('file://' + __filename)"));
assert!(out.contains("require.resolve('x')"));
}
#[test]
fn rewrites_bare_import_meta() {
let s = "const m = import.meta;";
let out = rewrite_import_meta(s);
assert!(out.contains("url: 'file://' + __filename"));
assert!(out.contains("dirname: __dirname"));
}
#[test]
fn skips_inside_strings() {
let s = "const t = 'import.meta.dirname'; const r = import.meta.dirname;";
let out = rewrite_import_meta(s);
assert!(out.contains("'import.meta.dirname'"));
assert!(out.ends_with("__dirname;"));
}
#[test]
fn skips_inside_template_literals() {
let s = "const t = `${import.meta.dirname}`; const r = import.meta.dirname;";
let out = rewrite_import_meta(s);
assert!(out.ends_with("__dirname;"));
}
#[test]
fn skips_inside_comments() {
let s = "// import.meta.dirname\n/* import.meta.url */\nconst x = import.meta.dirname;";
let out = rewrite_import_meta(s);
assert!(out.contains("// import.meta.dirname"));
assert!(out.contains("/* import.meta.url */"));
assert!(out.ends_with("__dirname;"));
}
#[test]
fn requires_word_boundary_before_token() {
let s = "obj.notimport.meta.dirname";
let out = rewrite_import_meta(s);
assert_eq!(out, s);
}
}