1use super::{FrameworkDetectionUtils, LanguageFrameworkDetector, TechnologyRule};
2use crate::analyzer::{DetectedLanguage, DetectedTechnology, LibraryType, TechnologyCategory};
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
18 .main_dependencies
19 .iter()
20 .chain(language.dev_dependencies.iter())
21 .cloned()
22 .collect();
23
24 if let Some(enhanced_techs) = detect_technologies_from_source_files(language, &rules) {
26 for enhanced_tech in enhanced_techs {
28 if let Some(existing) = technologies
29 .iter_mut()
30 .find(|t| t.name == enhanced_tech.name)
31 {
32 if enhanced_tech.confidence > existing.confidence {
34 existing.confidence = enhanced_tech.confidence;
35 }
36 } else {
37 technologies.push(enhanced_tech);
39 }
40 }
41 }
42
43 let dependency_based_techs = FrameworkDetectionUtils::detect_technologies_by_dependencies(
45 &rules,
46 &all_deps,
47 language.confidence,
48 );
49
50 for dep_tech in dependency_based_techs {
52 if let Some(existing) = technologies.iter_mut().find(|t| t.name == dep_tech.name) {
53 if dep_tech.confidence > existing.confidence {
55 existing.confidence = dep_tech.confidence;
56 }
57 } else {
58 technologies.push(dep_tech);
60 }
61 }
62
63 Ok(technologies)
64 }
65
66 fn supported_languages(&self) -> Vec<&'static str> {
67 vec!["JavaScript", "TypeScript", "JavaScript/TypeScript"]
68 }
69}
70
71fn detect_frameworks_from_files(
73 language: &DetectedLanguage,
74 rules: &[TechnologyRule],
75) -> Result<Vec<DetectedTechnology>> {
76 let mut detected = Vec::new();
77
78 if let Some(config_detections) = detect_by_config_files(language, rules) {
80 detected.extend(config_detections);
81 }
82
83 if detected.is_empty()
85 && let Some(structure_detections) = detect_by_project_structure(language, rules)
86 {
87 detected.extend(structure_detections);
88 }
89
90 if let Some(source_detections) = detect_by_source_patterns(language, rules) {
92 for source_tech in source_detections {
94 if let Some(existing_tech) = detected.iter_mut().find(|t| t.name == source_tech.name) {
95 if source_tech.confidence > existing_tech.confidence {
96 existing_tech.confidence = source_tech.confidence;
97 }
98 } else {
99 detected.push(source_tech);
100 }
101 }
102 }
103
104 Ok(detected)
105}
106
107fn detect_by_config_files(
109 language: &DetectedLanguage,
110 rules: &[TechnologyRule],
111) -> Option<Vec<DetectedTechnology>> {
112 let mut detected = Vec::new();
113
114 for file_path in &language.files {
116 if let Some(file_name) = file_path.file_name().and_then(|n| n.to_str()) {
117 if file_name == "app.json"
119 || file_name == "app.config.js"
120 || file_name == "app.config.ts"
121 {
122 if file_name == "app.config.js" || file_name == "app.config.ts" {
125 let has_expo_deps = language
127 .main_dependencies
128 .iter()
129 .any(|dep| dep == "expo" || dep == "react-native");
130 let has_tanstack_deps = language
131 .main_dependencies
132 .iter()
133 .any(|dep| dep.contains("tanstack") || dep.contains("vinxi"));
134
135 if has_expo_deps
136 && !has_tanstack_deps
137 && let Some(expo_rule) = rules.iter().find(|r| r.name == "Expo")
138 {
139 detected.push(DetectedTechnology {
140 name: expo_rule.name.clone(),
141 version: None,
142 category: expo_rule.category.clone(),
143 confidence: 1.0, requires: expo_rule.requires.clone(),
145 conflicts_with: expo_rule.conflicts_with.clone(),
146 is_primary: expo_rule.is_primary_indicator,
147 file_indicators: expo_rule.file_indicators.clone(),
148 });
149 } else if has_tanstack_deps
150 && !has_expo_deps
151 && let Some(tanstack_rule) =
152 rules.iter().find(|r| r.name == "Tanstack Start")
153 {
154 detected.push(DetectedTechnology {
155 name: tanstack_rule.name.clone(),
156 version: None,
157 category: tanstack_rule.category.clone(),
158 confidence: 1.0, requires: tanstack_rule.requires.clone(),
160 conflicts_with: tanstack_rule.conflicts_with.clone(),
161 is_primary: tanstack_rule.is_primary_indicator,
162 file_indicators: tanstack_rule.file_indicators.clone(),
163 });
164 }
165 } else if let Some(expo_rule) = rules.iter().find(|r| r.name == "Expo") {
167 detected.push(DetectedTechnology {
169 name: expo_rule.name.clone(),
170 version: None,
171 category: expo_rule.category.clone(),
172 confidence: 1.0, requires: expo_rule.requires.clone(),
174 conflicts_with: expo_rule.conflicts_with.clone(),
175 is_primary: expo_rule.is_primary_indicator,
176 file_indicators: expo_rule.file_indicators.clone(),
177 });
178 }
179 }
180 else if file_name.starts_with("next.config.")
182 && let Some(nextjs_rule) = rules.iter().find(|r| r.name == "Next.js")
183 {
184 detected.push(DetectedTechnology {
185 name: nextjs_rule.name.clone(),
186 version: None,
187 category: nextjs_rule.category.clone(),
188 confidence: 1.0, requires: nextjs_rule.requires.clone(),
190 conflicts_with: nextjs_rule.conflicts_with.clone(),
191 is_primary: nextjs_rule.is_primary_indicator,
192 file_indicators: nextjs_rule.file_indicators.clone(),
193 });
194 }
195 else if file_name == "react-native.config.js"
197 && let Some(rn_rule) = rules.iter().find(|r| r.name == "React Native")
198 {
199 detected.push(DetectedTechnology {
200 name: rn_rule.name.clone(),
201 version: None,
202 category: rn_rule.category.clone(),
203 confidence: 1.0, requires: rn_rule.requires.clone(),
205 conflicts_with: rn_rule.conflicts_with.clone(),
206 is_primary: rn_rule.is_primary_indicator,
207 file_indicators: rn_rule.file_indicators.clone(),
208 });
209 }
210 else if (file_name == "encore.app"
212 || file_name == "encore.service.ts"
213 || file_name == "encore.service.js")
214 && let Some(encore_rule) = rules.iter().find(|r| r.name == "Encore")
215 {
216 detected.push(DetectedTechnology {
217 name: encore_rule.name.clone(),
218 version: None,
219 category: encore_rule.category.clone(),
220 confidence: 1.0, requires: encore_rule.requires.clone(),
222 conflicts_with: encore_rule.conflicts_with.clone(),
223 is_primary: encore_rule.is_primary_indicator,
224 file_indicators: encore_rule.file_indicators.clone(),
225 });
226 }
227 }
228 }
229
230 if detected.is_empty() {
231 None
232 } else {
233 Some(detected)
234 }
235}
236
237fn detect_by_project_structure(
239 language: &DetectedLanguage,
240 rules: &[TechnologyRule],
241) -> Option<Vec<DetectedTechnology>> {
242 let mut detected = Vec::new();
243 let mut has_android_dir = false;
244 let mut has_ios_dir = false;
245 let mut has_pages_dir = false;
246 let mut has_app_dir = false;
247 let mut has_app_routes_dir = false;
248 let mut has_encore_app_file = false;
249 let mut has_encore_service_files = false;
250 let mut has_app_json = false;
251 let mut has_app_js_ts = false;
252 let mut has_next_config = false;
253 let mut has_tanstack_config = false;
254
255 for file_path in &language.files {
257 if let Some(parent) = file_path.parent() {
258 let path_str = parent.to_string_lossy();
259 let file_name = file_path.file_name().and_then(|n| n.to_str()).unwrap_or("");
260
261 if path_str.contains("android") {
263 has_android_dir = true;
264 } else if path_str.contains("ios") {
265 has_ios_dir = true;
266 }
267 else if has_path_component(parent, "pages") {
269 has_pages_dir = true;
270 } else if has_path_component(parent, "app")
271 && !file_name.contains("app.config")
272 && !file_name.contains("encore.app")
273 {
274 has_app_dir = true;
275 }
276 else if has_app_routes(parent) {
278 has_app_routes_dir = true;
279 }
280 else if file_name == "encore.app" {
282 has_encore_app_file = true;
283 } else if file_name.contains("encore.service.") {
284 has_encore_service_files = true;
285 }
286 else if file_name == "app.json" {
288 has_app_json = true;
289 } else if file_name == "App.js" || file_name == "App.tsx" {
290 has_app_js_ts = true;
291 }
292
293 if file_name.starts_with("next.config.") {
295 has_next_config = true;
296 }
297 if file_name == "app.config.ts"
298 || file_name == "app.config.js"
299 || file_name.starts_with("vinxi.config")
300 {
301 has_tanstack_config = true;
302 }
303 }
304 }
305
306 let has_expo_deps = language
308 .main_dependencies
309 .iter()
310 .any(|dep| dep == "expo" || dep == "react-native");
311 let has_next_dep = language
312 .main_dependencies
313 .iter()
314 .any(|dep| dep == "next" || dep.starts_with("next@"));
315 let has_tanstack_dep = language.main_dependencies.iter().any(|dep| {
316 dep.contains("tanstack/react-start")
317 || dep.contains("tanstack-start")
318 || dep.contains("vinxi")
319 });
320
321 if has_encore_app_file || has_encore_service_files {
323 if let Some(encore_rule) = rules.iter().find(|r| r.name == "Encore") {
325 detected.push(DetectedTechnology {
326 name: encore_rule.name.clone(),
327 version: None,
328 category: encore_rule.category.clone(),
329 confidence: 1.0, requires: encore_rule.requires.clone(),
331 conflicts_with: encore_rule.conflicts_with.clone(),
332 is_primary: encore_rule.is_primary_indicator,
333 file_indicators: encore_rule.file_indicators.clone(),
334 });
335 }
336 } else if has_app_routes_dir && (has_tanstack_dep || has_tanstack_config) {
337 if let Some(tanstack_rule) = rules.iter().find(|r| r.name == "Tanstack Start") {
339 detected.push(DetectedTechnology {
340 name: tanstack_rule.name.clone(),
341 version: None,
342 category: tanstack_rule.category.clone(),
343 confidence: 0.9, requires: tanstack_rule.requires.clone(),
345 conflicts_with: tanstack_rule.conflicts_with.clone(),
346 is_primary: tanstack_rule.is_primary_indicator,
347 file_indicators: tanstack_rule.file_indicators.clone(),
348 });
349 }
350 } else if (has_pages_dir || has_app_dir) && (has_next_dep || has_next_config) {
351 if let Some(nextjs_rule) = rules.iter().find(|r| r.name == "Next.js") {
353 detected.push(DetectedTechnology {
354 name: nextjs_rule.name.clone(),
355 version: None,
356 category: nextjs_rule.category.clone(),
357 confidence: 0.9, requires: nextjs_rule.requires.clone(),
359 conflicts_with: nextjs_rule.conflicts_with.clone(),
360 is_primary: nextjs_rule.is_primary_indicator,
361 file_indicators: nextjs_rule.file_indicators.clone(),
362 });
363 }
364 } else if (has_app_json || has_app_js_ts) && has_expo_deps {
365 if let Some(expo_rule) = rules.iter().find(|r| r.name == "Expo") {
367 detected.push(DetectedTechnology {
368 name: expo_rule.name.clone(),
369 version: None,
370 category: expo_rule.category.clone(),
371 confidence: 1.0, requires: expo_rule.requires.clone(),
373 conflicts_with: expo_rule.conflicts_with.clone(),
374 is_primary: expo_rule.is_primary_indicator,
375 file_indicators: expo_rule.file_indicators.clone(),
376 });
377 }
378 } else if has_android_dir && has_ios_dir {
379 if let Some(rn_rule) = rules.iter().find(|r| r.name == "React Native") {
381 detected.push(DetectedTechnology {
382 name: rn_rule.name.clone(),
383 version: None,
384 category: rn_rule.category.clone(),
385 confidence: 0.9, requires: rn_rule.requires.clone(),
387 conflicts_with: rn_rule.conflicts_with.clone(),
388 is_primary: rn_rule.is_primary_indicator,
389 file_indicators: rn_rule.file_indicators.clone(),
390 });
391 }
392 }
393
394 if detected.is_empty() {
395 None
396 } else {
397 Some(detected)
398 }
399}
400
401fn has_path_component(path: &Path, target: &str) -> bool {
403 path.components()
404 .any(|c| c.as_os_str().to_string_lossy() == target)
405}
406
407fn has_app_routes(path: &Path) -> bool {
409 let components: Vec<String> = path
410 .components()
411 .map(|c| c.as_os_str().to_string_lossy().to_string())
412 .collect();
413 components
414 .windows(2)
415 .any(|w| w[0] == "app" && w[1] == "routes")
416}
417
418fn detect_by_source_patterns(
420 language: &DetectedLanguage,
421 rules: &[TechnologyRule],
422) -> Option<Vec<DetectedTechnology>> {
423 let mut detected = Vec::new();
424
425 for file_path in &language.files {
427 if let Ok(content) = std::fs::read_to_string(file_path) {
428 if content.contains("expo")
430 && (content.contains("from 'expo'")
431 || content.contains("import {") && content.contains("registerRootComponent"))
432 && let Some(expo_rule) = rules.iter().find(|r| r.name == "Expo")
433 {
434 detected.push(DetectedTechnology {
435 name: expo_rule.name.clone(),
436 version: None,
437 category: expo_rule.category.clone(),
438 confidence: 0.8, requires: expo_rule.requires.clone(),
440 conflicts_with: expo_rule.conflicts_with.clone(),
441 is_primary: expo_rule.is_primary_indicator,
442 file_indicators: expo_rule.file_indicators.clone(),
443 });
444 }
445
446 if content.contains("next/")
448 && let Some(nextjs_rule) = rules.iter().find(|r| r.name == "Next.js")
449 {
450 detected.push(DetectedTechnology {
451 name: nextjs_rule.name.clone(),
452 version: None,
453 category: nextjs_rule.category.clone(),
454 confidence: 0.7, requires: nextjs_rule.requires.clone(),
456 conflicts_with: nextjs_rule.conflicts_with.clone(),
457 is_primary: nextjs_rule.is_primary_indicator,
458 file_indicators: nextjs_rule.file_indicators.clone(),
459 });
460 }
461
462 if content.contains("@tanstack/react-router")
464 && content.contains("createFileRoute")
465 && let Some(tanstack_rule) = rules.iter().find(|r| r.name == "Tanstack Start")
466 {
467 detected.push(DetectedTechnology {
468 name: tanstack_rule.name.clone(),
469 version: None,
470 category: tanstack_rule.category.clone(),
471 confidence: 0.7, requires: tanstack_rule.requires.clone(),
473 conflicts_with: tanstack_rule.conflicts_with.clone(),
474 is_primary: tanstack_rule.is_primary_indicator,
475 file_indicators: tanstack_rule.file_indicators.clone(),
476 });
477 }
478
479 if content.contains("react-router")
481 && content.contains("BrowserRouter")
482 && let Some(rr_rule) = rules.iter().find(|r| r.name == "React Router v7")
483 {
484 detected.push(DetectedTechnology {
485 name: rr_rule.name.clone(),
486 version: None,
487 category: rr_rule.category.clone(),
488 confidence: 0.7, requires: rr_rule.requires.clone(),
490 conflicts_with: rr_rule.conflicts_with.clone(),
491 is_primary: rr_rule.is_primary_indicator,
492 file_indicators: rr_rule.file_indicators.clone(),
493 });
494 }
495 }
496 }
497
498 if detected.is_empty() {
499 None
500 } else {
501 Some(detected)
502 }
503}
504
505fn detect_technologies_from_source_files(
507 language: &DetectedLanguage,
508 rules: &[TechnologyRule],
509) -> Option<Vec<DetectedTechnology>> {
510 let mut detected = Vec::new();
511
512 for file_path in &language.files {
514 if let Ok(content) = fs::read_to_string(file_path) {
515 if let Some(drizzle_confidence) = analyze_drizzle_usage(&content, file_path)
517 && let Some(drizzle_rule) = rules.iter().find(|r| r.name == "Drizzle ORM")
518 {
519 detected.push(DetectedTechnology {
520 name: "Drizzle ORM".to_string(),
521 version: None,
522 category: TechnologyCategory::Database,
523 confidence: drizzle_confidence,
524 requires: vec![],
525 conflicts_with: vec![],
526 is_primary: false,
527 file_indicators: drizzle_rule.file_indicators.clone(),
528 });
529 }
530
531 if let Some(prisma_confidence) = analyze_prisma_usage(&content, file_path)
533 && let Some(prisma_rule) = rules.iter().find(|r| r.name == "Prisma")
534 {
535 detected.push(DetectedTechnology {
536 name: "Prisma".to_string(),
537 version: None,
538 category: TechnologyCategory::Database,
539 confidence: prisma_confidence,
540 requires: vec![],
541 conflicts_with: vec![],
542 is_primary: false,
543 file_indicators: prisma_rule.file_indicators.clone(),
544 });
545 }
546
547 if let Some(encore_confidence) = analyze_encore_usage(&content, file_path)
549 && let Some(encore_rule) = rules.iter().find(|r| r.name == "Encore")
550 {
551 detected.push(DetectedTechnology {
552 name: "Encore".to_string(),
553 version: None,
554 category: TechnologyCategory::BackendFramework,
555 confidence: encore_confidence,
556 requires: vec![],
557 conflicts_with: vec![],
558 is_primary: true,
559 file_indicators: encore_rule.file_indicators.clone(),
560 });
561 }
562
563 if let Some(tanstack_confidence) = analyze_tanstack_start_usage(&content, file_path)
565 && let Some(tanstack_rule) = rules.iter().find(|r| r.name == "Tanstack Start")
566 {
567 detected.push(DetectedTechnology {
568 name: "Tanstack Start".to_string(),
569 version: None,
570 category: TechnologyCategory::MetaFramework,
571 confidence: tanstack_confidence,
572 requires: vec!["React".to_string()],
573 conflicts_with: vec![
574 "Next.js".to_string(),
575 "React Router v7".to_string(),
576 "SvelteKit".to_string(),
577 "Nuxt.js".to_string(),
578 ],
579 is_primary: true,
580 file_indicators: tanstack_rule.file_indicators.clone(),
581 });
582 }
583 }
584 }
585
586 if detected.is_empty() {
587 None
588 } else {
589 Some(detected)
590 }
591}
592
593fn analyze_drizzle_usage(content: &str, file_path: &Path) -> Option<f32> {
595 let file_name = file_path.file_name()?.to_string_lossy();
596 let mut confidence: f32 = 0.0;
597
598 if content.contains("drizzle-orm") {
600 confidence += 0.3;
601 }
602
603 if file_name.contains("schema") || file_name.contains("db.ts") || file_name.contains("database")
605 {
606 if content.contains("pgTable")
607 || content.contains("mysqlTable")
608 || content.contains("sqliteTable")
609 {
610 confidence += 0.4;
611 }
612 if content.contains("pgEnum") || content.contains("relations") {
613 confidence += 0.3;
614 }
615 }
616
617 if content.contains("from 'drizzle-orm/pg-core'")
619 || content.contains("from 'drizzle-orm/mysql-core'")
620 || content.contains("from 'drizzle-orm/sqlite-core'")
621 {
622 confidence += 0.3;
623 }
624
625 if content.contains("db.select()")
627 || content.contains("db.insert()")
628 || content.contains("db.update()")
629 || content.contains("db.delete()")
630 {
631 confidence += 0.2;
632 }
633
634 if content.contains("drizzle(")
636 && (content.contains("connectionString") || content.contains("postgres("))
637 {
638 confidence += 0.2;
639 }
640
641 if content.contains("drizzle.config") || file_name.contains("migrate") {
643 confidence += 0.2;
644 }
645
646 if content.contains(".prepare()") && content.contains("drizzle") {
648 confidence += 0.1;
649 }
650
651 if confidence > 0.0 {
652 Some(confidence.min(1.0_f32))
653 } else {
654 None
655 }
656}
657
658fn analyze_prisma_usage(content: &str, file_path: &Path) -> Option<f32> {
660 let file_name = file_path.file_name()?.to_string_lossy();
661 let mut confidence: f32 = 0.0;
662 let mut has_prisma_import = false;
663
664 if content.contains("@prisma/client") || content.contains("from '@prisma/client'") {
666 confidence += 0.4;
667 has_prisma_import = true;
668 }
669
670 if file_name == "schema.prisma"
672 && (content.contains("model ")
673 || content.contains("generator ")
674 || content.contains("datasource "))
675 {
676 confidence += 0.6;
677 has_prisma_import = true;
678 }
679
680 if has_prisma_import {
682 if content.contains("new PrismaClient") || content.contains("PrismaClient()") {
684 confidence += 0.3;
685 }
686
687 if content.contains("prisma.")
689 && (content.contains(".findUnique(")
690 || content.contains(".findFirst(")
691 || content.contains(".upsert(")
692 || content.contains(".$connect()")
693 || content.contains(".$disconnect()"))
694 {
695 confidence += 0.2;
696 }
697 }
698
699 if confidence > 0.0 && has_prisma_import {
701 Some(confidence.min(1.0_f32))
702 } else {
703 None
704 }
705}
706
707fn analyze_encore_usage(content: &str, file_path: &Path) -> Option<f32> {
709 let file_name = file_path.file_name()?.to_string_lossy();
710 let mut confidence: f32 = 0.0;
711
712 if content.contains("// Code generated by the Encore") || content.contains("DO NOT EDIT") {
714 return None;
715 }
716
717 if file_name.contains("client.ts") || file_name.contains("client.js") {
719 return None;
720 }
721
722 let mut has_service_patterns = false;
724
725 if file_name.contains("encore.service") || file_name.contains("service.ts") {
727 confidence += 0.4;
728 has_service_patterns = true;
729 }
730
731 if content.contains("encore.dev/api")
733 && (content.contains("export") || content.contains("api."))
734 {
735 confidence += 0.4;
736 has_service_patterns = true;
737 }
738
739 if content.contains("SQLDatabase") && content.contains("encore.dev") {
741 confidence += 0.3;
742 has_service_patterns = true;
743 }
744
745 if content.contains("secret(") && content.contains("encore.dev/config") {
747 confidence += 0.3;
748 has_service_patterns = true;
749 }
750
751 if content.contains("Topic") && content.contains("encore.dev/pubsub") {
753 confidence += 0.3;
754 has_service_patterns = true;
755 }
756
757 if content.contains("cron") && content.contains("encore.dev") {
759 confidence += 0.2;
760 has_service_patterns = true;
761 }
762
763 if confidence > 0.0 && has_service_patterns {
765 Some(confidence.min(1.0_f32))
766 } else {
767 None
768 }
769}
770
771fn analyze_tanstack_start_usage(content: &str, file_path: &Path) -> Option<f32> {
773 let file_name = file_path.file_name()?.to_string_lossy();
774 let mut confidence: f32 = 0.0;
775 let mut has_start_patterns = false;
776
777 if (file_name == "app.config.ts" || file_name == "app.config.js")
779 && (content.contains("@tanstack/react-start") || content.contains("tanstack"))
780 {
781 confidence += 0.5;
782 has_start_patterns = true;
783 }
784
785 if file_name.contains("router.") && (file_name.ends_with(".ts") || file_name.ends_with(".tsx"))
787 {
788 if content.contains("createRouter") && content.contains("@tanstack/react-router") {
789 confidence += 0.4;
790 has_start_patterns = true;
791 }
792 if content.contains("routeTree") {
793 confidence += 0.2;
794 has_start_patterns = true;
795 }
796 }
797
798 if (file_name == "ssr.tsx" || file_name == "ssr.ts")
800 && (content.contains("createStartHandler")
801 || content.contains("@tanstack/react-start/server"))
802 {
803 confidence += 0.5;
804 has_start_patterns = true;
805 }
806
807 if file_name == "client.tsx" || file_name == "client.ts" {
809 if content.contains("StartClient") && content.contains("@tanstack/react-start") {
810 confidence += 0.5;
811 has_start_patterns = true;
812 }
813 if content.contains("hydrateRoot") && content.contains("createRouter") {
814 confidence += 0.3;
815 has_start_patterns = true;
816 }
817 }
818
819 if file_name == "__root.tsx" || file_name == "__root.ts" {
821 if content.contains("createRootRoute") && content.contains("@tanstack/react-router") {
822 confidence += 0.4;
823 has_start_patterns = true;
824 }
825 if content.contains("HeadContent") && content.contains("Scripts") {
826 confidence += 0.3;
827 has_start_patterns = true;
828 }
829 }
830
831 if file_path.to_string_lossy().contains("routes/")
833 && content.contains("createFileRoute")
834 && content.contains("@tanstack/react-router")
835 {
836 confidence += 0.3;
837 has_start_patterns = true;
838 }
839
840 if content.contains("createServerFn") && content.contains("@tanstack/react-start") {
842 confidence += 0.4;
843 has_start_patterns = true;
844 }
845
846 if content.contains("from '@tanstack/react-start'") {
848 confidence += 0.3;
849 has_start_patterns = true;
850 }
851
852 if file_name == "vinxi.config.ts" || file_name == "vinxi.config.js" {
854 confidence += 0.2;
855 has_start_patterns = true;
856 }
857
858 if confidence > 0.0 && has_start_patterns {
860 Some(confidence.min(1.0_f32))
861 } else {
862 None
863 }
864}
865
866fn get_js_technology_rules() -> Vec<TechnologyRule> {
868 vec![
869 TechnologyRule {
871 name: "Next.js".to_string(),
872 category: TechnologyCategory::MetaFramework,
873 confidence: 0.95,
874 dependency_patterns: vec!["next".to_string()],
875 requires: vec!["React".to_string()],
876 conflicts_with: vec![
877 "Tanstack Start".to_string(),
878 "React Router v7".to_string(),
879 "SvelteKit".to_string(),
880 "Nuxt.js".to_string(),
881 "Expo".to_string(),
882 ],
883 is_primary_indicator: true,
884 alternative_names: vec!["nextjs".to_string()],
885 file_indicators: vec![
886 "next.config.js".to_string(),
887 "next.config.ts".to_string(),
888 "pages/".to_string(),
889 "app/".to_string(),
890 ],
891 },
892 TechnologyRule {
893 name: "Tanstack Start".to_string(),
894 category: TechnologyCategory::MetaFramework,
895 confidence: 0.95,
896 dependency_patterns: vec!["@tanstack/react-start".to_string()],
897 requires: vec!["React".to_string()],
898 conflicts_with: vec![
899 "Next.js".to_string(),
900 "React Router v7".to_string(),
901 "SvelteKit".to_string(),
902 "Nuxt.js".to_string(),
903 ],
904 is_primary_indicator: true,
905 alternative_names: vec!["tanstack-start".to_string(), "TanStack Start".to_string()],
906 file_indicators: vec![
907 "app.config.ts".to_string(),
908 "app.config.js".to_string(),
909 "app/routes/".to_string(),
910 "vite.config.ts".to_string(),
911 ],
912 },
913 TechnologyRule {
914 name: "React Router v7".to_string(),
915 category: TechnologyCategory::MetaFramework,
916 confidence: 0.95,
917 dependency_patterns: vec![
918 "react-router".to_string(),
919 "react-dom".to_string(),
920 "react-router-dom".to_string(),
921 ],
922 requires: vec!["React".to_string()],
923 conflicts_with: vec![
924 "Next.js".to_string(),
925 "Tanstack Start".to_string(),
926 "SvelteKit".to_string(),
927 "Nuxt.js".to_string(),
928 "React Native".to_string(),
929 "Expo".to_string(),
930 ],
931 is_primary_indicator: true,
932 alternative_names: vec!["remix".to_string(), "react-router".to_string()],
933 file_indicators: vec![],
934 },
935 TechnologyRule {
936 name: "SvelteKit".to_string(),
937 category: TechnologyCategory::MetaFramework,
938 confidence: 0.95,
939 dependency_patterns: vec!["@sveltejs/kit".to_string()],
940 requires: vec!["Svelte".to_string()],
941 conflicts_with: vec![
942 "Next.js".to_string(),
943 "Tanstack Start".to_string(),
944 "React Router v7".to_string(),
945 "Nuxt.js".to_string(),
946 ],
947 is_primary_indicator: true,
948 alternative_names: vec!["svelte-kit".to_string()],
949 file_indicators: vec![],
950 },
951 TechnologyRule {
952 name: "Nuxt.js".to_string(),
953 category: TechnologyCategory::MetaFramework,
954 confidence: 0.95,
955 dependency_patterns: vec!["nuxt".to_string(), "@nuxt/core".to_string()],
956 requires: vec!["Vue.js".to_string()],
957 conflicts_with: vec![
958 "Next.js".to_string(),
959 "Tanstack Start".to_string(),
960 "React Router v7".to_string(),
961 "SvelteKit".to_string(),
962 ],
963 is_primary_indicator: true,
964 alternative_names: vec!["nuxtjs".to_string()],
965 file_indicators: vec![],
966 },
967 TechnologyRule {
968 name: "Astro".to_string(),
969 category: TechnologyCategory::MetaFramework,
970 confidence: 0.95,
971 dependency_patterns: vec!["astro".to_string()],
972 requires: vec![],
973 conflicts_with: vec![],
974 is_primary_indicator: true,
975 alternative_names: vec![],
976 file_indicators: vec![],
977 },
978 TechnologyRule {
979 name: "SolidStart".to_string(),
980 category: TechnologyCategory::MetaFramework,
981 confidence: 0.95,
982 dependency_patterns: vec!["solid-start".to_string()],
983 requires: vec!["SolidJS".to_string()],
984 conflicts_with: vec![
985 "Next.js".to_string(),
986 "Tanstack Start".to_string(),
987 "React Router v7".to_string(),
988 "SvelteKit".to_string(),
989 ],
990 is_primary_indicator: true,
991 alternative_names: vec![],
992 file_indicators: vec![],
993 },
994 TechnologyRule {
996 name: "React Native".to_string(),
997 category: TechnologyCategory::FrontendFramework,
998 confidence: 0.95,
999 dependency_patterns: vec!["react-native".to_string()],
1000 requires: vec!["React".to_string()],
1001 conflicts_with: vec![
1002 "Next.js".to_string(),
1003 "React Router v7".to_string(),
1004 "SvelteKit".to_string(),
1005 "Nuxt.js".to_string(),
1006 "Tanstack Start".to_string(),
1007 ],
1008 is_primary_indicator: true,
1009 alternative_names: vec!["reactnative".to_string()],
1010 file_indicators: vec![
1011 "react-native.config.js".to_string(),
1012 "android/".to_string(),
1013 "ios/".to_string(),
1014 ],
1015 },
1016 TechnologyRule {
1017 name: "Expo".to_string(),
1018 category: TechnologyCategory::MetaFramework,
1019 confidence: 1.0,
1020 dependency_patterns: vec![
1021 "expo".to_string(),
1022 "expo-router".to_string(),
1023 "@expo/vector-icons".to_string(),
1024 ],
1025 requires: vec!["React Native".to_string()],
1026 conflicts_with: vec![
1027 "Next.js".to_string(),
1028 "React Router v7".to_string(),
1029 "SvelteKit".to_string(),
1030 "Nuxt.js".to_string(),
1031 "Tanstack Start".to_string(),
1032 ],
1033 is_primary_indicator: true,
1034 alternative_names: vec![],
1035 file_indicators: vec![
1036 "app.json".to_string(),
1037 "app.config.js".to_string(),
1038 "app.config.ts".to_string(),
1039 ],
1040 },
1041 TechnologyRule {
1043 name: "Angular".to_string(),
1044 category: TechnologyCategory::FrontendFramework,
1045 confidence: 0.90,
1046 dependency_patterns: vec!["@angular/core".to_string()],
1047 requires: vec![],
1048 conflicts_with: vec![],
1049 is_primary_indicator: true,
1050 alternative_names: vec!["angular".to_string()],
1051 file_indicators: vec![],
1052 },
1053 TechnologyRule {
1054 name: "Svelte".to_string(),
1055 category: TechnologyCategory::FrontendFramework,
1056 confidence: 0.95,
1057 dependency_patterns: vec!["svelte".to_string()],
1058 requires: vec![],
1059 conflicts_with: vec![],
1060 is_primary_indicator: false, alternative_names: vec![],
1062 file_indicators: vec![],
1063 },
1064 TechnologyRule {
1066 name: "React".to_string(),
1067 category: TechnologyCategory::Library(LibraryType::UI),
1068 confidence: 0.90,
1069 dependency_patterns: vec!["react".to_string()],
1070 requires: vec![],
1071 conflicts_with: vec![],
1072 is_primary_indicator: false, alternative_names: vec!["reactjs".to_string()],
1074 file_indicators: vec![],
1075 },
1076 TechnologyRule {
1077 name: "Vue.js".to_string(),
1078 category: TechnologyCategory::Library(LibraryType::UI),
1079 confidence: 0.90,
1080 dependency_patterns: vec!["vue".to_string()],
1081 requires: vec![],
1082 conflicts_with: vec![],
1083 is_primary_indicator: false,
1084 alternative_names: vec!["vuejs".to_string()],
1085 file_indicators: vec![],
1086 },
1087 TechnologyRule {
1088 name: "SolidJS".to_string(),
1089 category: TechnologyCategory::Library(LibraryType::UI),
1090 confidence: 0.95,
1091 dependency_patterns: vec!["solid-js".to_string()],
1092 requires: vec![],
1093 conflicts_with: vec![],
1094 is_primary_indicator: false,
1095 alternative_names: vec!["solid".to_string()],
1096 file_indicators: vec![],
1097 },
1098 TechnologyRule {
1099 name: "HTMX".to_string(),
1100 category: TechnologyCategory::Library(LibraryType::UI),
1101 confidence: 0.95,
1102 dependency_patterns: vec!["htmx.org".to_string()],
1103 requires: vec![],
1104 conflicts_with: vec![],
1105 is_primary_indicator: false,
1106 alternative_names: vec!["htmx".to_string()],
1107 file_indicators: vec![],
1108 },
1109 TechnologyRule {
1111 name: "Express.js".to_string(),
1112 category: TechnologyCategory::BackendFramework,
1113 confidence: 0.95,
1114 dependency_patterns: vec!["express".to_string()],
1115 requires: vec![],
1116 conflicts_with: vec![],
1117 is_primary_indicator: true,
1118 alternative_names: vec!["express".to_string()],
1119 file_indicators: vec![],
1120 },
1121 TechnologyRule {
1122 name: "Fastify".to_string(),
1123 category: TechnologyCategory::BackendFramework,
1124 confidence: 0.95,
1125 dependency_patterns: vec!["fastify".to_string()],
1126 requires: vec![],
1127 conflicts_with: vec![],
1128 is_primary_indicator: true,
1129 alternative_names: vec![],
1130 file_indicators: vec![],
1131 },
1132 TechnologyRule {
1133 name: "Nest.js".to_string(),
1134 category: TechnologyCategory::BackendFramework,
1135 confidence: 0.95,
1136 dependency_patterns: vec!["@nestjs/core".to_string()],
1137 requires: vec![],
1138 conflicts_with: vec![],
1139 is_primary_indicator: true,
1140 alternative_names: vec!["nestjs".to_string()],
1141 file_indicators: vec![],
1142 },
1143 TechnologyRule {
1144 name: "Hono".to_string(),
1145 category: TechnologyCategory::BackendFramework,
1146 confidence: 0.95,
1147 dependency_patterns: vec!["hono".to_string()],
1148 requires: vec![],
1149 conflicts_with: vec![],
1150 is_primary_indicator: true,
1151 alternative_names: vec![],
1152 file_indicators: vec![],
1153 },
1154 TechnologyRule {
1155 name: "Elysia".to_string(),
1156 category: TechnologyCategory::BackendFramework,
1157 confidence: 0.95,
1158 dependency_patterns: vec!["elysia".to_string()],
1159 requires: vec![],
1160 conflicts_with: vec![],
1161 is_primary_indicator: true,
1162 alternative_names: vec![],
1163 file_indicators: vec![],
1164 },
1165 TechnologyRule {
1166 name: "Encore".to_string(),
1167 category: TechnologyCategory::BackendFramework,
1168 confidence: 0.95,
1169 dependency_patterns: vec!["encore.dev".to_string(), "encore".to_string()],
1170 requires: vec![],
1171 conflicts_with: vec!["Next.js".to_string()],
1172 is_primary_indicator: true,
1173 alternative_names: vec!["encore-ts-starter".to_string()],
1174 file_indicators: vec![
1175 "encore.app".to_string(),
1176 "encore.service.ts".to_string(),
1177 "encore.service.js".to_string(),
1178 ],
1179 },
1180 TechnologyRule {
1182 name: "Vite".to_string(),
1183 category: TechnologyCategory::BuildTool,
1184 confidence: 0.80,
1185 dependency_patterns: vec!["vite".to_string()],
1186 requires: vec![],
1187 conflicts_with: vec![],
1188 is_primary_indicator: false,
1189 alternative_names: vec![],
1190 file_indicators: vec![],
1191 },
1192 TechnologyRule {
1193 name: "Webpack".to_string(),
1194 category: TechnologyCategory::BuildTool,
1195 confidence: 0.80,
1196 dependency_patterns: vec!["webpack".to_string()],
1197 requires: vec![],
1198 conflicts_with: vec![],
1199 is_primary_indicator: false,
1200 alternative_names: vec![],
1201 file_indicators: vec![],
1202 },
1203 TechnologyRule {
1205 name: "Prisma".to_string(),
1206 category: TechnologyCategory::Database,
1207 confidence: 0.90,
1208 dependency_patterns: vec!["prisma".to_string(), "@prisma/client".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: "Drizzle ORM".to_string(),
1217 category: TechnologyCategory::Database,
1218 confidence: 0.90,
1219 dependency_patterns: vec!["drizzle-orm".to_string(), "drizzle-kit".to_string()],
1220 requires: vec![],
1221 conflicts_with: vec![],
1222 is_primary_indicator: false,
1223 alternative_names: vec!["drizzle".to_string()],
1224 file_indicators: vec![],
1225 },
1226 TechnologyRule {
1227 name: "Sequelize".to_string(),
1228 category: TechnologyCategory::Database,
1229 confidence: 0.90,
1230 dependency_patterns: vec!["sequelize".to_string()],
1231 requires: vec![],
1232 conflicts_with: vec![],
1233 is_primary_indicator: false,
1234 alternative_names: vec![],
1235 file_indicators: vec![],
1236 },
1237 TechnologyRule {
1238 name: "TypeORM".to_string(),
1239 category: TechnologyCategory::Database,
1240 confidence: 0.90,
1241 dependency_patterns: vec!["typeorm".to_string()],
1242 requires: vec![],
1243 conflicts_with: vec![],
1244 is_primary_indicator: false,
1245 alternative_names: vec![],
1246 file_indicators: vec![],
1247 },
1248 TechnologyRule {
1249 name: "MikroORM".to_string(),
1250 category: TechnologyCategory::Database,
1251 confidence: 0.90,
1252 dependency_patterns: vec![
1253 "@mikro-orm/core".to_string(),
1254 "@mikro-orm/postgresql".to_string(),
1255 "@mikro-orm/mysql".to_string(),
1256 "@mikro-orm/sqlite".to_string(),
1257 "@mikro-orm/mongodb".to_string(),
1258 ],
1259 requires: vec![],
1260 conflicts_with: vec![],
1261 is_primary_indicator: false,
1262 alternative_names: vec!["mikro-orm".to_string()],
1263 file_indicators: vec![],
1264 },
1265 TechnologyRule {
1266 name: "Mongoose".to_string(),
1267 category: TechnologyCategory::Database,
1268 confidence: 0.95,
1269 dependency_patterns: vec!["mongoose".to_string()],
1270 requires: vec![],
1271 conflicts_with: vec![],
1272 is_primary_indicator: false,
1273 alternative_names: vec![],
1274 file_indicators: vec![],
1275 },
1276 TechnologyRule {
1277 name: "Typegoose".to_string(),
1278 category: TechnologyCategory::Database,
1279 confidence: 0.90,
1280 dependency_patterns: vec!["@typegoose/typegoose".to_string()],
1281 requires: vec!["Mongoose".to_string()],
1282 conflicts_with: vec![],
1283 is_primary_indicator: false,
1284 alternative_names: vec![],
1285 file_indicators: vec![],
1286 },
1287 TechnologyRule {
1288 name: "Objection.js".to_string(),
1289 category: TechnologyCategory::Database,
1290 confidence: 0.90,
1291 dependency_patterns: vec!["objection".to_string()],
1292 requires: vec!["Knex.js".to_string()],
1293 conflicts_with: vec![],
1294 is_primary_indicator: false,
1295 alternative_names: vec!["objectionjs".to_string()],
1296 file_indicators: vec![],
1297 },
1298 TechnologyRule {
1299 name: "Bookshelf".to_string(),
1300 category: TechnologyCategory::Database,
1301 confidence: 0.85,
1302 dependency_patterns: vec!["bookshelf".to_string()],
1303 requires: vec!["Knex.js".to_string()],
1304 conflicts_with: vec![],
1305 is_primary_indicator: false,
1306 alternative_names: vec![],
1307 file_indicators: vec![],
1308 },
1309 TechnologyRule {
1310 name: "Waterline".to_string(),
1311 category: TechnologyCategory::Database,
1312 confidence: 0.85,
1313 dependency_patterns: vec![
1314 "waterline".to_string(),
1315 "sails-mysql".to_string(),
1316 "sails-postgresql".to_string(),
1317 "sails-disk".to_string(),
1318 ],
1319 requires: vec![],
1320 conflicts_with: vec![],
1321 is_primary_indicator: false,
1322 alternative_names: vec![],
1323 file_indicators: vec![],
1324 },
1325 TechnologyRule {
1326 name: "Knex.js".to_string(),
1327 category: TechnologyCategory::Database,
1328 confidence: 0.85,
1329 dependency_patterns: vec!["knex".to_string()],
1330 requires: vec![],
1331 conflicts_with: vec![],
1332 is_primary_indicator: false,
1333 alternative_names: vec!["knexjs".to_string()],
1334 file_indicators: vec![],
1335 },
1336 TechnologyRule {
1338 name: "Node.js".to_string(),
1339 category: TechnologyCategory::Runtime,
1340 confidence: 0.90,
1341 dependency_patterns: vec!["node".to_string()], requires: vec![],
1343 conflicts_with: vec![],
1344 is_primary_indicator: false,
1345 alternative_names: vec!["nodejs".to_string()],
1346 file_indicators: vec![],
1347 },
1348 TechnologyRule {
1349 name: "Bun".to_string(),
1350 category: TechnologyCategory::Runtime,
1351 confidence: 0.95,
1352 dependency_patterns: vec!["bun".to_string()], requires: vec![],
1354 conflicts_with: vec![],
1355 is_primary_indicator: false,
1356 alternative_names: vec![],
1357 file_indicators: vec![],
1358 },
1359 TechnologyRule {
1360 name: "Deno".to_string(),
1361 category: TechnologyCategory::Runtime,
1362 confidence: 0.95,
1363 dependency_patterns: vec!["@deno/core".to_string(), "deno".to_string()],
1364 requires: vec![],
1365 conflicts_with: vec![],
1366 is_primary_indicator: false,
1367 alternative_names: vec![],
1368 file_indicators: vec![],
1369 },
1370 TechnologyRule {
1371 name: "WinterJS".to_string(),
1372 category: TechnologyCategory::Runtime,
1373 confidence: 0.95,
1374 dependency_patterns: vec!["winterjs".to_string(), "winter-js".to_string()],
1375 requires: vec![],
1376 conflicts_with: vec![],
1377 is_primary_indicator: false,
1378 alternative_names: vec!["winter.js".to_string()],
1379 file_indicators: vec![],
1380 },
1381 TechnologyRule {
1382 name: "Cloudflare Workers".to_string(),
1383 category: TechnologyCategory::Runtime,
1384 confidence: 0.90,
1385 dependency_patterns: vec![
1386 "@cloudflare/workers-types".to_string(),
1387 "@cloudflare/vitest-pool-workers".to_string(),
1388 "wrangler".to_string(),
1389 ],
1390 requires: vec![],
1391 conflicts_with: vec![],
1392 is_primary_indicator: false,
1393 alternative_names: vec!["cloudflare-workers".to_string()],
1394 file_indicators: vec![],
1395 },
1396 TechnologyRule {
1397 name: "Vercel Edge Runtime".to_string(),
1398 category: TechnologyCategory::Runtime,
1399 confidence: 0.90,
1400 dependency_patterns: vec![
1401 "@vercel/edge-runtime".to_string(),
1402 "@edge-runtime/vm".to_string(),
1403 ],
1404 requires: vec![],
1405 conflicts_with: vec![],
1406 is_primary_indicator: false,
1407 alternative_names: vec!["vercel-edge".to_string()],
1408 file_indicators: vec![],
1409 },
1410 TechnologyRule {
1411 name: "Hermes".to_string(),
1412 category: TechnologyCategory::Runtime,
1413 confidence: 0.85,
1414 dependency_patterns: vec!["hermes-engine".to_string()],
1415 requires: vec!["React Native".to_string()],
1416 conflicts_with: vec![],
1417 is_primary_indicator: false,
1418 alternative_names: vec![],
1419 file_indicators: vec![],
1420 },
1421 TechnologyRule {
1422 name: "Electron".to_string(),
1423 category: TechnologyCategory::Runtime,
1424 confidence: 0.95,
1425 dependency_patterns: vec!["electron".to_string()],
1426 requires: vec![],
1427 conflicts_with: vec![],
1428 is_primary_indicator: false,
1429 alternative_names: vec![],
1430 file_indicators: vec![],
1431 },
1432 TechnologyRule {
1433 name: "Tauri".to_string(),
1434 category: TechnologyCategory::Runtime,
1435 confidence: 0.95,
1436 dependency_patterns: vec!["@tauri-apps/cli".to_string(), "@tauri-apps/api".to_string()],
1437 requires: vec![],
1438 conflicts_with: vec!["Electron".to_string()],
1439 is_primary_indicator: false,
1440 alternative_names: vec![],
1441 file_indicators: vec![],
1442 },
1443 TechnologyRule {
1444 name: "QuickJS".to_string(),
1445 category: TechnologyCategory::Runtime,
1446 confidence: 0.85,
1447 dependency_patterns: vec!["quickjs".to_string(), "quickjs-emscripten".to_string()],
1448 requires: vec![],
1449 conflicts_with: vec![],
1450 is_primary_indicator: false,
1451 alternative_names: vec![],
1452 file_indicators: vec![],
1453 },
1454 TechnologyRule {
1456 name: "Jest".to_string(),
1457 category: TechnologyCategory::Testing,
1458 confidence: 0.85,
1459 dependency_patterns: vec!["jest".to_string()],
1460 requires: vec![],
1461 conflicts_with: vec![],
1462 is_primary_indicator: false,
1463 alternative_names: vec![],
1464 file_indicators: vec![],
1465 },
1466 TechnologyRule {
1467 name: "Vitest".to_string(),
1468 category: TechnologyCategory::Testing,
1469 confidence: 0.85,
1470 dependency_patterns: vec!["vitest".to_string()],
1471 requires: vec![],
1472 conflicts_with: vec![],
1473 is_primary_indicator: false,
1474 alternative_names: vec![],
1475 file_indicators: vec![],
1476 },
1477 ]
1478}