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 {
820 name: "React Router v7".to_string(),
821 category: TechnologyCategory::MetaFramework,
822 confidence: 0.95,
823 dependency_patterns: vec!["@react-router/dev".to_string(), "@react-router/node".to_string(), "@react-router/serve".to_string()],
825 requires: vec!["React".to_string()],
826 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(), "Encore".to_string()],
827 is_primary_indicator: true,
828 alternative_names: vec!["remix".to_string()],
829 file_indicators: vec!["react-router.config.ts".to_string(), "react-router.config.js".to_string()],
830 },
831 TechnologyRule {
832 name: "SvelteKit".to_string(),
833 category: TechnologyCategory::MetaFramework,
834 confidence: 0.95,
835 dependency_patterns: vec!["@sveltejs/kit".to_string()],
836 requires: vec!["Svelte".to_string()],
837 conflicts_with: vec!["Next.js".to_string(), "Tanstack Start".to_string(), "React Router v7".to_string(), "Nuxt.js".to_string()],
838 is_primary_indicator: true,
839 alternative_names: vec!["svelte-kit".to_string()],
840 file_indicators: vec!["svelte.config.js".to_string(), "svelte.config.ts".to_string()],
841 },
842 TechnologyRule {
843 name: "Nuxt.js".to_string(),
844 category: TechnologyCategory::MetaFramework,
845 confidence: 0.95,
846 dependency_patterns: vec!["nuxt".to_string()],
847 requires: vec!["Vue.js".to_string()],
848 conflicts_with: vec!["Next.js".to_string(), "Tanstack Start".to_string(), "React Router v7".to_string(), "SvelteKit".to_string()],
849 is_primary_indicator: true,
850 alternative_names: vec!["nuxtjs".to_string()],
851 file_indicators: vec!["nuxt.config.ts".to_string(), "nuxt.config.js".to_string()],
852 },
853 TechnologyRule {
854 name: "Astro".to_string(),
855 category: TechnologyCategory::MetaFramework,
856 confidence: 0.95,
857 dependency_patterns: vec!["astro".to_string()],
858 requires: vec![],
859 conflicts_with: vec![],
860 is_primary_indicator: true,
861 alternative_names: vec![],
862 file_indicators: vec!["astro.config.mjs".to_string(), "astro.config.ts".to_string()],
863 },
864 TechnologyRule {
865 name: "SolidStart".to_string(),
866 category: TechnologyCategory::MetaFramework,
867 confidence: 0.95,
868 dependency_patterns: vec!["solid-start".to_string(), "@solidjs/start".to_string()],
869 requires: vec!["SolidJS".to_string()],
870 conflicts_with: vec!["Next.js".to_string(), "Tanstack Start".to_string(), "React Router v7".to_string(), "SvelteKit".to_string()],
871 is_primary_indicator: true,
872 alternative_names: vec![],
873 file_indicators: vec!["app.config.ts".to_string(), "app.config.js".to_string()],
874 },
875
876 TechnologyRule {
878 name: "React Native".to_string(),
879 category: TechnologyCategory::FrontendFramework,
880 confidence: 0.95,
881 dependency_patterns: vec!["react-native".to_string()],
882 requires: vec!["React".to_string()],
883 conflicts_with: vec!["Next.js".to_string(), "React Router v7".to_string(), "SvelteKit".to_string(), "Nuxt.js".to_string(), "Tanstack Start".to_string()],
884 is_primary_indicator: true,
885 alternative_names: vec!["reactnative".to_string()],
886 file_indicators: vec!["react-native.config.js".to_string(), "android/".to_string(), "ios/".to_string()],
887 },
888 TechnologyRule {
889 name: "Expo".to_string(),
890 category: TechnologyCategory::MetaFramework,
891 confidence: 1.0,
892 dependency_patterns: vec!["expo".to_string(), "expo-router".to_string(), "@expo/vector-icons".to_string()],
893 requires: vec!["React Native".to_string()],
894 conflicts_with: vec!["Next.js".to_string(), "React Router v7".to_string(), "SvelteKit".to_string(), "Nuxt.js".to_string(), "Tanstack Start".to_string()],
895 is_primary_indicator: true,
896 alternative_names: vec![],
897 file_indicators: vec!["app.json".to_string(), "app.config.js".to_string(), "app.config.ts".to_string()],
898 },
899
900 TechnologyRule {
902 name: "Angular".to_string(),
903 category: TechnologyCategory::FrontendFramework,
904 confidence: 0.90,
905 dependency_patterns: vec!["@angular/core".to_string()],
906 requires: vec![],
907 conflicts_with: vec![],
908 is_primary_indicator: true,
909 alternative_names: vec!["angular".to_string()],
910 file_indicators: vec!["angular.json".to_string(), "angular.cli.json".to_string()],
911 },
912 TechnologyRule {
913 name: "Svelte".to_string(),
914 category: TechnologyCategory::FrontendFramework,
915 confidence: 0.95,
916 dependency_patterns: vec!["svelte".to_string()],
917 requires: vec![],
918 conflicts_with: vec![],
919 is_primary_indicator: false, alternative_names: vec![],
921 file_indicators: vec!["svelte.config.js".to_string()],
922 },
923
924 TechnologyRule {
926 name: "React Router".to_string(),
927 category: TechnologyCategory::Library(LibraryType::Routing),
928 confidence: 0.85,
929 dependency_patterns: vec!["react-router-dom".to_string()],
931 requires: vec!["React".to_string()],
932 conflicts_with: vec![],
933 is_primary_indicator: false,
934 alternative_names: vec![],
935 file_indicators: vec![],
936 },
937
938 TechnologyRule {
940 name: "React".to_string(),
941 category: TechnologyCategory::Library(LibraryType::UI),
942 confidence: 0.90,
943 dependency_patterns: vec!["react".to_string()],
944 requires: vec![],
945 conflicts_with: vec![],
946 is_primary_indicator: false, alternative_names: vec!["reactjs".to_string()],
948 file_indicators: vec![],
949 },
950 TechnologyRule {
951 name: "Vue.js".to_string(),
952 category: TechnologyCategory::Library(LibraryType::UI),
953 confidence: 0.90,
954 dependency_patterns: vec!["vue".to_string()],
955 requires: vec![],
956 conflicts_with: vec![],
957 is_primary_indicator: false,
958 alternative_names: vec!["vuejs".to_string()],
959 file_indicators: vec![],
960 },
961 TechnologyRule {
962 name: "SolidJS".to_string(),
963 category: TechnologyCategory::Library(LibraryType::UI),
964 confidence: 0.95,
965 dependency_patterns: vec!["solid-js".to_string()],
966 requires: vec![],
967 conflicts_with: vec![],
968 is_primary_indicator: false,
969 alternative_names: vec!["solid".to_string()],
970 file_indicators: vec![],
971 },
972 TechnologyRule {
973 name: "HTMX".to_string(),
974 category: TechnologyCategory::Library(LibraryType::UI),
975 confidence: 0.95,
976 dependency_patterns: vec!["htmx.org".to_string()],
977 requires: vec![],
978 conflicts_with: vec![],
979 is_primary_indicator: false,
980 alternative_names: vec!["htmx".to_string()],
981 file_indicators: vec![],
982 },
983
984 TechnologyRule {
986 name: "Express.js".to_string(),
987 category: TechnologyCategory::BackendFramework,
988 confidence: 0.95,
989 dependency_patterns: vec!["express".to_string()],
990 requires: vec![],
991 conflicts_with: vec![],
992 is_primary_indicator: true,
993 alternative_names: vec!["express".to_string()],
994 file_indicators: vec!["app.js".to_string(), "server.js".to_string()],
995 },
996 TechnologyRule {
997 name: "Fastify".to_string(),
998 category: TechnologyCategory::BackendFramework,
999 confidence: 0.95,
1000 dependency_patterns: vec!["fastify".to_string()],
1001 requires: vec![],
1002 conflicts_with: vec![],
1003 is_primary_indicator: true,
1004 alternative_names: vec![],
1005 file_indicators: vec!["fastify.config.js".to_string()],
1006 },
1007 TechnologyRule {
1008 name: "Nest.js".to_string(),
1009 category: TechnologyCategory::BackendFramework,
1010 confidence: 0.95,
1011 dependency_patterns: vec!["@nestjs/core".to_string()],
1012 requires: vec![],
1013 conflicts_with: vec![],
1014 is_primary_indicator: true,
1015 alternative_names: vec!["nestjs".to_string()],
1016 file_indicators: vec!["nest-cli.json".to_string()],
1017 },
1018 TechnologyRule {
1019 name: "Hono".to_string(),
1020 category: TechnologyCategory::BackendFramework,
1021 confidence: 0.95,
1022 dependency_patterns: vec!["hono".to_string()],
1023 requires: vec![],
1024 conflicts_with: vec![],
1025 is_primary_indicator: true,
1026 alternative_names: vec![],
1027 file_indicators: vec![],
1028 },
1029 TechnologyRule {
1030 name: "Elysia".to_string(),
1031 category: TechnologyCategory::BackendFramework,
1032 confidence: 0.95,
1033 dependency_patterns: vec!["elysia".to_string()],
1034 requires: vec![],
1035 conflicts_with: vec![],
1036 is_primary_indicator: true,
1037 alternative_names: vec![],
1038 file_indicators: vec![],
1039 },
1040 TechnologyRule {
1043 name: "Encore".to_string(),
1044 category: TechnologyCategory::BackendFramework,
1045 confidence: 0.95,
1046 dependency_patterns: vec!["encore.dev".to_string()],
1047 requires: vec![],
1048 conflicts_with: vec!["Next.js".to_string(), "React Router v7".to_string(), "Tanstack Start".to_string()],
1049 is_primary_indicator: true,
1050 alternative_names: vec![],
1051 file_indicators: vec!["encore.app".to_string(), "encore.service.ts".to_string(), "encore.service.js".to_string()],
1052 },
1053
1054 TechnologyRule {
1056 name: "Vite".to_string(),
1057 category: TechnologyCategory::BuildTool,
1058 confidence: 0.80,
1059 dependency_patterns: vec!["vite".to_string()],
1060 requires: vec![],
1061 conflicts_with: vec![],
1062 is_primary_indicator: false,
1063 alternative_names: vec![],
1064 file_indicators: vec![],
1065 },
1066 TechnologyRule {
1067 name: "Webpack".to_string(),
1068 category: TechnologyCategory::BuildTool,
1069 confidence: 0.80,
1070 dependency_patterns: vec!["webpack".to_string()],
1071 requires: vec![],
1072 conflicts_with: vec![],
1073 is_primary_indicator: false,
1074 alternative_names: vec![],
1075 file_indicators: vec![],
1076 },
1077
1078 TechnologyRule {
1080 name: "Prisma".to_string(),
1081 category: TechnologyCategory::Database,
1082 confidence: 0.90,
1083 dependency_patterns: vec!["prisma".to_string(), "@prisma/client".to_string()],
1084 requires: vec![],
1085 conflicts_with: vec![],
1086 is_primary_indicator: false,
1087 alternative_names: vec![],
1088 file_indicators: vec![],
1089 },
1090 TechnologyRule {
1091 name: "Drizzle ORM".to_string(),
1092 category: TechnologyCategory::Database,
1093 confidence: 0.90,
1094 dependency_patterns: vec!["drizzle-orm".to_string(), "drizzle-kit".to_string()],
1095 requires: vec![],
1096 conflicts_with: vec![],
1097 is_primary_indicator: false,
1098 alternative_names: vec!["drizzle".to_string()],
1099 file_indicators: vec![],
1100 },
1101 TechnologyRule {
1102 name: "Sequelize".to_string(),
1103 category: TechnologyCategory::Database,
1104 confidence: 0.90,
1105 dependency_patterns: vec!["sequelize".to_string()],
1106 requires: vec![],
1107 conflicts_with: vec![],
1108 is_primary_indicator: false,
1109 alternative_names: vec![],
1110 file_indicators: vec![],
1111 },
1112 TechnologyRule {
1113 name: "TypeORM".to_string(),
1114 category: TechnologyCategory::Database,
1115 confidence: 0.90,
1116 dependency_patterns: vec!["typeorm".to_string()],
1117 requires: vec![],
1118 conflicts_with: vec![],
1119 is_primary_indicator: false,
1120 alternative_names: vec![],
1121 file_indicators: vec![],
1122 },
1123 TechnologyRule {
1124 name: "MikroORM".to_string(),
1125 category: TechnologyCategory::Database,
1126 confidence: 0.90,
1127 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()],
1128 requires: vec![],
1129 conflicts_with: vec![],
1130 is_primary_indicator: false,
1131 alternative_names: vec!["mikro-orm".to_string()],
1132 file_indicators: vec![],
1133 },
1134 TechnologyRule {
1135 name: "Mongoose".to_string(),
1136 category: TechnologyCategory::Database,
1137 confidence: 0.95,
1138 dependency_patterns: vec!["mongoose".to_string()],
1139 requires: vec![],
1140 conflicts_with: vec![],
1141 is_primary_indicator: false,
1142 alternative_names: vec![],
1143 file_indicators: vec![],
1144 },
1145 TechnologyRule {
1146 name: "Typegoose".to_string(),
1147 category: TechnologyCategory::Database,
1148 confidence: 0.90,
1149 dependency_patterns: vec!["@typegoose/typegoose".to_string()],
1150 requires: vec!["Mongoose".to_string()],
1151 conflicts_with: vec![],
1152 is_primary_indicator: false,
1153 alternative_names: vec![],
1154 file_indicators: vec![],
1155 },
1156 TechnologyRule {
1157 name: "Objection.js".to_string(),
1158 category: TechnologyCategory::Database,
1159 confidence: 0.90,
1160 dependency_patterns: vec!["objection".to_string()],
1161 requires: vec!["Knex.js".to_string()],
1162 conflicts_with: vec![],
1163 is_primary_indicator: false,
1164 alternative_names: vec!["objectionjs".to_string()],
1165 file_indicators: vec![],
1166 },
1167 TechnologyRule {
1168 name: "Bookshelf".to_string(),
1169 category: TechnologyCategory::Database,
1170 confidence: 0.85,
1171 dependency_patterns: vec!["bookshelf".to_string()],
1172 requires: vec!["Knex.js".to_string()],
1173 conflicts_with: vec![],
1174 is_primary_indicator: false,
1175 alternative_names: vec![],
1176 file_indicators: vec![],
1177 },
1178 TechnologyRule {
1179 name: "Waterline".to_string(),
1180 category: TechnologyCategory::Database,
1181 confidence: 0.85,
1182 dependency_patterns: vec!["waterline".to_string(), "sails-mysql".to_string(), "sails-postgresql".to_string(), "sails-disk".to_string()],
1183 requires: vec![],
1184 conflicts_with: vec![],
1185 is_primary_indicator: false,
1186 alternative_names: vec![],
1187 file_indicators: vec![],
1188 },
1189 TechnologyRule {
1190 name: "Knex.js".to_string(),
1191 category: TechnologyCategory::Database,
1192 confidence: 0.85,
1193 dependency_patterns: vec!["knex".to_string()],
1194 requires: vec![],
1195 conflicts_with: vec![],
1196 is_primary_indicator: false,
1197 alternative_names: vec!["knexjs".to_string()],
1198 file_indicators: vec![],
1199 },
1200
1201 TechnologyRule {
1203 name: "Node.js".to_string(),
1204 category: TechnologyCategory::Runtime,
1205 confidence: 0.90,
1206 dependency_patterns: vec!["node".to_string()], requires: vec![],
1208 conflicts_with: vec![],
1209 is_primary_indicator: false,
1210 alternative_names: vec!["nodejs".to_string()],
1211 file_indicators: vec![],
1212 },
1213 TechnologyRule {
1214 name: "Bun".to_string(),
1215 category: TechnologyCategory::Runtime,
1216 confidence: 0.95,
1217 dependency_patterns: vec!["bun".to_string()], requires: vec![],
1219 conflicts_with: vec![],
1220 is_primary_indicator: false,
1221 alternative_names: vec![],
1222 file_indicators: vec![],
1223 },
1224 TechnologyRule {
1225 name: "Deno".to_string(),
1226 category: TechnologyCategory::Runtime,
1227 confidence: 0.95,
1228 dependency_patterns: vec!["@deno/core".to_string(), "deno".to_string()],
1229 requires: vec![],
1230 conflicts_with: vec![],
1231 is_primary_indicator: false,
1232 alternative_names: vec![],
1233 file_indicators: vec![],
1234 },
1235 TechnologyRule {
1236 name: "WinterJS".to_string(),
1237 category: TechnologyCategory::Runtime,
1238 confidence: 0.95,
1239 dependency_patterns: vec!["winterjs".to_string(), "winter-js".to_string()],
1240 requires: vec![],
1241 conflicts_with: vec![],
1242 is_primary_indicator: false,
1243 alternative_names: vec!["winter.js".to_string()],
1244 file_indicators: vec![],
1245 },
1246 TechnologyRule {
1247 name: "Cloudflare Workers".to_string(),
1248 category: TechnologyCategory::Runtime,
1249 confidence: 0.90,
1250 dependency_patterns: vec!["@cloudflare/workers-types".to_string(), "@cloudflare/vitest-pool-workers".to_string(), "wrangler".to_string()],
1251 requires: vec![],
1252 conflicts_with: vec![],
1253 is_primary_indicator: false,
1254 alternative_names: vec!["cloudflare-workers".to_string()],
1255 file_indicators: vec![],
1256 },
1257 TechnologyRule {
1258 name: "Vercel Edge Runtime".to_string(),
1259 category: TechnologyCategory::Runtime,
1260 confidence: 0.90,
1261 dependency_patterns: vec!["@vercel/edge-runtime".to_string(), "@edge-runtime/vm".to_string()],
1262 requires: vec![],
1263 conflicts_with: vec![],
1264 is_primary_indicator: false,
1265 alternative_names: vec!["vercel-edge".to_string()],
1266 file_indicators: vec![],
1267 },
1268 TechnologyRule {
1269 name: "Hermes".to_string(),
1270 category: TechnologyCategory::Runtime,
1271 confidence: 0.85,
1272 dependency_patterns: vec!["hermes-engine".to_string()],
1273 requires: vec!["React Native".to_string()],
1274 conflicts_with: vec![],
1275 is_primary_indicator: false,
1276 alternative_names: vec![],
1277 file_indicators: vec![],
1278 },
1279 TechnologyRule {
1280 name: "Electron".to_string(),
1281 category: TechnologyCategory::Runtime,
1282 confidence: 0.95,
1283 dependency_patterns: vec!["electron".to_string()],
1284 requires: vec![],
1285 conflicts_with: vec![],
1286 is_primary_indicator: false,
1287 alternative_names: vec![],
1288 file_indicators: vec![],
1289 },
1290 TechnologyRule {
1291 name: "Tauri".to_string(),
1292 category: TechnologyCategory::Runtime,
1293 confidence: 0.95,
1294 dependency_patterns: vec!["@tauri-apps/cli".to_string(), "@tauri-apps/api".to_string()],
1295 requires: vec![],
1296 conflicts_with: vec!["Electron".to_string()],
1297 is_primary_indicator: false,
1298 alternative_names: vec![],
1299 file_indicators: vec![],
1300 },
1301 TechnologyRule {
1302 name: "QuickJS".to_string(),
1303 category: TechnologyCategory::Runtime,
1304 confidence: 0.85,
1305 dependency_patterns: vec!["quickjs".to_string(), "quickjs-emscripten".to_string()],
1306 requires: vec![],
1307 conflicts_with: vec![],
1308 is_primary_indicator: false,
1309 alternative_names: vec![],
1310 file_indicators: vec![],
1311 },
1312
1313 TechnologyRule {
1315 name: "Jest".to_string(),
1316 category: TechnologyCategory::Testing,
1317 confidence: 0.85,
1318 dependency_patterns: vec!["jest".to_string()],
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: "Vitest".to_string(),
1327 category: TechnologyCategory::Testing,
1328 confidence: 0.85,
1329 dependency_patterns: vec!["vitest".to_string()],
1330 requires: vec![],
1331 conflicts_with: vec![],
1332 is_primary_indicator: false,
1333 alternative_names: vec![],
1334 file_indicators: vec![],
1335 },
1336 ]
1337}