use oxc::allocator::Allocator;
use oxc::ast::ast::{
BindingPattern, Declaration, ExportDefaultDeclarationKind, ImportDeclarationSpecifier,
ModuleExportName, Statement,
};
use oxc::parser::Parser;
use oxc::span::{GetSpan, SourceType};
use std::path::Path;
pub fn rewrite_esm_to_cjs(source: &str, path: &Path) -> Result<String, String> {
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 mut edits: Vec<(usize, usize, String)> = Vec::new();
let mut has_named_or_default_export = false;
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 replacement =
rewrite_import(src_name, decl.specifiers.as_ref().map(|v| v.as_slice()));
edits.push((start, end, replacement));
}
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;
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 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]>) -> String {
let src_lit = js_string_literal(src);
let Some(specs) = specifiers else {
return format!("require({src_lit});");
};
if specs.is_empty() {
return format!("require({src_lit});");
}
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((imported, local));
}
}
}
let temp = next_temp();
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"));
}
if !named.is_empty() {
let bindings: Vec<String> = named
.iter()
.map(|(imp, local)| {
if imp == local {
local.clone()
} else {
format!("{imp}: {local}")
}
})
.collect();
out.push_str(&format!("const {{ {} }} = {temp};\n", bindings.join(", ")));
}
out
}
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_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}")
}