use std::fs;
use std::path::Path;
use std::time::{SystemTime, UNIX_EPOCH};
use anyhow::{Result, anyhow, bail};
use swc_common::DUMMY_SP;
use swc_ecma_visit::VisitWith;
use swc_ecma_ast::{
AssignTarget, BindingIdent, Decl, ExportDecl, ExportDefaultDecl, ExportDefaultExpr,
ExportSpecifier, Expr, ExprStmt, Ident, ExportAll, ImportStarAsSpecifier,
ImportDecl, ImportDefaultSpecifier, ImportNamedSpecifier, ImportSpecifier, KeyValuePatProp,
Lit, MemberExpr, MemberProp, Module, ModuleDecl, ModuleExportName, ModuleItem, NamedExport,
ObjectPat, ObjectPatProp, Pat, Str, VarDecl, VarDeclKind, VarDeclarator, DefaultDecl,
};
use crate::core::ast::parser::{parse_file, parse_source};
use crate::core::ast::printer::print_module;
use crate::core::recipe::{
DetectionReport, FileClassification, SkippedTransform, TransformMode, TransformOptions,
TransformReport, UnsupportedPatternReport,
};
pub fn transform_report(
report: &DetectionReport,
options: TransformOptions,
) -> Result<TransformReport> {
let mut summary = TransformReport::default();
for analysis in &report.analyses {
if !analysis.is_transform_safe || analysis.classification != FileClassification::Safe {
summary.skipped_files.push(SkippedTransform {
path: analysis.path.clone(),
reason: "file is not considered safe for automatic migration".to_owned(),
});
summary.unsupported_patterns.push(UnsupportedPatternReport {
path: analysis.path.clone(),
patterns: analysis.detected_patterns.clone(),
});
continue;
}
match transform_file(&analysis.path, options) {
Ok(TransformOutcome::Changed) => summary.changed_files.push(analysis.path.clone()),
Ok(TransformOutcome::Unchanged(reason)) => {
summary.skipped_files.push(SkippedTransform {
path: analysis.path.clone(),
reason,
});
}
Err(error) => {
summary.skipped_files.push(SkippedTransform {
path: analysis.path.clone(),
reason: error.to_string(),
});
summary.unsupported_patterns.push(UnsupportedPatternReport {
path: analysis.path.clone(),
patterns: analysis.detected_patterns.clone(),
});
}
}
}
Ok(summary)
}
enum TransformOutcome {
Changed,
Unchanged(String),
}
fn transform_file(path: &Path, options: TransformOptions) -> Result<TransformOutcome> {
let original_content = std::fs::read_to_string(path)?;
let parsed = parse_file(path).map_err(|error| anyhow!(error))?;
let mut transformed = transform_module(&parsed.module)?;
if options.autofix {
crate::core::ast::cleanup::cleanup_imports_exports(&mut transformed.module);
}
let safety = crate::recipes::commonjs_to_esm::safety::SafetyAnalyzer::analyze(&parsed);
for warning in &safety.warnings {
eprintln!("Warning [{}]: {}", path.display(), warning);
}
for warning in &transformed.warnings {
eprintln!("Warning [{}]: {}", path.display(), warning);
}
let semantic = crate::core::ast::semantic::SemanticModel::new(&transformed.module);
let unused = semantic.get_unused_imports();
if !unused.is_empty() {
eprintln!("Warning [{}]: Unused imports after transform: {}", path.display(), unused.join(", "));
}
if !semantic.collisions.is_empty() {
let collisions: Vec<_> = semantic.collisions.into_iter().collect();
eprintln!("Warning [{}]: Naming collisions detected: {}", path.display(), collisions.join(", "));
}
if transformed.changed_count == 0 {
return Ok(TransformOutcome::Unchanged(
"no supported top-level CommonJS transforms were found".to_owned(),
));
}
let mut output = print_module(&parsed, &transformed.module).map_err(|error| anyhow!(error))?;
let format_opts = crate::core::format::FormatOptions {
enabled: !options.no_format,
use_prettier: options.prettier,
preserve_indent: true,
preserve_quotes: true,
preserve_semicolons: true,
normalize_newlines: true,
};
let mut pipeline = crate::core::format::FormatPipeline::new(format_opts);
output = pipeline.format(&output, Some(&original_content), path);
output = crate::core::format::normalize::insert_newline_after_imports(&output);
if output == original_content {
return Ok(TransformOutcome::Unchanged(
"no changes detected".to_owned(),
));
}
parse_source(path, &output).map_err(|error| anyhow!(error))?;
let mut should_write = options.mode == TransformMode::Write;
if should_write && options.review {
let renderer = crate::core::diff::renderer::DiffRenderer::new(
crate::core::diff::preview::PreviewConfig {
max_lines: 100,
show_line_numbers: true,
summary_only: false,
verbose: false,
},
);
match crate::core::diff::preview::prompt_review(path, &original_content, &output, &renderer)? {
crate::core::diff::preview::ReviewAction::Apply => should_write = true,
crate::core::diff::preview::ReviewAction::Skip => {
return Ok(TransformOutcome::Unchanged("Skipped by user".to_owned()));
}
crate::core::diff::preview::ReviewAction::Abort => {
bail!("Migration aborted by user");
}
}
}
if should_write {
write_file_atomically(path, &output)?;
}
Ok(TransformOutcome::Changed)
}
pub(crate) struct ModuleTransformResult {
pub module: Module,
pub changed_count: usize,
pub warnings: Vec<String>,
}
pub(crate) fn transform_module(module: &Module) -> Result<ModuleTransformResult> {
let mut items = Vec::with_capacity(module.body.len());
let mut changed_count = 0usize;
let mut warnings = Vec::new();
for item in &module.body {
match item {
ModuleItem::Stmt(stmt) => {
if let Some(imports) = transform_require_stmt(stmt, module, &mut warnings)? {
changed_count += imports.len();
items.extend(imports.into_iter().map(ModuleItem::ModuleDecl));
continue;
}
match transform_export_stmt_extended(stmt, &mut warnings) {
Ok(Some(decls)) => {
changed_count += decls.len();
items.extend(decls.into_iter().map(ModuleItem::ModuleDecl));
continue;
}
Ok(None) => {}
Err(e) => {
eprintln!("Warning: skipping unsupported export pattern: {e}");
items.push(item.clone());
continue;
}
}
if let Some(module_decl) = transform_export_stmt(stmt)? {
changed_count += 1;
items.push(ModuleItem::ModuleDecl(module_decl));
continue;
}
items.push(item.clone());
}
ModuleItem::ModuleDecl(_) => items.push(item.clone()),
}
}
Ok(ModuleTransformResult {
module: Module {
span: module.span,
body: items,
shebang: module.shebang.clone(),
},
changed_count,
warnings,
})
}
fn transform_require_stmt(
stmt: &swc_ecma_ast::Stmt,
module: &Module,
warnings: &mut Vec<String>,
) -> Result<Option<Vec<ModuleDecl>>> {
let swc_ecma_ast::Stmt::Decl(Decl::Var(var_decl_box)) = stmt else {
return Ok(None);
};
let var_decl = var_decl_box.as_ref();
if var_decl.kind != VarDeclKind::Const {
return Ok(None);
}
let mut imports = Vec::new();
let mut matched_any = false;
for declarator in &var_decl.decls {
if let Some((source, import_decl)) = try_require_dot_default(declarator)? {
let _ = source; matched_any = true;
imports.push(import_decl);
continue;
}
let Some(source) = require_source(declarator)? else {
if contains_require_init(declarator) {
bail!("unsupported require declarator shape");
}
return Ok(None);
};
matched_any = true;
let is_namespace = if let Pat::Ident(binding) = &declarator.name {
let name = binding.id.sym.to_string();
let namespace_used = is_used_as_namespace(&name, module);
if namespace_used {
warnings.push(format!(
"Ambiguous interop: Verify if module '{}' provides a default export or requires a namespace import (import * as {}).",
source, name
));
} else {
warnings.push(format!(
"Ambiguous interop: Verify if module '{}' provides a default export.",
source
));
}
namespace_used
} else {
false
};
imports.push(build_import_decl(&declarator.name, &source, is_namespace)?);
}
if !matched_any {
return Ok(None);
}
Ok(Some(imports))
}
fn try_require_dot_default(declarator: &VarDeclarator) -> Result<Option<(String, ModuleDecl)>> {
let Some(init) = &declarator.init else { return Ok(None) };
let Expr::Member(member) = &**init else { return Ok(None) };
let MemberProp::Ident(prop) = &member.prop else { return Ok(None) };
if prop.sym != *"default" {
return Ok(None);
}
let Expr::Call(call) = &*member.obj else { return Ok(None) };
let swc_ecma_ast::Callee::Expr(callee) = &call.callee else { return Ok(None) };
if !matches!(&**callee, Expr::Ident(id) if id.sym == *"require") {
return Ok(None);
}
if call.args.len() != 1 || call.args[0].spread.is_some() {
return Ok(None);
}
let Expr::Lit(Lit::Str(src)) = &*call.args[0].expr else { return Ok(None) };
let source = src.value.to_string();
let Pat::Ident(binding) = &declarator.name else {
bail!("unsupported binding for require().default");
};
let decl = ModuleDecl::Import(ImportDecl {
span: DUMMY_SP,
specifiers: vec![ImportSpecifier::Default(ImportDefaultSpecifier {
span: DUMMY_SP,
local: binding.id.clone(),
})],
src: Box::new(Str { span: DUMMY_SP, value: source.clone().into(), raw: None }),
type_only: false,
with: None,
phase: Default::default(),
});
Ok(Some((source, decl)))
}
fn transform_export_stmt_extended(
stmt: &swc_ecma_ast::Stmt,
warnings: &mut Vec<String>,
) -> Result<Option<Vec<ModuleDecl>>> {
let swc_ecma_ast::Stmt::Expr(ExprStmt { expr, .. }) = stmt else {
return Ok(None);
};
let Expr::Assign(assign) = &**expr else {
return Ok(None);
};
if is_module_exports_target(&assign.left) {
if let Some(decls) = try_object_shorthand_exports(&assign.right) {
return Ok(Some(decls));
}
if let Some(decl) = try_fn_class_export(&assign.right) {
return Ok(Some(vec![decl]));
}
if let Some(decl) = try_reexport_all(&assign.right) {
if let ModuleDecl::ExportAll(export_all) = &decl {
warnings.push(format!(
"Potentially breaking rewrite: re-exporting '{}' via 'export *' does not forward the default export. Verify if 'export {{ default }} from ...' is also needed.",
export_all.src.value
));
}
return Ok(Some(vec![decl]));
}
return Ok(None);
}
if let Some(name) = exports_member_name(&assign.left) {
if let Some(reexport) = try_reexport(&name, &assign.right) {
return Ok(Some(vec![reexport]));
}
}
Ok(None)
}
fn try_object_shorthand_exports(expr: &Expr) -> Option<Vec<ModuleDecl>> {
let Expr::Object(obj) = expr else { return None };
if obj.props.is_empty() {
return None;
}
let mut specifiers: Vec<ExportSpecifier> = Vec::new();
for prop in &obj.props {
match prop {
swc_ecma_ast::PropOrSpread::Prop(p) => match p.as_ref() {
swc_ecma_ast::Prop::Shorthand(ident) => {
specifiers.push(ExportSpecifier::Named(
swc_ecma_ast::ExportNamedSpecifier {
span: DUMMY_SP,
orig: ModuleExportName::Ident(ident.clone()),
exported: None,
is_type_only: false,
},
));
}
swc_ecma_ast::Prop::KeyValue(kv) => {
let exported_name = match &kv.key {
swc_ecma_ast::PropName::Ident(id) => id.clone(),
_ => return None, };
let local_ident = match kv.value.as_ref() {
Expr::Ident(id) => id.clone(),
_ => return None, };
specifiers.push(ExportSpecifier::Named(
swc_ecma_ast::ExportNamedSpecifier {
span: DUMMY_SP,
orig: ModuleExportName::Ident(local_ident),
exported: if exported_name.sym == *"default" {
None
} else {
Some(ModuleExportName::Ident(exported_name.into()))
},
is_type_only: false,
},
));
}
_ => return None,
},
swc_ecma_ast::PropOrSpread::Spread(_) => return None,
}
}
Some(vec![ModuleDecl::ExportNamed(NamedExport {
span: DUMMY_SP,
specifiers,
src: None,
type_only: false,
with: None,
})])
}
fn try_fn_class_export(expr: &Expr) -> Option<ModuleDecl> {
match expr {
Expr::Fn(fn_expr) => {
Some(ModuleDecl::ExportDefaultDecl(ExportDefaultDecl {
span: DUMMY_SP,
decl: DefaultDecl::Fn(fn_expr.clone()),
}))
}
Expr::Class(class_expr) => {
Some(ModuleDecl::ExportDefaultDecl(ExportDefaultDecl {
span: DUMMY_SP,
decl: DefaultDecl::Class(class_expr.clone()),
}))
}
_ => None,
}
}
fn try_reexport(exported_name: &str, expr: &Expr) -> Option<ModuleDecl> {
let Expr::Call(call) = expr else { return None };
let swc_ecma_ast::Callee::Expr(callee) = &call.callee else { return None };
if !matches!(&**callee, Expr::Ident(id) if id.sym == *"require") {
return None;
}
if call.args.len() != 1 || call.args[0].spread.is_some() {
return None;
}
let Expr::Lit(Lit::Str(src)) = &*call.args[0].expr else { return None };
let local_ident = Ident::new("default".into(), DUMMY_SP, Default::default());
let exported_ident = Ident::new(exported_name.into(), DUMMY_SP, Default::default());
Some(ModuleDecl::ExportNamed(NamedExport {
span: DUMMY_SP,
specifiers: vec![ExportSpecifier::Named(swc_ecma_ast::ExportNamedSpecifier {
span: DUMMY_SP,
orig: ModuleExportName::Ident(local_ident),
exported: Some(ModuleExportName::Ident(exported_ident)),
is_type_only: false,
})],
src: Some(Box::new(Str {
span: DUMMY_SP,
value: src.value.clone(),
raw: None,
})),
type_only: false,
with: None,
}))
}
fn try_reexport_all(expr: &Expr) -> Option<ModuleDecl> {
let Expr::Call(call) = expr else { return None };
let swc_ecma_ast::Callee::Expr(callee) = &call.callee else { return None };
if !matches!(&**callee, Expr::Ident(id) if id.sym == *"require") {
return None;
}
if call.args.len() != 1 || call.args[0].spread.is_some() {
return None;
}
let Expr::Lit(Lit::Str(src)) = &*call.args[0].expr else { return None };
Some(ModuleDecl::ExportAll(ExportAll {
span: DUMMY_SP,
src: Box::new(Str {
span: DUMMY_SP,
value: src.value.clone(),
raw: None,
}),
type_only: false,
with: None,
}))
}
struct NamespaceUsageVisitor {
target_ident: String,
has_non_member_usage: bool,
has_member_usage: bool,
}
impl swc_ecma_visit::Visit for NamespaceUsageVisitor {
fn visit_ident(&mut self, ident: &Ident) {
if ident.sym == *self.target_ident {
self.has_non_member_usage = true;
}
}
fn visit_var_declarator(&mut self, decl: &VarDeclarator) {
if let Pat::Ident(binding) = &decl.name {
if binding.id.sym == *self.target_ident {
decl.init.visit_with(self);
return;
}
}
decl.visit_children_with(self);
}
fn visit_member_expr(&mut self, member: &MemberExpr) {
if let Expr::Ident(ident) = &*member.obj {
if ident.sym == *self.target_ident {
self.has_member_usage = true;
member.prop.visit_with(self);
return;
}
}
member.visit_children_with(self);
}
}
fn is_used_as_namespace(ident_name: &str, module: &Module) -> bool {
use swc_ecma_visit::VisitWith;
let mut visitor = NamespaceUsageVisitor {
target_ident: ident_name.to_string(),
has_non_member_usage: false,
has_member_usage: false,
};
module.visit_with(&mut visitor);
visitor.has_member_usage && !visitor.has_non_member_usage
}
fn transform_export_stmt(stmt: &swc_ecma_ast::Stmt) -> Result<Option<ModuleDecl>> {
let swc_ecma_ast::Stmt::Expr(ExprStmt { expr, .. }) = stmt else {
return Ok(None);
};
let Expr::Assign(assign) = &**expr else {
return Ok(None);
};
if is_module_exports_target(&assign.left) {
return Ok(Some(ModuleDecl::ExportDefaultExpr(ExportDefaultExpr {
span: DUMMY_SP,
expr: assign.right.clone(),
})));
}
if let Some(name) = exports_member_name(&assign.left) {
let declarator = VarDeclarator {
span: DUMMY_SP,
name: Pat::Ident(BindingIdent {
id: Ident::new(name.into(), DUMMY_SP, Default::default()),
type_ann: None,
}),
init: Some(assign.right.clone()),
definite: false,
};
return Ok(Some(ModuleDecl::ExportDecl(ExportDecl {
span: DUMMY_SP,
decl: Decl::Var(Box::new(VarDecl {
span: DUMMY_SP,
ctxt: Default::default(),
kind: VarDeclKind::Const,
declare: false,
decls: vec![declarator],
})),
})));
}
Ok(None)
}
fn build_import_decl(pattern: &Pat, source: &str, is_namespace: bool) -> Result<ModuleDecl> {
let specifiers = match pattern {
Pat::Ident(ident) => {
if is_namespace {
vec![ImportSpecifier::Namespace(ImportStarAsSpecifier {
span: DUMMY_SP,
local: ident.id.clone(),
})]
} else {
vec![ImportSpecifier::Default(ImportDefaultSpecifier {
span: DUMMY_SP,
local: ident.id.clone(),
})]
}
}
Pat::Object(object) => build_named_specifiers(object)?,
_ => bail!("unsupported require binding pattern"),
};
Ok(ModuleDecl::Import(ImportDecl {
span: DUMMY_SP,
specifiers,
src: Box::new(Str {
span: DUMMY_SP,
value: source.into(),
raw: None,
}),
type_only: false,
with: None,
phase: Default::default(),
}))
}
fn build_named_specifiers(object: &ObjectPat) -> Result<Vec<ImportSpecifier>> {
let mut specifiers = Vec::with_capacity(object.props.len());
for prop in &object.props {
match prop {
ObjectPatProp::Assign(assign) => {
specifiers.push(ImportSpecifier::Named(ImportNamedSpecifier {
span: DUMMY_SP,
local: assign.key.id.clone(),
imported: None,
is_type_only: false,
}));
}
ObjectPatProp::KeyValue(KeyValuePatProp { key, value }) => {
let imported = match key {
swc_ecma_ast::PropName::Ident(ident) => ident.clone(),
_ => bail!("unsupported destructured import key"),
};
let local = match &**value {
Pat::Ident(binding) => binding.id.clone(),
_ => bail!("unsupported destructured import binding"),
};
specifiers.push(ImportSpecifier::Named(ImportNamedSpecifier {
span: DUMMY_SP,
local,
imported: Some(ModuleExportName::Ident(imported.into())),
is_type_only: false,
}));
}
ObjectPatProp::Rest(_) => bail!("rest destructuring is not supported"),
}
}
Ok(specifiers)
}
fn require_source(declarator: &VarDeclarator) -> Result<Option<String>> {
let Some(init) = &declarator.init else {
return Ok(None);
};
let Expr::Call(call) = &**init else {
return Ok(None);
};
let swc_ecma_ast::Callee::Expr(callee) = &call.callee else {
return Ok(None);
};
if !matches!(&**callee, Expr::Ident(ident) if ident.sym == *"require") {
return Ok(None);
}
if call.args.len() != 1 || call.args[0].spread.is_some() {
bail!("dynamic require is not supported");
}
match &*call.args[0].expr {
Expr::Lit(Lit::Str(source)) => Ok(Some(source.value.to_string())),
_ => bail!("dynamic require is not supported"),
}
}
fn contains_require_init(declarator: &VarDeclarator) -> bool {
matches!(
declarator.init.as_deref(),
Some(Expr::Call(call))
if matches!(&call.callee, swc_ecma_ast::Callee::Expr(expr)
if matches!(&**expr, Expr::Ident(ident) if ident.sym == *"require"))
)
}
fn is_module_exports_target(target: &AssignTarget) -> bool {
matches!(
target,
AssignTarget::Simple(swc_ecma_ast::SimpleAssignTarget::Member(member))
if matches!(
(&*member.obj, &member.prop),
(Expr::Ident(obj), MemberProp::Ident(prop))
if obj.sym == *"module" && prop.sym == *"exports"
)
)
}
fn exports_member_name(target: &AssignTarget) -> Option<String> {
match target {
AssignTarget::Simple(swc_ecma_ast::SimpleAssignTarget::Member(MemberExpr {
obj,
prop: MemberProp::Ident(ident),
..
})) if matches!(&**obj, Expr::Ident(base) if base.sym == *"exports") => {
Some(ident.sym.to_string())
}
_ => None,
}
}
fn write_file_atomically(path: &Path, contents: &str) -> Result<()> {
let parent = path
.parent()
.ok_or_else(|| anyhow!("cannot determine parent directory for {}", path.display()))?;
let nanos = SystemTime::now()
.duration_since(UNIX_EPOCH)
.map_err(|error| anyhow!("system clock error: {error}"))?
.as_nanos();
let temp_path = parent.join(format!(
".{}.morph.{}.tmp",
path.file_name()
.and_then(|name| name.to_str())
.unwrap_or("morph"),
nanos
));
fs::write(&temp_path, contents)?;
if let Ok(metadata) = fs::metadata(path) {
fs::set_permissions(&temp_path, metadata.permissions())?;
}
fs::rename(&temp_path, path)?;
Ok(())
}
#[cfg(test)]
mod tests {
use std::fs;
use std::path::Path;
use super::{transform_module, transform_report};
use crate::core::ast::parser::parse_source;
use crate::core::ast::printer::print_module;
use crate::core::recipe::{
DetectionReport, FileAnalysis, FileClassification, TransformMode, TransformOptions,
};
fn render(source: &str) -> String {
let parsed = parse_source(Path::new("fixture.js"), source).expect("source should parse");
let transformed = transform_module(&parsed.module).expect("transform should succeed");
print_module(&parsed, &transformed.module).expect("module should print")
}
#[test]
fn rewrites_default_require_to_import() {
let output = render("const lib = require('lib');");
assert!(output.contains("import lib from \"lib\";"));
}
#[test]
fn rewrites_destructured_require_to_named_imports() {
let output = render("const { a, b } = require('lib');");
assert!(output.contains("import { a, b } from \"lib\";"));
}
#[test]
fn rewrites_module_exports_to_default_export() {
let output = render("module.exports = factory;");
assert!(output.contains("export default factory;"));
}
#[test]
fn rewrites_exports_member_to_named_export() {
let output = render("exports.foo = bar;");
assert!(output.contains("export const foo = bar;"));
}
#[test]
fn rewrites_require_dot_default_to_default_import() {
let output = render("const lib = require('lib').default;");
assert!(output.contains("import lib from \"lib\";"));
}
#[test]
fn rewrites_module_exports_object_shorthand_to_named_exports() {
let output = render("module.exports = { foo, bar };");
assert!(output.contains("export") && output.contains("foo") && output.contains("bar"));
assert!(!output.contains("export default {"));
}
#[test]
fn rewrites_module_exports_object_keyvalue_idents() {
let output = render("module.exports = { foo: myFoo, bar: myBar };");
assert!(output.contains("export") && output.contains("myFoo") && output.contains("myBar"));
assert!(!output.contains("export default {"));
}
#[test]
fn rewrites_module_exports_function_to_export_default_function() {
let output = render("module.exports = function greet() { return 1; };");
assert!(output.contains("export default function"));
}
#[test]
fn rewrites_module_exports_class_to_export_default_class() {
let output = render("module.exports = class MyClass {};");
assert!(output.contains("export default class"));
}
#[test]
fn rewrites_exports_member_require_as_reexport() {
let output = render("exports.utils = require('utils');");
assert!(output.contains("from \"utils\""));
assert!(output.contains("export"));
}
#[test]
fn fixture_transforms_match_expected_output() {
let input = fs::read_to_string("src/recipes/commonjs_to_esm/fixtures/safe_module.input.js")
.expect("fixture input should exist");
let expected =
fs::read_to_string("src/recipes/commonjs_to_esm/fixtures/safe_module.output.js")
.expect("fixture output should exist");
let parsed =
parse_source(Path::new("fixture.js"), &input).expect("fixture source should parse");
let transformed = transform_module(&parsed.module).expect("fixture should transform");
let output = print_module(&parsed, &transformed.module).expect("fixture should print");
assert_eq!(output.trim(), expected.trim());
}
#[test]
fn risky_files_are_reported_as_unsupported() {
let report = DetectionReport {
analyses: vec![FileAnalysis {
path: Path::new("fixture.js").to_path_buf(),
detected_patterns: vec!["dynamic require".to_owned()],
confidence_score: 35,
classification: FileClassification::Risky,
is_transform_safe: false,
tags: Default::default(),
}],
..DetectionReport::default()
};
let summary = transform_report(
&report,
TransformOptions {
mode: TransformMode::DryRun,
review: false,
autofix: false,
format: false,
prettier: false,
no_format: false,
},
)
.expect("transform report should succeed");
assert_eq!(summary.changed_file_count(), 0);
assert_eq!(summary.unsupported_patterns.len(), 1);
assert_eq!(summary.skipped_files.len(), 1);
}
#[test]
fn namespace_require_usage_to_import_star() {
let output = render("const ns = require('lib'); console.log(ns.foo, ns.bar);");
assert!(output.contains("import * as ns from \"lib\";"));
}
#[test]
fn mixed_module_warnings_collected() {
let parsed = parse_source(Path::new("fixture.js"), "import foo from 'bar'; const baz = require('qux');").expect("parse");
let res = transform_module(&parsed.module).expect("transform");
assert!(!res.warnings.is_empty());
assert!(res.warnings.iter().any(|w| w.contains("Ambiguous interop")));
}
#[test]
fn reexport_all_to_export_star() {
let output = render("module.exports = require('lib');");
assert!(output.contains("export * from \"lib\";"));
}
}