morph-cli 0.1.0

AST-based codebase migration and codemod tool for JavaScript and TypeScript projects.
Documentation
use crate::core::detection::PackageJson;

#[derive(Debug, Clone)]
pub struct DetectedFramework {
    pub name: String,
    pub version: Option<String>,
    pub confidence: u8,
}

impl DetectedFramework {
    pub fn new(name: &str, version: Option<&str>, confidence: u8) -> Self {
        Self {
            name: name.into(),
            version: version.map(|s| s.into()),
            confidence,
        }
    }
}

#[derive(Debug, Clone)]
#[allow(unused)]
pub struct Framework {
    pub name: &'static str,
    pub package_names: &'static [&'static str],
    pub config_files: &'static [&'static str],
    pub detections: fn(&PackageJson) -> Option<DetectedFramework>,
}

impl Framework {
    pub fn all() -> Vec<Framework> {
        vec![
            express(),
            fastify(),
            react(),
            nextjs(),
            vue(),
            nodejs(),
            typescript(),
            jest(),
            vite(),
            tailwind(),
            commonjs(),
            esm(),
        ]
    }

    pub fn detect(&self, pkg: &PackageJson) -> Option<DetectedFramework> {
        (self.detections)(pkg)
    }
}

fn detect_dep(pkg: &PackageJson, name: &str) -> Option<String> {
    pkg.dependencies
        .get(name)
        .cloned()
        .or_else(|| pkg.dev_dependencies.get(name).cloned())
}

fn express() -> Framework {
    Framework {
        name: "Express",
        package_names: &["express"],
        config_files: &[],
        detections: |pkg| {
            detect_dep(pkg, "express").map(|v| DetectedFramework::new("Express", Some(&v), 95))
        },
    }
}

fn fastify() -> Framework {
    Framework {
        name: "Fastify",
        package_names: &["fastify"],
        config_files: &[],
        detections: |pkg| {
            detect_dep(pkg, "fastify").map(|v| DetectedFramework {
                name: "Fastify".into(),
                version: Some(v),
                confidence: 95,
            })
        },
    }
}

fn react() -> Framework {
    Framework {
        name: "React",
        package_names: &["react", "react-dom"],
        config_files: &["vite.config.ts", "vite.config.js", "webpack.config.js"],
        detections: |pkg| {
            let has_react = pkg.dependencies.contains_key("react")
                || pkg.dev_dependencies.contains_key("react");
            let has_dom = pkg.dependencies.contains_key("react-dom")
                || pkg.dev_dependencies.contains_key("react-dom");
            if has_react || has_dom {
                let version = detect_dep(pkg, "react");
                Some(DetectedFramework {
                    name: "React".into(),
                    version,
                    confidence: if has_dom { 95 } else { 80 },
                })
            } else {
                None
            }
        },
    }
}

fn nextjs() -> Framework {
    Framework {
        name: "Next.js",
        package_names: &["next"],
        config_files: &["next.config.js", "next.config.ts"],
        detections: |pkg| {
            detect_dep(pkg, "next").map(|v| DetectedFramework {
                name: "Next.js".into(),
                version: Some(v),
                confidence: 95,
            })
        },
    }
}

fn vue() -> Framework {
    Framework {
        name: "Vue",
        package_names: &["vue"],
        config_files: &["vite.config.ts", "vite.config.js", "vue.config.js"],
        detections: |pkg| {
            detect_dep(pkg, "vue").map(|v| DetectedFramework {
                name: "Vue".into(),
                version: Some(v),
                confidence: 95,
            })
        },
    }
}

fn nodejs() -> Framework {
    Framework {
        name: "Node.js",
        package_names: &[],
        config_files: &["package.json"],
        detections: |pkg| {
            if !pkg.dependencies.is_empty() || !pkg.dev_dependencies.is_empty() {
                Some(DetectedFramework {
                    name: "Node.js".into(),
                    version: None,
                    confidence: 60,
                })
            } else {
                None
            }
        },
    }
}

