Skip to main content

morph_cli/core/detection/
frameworks.rs

1use crate::core::detection::PackageJson;
2
3#[derive(Debug, Clone)]
4pub struct DetectedFramework {
5    pub name: String,
6    pub version: Option<String>,
7    pub confidence: u8,
8}
9
10impl DetectedFramework {
11    pub fn new(name: &str, version: Option<&str>, confidence: u8) -> Self {
12        Self {
13            name: name.into(),
14            version: version.map(|s| s.into()),
15            confidence,
16        }
17    }
18}
19
20#[derive(Debug, Clone)]
21#[allow(unused)]
22pub struct Framework {
23    pub name: &'static str,
24    pub package_names: &'static [&'static str],
25    pub config_files: &'static [&'static str],
26    pub detections: fn(&PackageJson) -> Option<DetectedFramework>,
27}
28
29impl Framework {
30    pub fn all() -> Vec<Framework> {
31        vec![
32            express(),
33            fastify(),
34            react(),
35            nextjs(),
36            vue(),
37            nodejs(),
38            typescript(),
39            jest(),
40            vite(),
41            tailwind(),
42            commonjs(),
43            esm(),
44        ]
45    }
46
47    pub fn detect(&self, pkg: &PackageJson) -> Option<DetectedFramework> {
48        (self.detections)(pkg)
49    }
50}
51
52fn detect_dep(pkg: &PackageJson, name: &str) -> Option<String> {
53    pkg.dependencies
54        .get(name)
55        .cloned()
56        .or_else(|| pkg.dev_dependencies.get(name).cloned())
57}
58
59fn express() -> Framework {
60    Framework {
61        name: "Express",
62        package_names: &["express"],
63        config_files: &[],
64        detections: |pkg| {
65            detect_dep(pkg, "express").map(|v| DetectedFramework::new("Express", Some(&v), 95))
66        },
67    }
68}
69
70fn fastify() -> Framework {
71    Framework {
72        name: "Fastify",
73        package_names: &["fastify"],
74        config_files: &[],
75        detections: |pkg| {
76            detect_dep(pkg, "fastify").map(|v| DetectedFramework {
77                name: "Fastify".into(),
78                version: Some(v),
79                confidence: 95,
80            })
81        },
82    }
83}
84
85fn react() -> Framework {
86    Framework {
87        name: "React",
88        package_names: &["react", "react-dom"],
89        config_files: &["vite.config.ts", "vite.config.js", "webpack.config.js"],
90        detections: |pkg| {
91            let has_react = pkg.dependencies.contains_key("react")
92                || pkg.dev_dependencies.contains_key("react");
93            let has_dom = pkg.dependencies.contains_key("react-dom")
94                || pkg.dev_dependencies.contains_key("react-dom");
95            if has_react || has_dom {
96                let version = detect_dep(pkg, "react");
97                Some(DetectedFramework {
98                    name: "React".into(),
99                    version,
100                    confidence: if has_dom { 95 } else { 80 },
101                })
102            } else {
103                None
104            }
105        },
106    }
107}
108
109fn nextjs() -> Framework {
110    Framework {
111        name: "Next.js",
112        package_names: &["next"],
113        config_files: &["next.config.js", "next.config.ts"],
114        detections: |pkg| {
115            detect_dep(pkg, "next").map(|v| DetectedFramework {
116                name: "Next.js".into(),
117                version: Some(v),
118                confidence: 95,
119            })
120        },
121    }
122}
123
124fn vue() -> Framework {
125    Framework {
126        name: "Vue",
127        package_names: &["vue"],
128        config_files: &["vite.config.ts", "vite.config.js", "vue.config.js"],
129        detections: |pkg| {
130            detect_dep(pkg, "vue").map(|v| DetectedFramework {
131                name: "Vue".into(),
132                version: Some(v),
133                confidence: 95,
134            })
135        },
136    }
137}
138
139fn nodejs() -> Framework {
140    Framework {
141        name: "Node.js",
142        package_names: &[],
143        config_files: &["package.json"],
144        detections: |pkg| {
145            if !pkg.dependencies.is_empty() || !pkg.dev_dependencies.is_empty() {
146                Some(DetectedFramework {
147                    name: "Node.js".into(),
148                    version: None,
149                    confidence: 60,
150                })
151            } else {
152                None
153            }
154        },
155    }
156}
157
158fn typescript() -> Framework {
159    Framework {
160        name: "TypeScript",
161        package_names: &["typescript"],
162        config_files: &["tsconfig.json", "tsconfig.build.json"],
163        detections: |pkg| {
164            detect_dep(pkg, "typescript").map(|v| DetectedFramework {
165                name: "TypeScript".into(),
166                version: Some(v),
167                confidence: 90,
168            })
169        },
170    }
171}
172
173fn jest() -> Framework {
174    Framework {
175        name: "Jest",
176        package_names: &["jest", "@types/jest"],
177        config_files: &["jest.config.js", "jest.config.ts", "jest.config.json"],
178        detections: |pkg| {
179            detect_dep(pkg, "jest").map(|v| DetectedFramework {
180                name: "Jest".into(),
181                version: Some(v),
182                confidence: 95,
183            })
184        },
185    }
186}
187
188fn vite() -> Framework {
189    Framework {
190        name: "Vite",
191        package_names: &["vite"],
192        config_files: &["vite.config.ts", "vite.config.js"],
193        detections: |pkg| {
194            detect_dep(pkg, "vite").map(|v| DetectedFramework {
195                name: "Vite".into(),
196                version: Some(v),
197                confidence: 95,
198            })
199        },
200    }
201}
202
203fn tailwind() -> Framework {
204    Framework {
205        name: "Tailwind",
206        package_names: &["tailwindcss", "autoprefixer"],
207        config_files: &[
208            "tailwind.config.js",
209            "tailwind.config.ts",
210            "postcss.config.js",
211        ],
212        detections: |pkg| {
213            if pkg.dependencies.contains_key("tailwindcss")
214                || pkg.dev_dependencies.contains_key("tailwindcss")
215            {
216                Some(DetectedFramework {
217                    name: "Tailwind".into(),
218                    version: detect_dep(pkg, "tailwindcss"),
219                    confidence: 90,
220                })
221            } else {
222                None
223            }
224        },
225    }
226}
227
228fn commonjs() -> Framework {
229    Framework {
230        name: "CommonJS",
231        package_names: &[],
232        config_files: &[],
233        detections: |pkg| {
234            if pkg.typ.as_deref() == Some("module") {
235                None
236            } else {
237                Some(DetectedFramework {
238                    name: "CommonJS".into(),
239                    version: None,
240                    confidence: 70,
241                })
242            }
243        },
244    }
245}
246
247fn esm() -> Framework {
248    Framework {
249        name: "ESM",
250        package_names: &[],
251        config_files: &[],
252        detections: |pkg| {
253            if pkg.typ.as_deref() == Some("module") {
254                Some(DetectedFramework {
255                    name: "ESM".into(),
256                    version: None,
257                    confidence: 90,
258                })
259            } else {
260                None
261            }
262        },
263    }
264}
265
266#[cfg(test)]
267mod tests {
268    use super::*;
269
270    #[test]
271    fn test_express_detection() {
272        let pkg = PackageJson {
273            name: "test".into(),
274            version: "1.0.0".into(),
275            dependencies: [("express".into(), "^4.18.0".into())].into(),
276            dev_dependencies: Default::default(),
277            scripts: Default::default(),
278            typ: None,
279            workspaces: None,
280        };
281        let frameworks = Framework::all();
282        let detected: Vec<_> = frameworks.iter().filter_map(|f| f.detect(&pkg)).collect();
283        assert!(detected.iter().any(|f| f.name == "Express"));
284    }
285
286    #[test]
287    fn test_react_detection() {
288        let pkg = PackageJson {
289            name: "test".into(),
290            version: "1.0.0".into(),
291            dependencies: [
292                ("react".into(), "^18.0.0".into()),
293                ("react-dom".into(), "^18.0.0".into()),
294            ]
295            .into(),
296            dev_dependencies: Default::default(),
297            scripts: Default::default(),
298            typ: None,
299            workspaces: None,
300        };
301        let frameworks = Framework::all();
302        let detected: Vec<_> = frameworks.iter().filter_map(|f| f.detect(&pkg)).collect();
303        assert!(detected.iter().any(|f| f.name == "React"));
304    }
305
306    #[test]
307    fn test_typescript_detection() {
308        let pkg = PackageJson {
309            name: "test".into(),
310            version: "1.0.0".into(),
311            dependencies: Default::default(),
312            dev_dependencies: [("typescript".into(), "^5.0.0".into())].into(),
313            scripts: Default::default(),
314            typ: None,
315            workspaces: None,
316        };
317        let frameworks = Framework::all();
318        let detected: Vec<_> = frameworks.iter().filter_map(|f| f.detect(&pkg)).collect();
319        assert!(detected.iter().any(|f| f.name == "TypeScript"));
320    }
321}