mod detect;
pub(crate) mod safety;
mod transform;
use std::path::Path;
use anyhow::Result;
use indicatif::ProgressBar;
use walkdir::WalkDir;
use crate::core::ast::parser::parse_file;
use crate::core::cache::{CachedDetectionOutcome, load_detection, record_miss, save_detection};
use crate::core::recipe::{
DetectionFailure, DetectionReport, Recipe, RecipeMetadata, TransformOptions, TransformReport,
};
use self::detect::analyze_parsed_module;
use self::transform::transform_report;
pub struct CommonJsToEsmRecipe;
const METADATA: RecipeMetadata = RecipeMetadata {
name: "commonjs-to-esm",
description: "Scans JavaScript CommonJS candidates and prepares a future ESM migration.",
supported_extensions: &["js", "cjs", "mjs"],
required_recipes: &[],
incompatible_recipes: &[],
should_run_before: &["js-to-ts"],
should_run_after: &[],
maturity: crate::core::recipe::RecipeMaturity::Stable,
category: crate::core::recipe::RecipeCategory::Migration,
tags: &["safe", "fast", "backend", "commonjs", "esm", "require", "import", "exports"],
};
impl Recipe for CommonJsToEsmRecipe {
fn metadata(&self) -> &'static RecipeMetadata {
&METADATA
}
fn detect(&self, root: &Path, progress: &ProgressBar) -> Result<DetectionReport> {
let mut report = DetectionReport::default();
for entry in WalkDir::new(root)
.into_iter()
.filter_entry(|e| {
let name = e.file_name().to_string_lossy();
name != "node_modules" && name != ".git" && name != "target" && name != "dist" && name != "build"
})
.filter_map(|entry| entry.ok())
{
let path = entry.path();
progress.set_message(format!("Scanning {}", path.display()));
if entry.file_type().is_file() && has_supported_extension(path) {
report.total_files += 1;
if let Some(outcome) = load_detection(METADATA.name, path) {
apply_cached_outcome(&mut report, outcome);
continue;
}
record_miss();
match parse_file(path) {
Ok(parsed) => {
report.parseable_files += 1;
if let Some(analysis) = analyze_parsed_module(&parsed) {
let _ = save_detection(
METADATA.name,
path,
&CachedDetectionOutcome::Analysis(analysis.clone()),
);
report.analyses.push(analysis);
} else {
let skipped = path.to_path_buf();
let _ = save_detection(
METADATA.name,
path,
&CachedDetectionOutcome::Skipped(skipped.clone()),
);
report.skipped_files.push(path.to_path_buf());
}
}
Err(error) => {
let failure = DetectionFailure {
path: error.path().to_path_buf(),
error: error.to_string(),
};
let _ = save_detection(
METADATA.name,
path,
&CachedDetectionOutcome::Failure(failure.clone()),
);
report.failed_files.push(failure);
}
}
}
}
Ok(report)
}
fn transform(
&self,
report: &DetectionReport,
options: TransformOptions,
) -> Result<TransformReport> {
transform_report(report, options)
}
}
fn apply_cached_outcome(report: &mut DetectionReport, outcome: CachedDetectionOutcome) {
match outcome {
CachedDetectionOutcome::Analysis(analysis) => {
report.parseable_files += 1;
report.analyses.push(analysis);
}
CachedDetectionOutcome::Skipped(path) => {
report.parseable_files += 1;
report.skipped_files.push(path);
}
CachedDetectionOutcome::Failure(failure) => report.failed_files.push(failure),
}
}
fn has_supported_extension(path: &Path) -> bool {
path.extension()
.and_then(|extension| extension.to_str())
.is_some_and(|extension| METADATA.supported_extensions.contains(&extension))
}