fn typescript() -> Framework {
    Framework {
        name: "TypeScript",
        package_names: &["typescript"],
        config_files: &["tsconfig.json", "tsconfig.build.json"],
        detections: |pkg| {
            detect_dep(pkg, "typescript").map(|v| DetectedFramework {
                name: "TypeScript".into(),
                version: Some(v),
                confidence: 90,
            })
        },
    }
}

fn jest() -> Framework {
    Framework {
        name: "Jest",
        package_names: &["jest", "@types/jest"],
        config_files: &["jest.config.js", "jest.config.ts", "jest.config.json"],
        detections: |pkg| {
            detect_dep(pkg, "jest").map(|v| DetectedFramework {
                name: "Jest".into(),
                version: Some(v),
                confidence: 95,
            })
        },
    }
}

fn vite() -> Framework {
    Framework {
        name: "Vite",
        package_names: &["vite"],
        config_files: &["vite.config.ts", "vite.config.js"],
        detections: |pkg| {
            detect_dep(pkg, "vite").map(|v| DetectedFramework {
                name: "Vite".into(),
                version: Some(v),
                confidence: 95,
            })
        },
    }
}

fn tailwind() -> Framework {
    Framework {
        name: "Tailwind",
        package_names: &["tailwindcss", "autoprefixer"],
        config_files: &[
            "tailwind.config.js",
            "tailwind.config.ts",
            "postcss.config.js",
        ],
        detections: |pkg| {
            if pkg.dependencies.contains_key("tailwindcss")
                || pkg.dev_dependencies.contains_key("tailwindcss")
            {
                Some(DetectedFramework {
                    name: "Tailwind".into(),
                    version: detect_dep(pkg, "tailwindcss"),
                    confidence: 90,
                })
            } else {
                None
            }
        },
    }
}

fn commonjs() -> Framework {
    Framework {
        name: "CommonJS",
        package_names: &[],
        config_files: &[],
        detections: |pkg| {
            if pkg.typ.as_deref() == Some("module") {
                None
            } else {
                Some(DetectedFramework {
                    name: "CommonJS".into(),
                    version: None,
                    confidence: 70,
                })
            }
        },
    }
}

fn esm() -> Framework {
    Framework {
        name: "ESM",
        package_names: &[],
        config_files: &[],
        detections: |pkg| {
            if pkg.typ.as_deref() == Some("module") {
                Some(DetectedFramework {
                    name: "ESM".into(),
                    version: None,
                    confidence: 90,
                })
            } else {
                None
            }
        },
    }
}

#[cfg(test)]
mod tests {
    use super::*;

    #[test]
    fn test_express_detection() {
        let pkg = PackageJson {
            name: "test".into(),
            version: "1.0.0".into(),
            dependencies: [("express".into(), "^4.18.0".into())].into(),
            dev_dependencies: Default::default(),
            scripts: Default::default(),
            typ: None,
            workspaces: None,
        };
        let frameworks = Framework::all();
        let detected: Vec<_> = frameworks.iter().filter_map(|f| f.detect(&pkg)).collect();
        assert!(detected.iter().any(|f| f.name == "Express"));
    }

    #[test]
    fn test_react_detection() {
        let pkg = PackageJson {
            name: "test".into(),
            version: "1.0.0".into(),
            dependencies: [
                ("react".into(), "^18.0.0".into()),
                ("react-dom".into(), "^18.0.0".into()),
            ]
            .into(),
            dev_dependencies: Default::default(),
            scripts: Default::default(),
            typ: None,
            workspaces: None,
        };
        let frameworks = Framework::all();
        let detected: Vec<_> = frameworks.iter().filter_map(|f| f.detect(&pkg)).collect();
        assert!(detected.iter().any(|f| f.name == "React"));
    }

    #[test]
    fn test_typescript_detection() {
        let pkg = PackageJson {
            name: "test".into(),
            version: "1.0.0".into(),
            dependencies: Default::default(),
            dev_dependencies: [("typescript".into(), "^5.0.0".into())].into(),
            scripts: Default::default(),
            typ: None,
            workspaces: None,
        };
        let frameworks = Framework::all();
        let detected: Vec<_> = frameworks.iter().filter_map(|f| f.detect(&pkg)).collect();
        assert!(detected.iter().any(|f| f.name == "TypeScript"));
    }
}