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 if let Some(structure_detections) = detect_by_project_structure(language, rules) {
86 detected.extend(structure_detections);
87 }
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 && !has_tanstack_deps {
136 if let Some(expo_rule) = rules.iter().find(|r| r.name == "Expo") {
137 detected.push(DetectedTechnology {
138 name: expo_rule.name.clone(),
139 version: None,
140 category: expo_rule.category.clone(),
141 confidence: 1.0, requires: expo_rule.requires.clone(),
143 conflicts_with: expo_rule.conflicts_with.clone(),
144 is_primary: expo_rule.is_primary_indicator,
145 file_indicators: expo_rule.file_indicators.clone(),
146 });
147 }
148 } else if has_tanstack_deps && !has_expo_deps {
149 if let Some(tanstack_rule) =
150 rules.iter().find(|r| r.name == "Tanstack Start")
151 {
152 detected.push(DetectedTechnology {
153 name: tanstack_rule.name.clone(),
154 version: None,
155 category: tanstack_rule.category.clone(),
156 confidence: 1.0, requires: tanstack_rule.requires.clone(),
158 conflicts_with: tanstack_rule.conflicts_with.clone(),
159 is_primary: tanstack_rule.is_primary_indicator,
160 file_indicators: tanstack_rule.file_indicators.clone(),
161 });
162 }
163 }
164 } else {
166 if let Some(expo_rule) = rules.iter().find(|r| r.name == "Expo") {
168 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 }
181 else if file_name.starts_with("next.config.") {
183 if let Some(nextjs_rule) = rules.iter().find(|r| r.name == "Next.js") {
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 }
196 else if file_name == "react-native.config.js" {
198 if let Some(rn_rule) = rules.iter().find(|r| r.name == "React Native") {
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 }
211 else if file_name == "encore.app"
213 || file_name == "encore.service.ts"
214 || file_name == "encore.service.js"
215 {
216 if let Some(encore_rule) = rules.iter().find(|r| r.name == "Encore") {
217 detected.push(DetectedTechnology {
218 name: encore_rule.name.clone(),
219 version: None,
220 category: encore_rule.category.clone(),
221 confidence: 1.0, requires: encore_rule.requires.clone(),
223 conflicts_with: encore_rule.conflicts_with.clone(),
224 is_primary: encore_rule.is_primary_indicator,
225 file_indicators: encore_rule.file_indicators.clone(),
226 });
227 }
228 }
229 }
230 }
231
232 if detected.is_empty() {
233 None
234 } else {
235 Some(detected)
236 }
237}
238
239fn detect_by_project_structure(
241 language: &DetectedLanguage,
242 rules: &[TechnologyRule],
243) -> Option<Vec<DetectedTechnology>> {
244 let mut detected = Vec::new();
245 let mut has_android_dir = false;
246 let mut has_ios_dir = false;
247 let mut has_pages_dir = false;
248 let mut has_app_dir = false;
249 let mut has_app_routes_dir = false;
250 let mut has_encore_app_file = false;
251 let mut has_encore_service_files = false;
252 let mut has_app_json = false;
253 let mut has_app_js_ts = false;
254 let mut has_next_config = false;
255 let mut has_tanstack_config = false;
256
257 for file_path in &language.files {
259 if let Some(parent) = file_path.parent() {
260 let path_str = parent.to_string_lossy();
261 let file_name = file_path.file_name().and_then(|n| n.to_str()).unwrap_or("");
262
263 if path_str.contains("android") {
265 has_android_dir = true;
266 } else if path_str.contains("ios") {
267 has_ios_dir = true;
268 }
269 else if has_path_component(parent, "pages") {
271 has_pages_dir = true;
272 } else if has_path_component(parent, "app")
273 && !file_name.contains("app.config")
274 && !file_name.contains("encore.app")
275 {
276 has_app_dir = true;
277 }
278 else if has_app_routes(parent) {
280 has_app_routes_dir = true;
281 }
282 else if file_name == "encore.app" {
284 has_encore_app_file = true;
285 } else if file_name.contains("encore.service.") {
286 has_encore_service_files = true;
287 }
288 else if file_name == "app.json" {
290 has_app_json = true;
291 } else if file_name == "App.js" || file_name == "App.tsx" {
292 has_app_js_ts = true;
293 }
294
295 if file_name.starts_with("next.config.") {
297 has_next_config = true;
298 }
299 if file_name == "app.config.ts"
300 || file_name == "app.config.js"
301 || file_name.starts_with("vinxi.config")
302 {
303 has_tanstack_config = true;
304 }
305 }
306 }
307
308 let has_expo_deps = language
310 .main_dependencies
311 .iter()
312 .any(|dep| dep == "expo" || dep == "react-native");
313 let has_next_dep = language
314 .main_dependencies
315 .iter()
316 .any(|dep| dep == "next" || dep.starts_with("next@"));
317 let has_tanstack_dep = language.main_dependencies.iter().any(|dep| {
318 dep.contains("tanstack/react-start")
319 || dep.contains("tanstack-start")
320 || dep.contains("vinxi")
321 });
322
323 if has_encore_app_file || has_encore_service_files {
325 if let Some(encore_rule) = rules.iter().find(|r| r.name == "Encore") {
327 detected.push(DetectedTechnology {
328 name: encore_rule.name.clone(),
329 version: None,
330 category: encore_rule.category.clone(),
331 confidence: 1.0, requires: encore_rule.requires.clone(),
333 conflicts_with: encore_rule.conflicts_with.clone(),
334 is_primary: encore_rule.is_primary_indicator,
335 file_indicators: encore_rule.file_indicators.clone(),
336 });
337 }
338 } else if has_app_routes_dir && (has_tanstack_dep || has_tanstack_config) {
339 if let Some(tanstack_rule) = rules.iter().find(|r| r.name == "Tanstack Start") {
341 detected.push(DetectedTechnology {
342 name: tanstack_rule.name.clone(),
343 version: None,
344 category: tanstack_rule.category.clone(),
345 confidence: 0.9, requires: tanstack_rule.requires.clone(),
347 conflicts_with: tanstack_rule.conflicts_with.clone(),
348 is_primary: tanstack_rule.is_primary_indicator,
349 file_indicators: tanstack_rule.file_indicators.clone(),
350 });
351 }
352 } else if (has_pages_dir || has_app_dir) && (has_next_dep || has_next_config) {
353 if let Some(nextjs_rule) = rules.iter().find(|r| r.name == "Next.js") {
355 detected.push(DetectedTechnology {
356 name: nextjs_rule.name.clone(),
357 version: None,
358 category: nextjs_rule.category.clone(),
359 confidence: 0.9, requires: nextjs_rule.requires.clone(),
361 conflicts_with: nextjs_rule.conflicts_with.clone(),
362 is_primary: nextjs_rule.is_primary_indicator,
363 file_indicators: nextjs_rule.file_indicators.clone(),
364 });
365 }
366 } else if (has_app_json || has_app_js_ts) && has_expo_deps {
367 if let Some(expo_rule) = rules.iter().find(|r| r.name == "Expo") {
369 detected.push(DetectedTechnology {
370 name: expo_rule.name.clone(),
371 version: None,
372 category: expo_rule.category.clone(),
373 confidence: 1.0, requires: expo_rule.requires.clone(),
375 conflicts_with: expo_rule.conflicts_with.clone(),
376 is_primary: expo_rule.is_primary_indicator,
377 file_indicators: expo_rule.file_indicators.clone(),
378 });
379 }
380 } else if has_android_dir && has_ios_dir {
381 if let Some(rn_rule) = rules.iter().find(|r| r.name == "React Native") {
383 detected.push(DetectedTechnology {
384 name: rn_rule.name.clone(),
385 version: None,
386 category: rn_rule.category.clone(),
387 confidence: 0.9, requires: rn_rule.requires.clone(),
389 conflicts_with: rn_rule.conflicts_with.clone(),
390 is_primary: rn_rule.is_primary_indicator,
391 file_indicators: rn_rule.file_indicators.clone(),
392 });
393 }
394 }
395
396 if detected.is_empty() {
397 None
398 } else {
399 Some(detected)
400 }
401}
402
403fn has_path_component(path: &Path, target: &str) -> bool {
405 path.components()
406 .any(|c| c.as_os_str().to_string_lossy() == target)
407}
408
409fn has_app_routes(path: &Path) -> bool {
411 let components: Vec<String> = path
412 .components()
413 .map(|c| c.as_os_str().to_string_lossy().to_string())
414 .collect();
415 components
416 .windows(2)
417 .any(|w| w[0] == "app" && w[1] == "routes")
418}
419
420fn detect_by_source_patterns(
422 language: &DetectedLanguage,
423 rules: &[TechnologyRule],
424) -> Option<Vec<DetectedTechnology>> {
425 let mut detected = Vec::new();
426
427 for file_path in &language.files {
429 if let Ok(content) = std::fs::read_to_string(file_path) {
430 if content.contains("expo")
432 && (content.contains("from 'expo'")
433 || content.contains("import {") && content.contains("registerRootComponent"))
434 {
435 if let Some(expo_rule) = rules.iter().find(|r| r.name == "Expo") {
436 detected.push(DetectedTechnology {
437 name: expo_rule.name.clone(),
438 version: None,
439 category: expo_rule.category.clone(),
440 confidence: 0.8, requires: expo_rule.requires.clone(),
442 conflicts_with: expo_rule.conflicts_with.clone(),
443 is_primary: expo_rule.is_primary_indicator,
444 file_indicators: expo_rule.file_indicators.clone(),
445 });
446 }
447 }
448
449 if content.contains("next/") {
451 if let Some(nextjs_rule) = rules.iter().find(|r| r.name == "Next.js") {
452 detected.push(DetectedTechnology {
453 name: nextjs_rule.name.clone(),
454 version: None,
455 category: nextjs_rule.category.clone(),
456 confidence: 0.7, requires: nextjs_rule.requires.clone(),
458 conflicts_with: nextjs_rule.conflicts_with.clone(),
459 is_primary: nextjs_rule.is_primary_indicator,
460 file_indicators: nextjs_rule.file_indicators.clone(),
461 });
462 }
463 }
464
465 if content.contains("@tanstack/react-router") && content.contains("createFileRoute") {
467 if let Some(tanstack_rule) = rules.iter().find(|r| r.name == "Tanstack Start") {
468 detected.push(DetectedTechnology {
469 name: tanstack_rule.name.clone(),
470 version: None,
471 category: tanstack_rule.category.clone(),
472 confidence: 0.7, requires: tanstack_rule.requires.clone(),
474 conflicts_with: tanstack_rule.conflicts_with.clone(),
475 is_primary: tanstack_rule.is_primary_indicator,
476 file_indicators: tanstack_rule.file_indicators.clone(),
477 });
478 }
479 }
480
481 if content.contains("react-router") && content.contains("BrowserRouter") {
483 if let Some(rr_rule) = rules.iter().find(|r| r.name == "React Router v7") {
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
499 if detected.is_empty() {
500 None
501 } else {
502 Some(detected)
503 }
504}
505
506fn detect_technologies_from_source_files(
508 language: &DetectedLanguage,
509 rules: &[TechnologyRule],
510) -> Option<Vec<DetectedTechnology>> {
511 let mut detected = Vec::new();
512
513 for file_path in &language.files {
515 if let Ok(content) = fs::read_to_string(file_path) {
516 if let Some(drizzle_confidence) = analyze_drizzle_usage(&content, file_path) {
518 if let Some(drizzle_rule) = rules.iter().find(|r| r.name == "Drizzle ORM") {
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
532 if let Some(prisma_confidence) = analyze_prisma_usage(&content, file_path) {
534 if let Some(prisma_rule) = rules.iter().find(|r| r.name == "Prisma") {
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
548 if let Some(encore_confidence) = analyze_encore_usage(&content, file_path) {
550 if let Some(encore_rule) = rules.iter().find(|r| r.name == "Encore") {
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
564 if let Some(tanstack_confidence) = analyze_tanstack_start_usage(&content, file_path) {
566 if let Some(tanstack_rule) = rules.iter().find(|r| r.name == "Tanstack Start") {
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
587 if detected.is_empty() {
588 None
589 } else {
590 Some(detected)
591 }
592}
593
594fn analyze_drizzle_usage(content: &str, file_path: &Path) -> Option<f32> {
596 let file_name = file_path.file_name()?.to_string_lossy();
597 let mut confidence: f32 = 0.0;
598
599 if content.contains("drizzle-orm") {
601 confidence += 0.3;
602 }
603
604 if file_name.contains("schema") || file_name.contains("db.ts") || file_name.contains("database")
606 {
607 if content.contains("pgTable")
608 || content.contains("mysqlTable")
609 || content.contains("sqliteTable")
610 {
611 confidence += 0.4;
612 }
613 if content.contains("pgEnum") || content.contains("relations") {
614 confidence += 0.3;
615 }
616 }
617
618 if content.contains("from 'drizzle-orm/pg-core'")
620 || content.contains("from 'drizzle-orm/mysql-core'")
621 || content.contains("from 'drizzle-orm/sqlite-core'")
622 {
623 confidence += 0.3;
624 }
625
626 if content.contains("db.select()")
628 || content.contains("db.insert()")
629 || content.contains("db.update()")
630 || content.contains("db.delete()")
631 {
632 confidence += 0.2;
633 }
634
635 if content.contains("drizzle(")
637 && (content.contains("connectionString") || content.contains("postgres("))
638 {
639 confidence += 0.2;
640 }
641
642 if content.contains("drizzle.config") || file_name.contains("migrate") {
644 confidence += 0.2;
645 }
646
647 if content.contains(".prepare()") && content.contains("drizzle") {
649 confidence += 0.1;
650 }
651
652 if confidence > 0.0 {
653 Some(confidence.min(1.0_f32))
654 } else {
655 None
656 }
657}
658
659fn analyze_prisma_usage(content: &str, file_path: &Path) -> Option<f32> {
661 let file_name = file_path.file_name()?.to_string_lossy();
662 let mut confidence: f32 = 0.0;
663 let mut has_prisma_import = false;
664
665 if content.contains("@prisma/client") || content.contains("from '@prisma/client'") {
667 confidence += 0.4;
668 has_prisma_import = true;
669 }
670
671 if file_name == "schema.prisma" {
673 if content.contains("model ")
674 || content.contains("generator ")
675 || content.contains("datasource ")
676 {
677 confidence += 0.6;
678 has_prisma_import = true;
679 }
680 }
681
682 if has_prisma_import {
684 if content.contains("new PrismaClient") || content.contains("PrismaClient()") {
686 confidence += 0.3;
687 }
688
689 if content.contains("prisma.")
691 && (content.contains(".findUnique(")
692 || content.contains(".findFirst(")
693 || content.contains(".upsert(")
694 || content.contains(".$connect()")
695 || content.contains(".$disconnect()"))
696 {
697 confidence += 0.2;
698 }
699 }
700
701 if confidence > 0.0 && has_prisma_import {
703 Some(confidence.min(1.0_f32))
704 } else {
705 None
706 }
707}
708
709fn analyze_encore_usage(content: &str, file_path: &Path) -> Option<f32> {
711 let file_name = file_path.file_name()?.to_string_lossy();
712 let mut confidence: f32 = 0.0;
713
714 if content.contains("// Code generated by the Encore") || content.contains("DO NOT EDIT") {
716 return None;
717 }
718
719 if file_name.contains("client.ts") || file_name.contains("client.js") {
721 return None;
722 }
723
724 let mut has_service_patterns = false;
726
727 if file_name.contains("encore.service") || file_name.contains("service.ts") {
729 confidence += 0.4;
730 has_service_patterns = true;
731 }
732
733 if content.contains("encore.dev/api")
735 && (content.contains("export") || content.contains("api."))
736 {
737 confidence += 0.4;
738 has_service_patterns = true;
739 }
740
741 if content.contains("SQLDatabase") && content.contains("encore.dev") {
743 confidence += 0.3;
744 has_service_patterns = true;
745 }
746
747 if content.contains("secret(") && content.contains("encore.dev/config") {
749 confidence += 0.3;
750 has_service_patterns = true;
751 }
752
753 if content.contains("Topic") && content.contains("encore.dev/pubsub") {
755 confidence += 0.3;
756 has_service_patterns = true;
757 }
758
759 if content.contains("cron") && content.contains("encore.dev") {
761 confidence += 0.2;
762 has_service_patterns = true;
763 }
764
765 if confidence > 0.0 && has_service_patterns {
767 Some(confidence.min(1.0_f32))
768 } else {
769 None
770 }
771}
772
773fn analyze_tanstack_start_usage(content: &str, file_path: &Path) -> Option<f32> {
775 let file_name = file_path.file_name()?.to_string_lossy();
776 let mut confidence: f32 = 0.0;
777 let mut has_start_patterns = false;
778
779 if file_name == "app.config.ts" || file_name == "app.config.js" {
781 if content.contains("@tanstack/react-start") || content.contains("tanstack") {
782 confidence += 0.5;
783 has_start_patterns = true;
784 }
785 }
786
787 if file_name.contains("router.") && (file_name.ends_with(".ts") || file_name.ends_with(".tsx"))
789 {
790 if content.contains("createRouter") && content.contains("@tanstack/react-router") {
791 confidence += 0.4;
792 has_start_patterns = true;
793 }
794 if content.contains("routeTree") {
795 confidence += 0.2;
796 has_start_patterns = true;
797 }
798 }
799
800 if file_name == "ssr.tsx" || file_name == "ssr.ts" {
802 if content.contains("createStartHandler")
803 || content.contains("@tanstack/react-start/server")
804 {
805 confidence += 0.5;
806 has_start_patterns = true;
807 }
808 }
809
810 if file_name == "client.tsx" || file_name == "client.ts" {
812 if content.contains("StartClient") && content.contains("@tanstack/react-start") {
813 confidence += 0.5;
814 has_start_patterns = true;
815 }
816 if content.contains("hydrateRoot") && content.contains("createRouter") {
817 confidence += 0.3;
818 has_start_patterns = true;
819 }
820 }
821
822 if file_name == "__root.tsx" || file_name == "__root.ts" {
824 if content.contains("createRootRoute") && content.contains("@tanstack/react-router") {
825 confidence += 0.4;
826 has_start_patterns = true;
827 }
828 if content.contains("HeadContent") && content.contains("Scripts") {
829 confidence += 0.3;
830 has_start_patterns = true;
831 }
832 }
833
834 if file_path.to_string_lossy().contains("routes/") {
836 if content.contains("createFileRoute") && content.contains("@tanstack/react-router") {
837 confidence += 0.3;
838 has_start_patterns = true;
839 }
840 }
841
842 if content.contains("createServerFn") && content.contains("@tanstack/react-start") {
844 confidence += 0.4;
845 has_start_patterns = true;
846 }
847
848 if content.contains("from '@tanstack/react-start'") {
850 confidence += 0.3;
851 has_start_patterns = true;
852 }
853
854 if file_name == "vinxi.config.ts" || file_name == "vinxi.config.js" {
856 confidence += 0.2;
857 has_start_patterns = true;
858 }
859
860 if confidence > 0.0 && has_start_patterns {
862 Some(confidence.min(1.0_f32))
863 } else {
864 None
865 }
866}
867
868fn get_js_technology_rules() -> Vec<TechnologyRule> {
870 vec![
871 TechnologyRule {
873 name: "Next.js".to_string(),
874 category: TechnologyCategory::MetaFramework,
875 confidence: 0.95,
876 dependency_patterns: vec!["next".to_string()],
877 requires: vec!["React".to_string()],
878 conflicts_with: vec![
879 "Tanstack Start".to_string(),
880 "React Router v7".to_string(),
881 "SvelteKit".to_string(),
882 "Nuxt.js".to_string(),
883 "Expo".to_string(),
884 ],
885 is_primary_indicator: true,
886 alternative_names: vec!["nextjs".to_string()],
887 file_indicators: vec![
888 "next.config.js".to_string(),
889 "next.config.ts".to_string(),
890 "pages/".to_string(),
891 "app/".to_string(),
892 ],
893 },
894 TechnologyRule {
895 name: "Tanstack Start".to_string(),
896 category: TechnologyCategory::MetaFramework,
897 confidence: 0.95,
898 dependency_patterns: vec!["@tanstack/react-start".to_string()],
899 requires: vec!["React".to_string()],
900 conflicts_with: vec![
901 "Next.js".to_string(),
902 "React Router v7".to_string(),
903 "SvelteKit".to_string(),
904 "Nuxt.js".to_string(),
905 ],
906 is_primary_indicator: true,
907 alternative_names: vec!["tanstack-start".to_string(), "TanStack Start".to_string()],
908 file_indicators: vec![
909 "app.config.ts".to_string(),
910 "app.config.js".to_string(),
911 "app/routes/".to_string(),
912 "vite.config.ts".to_string(),
913 ],
914 },
915 TechnologyRule {
916 name: "React Router v7".to_string(),
917 category: TechnologyCategory::MetaFramework,
918 confidence: 0.95,
919 dependency_patterns: vec![
920 "react-router".to_string(),
921 "react-dom".to_string(),
922 "react-router-dom".to_string(),
923 ],
924 requires: vec!["React".to_string()],
925 conflicts_with: vec![
926 "Next.js".to_string(),
927 "Tanstack Start".to_string(),
928 "SvelteKit".to_string(),
929 "Nuxt.js".to_string(),
930 "React Native".to_string(),
931 "Expo".to_string(),
932 ],
933 is_primary_indicator: true,
934 alternative_names: vec!["remix".to_string(), "react-router".to_string()],
935 file_indicators: vec![],
936 },
937 TechnologyRule {
938 name: "SvelteKit".to_string(),
939 category: TechnologyCategory::MetaFramework,
940 confidence: 0.95,
941 dependency_patterns: vec!["@sveltejs/kit".to_string()],
942 requires: vec!["Svelte".to_string()],
943 conflicts_with: vec![
944 "Next.js".to_string(),
945 "Tanstack Start".to_string(),
946 "React Router v7".to_string(),
947 "Nuxt.js".to_string(),
948 ],
949 is_primary_indicator: true,
950 alternative_names: vec!["svelte-kit".to_string()],
951 file_indicators: vec![],
952 },
953 TechnologyRule {
954 name: "Nuxt.js".to_string(),
955 category: TechnologyCategory::MetaFramework,
956 confidence: 0.95,
957 dependency_patterns: vec!["nuxt".to_string(), "@nuxt/core".to_string()],
958 requires: vec!["Vue.js".to_string()],
959 conflicts_with: vec![
960 "Next.js".to_string(),
961 "Tanstack Start".to_string(),
962 "React Router v7".to_string(),
963 "SvelteKit".to_string(),
964 ],
965 is_primary_indicator: true,
966 alternative_names: vec!["nuxtjs".to_string()],
967 file_indicators: vec![],
968 },
969 TechnologyRule {
970 name: "Astro".to_string(),
971 category: TechnologyCategory::MetaFramework,
972 confidence: 0.95,
973 dependency_patterns: vec!["astro".to_string()],
974 requires: vec![],
975 conflicts_with: vec![],
976 is_primary_indicator: true,
977 alternative_names: vec![],
978 file_indicators: vec![],
979 },
980 TechnologyRule {
981 name: "SolidStart".to_string(),
982 category: TechnologyCategory::MetaFramework,
983 confidence: 0.95,
984 dependency_patterns: vec!["solid-start".to_string()],
985 requires: vec!["SolidJS".to_string()],
986 conflicts_with: vec![
987 "Next.js".to_string(),
988 "Tanstack Start".to_string(),
989 "React Router v7".to_string(),
990 "SvelteKit".to_string(),
991 ],
992 is_primary_indicator: true,
993 alternative_names: vec![],
994 file_indicators: vec![],
995 },
996 TechnologyRule {
998 name: "React Native".to_string(),
999 category: TechnologyCategory::FrontendFramework,
1000 confidence: 0.95,
1001 dependency_patterns: vec!["react-native".to_string()],
1002 requires: vec!["React".to_string()],
1003 conflicts_with: vec![
1004 "Next.js".to_string(),
1005 "React Router v7".to_string(),
1006 "SvelteKit".to_string(),
1007 "Nuxt.js".to_string(),
1008 "Tanstack Start".to_string(),
1009 ],
1010 is_primary_indicator: true,
1011 alternative_names: vec!["reactnative".to_string()],
1012 file_indicators: vec![
1013 "react-native.config.js".to_string(),
1014 "android/".to_string(),
1015 "ios/".to_string(),
1016 ],
1017 },
1018 TechnologyRule {
1019 name: "Expo".to_string(),
1020 category: TechnologyCategory::MetaFramework,
1021 confidence: 1.0,
1022 dependency_patterns: vec![
1023 "expo".to_string(),
1024 "expo-router".to_string(),
1025 "@expo/vector-icons".to_string(),
1026 ],
1027 requires: vec!["React Native".to_string()],
1028 conflicts_with: vec![
1029 "Next.js".to_string(),
1030 "React Router v7".to_string(),
1031 "SvelteKit".to_string(),
1032 "Nuxt.js".to_string(),
1033 "Tanstack Start".to_string(),
1034 ],
1035 is_primary_indicator: true,
1036 alternative_names: vec![],
1037 file_indicators: vec![
1038 "app.json".to_string(),
1039 "app.config.js".to_string(),
1040 "app.config.ts".to_string(),
1041 ],
1042 },
1043 TechnologyRule {
1045 name: "Angular".to_string(),
1046 category: TechnologyCategory::FrontendFramework,
1047 confidence: 0.90,
1048 dependency_patterns: vec!["@angular/core".to_string()],
1049 requires: vec![],
1050 conflicts_with: vec![],
1051 is_primary_indicator: true,
1052 alternative_names: vec!["angular".to_string()],
1053 file_indicators: vec![],
1054 },
1055 TechnologyRule {
1056 name: "Svelte".to_string(),
1057 category: TechnologyCategory::FrontendFramework,
1058 confidence: 0.95,
1059 dependency_patterns: vec!["svelte".to_string()],
1060 requires: vec![],
1061 conflicts_with: vec![],
1062 is_primary_indicator: false, alternative_names: vec![],
1064 file_indicators: vec![],
1065 },
1066 TechnologyRule {
1068 name: "React".to_string(),
1069 category: TechnologyCategory::Library(LibraryType::UI),
1070 confidence: 0.90,
1071 dependency_patterns: vec!["react".to_string()],
1072 requires: vec![],
1073 conflicts_with: vec![],
1074 is_primary_indicator: false, alternative_names: vec!["reactjs".to_string()],
1076 file_indicators: vec![],
1077 },
1078 TechnologyRule {
1079 name: "Vue.js".to_string(),
1080 category: TechnologyCategory::Library(LibraryType::UI),
1081 confidence: 0.90,
1082 dependency_patterns: vec!["vue".to_string()],
1083 requires: vec![],
1084 conflicts_with: vec![],
1085 is_primary_indicator: false,
1086 alternative_names: vec!["vuejs".to_string()],
1087 file_indicators: vec![],
1088 },
1089 TechnologyRule {
1090 name: "SolidJS".to_string(),
1091 category: TechnologyCategory::Library(LibraryType::UI),
1092 confidence: 0.95,
1093 dependency_patterns: vec!["solid-js".to_string()],
1094 requires: vec![],
1095 conflicts_with: vec![],
1096 is_primary_indicator: false,
1097 alternative_names: vec!["solid".to_string()],
1098 file_indicators: vec![],
1099 },
1100 TechnologyRule {
1101 name: "HTMX".to_string(),
1102 category: TechnologyCategory::Library(LibraryType::UI),
1103 confidence: 0.95,
1104 dependency_patterns: vec!["htmx.org".to_string()],
1105 requires: vec![],
1106 conflicts_with: vec![],
1107 is_primary_indicator: false,
1108 alternative_names: vec!["htmx".to_string()],
1109 file_indicators: vec![],
1110 },
1111 TechnologyRule {
1113 name: "Express.js".to_string(),
1114 category: TechnologyCategory::BackendFramework,
1115 confidence: 0.95,
1116 dependency_patterns: vec!["express".to_string()],
1117 requires: vec![],
1118 conflicts_with: vec![],
1119 is_primary_indicator: true,
1120 alternative_names: vec!["express".to_string()],
1121 file_indicators: vec![],
1122 },
1123 TechnologyRule {
1124 name: "Fastify".to_string(),
1125 category: TechnologyCategory::BackendFramework,
1126 confidence: 0.95,
1127 dependency_patterns: vec!["fastify".to_string()],
1128 requires: vec![],
1129 conflicts_with: vec![],
1130 is_primary_indicator: true,
1131 alternative_names: vec![],
1132 file_indicators: vec![],
1133 },
1134 TechnologyRule {
1135 name: "Nest.js".to_string(),
1136 category: TechnologyCategory::BackendFramework,
1137 confidence: 0.95,
1138 dependency_patterns: vec!["@nestjs/core".to_string()],
1139 requires: vec![],
1140 conflicts_with: vec![],
1141 is_primary_indicator: true,
1142 alternative_names: vec!["nestjs".to_string()],
1143 file_indicators: vec![],
1144 },
1145 TechnologyRule {
1146 name: "Hono".to_string(),
1147 category: TechnologyCategory::BackendFramework,
1148 confidence: 0.95,
1149 dependency_patterns: vec!["hono".to_string()],
1150 requires: vec![],
1151 conflicts_with: vec![],
1152 is_primary_indicator: true,
1153 alternative_names: vec![],
1154 file_indicators: vec![],
1155 },
1156 TechnologyRule {
1157 name: "Elysia".to_string(),
1158 category: TechnologyCategory::BackendFramework,
1159 confidence: 0.95,
1160 dependency_patterns: vec!["elysia".to_string()],
1161 requires: vec![],
1162 conflicts_with: vec![],
1163 is_primary_indicator: true,
1164 alternative_names: vec![],
1165 file_indicators: vec![],
1166 },
1167 TechnologyRule {
1168 name: "Encore".to_string(),
1169 category: TechnologyCategory::BackendFramework,
1170 confidence: 0.95,
1171 dependency_patterns: vec!["encore.dev".to_string(), "encore".to_string()],
1172 requires: vec![],
1173 conflicts_with: vec!["Next.js".to_string()],
1174 is_primary_indicator: true,
1175 alternative_names: vec!["encore-ts-starter".to_string()],
1176 file_indicators: vec![
1177 "encore.app".to_string(),
1178 "encore.service.ts".to_string(),
1179 "encore.service.js".to_string(),
1180 ],
1181 },
1182 TechnologyRule {
1184 name: "Vite".to_string(),
1185 category: TechnologyCategory::BuildTool,
1186 confidence: 0.80,
1187 dependency_patterns: vec!["vite".to_string()],
1188 requires: vec![],
1189 conflicts_with: vec![],
1190 is_primary_indicator: false,
1191 alternative_names: vec![],
1192 file_indicators: vec![],
1193 },
1194 TechnologyRule {
1195 name: "Webpack".to_string(),
1196 category: TechnologyCategory::BuildTool,
1197 confidence: 0.80,
1198 dependency_patterns: vec!["webpack".to_string()],
1199 requires: vec![],
1200 conflicts_with: vec![],
1201 is_primary_indicator: false,
1202 alternative_names: vec![],
1203 file_indicators: vec![],
1204 },
1205 TechnologyRule {
1207 name: "Prisma".to_string(),
1208 category: TechnologyCategory::Database,
1209 confidence: 0.90,
1210 dependency_patterns: vec!["prisma".to_string(), "@prisma/client".to_string()],
1211 requires: vec![],
1212 conflicts_with: vec![],
1213 is_primary_indicator: false,
1214 alternative_names: vec![],
1215 file_indicators: vec![],
1216 },
1217 TechnologyRule {
1218 name: "Drizzle ORM".to_string(),
1219 category: TechnologyCategory::Database,
1220 confidence: 0.90,
1221 dependency_patterns: vec!["drizzle-orm".to_string(), "drizzle-kit".to_string()],
1222 requires: vec![],
1223 conflicts_with: vec![],
1224 is_primary_indicator: false,
1225 alternative_names: vec!["drizzle".to_string()],
1226 file_indicators: vec![],
1227 },
1228 TechnologyRule {
1229 name: "Sequelize".to_string(),
1230 category: TechnologyCategory::Database,
1231 confidence: 0.90,
1232 dependency_patterns: vec!["sequelize".to_string()],
1233 requires: vec![],
1234 conflicts_with: vec![],
1235 is_primary_indicator: false,
1236 alternative_names: vec![],
1237 file_indicators: vec![],
1238 },
1239 TechnologyRule {
1240 name: "TypeORM".to_string(),
1241 category: TechnologyCategory::Database,
1242 confidence: 0.90,
1243 dependency_patterns: vec!["typeorm".to_string()],
1244 requires: vec![],
1245 conflicts_with: vec![],
1246 is_primary_indicator: false,
1247 alternative_names: vec![],
1248 file_indicators: vec![],
1249 },
1250 TechnologyRule {
1251 name: "MikroORM".to_string(),
1252 category: TechnologyCategory::Database,
1253 confidence: 0.90,
1254 dependency_patterns: vec![
1255 "@mikro-orm/core".to_string(),
1256 "@mikro-orm/postgresql".to_string(),
1257 "@mikro-orm/mysql".to_string(),
1258 "@mikro-orm/sqlite".to_string(),
1259 "@mikro-orm/mongodb".to_string(),
1260 ],
1261 requires: vec![],
1262 conflicts_with: vec![],
1263 is_primary_indicator: false,
1264 alternative_names: vec!["mikro-orm".to_string()],
1265 file_indicators: vec![],
1266 },
1267 TechnologyRule {
1268 name: "Mongoose".to_string(),
1269 category: TechnologyCategory::Database,
1270 confidence: 0.95,
1271 dependency_patterns: vec!["mongoose".to_string()],
1272 requires: vec![],
1273 conflicts_with: vec![],
1274 is_primary_indicator: false,
1275 alternative_names: vec![],
1276 file_indicators: vec![],
1277 },
1278 TechnologyRule {
1279 name: "Typegoose".to_string(),
1280 category: TechnologyCategory::Database,
1281 confidence: 0.90,
1282 dependency_patterns: vec!["@typegoose/typegoose".to_string()],
1283 requires: vec!["Mongoose".to_string()],
1284 conflicts_with: vec![],
1285 is_primary_indicator: false,
1286 alternative_names: vec![],
1287 file_indicators: vec![],
1288 },
1289 TechnologyRule {
1290 name: "Objection.js".to_string(),
1291 category: TechnologyCategory::Database,
1292 confidence: 0.90,
1293 dependency_patterns: vec!["objection".to_string()],
1294 requires: vec!["Knex.js".to_string()],
1295 conflicts_with: vec![],
1296 is_primary_indicator: false,
1297 alternative_names: vec!["objectionjs".to_string()],
1298 file_indicators: vec![],
1299 },
1300 TechnologyRule {
1301 name: "Bookshelf".to_string(),
1302 category: TechnologyCategory::Database,
1303 confidence: 0.85,
1304 dependency_patterns: vec!["bookshelf".to_string()],
1305 requires: vec!["Knex.js".to_string()],
1306 conflicts_with: vec![],
1307 is_primary_indicator: false,
1308 alternative_names: vec![],
1309 file_indicators: vec![],
1310 },
1311 TechnologyRule {
1312 name: "Waterline".to_string(),
1313 category: TechnologyCategory::Database,
1314 confidence: 0.85,
1315 dependency_patterns: vec![
1316 "waterline".to_string(),
1317 "sails-mysql".to_string(),
1318 "sails-postgresql".to_string(),
1319 "sails-disk".to_string(),
1320 ],
1321 requires: vec![],
1322 conflicts_with: vec![],
1323 is_primary_indicator: false,
1324 alternative_names: vec![],
1325 file_indicators: vec![],
1326 },
1327 TechnologyRule {
1328 name: "Knex.js".to_string(),
1329 category: TechnologyCategory::Database,
1330 confidence: 0.85,
1331 dependency_patterns: vec!["knex".to_string()],
1332 requires: vec![],
1333 conflicts_with: vec![],
1334 is_primary_indicator: false,
1335 alternative_names: vec!["knexjs".to_string()],
1336 file_indicators: vec![],
1337 },
1338 TechnologyRule {
1340 name: "Node.js".to_string(),
1341 category: TechnologyCategory::Runtime,
1342 confidence: 0.90,
1343 dependency_patterns: vec!["node".to_string()], requires: vec![],
1345 conflicts_with: vec![],
1346 is_primary_indicator: false,
1347 alternative_names: vec!["nodejs".to_string()],
1348 file_indicators: vec![],
1349 },
1350 TechnologyRule {
1351 name: "Bun".to_string(),
1352 category: TechnologyCategory::Runtime,
1353 confidence: 0.95,
1354 dependency_patterns: vec!["bun".to_string()], requires: vec![],
1356 conflicts_with: vec![],
1357 is_primary_indicator: false,
1358 alternative_names: vec![],
1359 file_indicators: vec![],
1360 },
1361 TechnologyRule {
1362 name: "Deno".to_string(),
1363 category: TechnologyCategory::Runtime,
1364 confidence: 0.95,
1365 dependency_patterns: vec!["@deno/core".to_string(), "deno".to_string()],
1366 requires: vec![],
1367 conflicts_with: vec![],
1368 is_primary_indicator: false,
1369 alternative_names: vec![],
1370 file_indicators: vec![],
1371 },
1372 TechnologyRule {
1373 name: "WinterJS".to_string(),
1374 category: TechnologyCategory::Runtime,
1375 confidence: 0.95,
1376 dependency_patterns: vec!["winterjs".to_string(), "winter-js".to_string()],
1377 requires: vec![],
1378 conflicts_with: vec![],
1379 is_primary_indicator: false,
1380 alternative_names: vec!["winter.js".to_string()],
1381 file_indicators: vec![],
1382 },
1383 TechnologyRule {
1384 name: "Cloudflare Workers".to_string(),
1385 category: TechnologyCategory::Runtime,
1386 confidence: 0.90,
1387 dependency_patterns: vec![
1388 "@cloudflare/workers-types".to_string(),
1389 "@cloudflare/vitest-pool-workers".to_string(),
1390 "wrangler".to_string(),
1391 ],
1392 requires: vec![],
1393 conflicts_with: vec![],
1394 is_primary_indicator: false,
1395 alternative_names: vec!["cloudflare-workers".to_string()],
1396 file_indicators: vec![],
1397 },
1398 TechnologyRule {
1399 name: "Vercel Edge Runtime".to_string(),
1400 category: TechnologyCategory::Runtime,
1401 confidence: 0.90,
1402 dependency_patterns: vec![
1403 "@vercel/edge-runtime".to_string(),
1404 "@edge-runtime/vm".to_string(),
1405 ],
1406 requires: vec![],
1407 conflicts_with: vec![],
1408 is_primary_indicator: false,
1409 alternative_names: vec!["vercel-edge".to_string()],
1410 file_indicators: vec![],
1411 },
1412 TechnologyRule {
1413 name: "Hermes".to_string(),
1414 category: TechnologyCategory::Runtime,
1415 confidence: 0.85,
1416 dependency_patterns: vec!["hermes-engine".to_string()],
1417 requires: vec!["React Native".to_string()],
1418 conflicts_with: vec![],
1419 is_primary_indicator: false,
1420 alternative_names: vec![],
1421 file_indicators: vec![],
1422 },
1423 TechnologyRule {
1424 name: "Electron".to_string(),
1425 category: TechnologyCategory::Runtime,
1426 confidence: 0.95,
1427 dependency_patterns: vec!["electron".to_string()],
1428 requires: vec![],
1429 conflicts_with: vec![],
1430 is_primary_indicator: false,
1431 alternative_names: vec![],
1432 file_indicators: vec![],
1433 },
1434 TechnologyRule {
1435 name: "Tauri".to_string(),
1436 category: TechnologyCategory::Runtime,
1437 confidence: 0.95,
1438 dependency_patterns: vec!["@tauri-apps/cli".to_string(), "@tauri-apps/api".to_string()],
1439 requires: vec![],
1440 conflicts_with: vec!["Electron".to_string()],
1441 is_primary_indicator: false,
1442 alternative_names: vec![],
1443 file_indicators: vec![],
1444 },
1445 TechnologyRule {
1446 name: "QuickJS".to_string(),
1447 category: TechnologyCategory::Runtime,
1448 confidence: 0.85,
1449 dependency_patterns: vec!["quickjs".to_string(), "quickjs-emscripten".to_string()],
1450 requires: vec![],
1451 conflicts_with: vec![],
1452 is_primary_indicator: false,
1453 alternative_names: vec![],
1454 file_indicators: vec![],
1455 },
1456 TechnologyRule {
1458 name: "Jest".to_string(),
1459 category: TechnologyCategory::Testing,
1460 confidence: 0.85,
1461 dependency_patterns: vec!["jest".to_string()],
1462 requires: vec![],
1463 conflicts_with: vec![],
1464 is_primary_indicator: false,
1465 alternative_names: vec![],
1466 file_indicators: vec![],
1467 },
1468 TechnologyRule {
1469 name: "Vitest".to_string(),
1470 category: TechnologyCategory::Testing,
1471 confidence: 0.85,
1472 dependency_patterns: vec!["vitest".to_string()],
1473 requires: vec![],
1474 conflicts_with: vec![],
1475 is_primary_indicator: false,
1476 alternative_names: vec![],
1477 file_indicators: vec![],
1478 },
1479 ]
1480}