morph_cli/core/detection/
frameworks.rs1use 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}