1use super::{LanguageFrameworkDetector, TechnologyRule, FrameworkDetectionUtils};
2use crate::analyzer::{DetectedTechnology, DetectedLanguage, TechnologyCategory, LibraryType};
3use crate::error::Result;
4use std::fs;
5use std::path::Path;
6
7pub struct JavaScriptFrameworkDetector;
8
9impl LanguageFrameworkDetector for JavaScriptFrameworkDetector {
10 fn detect_frameworks(&self, language: &DetectedLanguage) -> Result<Vec<DetectedTechnology>> {
11 let rules = get_js_technology_rules();
12
13 let mut technologies = detect_frameworks_from_files(language, &rules)?;
15
16 let all_deps: Vec<String> = language.main_dependencies.iter()
18 .chain(language.dev_dependencies.iter())
19 .cloned()
20 .collect();
21
22 if let Some(enhanced_techs) = detect_technologies_from_source_files(language, &rules) {
24 for enhanced_tech in enhanced_techs {
26 if let Some(existing) = technologies.iter_mut().find(|t| t.name == enhanced_tech.name) {
27 if enhanced_tech.confidence > existing.confidence {
29 existing.confidence = enhanced_tech.confidence;
30 }
31 } else {
32 technologies.push(enhanced_tech);
34 }
35 }
36 }
37
38 let dependency_based_techs = FrameworkDetectionUtils::detect_technologies_by_dependencies(
40 &rules, &all_deps, language.confidence
41 );
42
43 for dep_tech in dependency_based_techs {
45 if let Some(existing) = technologies.iter_mut().find(|t| t.name == dep_tech.name) {
46 if dep_tech.confidence > existing.confidence {
48 existing.confidence = dep_tech.confidence;
49 }
50 } else {
51 technologies.push(dep_tech);
53 }
54 }
55
56 Ok(technologies)
57 }
58
59 fn supported_languages(&self) -> Vec<&'static str> {
60 vec!["JavaScript", "TypeScript", "JavaScript/TypeScript"]
61 }
62}
63
64fn detect_frameworks_from_files(language: &DetectedLanguage, rules: &[TechnologyRule]) -> Result<Vec<DetectedTechnology>> {
66 let mut detected = Vec::new();
67
68 if let Some(config_detections) = detect_by_config_files(language, rules) {
70 detected.extend(config_detections);
71 }
72
73 if detected.is_empty() {
75 if let Some(structure_detections) = detect_by_project_structure(language, rules) {
76 detected.extend(structure_detections);
77 }
78 }
79
80 if let Some(source_detections) = detect_by_source_patterns(language, rules) {
82 for source_tech in source_detections {
84 if let Some(existing_tech) = detected.iter_mut().find(|t| t.name == source_tech.name) {
85 if source_tech.confidence > existing_tech.confidence {
86 existing_tech.confidence = source_tech.confidence;
87 }
88 } else {
89 detected.push(source_tech);
90 }
91 }
92 }
93
94 Ok(detected)
95}
96
97fn detect_by_config_files(language: &DetectedLanguage, rules: &[TechnologyRule]) -> Option<Vec<DetectedTechnology>> {
99 let mut detected = Vec::new();
100
101 for file_path in &language.files {
103 if let Some(file_name) = file_path.file_name().and_then(|n| n.to_str()) {
104 if file_name == "app.json" || file_name == "app.config.js" || file_name == "app.config.ts" {
106 if file_name == "app.config.js" || file_name == "app.config.ts" {
109 let has_expo_deps = language.main_dependencies.iter().any(|dep| dep == "expo" || dep == "react-native");
111 let has_tanstack_deps = language.main_dependencies.iter().any(|dep| dep.contains("tanstack") || dep.contains("vinxi"));
112
113 if has_expo_deps && !has_tanstack_deps {
114 if let Some(expo_rule) = rules.iter().find(|r| r.name == "Expo") {
115 detected.push(DetectedTechnology {
116 name: expo_rule.name.clone(),
117 version: None,
118 category: expo_rule.category.clone(),
119 confidence: 1.0, requires: expo_rule.requires.clone(),
121 conflicts_with: expo_rule.conflicts_with.clone(),
122 is_primary: expo_rule.is_primary_indicator,
123 file_indicators: expo_rule.file_indicators.clone(),
124 });
125 }
126 } else if has_tanstack_deps && !has_expo_deps {
127 if let Some(tanstack_rule) = rules.iter().find(|r| r.name == "Tanstack Start") {
128 detected.push(DetectedTechnology {
129 name: tanstack_rule.name.clone(),
130 version: None,
131 category: tanstack_rule.category.clone(),
132 confidence: 1.0, requires: tanstack_rule.requires.clone(),
134 conflicts_with: tanstack_rule.conflicts_with.clone(),
135 is_primary: tanstack_rule.is_primary_indicator,
136 file_indicators: tanstack_rule.file_indicators.clone(),
137 });
138 }
139 }
140 } else {
142 if let Some(expo_rule) = rules.iter().find(|r| r.name == "Expo") {
144 detected.push(DetectedTechnology {
145 name: expo_rule.name.clone(),
146 version: None,
147 category: expo_rule.category.clone(),
148 confidence: 1.0, requires: expo_rule.requires.clone(),
150 conflicts_with: expo_rule.conflicts_with.clone(),
151 is_primary: expo_rule.is_primary_indicator,
152 file_indicators: expo_rule.file_indicators.clone(),
153 });
154 }
155 }
156 }
157 else if file_name.starts_with("next.config.") {
159 if let Some(nextjs_rule) = rules.iter().find(|r| r.name == "Next.js") {
160 detected.push(DetectedTechnology {
161 name: nextjs_rule.name.clone(),
162 version: None,
163 category: nextjs_rule.category.clone(),
164 confidence: 1.0, requires: nextjs_rule.requires.clone(),
166 conflicts_with: nextjs_rule.conflicts_with.clone(),
167 is_primary: nextjs_rule.is_primary_indicator,
168 file_indicators: nextjs_rule.file_indicators.clone(),
169 });
170 }
171 }
172 else if file_name == "react-native.config.js" {
174 if let Some(rn_rule) = rules.iter().find(|r| r.name == "React Native") {
175 detected.push(DetectedTechnology {
176 name: rn_rule.name.clone(),
177 version: None,
178 category: rn_rule.category.clone(),
179 confidence: 1.0, requires: rn_rule.requires.clone(),
181 conflicts_with: rn_rule.conflicts_with.clone(),
182 is_primary: rn_rule.is_primary_indicator,
183 file_indicators: rn_rule.file_indicators.clone(),
184 });
185 }
186 }
187 else if file_name == "encore.app" || file_name == "encore.service.ts" || file_name == "encore.service.js" {
189 if let Some(encore_rule) = rules.iter().find(|r| r.name == "Encore") {
190 detected.push(DetectedTechnology {
191 name: encore_rule.name.clone(),
192 version: None,
193 category: encore_rule.category.clone(),
194 confidence: 1.0, requires: encore_rule.requires.clone(),
196 conflicts_with: encore_rule.conflicts_with.clone(),
197 is_primary: encore_rule.is_primary_indicator,
198 file_indicators: encore_rule.file_indicators.clone(),
199 });
200 }
201 }
202 }
203 }
204
205 if detected.is_empty() {
206 None
207 } else {
208 Some(detected)
209 }
210}
211
212fn detect_by_project_structure(language: &DetectedLanguage, rules: &[TechnologyRule]) -> Option<Vec<DetectedTechnology>> {
214 let mut detected = Vec::new();
215 let mut has_android_dir = false;
216 let mut has_ios_dir = false;
217 let mut has_pages_dir = false;
218 let mut has_app_dir = false;
219 let mut has_app_routes_dir = false;
220 let mut has_encore_app_file = false;
221 let mut has_encore_service_files = false;
222 let mut has_app_json = false;
223 let mut has_app_js_ts = false;
224 let mut has_next_config = false;
225 let mut has_tanstack_config = false;
226
227 for file_path in &language.files {
229 if let Some(parent) = file_path.parent() {
230 let path_str = parent.to_string_lossy();
231 let file_name = file_path.file_name().and_then(|n| n.to_str()).unwrap_or("");
232
233 if path_str.contains("android") {
235 has_android_dir = true;
236 } else if path_str.contains("ios") {
237 has_ios_dir = true;
238 }
239 else if has_path_component(parent, "pages") {
241 has_pages_dir = true;
242 } else if has_path_component(parent, "app") && !file_name.contains("app.config") && !file_name.contains("encore.app") {
243 has_app_dir = true;
244 }
245 else if has_app_routes(parent) {
247 has_app_routes_dir = true;
248 }
249 else if file_name == "encore.app" {
251 has_encore_app_file = true;
252 } else if file_name.contains("encore.service.") {
253 has_encore_service_files = true;
254 }
255 else if file_name == "app.json" {
257 has_app_json = true;
258 } else if file_name == "App.js" || file_name == "App.tsx" {
259 has_app_js_ts = true;
260 }
261
262 if file_name.starts_with("next.config.") {
264 has_next_config = true;
265 }
266 if file_name == "app.config.ts" || file_name == "app.config.js" || file_name.starts_with("vinxi.config") {
267 has_tanstack_config = true;
268 }
269 }
270 }
271
272 let has_expo_deps = language.main_dependencies.iter().any(|dep| dep == "expo" || dep == "react-native");
274 let has_next_dep = language.main_dependencies.iter().any(|dep| dep == "next" || dep.starts_with("next@"));
275 let has_tanstack_dep = language.main_dependencies.iter().any(|dep| dep.contains("tanstack/react-start") || dep.contains("tanstack-start") || dep.contains("vinxi"));
276
277 if has_encore_app_file || has_encore_service_files {
279 if let Some(encore_rule) = rules.iter().find(|r| r.name == "Encore") {
281 detected.push(DetectedTechnology {
282 name: encore_rule.name.clone(),
283 version: None,
284 category: encore_rule.category.clone(),
285 confidence: 1.0, requires: encore_rule.requires.clone(),
287 conflicts_with: encore_rule.conflicts_with.clone(),
288 is_primary: encore_rule.is_primary_indicator,
289 file_indicators: encore_rule.file_indicators.clone(),
290 });
291 }
292 } else if has_app_routes_dir && (has_tanstack_dep || has_tanstack_config) {
293 if let Some(tanstack_rule) = rules.iter().find(|r| r.name == "Tanstack Start") {
295 detected.push(DetectedTechnology {
296 name: tanstack_rule.name.clone(),
297 version: None,
298 category: tanstack_rule.category.clone(),
299 confidence: 0.9, requires: tanstack_rule.requires.clone(),
301 conflicts_with: tanstack_rule.conflicts_with.clone(),
302 is_primary: tanstack_rule.is_primary_indicator,
303 file_indicators: tanstack_rule.file_indicators.clone(),
304 });
305 }
306 } else if (has_pages_dir || has_app_dir) && (has_next_dep || has_next_config) {
307 if let Some(nextjs_rule) = rules.iter().find(|r| r.name == "Next.js") {
309 detected.push(DetectedTechnology {
310 name: nextjs_rule.name.clone(),
311 version: None,
312 category: nextjs_rule.category.clone(),
313 confidence: 0.9, requires: nextjs_rule.requires.clone(),
315 conflicts_with: nextjs_rule.conflicts_with.clone(),
316 is_primary: nextjs_rule.is_primary_indicator,
317 file_indicators: nextjs_rule.file_indicators.clone(),
318 });
319 }
320 } else if (has_app_json || has_app_js_ts) && has_expo_deps {
321 if let Some(expo_rule) = rules.iter().find(|r| r.name == "Expo") {
323 detected.push(DetectedTechnology {
324 name: expo_rule.name.clone(),
325 version: None,
326 category: expo_rule.category.clone(),
327 confidence: 1.0, requires: expo_rule.requires.clone(),
329 conflicts_with: expo_rule.conflicts_with.clone(),
330 is_primary: expo_rule.is_primary_indicator,
331 file_indicators: expo_rule.file_indicators.clone(),
332 });
333 }
334 } else if has_android_dir && has_ios_dir {
335 if let Some(rn_rule) = rules.iter().find(|r| r.name == "React Native") {
337 detected.push(DetectedTechnology {
338 name: rn_rule.name.clone(),
339 version: None,
340 category: rn_rule.category.clone(),
341 confidence: 0.9, requires: rn_rule.requires.clone(),
343 conflicts_with: rn_rule.conflicts_with.clone(),
344 is_primary: rn_rule.is_primary_indicator,
345 file_indicators: rn_rule.file_indicators.clone(),
346 });
347 }
348 }
349
350 if detected.is_empty() {
351 None
352 } else {
353 Some(detected)
354 }
355}
356
357fn has_path_component(path: &Path, target: &str) -> bool {
359 path.components()
360 .any(|c| c.as_os_str().to_string_lossy() == target)
361}
362
363fn has_app_routes(path: &Path) -> bool {
365 let components: Vec<String> = path
366 .components()
367 .map(|c| c.as_os_str().to_string_lossy().to_string())
368 .collect();
369 components.windows(2).any(|w| w[0] == "app" && w[1] == "routes")
370}
371
372fn detect_by_source_patterns(language: &DetectedLanguage, rules: &[TechnologyRule]) -> Option<Vec<DetectedTechnology>> {
374 let mut detected = Vec::new();
375
376 for file_path in &language.files {
378 if let Ok(content) = std::fs::read_to_string(file_path) {
379 if content.contains("expo") && (content.contains("from 'expo'") || content.contains("import {") && content.contains("registerRootComponent")) {
381 if let Some(expo_rule) = rules.iter().find(|r| r.name == "Expo") {
382 detected.push(DetectedTechnology {
383 name: expo_rule.name.clone(),
384 version: None,
385 category: expo_rule.category.clone(),
386 confidence: 0.8, requires: expo_rule.requires.clone(),
388 conflicts_with: expo_rule.conflicts_with.clone(),
389 is_primary: expo_rule.is_primary_indicator,
390 file_indicators: expo_rule.file_indicators.clone(),
391 });
392 }
393 }
394
395 if content.contains("next/") {
397 if let Some(nextjs_rule) = rules.iter().find(|r| r.name == "Next.js") {
398 detected.push(DetectedTechnology {
399 name: nextjs_rule.name.clone(),
400 version: None,
401 category: nextjs_rule.category.clone(),
402 confidence: 0.7, requires: nextjs_rule.requires.clone(),
404 conflicts_with: nextjs_rule.conflicts_with.clone(),
405 is_primary: nextjs_rule.is_primary_indicator,
406 file_indicators: nextjs_rule.file_indicators.clone(),
407 });
408 }
409 }
410
411 if content.contains("@tanstack/react-router") && content.contains("createFileRoute") {
413 if let Some(tanstack_rule) = rules.iter().find(|r| r.name == "Tanstack Start") {
414 detected.push(DetectedTechnology {
415 name: tanstack_rule.name.clone(),
416 version: None,
417 category: tanstack_rule.category.clone(),
418 confidence: 0.7, requires: tanstack_rule.requires.clone(),
420 conflicts_with: tanstack_rule.conflicts_with.clone(),
421 is_primary: tanstack_rule.is_primary_indicator,
422 file_indicators: tanstack_rule.file_indicators.clone(),
423 });
424 }
425 }
426
427 if content.contains("react-router") && content.contains("BrowserRouter") {
429 if let Some(rr_rule) = rules.iter().find(|r| r.name == "React Router v7") {
430 detected.push(DetectedTechnology {
431 name: rr_rule.name.clone(),
432 version: None,
433 category: rr_rule.category.clone(),
434 confidence: 0.7, requires: rr_rule.requires.clone(),
436 conflicts_with: rr_rule.conflicts_with.clone(),
437 is_primary: rr_rule.is_primary_indicator,
438 file_indicators: rr_rule.file_indicators.clone(),
439 });
440 }
441 }
442 }
443 }
444
445 if detected.is_empty() {
446 None
447 } else {
448 Some(detected)
449 }
450}
451
452fn detect_technologies_from_source_files(language: &DetectedLanguage, rules: &[TechnologyRule]) -> Option<Vec<DetectedTechnology>> {
454
455
456 let mut detected = Vec::new();
457
458 for file_path in &language.files {
460 if let Ok(content) = fs::read_to_string(file_path) {
461 if let Some(drizzle_confidence) = analyze_drizzle_usage(&content, file_path) {
463 if let Some(drizzle_rule) = rules.iter().find(|r| r.name == "Drizzle ORM") {
464 detected.push(DetectedTechnology {
465 name: "Drizzle ORM".to_string(),
466 version: None,
467 category: TechnologyCategory::Database,
468 confidence: drizzle_confidence,
469 requires: vec![],
470 conflicts_with: vec![],
471 is_primary: false,
472 file_indicators: drizzle_rule.file_indicators.clone(),
473 });
474 }
475 }
476
477 if let Some(prisma_confidence) = analyze_prisma_usage(&content, file_path) {
479 if let Some(prisma_rule) = rules.iter().find(|r| r.name == "Prisma") {
480 detected.push(DetectedTechnology {
481 name: "Prisma".to_string(),
482 version: None,
483 category: TechnologyCategory::Database,
484 confidence: prisma_confidence,
485 requires: vec![],
486 conflicts_with: vec![],
487 is_primary: false,
488 file_indicators: prisma_rule.file_indicators.clone(),
489 });
490 }
491 }
492
493 if let Some(encore_confidence) = analyze_encore_usage(&content, file_path) {
495 if let Some(encore_rule) = rules.iter().find(|r| r.name == "Encore") {
496 detected.push(DetectedTechnology {
497 name: "Encore".to_string(),
498 version: None,
499 category: TechnologyCategory::BackendFramework,
500 confidence: encore_confidence,
501 requires: vec![],
502 conflicts_with: vec![],
503 is_primary: true,
504 file_indicators: encore_rule.file_indicators.clone(),
505 });
506 }
507 }
508
509 if let Some(tanstack_confidence) = analyze_tanstack_start_usage(&content, file_path) {
511 if let Some(tanstack_rule) = rules.iter().find(|r| r.name == "Tanstack Start") {
512 detected.push(DetectedTechnology {
513 name: "Tanstack Start".to_string(),
514 version: None,
515 category: TechnologyCategory::MetaFramework,
516 confidence: tanstack_confidence,
517 requires: vec!["React".to_string()],
518 conflicts_with: vec!["Next.js".to_string(), "React Router v7".to_string(), "SvelteKit".to_string(), "Nuxt.js".to_string()],
519 is_primary: true,
520 file_indicators: tanstack_rule.file_indicators.clone(),
521 });
522 }
523 }
524 }
525 }
526
527 if detected.is_empty() {
528 None
529 } else {
530 Some(detected)
531 }
532}
533
534fn analyze_drizzle_usage(content: &str, file_path: &Path) -> Option<f32> {
536 let file_name = file_path.file_name()?.to_string_lossy();
537 let mut confidence: f32 = 0.0;
538
539 if content.contains("drizzle-orm") {
541 confidence += 0.3;
542 }
543
544 if file_name.contains("schema") || file_name.contains("db.ts") || file_name.contains("database") {
546 if content.contains("pgTable") || content.contains("mysqlTable") || content.contains("sqliteTable") {
547 confidence += 0.4;
548 }
549 if content.contains("pgEnum") || content.contains("relations") {
550 confidence += 0.3;
551 }
552 }
553
554 if content.contains("from 'drizzle-orm/pg-core'") ||
556 content.contains("from 'drizzle-orm/mysql-core'") ||
557 content.contains("from 'drizzle-orm/sqlite-core'") {
558 confidence += 0.3;
559 }
560
561 if content.contains("db.select()") || content.contains("db.insert()") ||
563 content.contains("db.update()") || content.contains("db.delete()") {
564 confidence += 0.2;
565 }
566
567 if content.contains("drizzle(") && (content.contains("connectionString") || content.contains("postgres(")) {
569 confidence += 0.2;
570 }
571
572 if content.contains("drizzle.config") || file_name.contains("migrate") {
574 confidence += 0.2;
575 }
576
577 if content.contains(".prepare()") && content.contains("drizzle") {
579 confidence += 0.1;
580 }
581
582 if confidence > 0.0 {
583 Some(confidence.min(1.0_f32))
584 } else {
585 None
586 }
587}
588
589fn analyze_prisma_usage(content: &str, file_path: &Path) -> Option<f32> {
591 let file_name = file_path.file_name()?.to_string_lossy();
592 let mut confidence: f32 = 0.0;
593 let mut has_prisma_import = false;
594
595 if content.contains("@prisma/client") || content.contains("from '@prisma/client'") {
597 confidence += 0.4;
598 has_prisma_import = true;
599 }
600
601 if file_name == "schema.prisma" {
603 if content.contains("model ") || content.contains("generator ") || content.contains("datasource ") {
604 confidence += 0.6;
605 has_prisma_import = true;
606 }
607 }
608
609 if has_prisma_import {
611 if content.contains("new PrismaClient") || content.contains("PrismaClient()") {
613 confidence += 0.3;
614 }
615
616 if content.contains("prisma.") && (
618 content.contains(".findUnique(") ||
619 content.contains(".findFirst(") ||
620 content.contains(".upsert(") ||
621 content.contains(".$connect()") ||
622 content.contains(".$disconnect()")
623 ) {
624 confidence += 0.2;
625 }
626 }
627
628 if confidence > 0.0 && has_prisma_import {
630 Some(confidence.min(1.0_f32))
631 } else {
632 None
633 }
634}
635
636fn analyze_encore_usage(content: &str, file_path: &Path) -> Option<f32> {
638 let file_name = file_path.file_name()?.to_string_lossy();
639 let mut confidence: f32 = 0.0;
640
641 if content.contains("// Code generated by the Encore") || content.contains("DO NOT EDIT") {
643 return None;
644 }
645
646 if file_name.contains("client.ts") || file_name.contains("client.js") {
648 return None;
649 }
650
651 let mut has_service_patterns = false;
653
654 if file_name.contains("encore.service") || file_name.contains("service.ts") {
656 confidence += 0.4;
657 has_service_patterns = true;
658 }
659
660 if content.contains("encore.dev/api") && (content.contains("export") || content.contains("api.")) {
662 confidence += 0.4;
663 has_service_patterns = true;
664 }
665
666 if content.contains("SQLDatabase") && content.contains("encore.dev") {
668 confidence += 0.3;
669 has_service_patterns = true;
670 }
671
672 if content.contains("secret(") && content.contains("encore.dev/config") {
674 confidence += 0.3;
675 has_service_patterns = true;
676 }
677
678 if content.contains("Topic") && content.contains("encore.dev/pubsub") {
680 confidence += 0.3;
681 has_service_patterns = true;
682 }
683
684 if content.contains("cron") && content.contains("encore.dev") {
686 confidence += 0.2;
687 has_service_patterns = true;
688 }
689
690 if confidence > 0.0 && has_service_patterns {
692 Some(confidence.min(1.0_f32))
693 } else {
694 None
695 }
696}
697
698fn analyze_tanstack_start_usage(content: &str, file_path: &Path) -> Option<f32> {
700 let file_name = file_path.file_name()?.to_string_lossy();
701 let mut confidence: f32 = 0.0;
702 let mut has_start_patterns = false;
703
704 if file_name == "app.config.ts" || file_name == "app.config.js" {
706 if content.contains("@tanstack/react-start") || content.contains("tanstack") {
707 confidence += 0.5;
708 has_start_patterns = true;
709 }
710 }
711
712 if file_name.contains("router.") && (file_name.ends_with(".ts") || file_name.ends_with(".tsx")) {
714 if content.contains("createRouter") && content.contains("@tanstack/react-router") {
715 confidence += 0.4;
716 has_start_patterns = true;
717 }
718 if content.contains("routeTree") {
719 confidence += 0.2;
720 has_start_patterns = true;
721 }
722 }
723
724 if file_name == "ssr.tsx" || file_name == "ssr.ts" {
726 if content.contains("createStartHandler") || content.contains("@tanstack/react-start/server") {
727 confidence += 0.5;
728 has_start_patterns = true;
729 }
730 }
731
732 if file_name == "client.tsx" || file_name == "client.ts" {
734 if content.contains("StartClient") && content.contains("@tanstack/react-start") {
735 confidence += 0.5;
736 has_start_patterns = true;
737 }
738 if content.contains("hydrateRoot") && content.contains("createRouter") {
739 confidence += 0.3;
740 has_start_patterns = true;
741 }
742 }
743
744 if file_name == "__root.tsx" || file_name == "__root.ts" {
746 if content.contains("createRootRoute") && content.contains("@tanstack/react-router") {
747 confidence += 0.4;
748 has_start_patterns = true;
749 }
750 if content.contains("HeadContent") && content.contains("Scripts") {
751 confidence += 0.3;
752 has_start_patterns = true;
753 }
754 }
755
756 if file_path.to_string_lossy().contains("routes/") {
758 if content.contains("createFileRoute") && content.contains("@tanstack/react-router") {
759 confidence += 0.3;
760 has_start_patterns = true;
761 }
762 }
763
764 if content.contains("createServerFn") && content.contains("@tanstack/react-start") {
766 confidence += 0.4;
767 has_start_patterns = true;
768 }
769
770 if content.contains("from '@tanstack/react-start'") {
772 confidence += 0.3;
773 has_start_patterns = true;
774 }
775
776 if file_name == "vinxi.config.ts" || file_name == "vinxi.config.js" {
778 confidence += 0.2;
779 has_start_patterns = true;
780 }
781
782 if confidence > 0.0 && has_start_patterns {
784 Some(confidence.min(1.0_f32))
785 } else {
786 None
787 }
788}
789
790fn get_js_technology_rules() -> Vec<TechnologyRule> {
792 vec![
793 TechnologyRule {
795 name: "Next.js".to_string(),
796 category: TechnologyCategory::MetaFramework,
797 confidence: 0.95,
798 dependency_patterns: vec!["next".to_string()],
799 requires: vec!["React".to_string()],
800 conflicts_with: vec!["Tanstack Start".to_string(), "React Router v7".to_string(), "SvelteKit".to_string(), "Nuxt.js".to_string(), "Expo".to_string()],
801 is_primary_indicator: true,
802 alternative_names: vec!["nextjs".to_string()],
803 file_indicators: vec!["next.config.js".to_string(), "next.config.ts".to_string(), "pages/".to_string(), "app/".to_string()],
804 },
805 TechnologyRule {
806 name: "Tanstack Start".to_string(),
807 category: TechnologyCategory::MetaFramework,
808 confidence: 0.95,
809 dependency_patterns: vec!["@tanstack/react-start".to_string()],
810 requires: vec!["React".to_string()],
811 conflicts_with: vec!["Next.js".to_string(), "React Router v7".to_string(), "SvelteKit".to_string(), "Nuxt.js".to_string()],
812 is_primary_indicator: true,
813 alternative_names: vec!["tanstack-start".to_string(), "TanStack Start".to_string()],
814 file_indicators: vec!["app.config.ts".to_string(), "app.config.js".to_string(), "app/routes/".to_string(), "vite.config.ts".to_string()],
815 },
816 TechnologyRule {
817 name: "React Router v7".to_string(),
818 category: TechnologyCategory::MetaFramework,
819 confidence: 0.95,
820 dependency_patterns: vec!["react-router".to_string(), "react-dom".to_string(), "react-router-dom".to_string()],
821 requires: vec!["React".to_string()],
822 conflicts_with: vec!["Next.js".to_string(), "Tanstack Start".to_string(), "SvelteKit".to_string(), "Nuxt.js".to_string(), "React Native".to_string(), "Expo".to_string()],
823 is_primary_indicator: true,
824 alternative_names: vec!["remix".to_string(), "react-router".to_string()],
825 file_indicators: vec![],
826 },
827 TechnologyRule {
828 name: "SvelteKit".to_string(),
829 category: TechnologyCategory::MetaFramework,
830 confidence: 0.95,
831 dependency_patterns: vec!["@sveltejs/kit".to_string()],
832 requires: vec!["Svelte".to_string()],
833 conflicts_with: vec!["Next.js".to_string(), "Tanstack Start".to_string(), "React Router v7".to_string(), "Nuxt.js".to_string()],
834 is_primary_indicator: true,
835 alternative_names: vec!["svelte-kit".to_string()],
836 file_indicators: vec![],
837 },
838 TechnologyRule {
839 name: "Nuxt.js".to_string(),
840 category: TechnologyCategory::MetaFramework,
841 confidence: 0.95,
842 dependency_patterns: vec!["nuxt".to_string(), "@nuxt/core".to_string()],
843 requires: vec!["Vue.js".to_string()],
844 conflicts_with: vec!["Next.js".to_string(), "Tanstack Start".to_string(), "React Router v7".to_string(), "SvelteKit".to_string()],
845 is_primary_indicator: true,
846 alternative_names: vec!["nuxtjs".to_string()],
847 file_indicators: vec![],
848 },
849 TechnologyRule {
850 name: "Astro".to_string(),
851 category: TechnologyCategory::MetaFramework,
852 confidence: 0.95,
853 dependency_patterns: vec!["astro".to_string()],
854 requires: vec![],
855 conflicts_with: vec![],
856 is_primary_indicator: true,
857 alternative_names: vec![],
858 file_indicators: vec![],
859 },
860 TechnologyRule {
861 name: "SolidStart".to_string(),
862 category: TechnologyCategory::MetaFramework,
863 confidence: 0.95,
864 dependency_patterns: vec!["solid-start".to_string()],
865 requires: vec!["SolidJS".to_string()],
866 conflicts_with: vec!["Next.js".to_string(), "Tanstack Start".to_string(), "React Router v7".to_string(), "SvelteKit".to_string()],
867 is_primary_indicator: true,
868 alternative_names: vec![],
869 file_indicators: vec![],
870 },
871
872 TechnologyRule {
874 name: "React Native".to_string(),
875 category: TechnologyCategory::FrontendFramework,
876 confidence: 0.95,
877 dependency_patterns: vec!["react-native".to_string()],
878 requires: vec!["React".to_string()],
879 conflicts_with: vec!["Next.js".to_string(), "React Router v7".to_string(), "SvelteKit".to_string(), "Nuxt.js".to_string(), "Tanstack Start".to_string()],
880 is_primary_indicator: true,
881 alternative_names: vec!["reactnative".to_string()],
882 file_indicators: vec!["react-native.config.js".to_string(), "android/".to_string(), "ios/".to_string()],
883 },
884 TechnologyRule {
885 name: "Expo".to_string(),
886 category: TechnologyCategory::MetaFramework,
887 confidence: 1.0,
888 dependency_patterns: vec!["expo".to_string(), "expo-router".to_string(), "@expo/vector-icons".to_string()],
889 requires: vec!["React Native".to_string()],
890 conflicts_with: vec!["Next.js".to_string(), "React Router v7".to_string(), "SvelteKit".to_string(), "Nuxt.js".to_string(), "Tanstack Start".to_string()],
891 is_primary_indicator: true,
892 alternative_names: vec![],
893 file_indicators: vec!["app.json".to_string(), "app.config.js".to_string(), "app.config.ts".to_string()],
894 },
895
896 TechnologyRule {
898 name: "Angular".to_string(),
899 category: TechnologyCategory::FrontendFramework,
900 confidence: 0.90,
901 dependency_patterns: vec!["@angular/core".to_string()],
902 requires: vec![],
903 conflicts_with: vec![],
904 is_primary_indicator: true,
905 alternative_names: vec!["angular".to_string()],
906 file_indicators: vec![],
907 },
908 TechnologyRule {
909 name: "Svelte".to_string(),
910 category: TechnologyCategory::FrontendFramework,
911 confidence: 0.95,
912 dependency_patterns: vec!["svelte".to_string()],
913 requires: vec![],
914 conflicts_with: vec![],
915 is_primary_indicator: false, alternative_names: vec![],
917 file_indicators: vec![],
918 },
919
920 TechnologyRule {
922 name: "React".to_string(),
923 category: TechnologyCategory::Library(LibraryType::UI),
924 confidence: 0.90,
925 dependency_patterns: vec!["react".to_string()],
926 requires: vec![],
927 conflicts_with: vec![],
928 is_primary_indicator: false, alternative_names: vec!["reactjs".to_string()],
930 file_indicators: vec![],
931 },
932 TechnologyRule {
933 name: "Vue.js".to_string(),
934 category: TechnologyCategory::Library(LibraryType::UI),
935 confidence: 0.90,
936 dependency_patterns: vec!["vue".to_string()],
937 requires: vec![],
938 conflicts_with: vec![],
939 is_primary_indicator: false,
940 alternative_names: vec!["vuejs".to_string()],
941 file_indicators: vec![],
942 },
943 TechnologyRule {
944 name: "SolidJS".to_string(),
945 category: TechnologyCategory::Library(LibraryType::UI),
946 confidence: 0.95,
947 dependency_patterns: vec!["solid-js".to_string()],
948 requires: vec![],
949 conflicts_with: vec![],
950 is_primary_indicator: false,
951 alternative_names: vec!["solid".to_string()],
952 file_indicators: vec![],
953 },
954 TechnologyRule {
955 name: "HTMX".to_string(),
956 category: TechnologyCategory::Library(LibraryType::UI),
957 confidence: 0.95,
958 dependency_patterns: vec!["htmx.org".to_string()],
959 requires: vec![],
960 conflicts_with: vec![],
961 is_primary_indicator: false,
962 alternative_names: vec!["htmx".to_string()],
963 file_indicators: vec![],
964 },
965
966 TechnologyRule {
968 name: "Express.js".to_string(),
969 category: TechnologyCategory::BackendFramework,
970 confidence: 0.95,
971 dependency_patterns: vec!["express".to_string()],
972 requires: vec![],
973 conflicts_with: vec![],
974 is_primary_indicator: true,
975 alternative_names: vec!["express".to_string()],
976 file_indicators: vec![],
977 },
978 TechnologyRule {
979 name: "Fastify".to_string(),
980 category: TechnologyCategory::BackendFramework,
981 confidence: 0.95,
982 dependency_patterns: vec!["fastify".to_string()],
983 requires: vec![],
984 conflicts_with: vec![],
985 is_primary_indicator: true,
986 alternative_names: vec![],
987 file_indicators: vec![],
988 },
989 TechnologyRule {
990 name: "Nest.js".to_string(),
991 category: TechnologyCategory::BackendFramework,
992 confidence: 0.95,
993 dependency_patterns: vec!["@nestjs/core".to_string()],
994 requires: vec![],
995 conflicts_with: vec![],
996 is_primary_indicator: true,
997 alternative_names: vec!["nestjs".to_string()],
998 file_indicators: vec![],
999 },
1000 TechnologyRule {
1001 name: "Hono".to_string(),
1002 category: TechnologyCategory::BackendFramework,
1003 confidence: 0.95,
1004 dependency_patterns: vec!["hono".to_string()],
1005 requires: vec![],
1006 conflicts_with: vec![],
1007 is_primary_indicator: true,
1008 alternative_names: vec![],
1009 file_indicators: vec![],
1010 },
1011 TechnologyRule {
1012 name: "Elysia".to_string(),
1013 category: TechnologyCategory::BackendFramework,
1014 confidence: 0.95,
1015 dependency_patterns: vec!["elysia".to_string()],
1016 requires: vec![],
1017 conflicts_with: vec![],
1018 is_primary_indicator: true,
1019 alternative_names: vec![],
1020 file_indicators: vec![],
1021 },
1022 TechnologyRule {
1023 name: "Encore".to_string(),
1024 category: TechnologyCategory::BackendFramework,
1025 confidence: 0.95,
1026 dependency_patterns: vec!["encore.dev".to_string(), "encore".to_string()],
1027 requires: vec![],
1028 conflicts_with: vec!["Next.js".to_string()],
1029 is_primary_indicator: true,
1030 alternative_names: vec!["encore-ts-starter".to_string()],
1031 file_indicators: vec!["encore.app".to_string(), "encore.service.ts".to_string(), "encore.service.js".to_string()],
1032 },
1033
1034 TechnologyRule {
1036 name: "Vite".to_string(),
1037 category: TechnologyCategory::BuildTool,
1038 confidence: 0.80,
1039 dependency_patterns: vec!["vite".to_string()],
1040 requires: vec![],
1041 conflicts_with: vec![],
1042 is_primary_indicator: false,
1043 alternative_names: vec![],
1044 file_indicators: vec![],
1045 },
1046 TechnologyRule {
1047 name: "Webpack".to_string(),
1048 category: TechnologyCategory::BuildTool,
1049 confidence: 0.80,
1050 dependency_patterns: vec!["webpack".to_string()],
1051 requires: vec![],
1052 conflicts_with: vec![],
1053 is_primary_indicator: false,
1054 alternative_names: vec![],
1055 file_indicators: vec![],
1056 },
1057
1058 TechnologyRule {
1060 name: "Prisma".to_string(),
1061 category: TechnologyCategory::Database,
1062 confidence: 0.90,
1063 dependency_patterns: vec!["prisma".to_string(), "@prisma/client".to_string()],
1064 requires: vec![],
1065 conflicts_with: vec![],
1066 is_primary_indicator: false,
1067 alternative_names: vec![],
1068 file_indicators: vec![],
1069 },
1070 TechnologyRule {
1071 name: "Drizzle ORM".to_string(),
1072 category: TechnologyCategory::Database,
1073 confidence: 0.90,
1074 dependency_patterns: vec!["drizzle-orm".to_string(), "drizzle-kit".to_string()],
1075 requires: vec![],
1076 conflicts_with: vec![],
1077 is_primary_indicator: false,
1078 alternative_names: vec!["drizzle".to_string()],
1079 file_indicators: vec![],
1080 },
1081 TechnologyRule {
1082 name: "Sequelize".to_string(),
1083 category: TechnologyCategory::Database,
1084 confidence: 0.90,
1085 dependency_patterns: vec!["sequelize".to_string()],
1086 requires: vec![],
1087 conflicts_with: vec![],
1088 is_primary_indicator: false,
1089 alternative_names: vec![],
1090 file_indicators: vec![],
1091 },
1092 TechnologyRule {
1093 name: "TypeORM".to_string(),
1094 category: TechnologyCategory::Database,
1095 confidence: 0.90,
1096 dependency_patterns: vec!["typeorm".to_string()],
1097 requires: vec![],
1098 conflicts_with: vec![],
1099 is_primary_indicator: false,
1100 alternative_names: vec![],
1101 file_indicators: vec![],
1102 },
1103 TechnologyRule {
1104 name: "MikroORM".to_string(),
1105 category: TechnologyCategory::Database,
1106 confidence: 0.90,
1107 dependency_patterns: vec!["@mikro-orm/core".to_string(), "@mikro-orm/postgresql".to_string(), "@mikro-orm/mysql".to_string(), "@mikro-orm/sqlite".to_string(), "@mikro-orm/mongodb".to_string()],
1108 requires: vec![],
1109 conflicts_with: vec![],
1110 is_primary_indicator: false,
1111 alternative_names: vec!["mikro-orm".to_string()],
1112 file_indicators: vec![],
1113 },
1114 TechnologyRule {
1115 name: "Mongoose".to_string(),
1116 category: TechnologyCategory::Database,
1117 confidence: 0.95,
1118 dependency_patterns: vec!["mongoose".to_string()],
1119 requires: vec![],
1120 conflicts_with: vec![],
1121 is_primary_indicator: false,
1122 alternative_names: vec![],
1123 file_indicators: vec![],
1124 },
1125 TechnologyRule {
1126 name: "Typegoose".to_string(),
1127 category: TechnologyCategory::Database,
1128 confidence: 0.90,
1129 dependency_patterns: vec!["@typegoose/typegoose".to_string()],
1130 requires: vec!["Mongoose".to_string()],
1131 conflicts_with: vec![],
1132 is_primary_indicator: false,
1133 alternative_names: vec![],
1134 file_indicators: vec![],
1135 },
1136 TechnologyRule {
1137 name: "Objection.js".to_string(),
1138 category: TechnologyCategory::Database,
1139 confidence: 0.90,
1140 dependency_patterns: vec!["objection".to_string()],
1141 requires: vec!["Knex.js".to_string()],
1142 conflicts_with: vec![],
1143 is_primary_indicator: false,
1144 alternative_names: vec!["objectionjs".to_string()],
1145 file_indicators: vec![],
1146 },
1147 TechnologyRule {
1148 name: "Bookshelf".to_string(),
1149 category: TechnologyCategory::Database,
1150 confidence: 0.85,
1151 dependency_patterns: vec!["bookshelf".to_string()],
1152 requires: vec!["Knex.js".to_string()],
1153 conflicts_with: vec![],
1154 is_primary_indicator: false,
1155 alternative_names: vec![],
1156 file_indicators: vec![],
1157 },
1158 TechnologyRule {
1159 name: "Waterline".to_string(),
1160 category: TechnologyCategory::Database,
1161 confidence: 0.85,
1162 dependency_patterns: vec!["waterline".to_string(), "sails-mysql".to_string(), "sails-postgresql".to_string(), "sails-disk".to_string()],
1163 requires: vec![],
1164 conflicts_with: vec![],
1165 is_primary_indicator: false,
1166 alternative_names: vec![],
1167 file_indicators: vec![],
1168 },
1169 TechnologyRule {
1170 name: "Knex.js".to_string(),
1171 category: TechnologyCategory::Database,
1172 confidence: 0.85,
1173 dependency_patterns: vec!["knex".to_string()],
1174 requires: vec![],
1175 conflicts_with: vec![],
1176 is_primary_indicator: false,
1177 alternative_names: vec!["knexjs".to_string()],
1178 file_indicators: vec![],
1179 },
1180
1181 TechnologyRule {
1183 name: "Node.js".to_string(),
1184 category: TechnologyCategory::Runtime,
1185 confidence: 0.90,
1186 dependency_patterns: vec!["node".to_string()], requires: vec![],
1188 conflicts_with: vec![],
1189 is_primary_indicator: false,
1190 alternative_names: vec!["nodejs".to_string()],
1191 file_indicators: vec![],
1192 },
1193 TechnologyRule {
1194 name: "Bun".to_string(),
1195 category: TechnologyCategory::Runtime,
1196 confidence: 0.95,
1197 dependency_patterns: vec!["bun".to_string()], requires: vec![],
1199 conflicts_with: vec![],
1200 is_primary_indicator: false,
1201 alternative_names: vec![],
1202 file_indicators: vec![],
1203 },
1204 TechnologyRule {
1205 name: "Deno".to_string(),
1206 category: TechnologyCategory::Runtime,
1207 confidence: 0.95,
1208 dependency_patterns: vec!["@deno/core".to_string(), "deno".to_string()],
1209 requires: vec![],
1210 conflicts_with: vec![],
1211 is_primary_indicator: false,
1212 alternative_names: vec![],
1213 file_indicators: vec![],
1214 },
1215 TechnologyRule {
1216 name: "WinterJS".to_string(),
1217 category: TechnologyCategory::Runtime,
1218 confidence: 0.95,
1219 dependency_patterns: vec!["winterjs".to_string(), "winter-js".to_string()],
1220 requires: vec![],
1221 conflicts_with: vec![],
1222 is_primary_indicator: false,
1223 alternative_names: vec!["winter.js".to_string()],
1224 file_indicators: vec![],
1225 },
1226 TechnologyRule {
1227 name: "Cloudflare Workers".to_string(),
1228 category: TechnologyCategory::Runtime,
1229 confidence: 0.90,
1230 dependency_patterns: vec!["@cloudflare/workers-types".to_string(), "@cloudflare/vitest-pool-workers".to_string(), "wrangler".to_string()],
1231 requires: vec![],
1232 conflicts_with: vec![],
1233 is_primary_indicator: false,
1234 alternative_names: vec!["cloudflare-workers".to_string()],
1235 file_indicators: vec![],
1236 },
1237 TechnologyRule {
1238 name: "Vercel Edge Runtime".to_string(),
1239 category: TechnologyCategory::Runtime,
1240 confidence: 0.90,
1241 dependency_patterns: vec!["@vercel/edge-runtime".to_string(), "@edge-runtime/vm".to_string()],
1242 requires: vec![],
1243 conflicts_with: vec![],
1244 is_primary_indicator: false,
1245 alternative_names: vec!["vercel-edge".to_string()],
1246 file_indicators: vec![],
1247 },
1248 TechnologyRule {
1249 name: "Hermes".to_string(),
1250 category: TechnologyCategory::Runtime,
1251 confidence: 0.85,
1252 dependency_patterns: vec!["hermes-engine".to_string()],
1253 requires: vec!["React Native".to_string()],
1254 conflicts_with: vec![],
1255 is_primary_indicator: false,
1256 alternative_names: vec![],
1257 file_indicators: vec![],
1258 },
1259 TechnologyRule {
1260 name: "Electron".to_string(),
1261 category: TechnologyCategory::Runtime,
1262 confidence: 0.95,
1263 dependency_patterns: vec!["electron".to_string()],
1264 requires: vec![],
1265 conflicts_with: vec![],
1266 is_primary_indicator: false,
1267 alternative_names: vec![],
1268 file_indicators: vec![],
1269 },
1270 TechnologyRule {
1271 name: "Tauri".to_string(),
1272 category: TechnologyCategory::Runtime,
1273 confidence: 0.95,
1274 dependency_patterns: vec!["@tauri-apps/cli".to_string(), "@tauri-apps/api".to_string()],
1275 requires: vec![],
1276 conflicts_with: vec!["Electron".to_string()],
1277 is_primary_indicator: false,
1278 alternative_names: vec![],
1279 file_indicators: vec![],
1280 },
1281 TechnologyRule {
1282 name: "QuickJS".to_string(),
1283 category: TechnologyCategory::Runtime,
1284 confidence: 0.85,
1285 dependency_patterns: vec!["quickjs".to_string(), "quickjs-emscripten".to_string()],
1286 requires: vec![],
1287 conflicts_with: vec![],
1288 is_primary_indicator: false,
1289 alternative_names: vec![],
1290 file_indicators: vec![],
1291 },
1292
1293 TechnologyRule {
1295 name: "Jest".to_string(),
1296 category: TechnologyCategory::Testing,
1297 confidence: 0.85,
1298 dependency_patterns: vec!["jest".to_string()],
1299 requires: vec![],
1300 conflicts_with: vec![],
1301 is_primary_indicator: false,
1302 alternative_names: vec![],
1303 file_indicators: vec![],
1304 },
1305 TechnologyRule {
1306 name: "Vitest".to_string(),
1307 category: TechnologyCategory::Testing,
1308 confidence: 0.85,
1309 dependency_patterns: vec!["vitest".to_string()],
1310 requires: vec![],
1311 conflicts_with: vec![],
1312 is_primary_indicator: false,
1313 alternative_names: vec![],
1314 file_indicators: vec![],
1315 },
1316 ]
1317}