Skip to main content

morph_cli/recipes/commonjs_to_esm/
mod.rs

1mod detect;
2pub(crate) mod safety;
3mod transform;
4
5use std::path::Path;
6
7use anyhow::Result;
8use indicatif::ProgressBar;
9use walkdir::WalkDir;
10
11use crate::core::ast::parser::parse_file;
12use crate::core::cache::{CachedDetectionOutcome, load_detection, record_miss, save_detection};
13use crate::core::recipe::{
14    DetectionFailure, DetectionReport, Recipe, RecipeMetadata, TransformOptions, TransformReport,
15};
16
17use self::detect::analyze_parsed_module;
18use self::transform::transform_report;
19
20pub struct CommonJsToEsmRecipe;
21
22const METADATA: RecipeMetadata = RecipeMetadata {
23    name: "commonjs-to-esm",
24    description: "Scans JavaScript CommonJS candidates and prepares a future ESM migration.",
25    supported_extensions: &["js", "cjs", "mjs"],
26    required_recipes: &[],
27    incompatible_recipes: &[],
28    should_run_before: &["js-to-ts"],
29    should_run_after: &[],
30    maturity: crate::core::recipe::RecipeMaturity::Stable,
31    category: crate::core::recipe::RecipeCategory::Migration,
32    tags: &["safe", "fast", "backend", "commonjs", "esm", "require", "import", "exports"],
33};
34
35impl Recipe for CommonJsToEsmRecipe {
36    fn metadata(&self) -> &'static RecipeMetadata {
37        &METADATA
38    }
39
40    fn detect(&self, root: &Path, progress: &ProgressBar) -> Result<DetectionReport> {
41        let mut report = DetectionReport::default();
42
43        for entry in WalkDir::new(root)
44            .into_iter()
45            .filter_entry(|e| {
46                let name = e.file_name().to_string_lossy();
47                name != "node_modules" && name != ".git" && name != "target" && name != "dist" && name != "build"
48            })
49            .filter_map(|entry| entry.ok())
50        {
51            let path = entry.path();
52            progress.set_message(format!("Scanning {}", path.display()));
53
54            if entry.file_type().is_file() && has_supported_extension(path) {
55                report.total_files += 1;
56
57                if let Some(outcome) = load_detection(METADATA.name, path) {
58                    apply_cached_outcome(&mut report, outcome);
59                    continue;
60                }
61                record_miss();
62
63                match parse_file(path) {
64                    Ok(parsed) => {
65                        report.parseable_files += 1;
66
67                        if let Some(analysis) = analyze_parsed_module(&parsed) {
68                            let _ = save_detection(
69                                METADATA.name,
70                                path,
71                                &CachedDetectionOutcome::Analysis(analysis.clone()),
72                            );
73                            report.analyses.push(analysis);
74                        } else {
75                            let skipped = path.to_path_buf();
76                            let _ = save_detection(
77                                METADATA.name,
78                                path,
79                                &CachedDetectionOutcome::Skipped(skipped.clone()),
80                            );
81                            report.skipped_files.push(path.to_path_buf());
82                        }
83                    }
84                    Err(error) => {
85                        let failure = DetectionFailure {
86                            path: error.path().to_path_buf(),
87                            error: error.to_string(),
88                        };
89                        let _ = save_detection(
90                            METADATA.name,
91                            path,
92                            &CachedDetectionOutcome::Failure(failure.clone()),
93                        );
94                        report.failed_files.push(failure);
95                    }
96                }
97            }
98        }
99
100        Ok(report)
101    }
102
103    fn transform(
104        &self,
105        report: &DetectionReport,
106        options: TransformOptions,
107    ) -> Result<TransformReport> {
108        transform_report(report, options)
109    }
110}
111
112fn apply_cached_outcome(report: &mut DetectionReport, outcome: CachedDetectionOutcome) {
113    match outcome {
114        CachedDetectionOutcome::Analysis(analysis) => {
115            report.parseable_files += 1;
116            report.analyses.push(analysis);
117        }
118        CachedDetectionOutcome::Skipped(path) => {
119            report.parseable_files += 1;
120            report.skipped_files.push(path);
121        }
122        CachedDetectionOutcome::Failure(failure) => report.failed_files.push(failure),
123    }
124}
125
126fn has_supported_extension(path: &Path) -> bool {
127    path.extension()
128        .and_then(|extension| extension.to_str())
129        .is_some_and(|extension| METADATA.supported_extensions.contains(&extension))
130}