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 == "next.config.js" || file_name == "next.config.ts" {
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
225 for file_path in &language.files {
227 if let Some(parent) = file_path.parent() {
228 let path_str = parent.to_string_lossy();
229 let file_name = file_path.file_name().and_then(|n| n.to_str()).unwrap_or("");
230
231 if path_str.contains("android") {
233 has_android_dir = true;
234 } else if path_str.contains("ios") {
235 has_ios_dir = true;
236 }
237 else if path_str.contains("pages") {
239 has_pages_dir = true;
240 } else if path_str.contains("app") && !path_str.contains("app.config") && !path_str.contains("encore.app") {
241 has_app_dir = true;
242 }
243 else if path_str.contains("app/routes") {
245 has_app_routes_dir = true;
246 }
247 else if file_name == "encore.app" {
249 has_encore_app_file = true;
250 } else if file_name.contains("encore.service.") {
251 has_encore_service_files = true;
252 }
253 else if file_name == "app.json" {
255 has_app_json = true;
256 } else if file_name == "App.js" || file_name == "App.tsx" {
257 has_app_js_ts = true;
258 }
259 }
260 }
261
262 let has_expo_deps = language.main_dependencies.iter().any(|dep| dep == "expo" || dep == "react-native");
264
265 if has_encore_app_file || has_encore_service_files {
267 if let Some(encore_rule) = rules.iter().find(|r| r.name == "Encore") {
269 detected.push(DetectedTechnology {
270 name: encore_rule.name.clone(),
271 version: None,
272 category: encore_rule.category.clone(),
273 confidence: 1.0, requires: encore_rule.requires.clone(),
275 conflicts_with: encore_rule.conflicts_with.clone(),
276 is_primary: encore_rule.is_primary_indicator,
277 file_indicators: encore_rule.file_indicators.clone(),
278 });
279 }
280 } else if has_app_routes_dir {
281 if let Some(tanstack_rule) = rules.iter().find(|r| r.name == "Tanstack Start") {
283 detected.push(DetectedTechnology {
284 name: tanstack_rule.name.clone(),
285 version: None,
286 category: tanstack_rule.category.clone(),
287 confidence: 0.9, requires: tanstack_rule.requires.clone(),
289 conflicts_with: tanstack_rule.conflicts_with.clone(),
290 is_primary: tanstack_rule.is_primary_indicator,
291 file_indicators: tanstack_rule.file_indicators.clone(),
292 });
293 }
294 } else if has_pages_dir || has_app_dir {
295 if let Some(nextjs_rule) = rules.iter().find(|r| r.name == "Next.js") {
297 detected.push(DetectedTechnology {
298 name: nextjs_rule.name.clone(),
299 version: None,
300 category: nextjs_rule.category.clone(),
301 confidence: 0.9, requires: nextjs_rule.requires.clone(),
303 conflicts_with: nextjs_rule.conflicts_with.clone(),
304 is_primary: nextjs_rule.is_primary_indicator,
305 file_indicators: nextjs_rule.file_indicators.clone(),
306 });
307 }
308 } else if (has_app_json || has_app_js_ts) && has_expo_deps {
309 if let Some(expo_rule) = rules.iter().find(|r| r.name == "Expo") {
311 detected.push(DetectedTechnology {
312 name: expo_rule.name.clone(),
313 version: None,
314 category: expo_rule.category.clone(),
315 confidence: 1.0, requires: expo_rule.requires.clone(),
317 conflicts_with: expo_rule.conflicts_with.clone(),
318 is_primary: expo_rule.is_primary_indicator,
319 file_indicators: expo_rule.file_indicators.clone(),
320 });
321 }
322 } else if has_android_dir && has_ios_dir {
323 if let Some(rn_rule) = rules.iter().find(|r| r.name == "React Native") {
325 detected.push(DetectedTechnology {
326 name: rn_rule.name.clone(),
327 version: None,
328 category: rn_rule.category.clone(),
329 confidence: 0.9, requires: rn_rule.requires.clone(),
331 conflicts_with: rn_rule.conflicts_with.clone(),
332 is_primary: rn_rule.is_primary_indicator,
333 file_indicators: rn_rule.file_indicators.clone(),
334 });
335 }
336 }
337
338 if detected.is_empty() {
339 None
340 } else {
341 Some(detected)
342 }
343}
344
345fn detect_by_source_patterns(language: &DetectedLanguage, rules: &[TechnologyRule]) -> Option<Vec<DetectedTechnology>> {
347 let mut detected = Vec::new();
348
349 for file_path in &language.files {
351 if let Ok(content) = std::fs::read_to_string(file_path) {
352 if content.contains("expo") && (content.contains("from 'expo'") || content.contains("import {") && content.contains("registerRootComponent")) {
354 if let Some(expo_rule) = rules.iter().find(|r| r.name == "Expo") {
355 detected.push(DetectedTechnology {
356 name: expo_rule.name.clone(),
357 version: None,
358 category: expo_rule.category.clone(),
359 confidence: 0.8, requires: expo_rule.requires.clone(),
361 conflicts_with: expo_rule.conflicts_with.clone(),
362 is_primary: expo_rule.is_primary_indicator,
363 file_indicators: expo_rule.file_indicators.clone(),
364 });
365 }
366 }
367
368 if content.contains("next/") {
370 if let Some(nextjs_rule) = rules.iter().find(|r| r.name == "Next.js") {
371 detected.push(DetectedTechnology {
372 name: nextjs_rule.name.clone(),
373 version: None,
374 category: nextjs_rule.category.clone(),
375 confidence: 0.7, requires: nextjs_rule.requires.clone(),
377 conflicts_with: nextjs_rule.conflicts_with.clone(),
378 is_primary: nextjs_rule.is_primary_indicator,
379 file_indicators: nextjs_rule.file_indicators.clone(),
380 });
381 }
382 }
383
384 if content.contains("@tanstack/react-router") && content.contains("createFileRoute") {
386 if let Some(tanstack_rule) = rules.iter().find(|r| r.name == "Tanstack Start") {
387 detected.push(DetectedTechnology {
388 name: tanstack_rule.name.clone(),
389 version: None,
390 category: tanstack_rule.category.clone(),
391 confidence: 0.7, requires: tanstack_rule.requires.clone(),
393 conflicts_with: tanstack_rule.conflicts_with.clone(),
394 is_primary: tanstack_rule.is_primary_indicator,
395 file_indicators: tanstack_rule.file_indicators.clone(),
396 });
397 }
398 }
399
400 if content.contains("react-router") && content.contains("BrowserRouter") {
402 if let Some(rr_rule) = rules.iter().find(|r| r.name == "React Router v7") {
403 detected.push(DetectedTechnology {
404 name: rr_rule.name.clone(),
405 version: None,
406 category: rr_rule.category.clone(),
407 confidence: 0.7, requires: rr_rule.requires.clone(),
409 conflicts_with: rr_rule.conflicts_with.clone(),
410 is_primary: rr_rule.is_primary_indicator,
411 file_indicators: rr_rule.file_indicators.clone(),
412 });
413 }
414 }
415 }
416 }
417
418 if detected.is_empty() {
419 None
420 } else {
421 Some(detected)
422 }
423}
424
425fn detect_technologies_from_source_files(language: &DetectedLanguage, rules: &[TechnologyRule]) -> Option<Vec<DetectedTechnology>> {
427
428
429 let mut detected = Vec::new();
430
431 for file_path in &language.files {
433 if let Ok(content) = fs::read_to_string(file_path) {
434 if let Some(drizzle_confidence) = analyze_drizzle_usage(&content, file_path) {
436 if let Some(drizzle_rule) = rules.iter().find(|r| r.name == "Drizzle ORM") {
437 detected.push(DetectedTechnology {
438 name: "Drizzle ORM".to_string(),
439 version: None,
440 category: TechnologyCategory::Database,
441 confidence: drizzle_confidence,
442 requires: vec![],
443 conflicts_with: vec![],
444 is_primary: false,
445 file_indicators: drizzle_rule.file_indicators.clone(),
446 });
447 }
448 }
449
450 if let Some(prisma_confidence) = analyze_prisma_usage(&content, file_path) {
452 if let Some(prisma_rule) = rules.iter().find(|r| r.name == "Prisma") {
453 detected.push(DetectedTechnology {
454 name: "Prisma".to_string(),
455 version: None,
456 category: TechnologyCategory::Database,
457 confidence: prisma_confidence,
458 requires: vec![],
459 conflicts_with: vec![],
460 is_primary: false,
461 file_indicators: prisma_rule.file_indicators.clone(),
462 });
463 }
464 }
465
466 if let Some(encore_confidence) = analyze_encore_usage(&content, file_path) {
468 if let Some(encore_rule) = rules.iter().find(|r| r.name == "Encore") {
469 detected.push(DetectedTechnology {
470 name: "Encore".to_string(),
471 version: None,
472 category: TechnologyCategory::BackendFramework,
473 confidence: encore_confidence,
474 requires: vec![],
475 conflicts_with: vec![],
476 is_primary: true,
477 file_indicators: encore_rule.file_indicators.clone(),
478 });
479 }
480 }
481
482 if let Some(tanstack_confidence) = analyze_tanstack_start_usage(&content, file_path) {
484 if let Some(tanstack_rule) = rules.iter().find(|r| r.name == "Tanstack Start") {
485 detected.push(DetectedTechnology {
486 name: "Tanstack Start".to_string(),
487 version: None,
488 category: TechnologyCategory::MetaFramework,
489 confidence: tanstack_confidence,
490 requires: vec!["React".to_string()],
491 conflicts_with: vec!["Next.js".to_string(), "React Router v7".to_string(), "SvelteKit".to_string(), "Nuxt.js".to_string()],
492 is_primary: true,
493 file_indicators: tanstack_rule.file_indicators.clone(),
494 });
495 }
496 }
497 }
498 }
499
500 if detected.is_empty() {
501 None
502 } else {
503 Some(detected)
504 }
505}
506
507fn analyze_drizzle_usage(content: &str, file_path: &Path) -> Option<f32> {
509 let file_name = file_path.file_name()?.to_string_lossy();
510 let mut confidence: f32 = 0.0;
511
512 if content.contains("drizzle-orm") {
514 confidence += 0.3;
515 }
516
517 if file_name.contains("schema") || file_name.contains("db.ts") || file_name.contains("database") {
519 if content.contains("pgTable") || content.contains("mysqlTable") || content.contains("sqliteTable") {
520 confidence += 0.4;
521 }
522 if content.contains("pgEnum") || content.contains("relations") {
523 confidence += 0.3;
524 }
525 }
526
527 if content.contains("from 'drizzle-orm/pg-core'") ||
529 content.contains("from 'drizzle-orm/mysql-core'") ||
530 content.contains("from 'drizzle-orm/sqlite-core'") {
531 confidence += 0.3;
532 }
533
534 if content.contains("db.select()") || content.contains("db.insert()") ||
536 content.contains("db.update()") || content.contains("db.delete()") {
537 confidence += 0.2;
538 }
539
540 if content.contains("drizzle(") && (content.contains("connectionString") || content.contains("postgres(")) {
542 confidence += 0.2;
543 }
544
545 if content.contains("drizzle.config") || file_name.contains("migrate") {
547 confidence += 0.2;
548 }
549
550 if content.contains(".prepare()") && content.contains("drizzle") {
552 confidence += 0.1;
553 }
554
555 if confidence > 0.0 {
556 Some(confidence.min(1.0_f32))
557 } else {
558 None
559 }
560}
561
562fn analyze_prisma_usage(content: &str, file_path: &Path) -> Option<f32> {
564 let file_name = file_path.file_name()?.to_string_lossy();
565 let mut confidence: f32 = 0.0;
566 let mut has_prisma_import = false;
567
568 if content.contains("@prisma/client") || content.contains("from '@prisma/client'") {
570 confidence += 0.4;
571 has_prisma_import = true;
572 }
573
574 if file_name == "schema.prisma" {
576 if content.contains("model ") || content.contains("generator ") || content.contains("datasource ") {
577 confidence += 0.6;
578 has_prisma_import = true;
579 }
580 }
581
582 if has_prisma_import {
584 if content.contains("new PrismaClient") || content.contains("PrismaClient()") {
586 confidence += 0.3;
587 }
588
589 if content.contains("prisma.") && (
591 content.contains(".findUnique(") ||
592 content.contains(".findFirst(") ||
593 content.contains(".upsert(") ||
594 content.contains(".$connect()") ||
595 content.contains(".$disconnect()")
596 ) {
597 confidence += 0.2;
598 }
599 }
600
601 if confidence > 0.0 && has_prisma_import {
603 Some(confidence.min(1.0_f32))
604 } else {
605 None
606 }
607}
608
609fn analyze_encore_usage(content: &str, file_path: &Path) -> Option<f32> {
611 let file_name = file_path.file_name()?.to_string_lossy();
612 let mut confidence: f32 = 0.0;
613
614 if content.contains("// Code generated by the Encore") || content.contains("DO NOT EDIT") {
616 return None;
617 }
618
619 if file_name.contains("client.ts") || file_name.contains("client.js") {
621 return None;
622 }
623
624 let mut has_service_patterns = false;
626
627 if file_name.contains("encore.service") || file_name.contains("service.ts") {
629 confidence += 0.4;
630 has_service_patterns = true;
631 }
632
633 if content.contains("encore.dev/api") && (content.contains("export") || content.contains("api.")) {
635 confidence += 0.4;
636 has_service_patterns = true;
637 }
638
639 if content.contains("SQLDatabase") && content.contains("encore.dev") {
641 confidence += 0.3;
642 has_service_patterns = true;
643 }
644
645 if content.contains("secret(") && content.contains("encore.dev/config") {
647 confidence += 0.3;
648 has_service_patterns = true;
649 }
650
651 if content.contains("Topic") && content.contains("encore.dev/pubsub") {
653 confidence += 0.3;
654 has_service_patterns = true;
655 }
656
657 if content.contains("cron") && content.contains("encore.dev") {
659 confidence += 0.2;
660 has_service_patterns = true;
661 }
662
663 if confidence > 0.0 && has_service_patterns {
665 Some(confidence.min(1.0_f32))
666 } else {
667 None
668 }
669}
670
671fn analyze_tanstack_start_usage(content: &str, file_path: &Path) -> Option<f32> {
673 let file_name = file_path.file_name()?.to_string_lossy();
674 let mut confidence: f32 = 0.0;
675 let mut has_start_patterns = false;
676
677 if file_name == "app.config.ts" || file_name == "app.config.js" {
679 if content.contains("@tanstack/react-start") || content.contains("tanstack") {
680 confidence += 0.5;
681 has_start_patterns = true;
682 }
683 }
684
685 if file_name.contains("router.") && (file_name.ends_with(".ts") || file_name.ends_with(".tsx")) {
687 if content.contains("createRouter") && content.contains("@tanstack/react-router") {
688 confidence += 0.4;
689 has_start_patterns = true;
690 }
691 if content.contains("routeTree") {
692 confidence += 0.2;
693 has_start_patterns = true;
694 }
695 }
696
697 if file_name == "ssr.tsx" || file_name == "ssr.ts" {
699 if content.contains("createStartHandler") || content.contains("@tanstack/react-start/server") {
700 confidence += 0.5;
701 has_start_patterns = true;
702 }
703 }
704
705 if file_name == "client.tsx" || file_name == "client.ts" {
707 if content.contains("StartClient") && content.contains("@tanstack/react-start") {
708 confidence += 0.5;
709 has_start_patterns = true;
710 }
711 if content.contains("hydrateRoot") && content.contains("createRouter") {
712 confidence += 0.3;
713 has_start_patterns = true;
714 }
715 }
716
717 if file_name == "__root.tsx" || file_name == "__root.ts" {
719 if content.contains("createRootRoute") && content.contains("@tanstack/react-router") {
720 confidence += 0.4;
721 has_start_patterns = true;
722 }
723 if content.contains("HeadContent") && content.contains("Scripts") {
724 confidence += 0.3;
725 has_start_patterns = true;
726 }
727 }
728
729 if file_path.to_string_lossy().contains("routes/") {
731 if content.contains("createFileRoute") && content.contains("@tanstack/react-router") {
732 confidence += 0.3;
733 has_start_patterns = true;
734 }
735 }
736
737 if content.contains("createServerFn") && content.contains("@tanstack/react-start") {
739 confidence += 0.4;
740 has_start_patterns = true;
741 }
742
743 if content.contains("from '@tanstack/react-start'") {
745 confidence += 0.3;
746 has_start_patterns = true;
747 }
748
749 if file_name == "vinxi.config.ts" || file_name == "vinxi.config.js" {
751 confidence += 0.2;
752 has_start_patterns = true;
753 }
754
755 if confidence > 0.0 && has_start_patterns {
757 Some(confidence.min(1.0_f32))
758 } else {
759 None
760 }
761}
762
763fn get_js_technology_rules() -> Vec<TechnologyRule> {
765 vec![
766 TechnologyRule {
768 name: "Next.js".to_string(),
769 category: TechnologyCategory::MetaFramework,
770 confidence: 0.95,
771 dependency_patterns: vec!["next".to_string()],
772 requires: vec!["React".to_string()],
773 conflicts_with: vec!["Tanstack Start".to_string(), "React Router v7".to_string(), "SvelteKit".to_string(), "Nuxt.js".to_string(), "Expo".to_string()],
774 is_primary_indicator: true,
775 alternative_names: vec!["nextjs".to_string()],
776 file_indicators: vec!["next.config.js".to_string(), "next.config.ts".to_string(), "pages/".to_string(), "app/".to_string()],
777 },
778 TechnologyRule {
779 name: "Tanstack Start".to_string(),
780 category: TechnologyCategory::MetaFramework,
781 confidence: 0.95,
782 dependency_patterns: vec!["@tanstack/react-start".to_string()],
783 requires: vec!["React".to_string()],
784 conflicts_with: vec!["Next.js".to_string(), "React Router v7".to_string(), "SvelteKit".to_string(), "Nuxt.js".to_string()],
785 is_primary_indicator: true,
786 alternative_names: vec!["tanstack-start".to_string(), "TanStack Start".to_string()],
787 file_indicators: vec!["app.config.ts".to_string(), "app.config.js".to_string(), "app/routes/".to_string(), "vite.config.ts".to_string()],
788 },
789 TechnologyRule {
790 name: "React Router v7".to_string(),
791 category: TechnologyCategory::MetaFramework,
792 confidence: 0.95,
793 dependency_patterns: vec!["react-router".to_string(), "react-dom".to_string(), "react-router-dom".to_string()],
794 requires: vec!["React".to_string()],
795 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()],
796 is_primary_indicator: true,
797 alternative_names: vec!["remix".to_string(), "react-router".to_string()],
798 file_indicators: vec![],
799 },
800 TechnologyRule {
801 name: "SvelteKit".to_string(),
802 category: TechnologyCategory::MetaFramework,
803 confidence: 0.95,
804 dependency_patterns: vec!["@sveltejs/kit".to_string()],
805 requires: vec!["Svelte".to_string()],
806 conflicts_with: vec!["Next.js".to_string(), "Tanstack Start".to_string(), "React Router v7".to_string(), "Nuxt.js".to_string()],
807 is_primary_indicator: true,
808 alternative_names: vec!["svelte-kit".to_string()],
809 file_indicators: vec![],
810 },
811 TechnologyRule {
812 name: "Nuxt.js".to_string(),
813 category: TechnologyCategory::MetaFramework,
814 confidence: 0.95,
815 dependency_patterns: vec!["nuxt".to_string(), "@nuxt/core".to_string()],
816 requires: vec!["Vue.js".to_string()],
817 conflicts_with: vec!["Next.js".to_string(), "Tanstack Start".to_string(), "React Router v7".to_string(), "SvelteKit".to_string()],
818 is_primary_indicator: true,
819 alternative_names: vec!["nuxtjs".to_string()],
820 file_indicators: vec![],
821 },
822 TechnologyRule {
823 name: "Astro".to_string(),
824 category: TechnologyCategory::MetaFramework,
825 confidence: 0.95,
826 dependency_patterns: vec!["astro".to_string()],
827 requires: vec![],
828 conflicts_with: vec![],
829 is_primary_indicator: true,
830 alternative_names: vec![],
831 file_indicators: vec![],
832 },
833 TechnologyRule {
834 name: "SolidStart".to_string(),
835 category: TechnologyCategory::MetaFramework,
836 confidence: 0.95,
837 dependency_patterns: vec!["solid-start".to_string()],
838 requires: vec!["SolidJS".to_string()],
839 conflicts_with: vec!["Next.js".to_string(), "Tanstack Start".to_string(), "React Router v7".to_string(), "SvelteKit".to_string()],
840 is_primary_indicator: true,
841 alternative_names: vec![],
842 file_indicators: vec![],
843 },
844
845 TechnologyRule {
847 name: "React Native".to_string(),
848 category: TechnologyCategory::FrontendFramework,
849 confidence: 0.95,
850 dependency_patterns: vec!["react-native".to_string()],
851 requires: vec!["React".to_string()],
852 conflicts_with: vec!["Next.js".to_string(), "React Router v7".to_string(), "SvelteKit".to_string(), "Nuxt.js".to_string(), "Tanstack Start".to_string()],
853 is_primary_indicator: true,
854 alternative_names: vec!["reactnative".to_string()],
855 file_indicators: vec!["react-native.config.js".to_string(), "android/".to_string(), "ios/".to_string()],
856 },
857 TechnologyRule {
858 name: "Expo".to_string(),
859 category: TechnologyCategory::MetaFramework,
860 confidence: 1.0,
861 dependency_patterns: vec!["expo".to_string(), "expo-router".to_string(), "@expo/vector-icons".to_string()],
862 requires: vec!["React Native".to_string()],
863 conflicts_with: vec!["Next.js".to_string(), "React Router v7".to_string(), "SvelteKit".to_string(), "Nuxt.js".to_string(), "Tanstack Start".to_string()],
864 is_primary_indicator: true,
865 alternative_names: vec![],
866 file_indicators: vec!["app.json".to_string(), "app.config.js".to_string(), "app.config.ts".to_string()],
867 },
868
869 TechnologyRule {
871 name: "Angular".to_string(),
872 category: TechnologyCategory::FrontendFramework,
873 confidence: 0.90,
874 dependency_patterns: vec!["@angular/core".to_string()],
875 requires: vec![],
876 conflicts_with: vec![],
877 is_primary_indicator: true,
878 alternative_names: vec!["angular".to_string()],
879 file_indicators: vec![],
880 },
881 TechnologyRule {
882 name: "Svelte".to_string(),
883 category: TechnologyCategory::FrontendFramework,
884 confidence: 0.95,
885 dependency_patterns: vec!["svelte".to_string()],
886 requires: vec![],
887 conflicts_with: vec![],
888 is_primary_indicator: false, alternative_names: vec![],
890 file_indicators: vec![],
891 },
892
893 TechnologyRule {
895 name: "React".to_string(),
896 category: TechnologyCategory::Library(LibraryType::UI),
897 confidence: 0.90,
898 dependency_patterns: vec!["react".to_string()],
899 requires: vec![],
900 conflicts_with: vec![],
901 is_primary_indicator: false, alternative_names: vec!["reactjs".to_string()],
903 file_indicators: vec![],
904 },
905 TechnologyRule {
906 name: "Vue.js".to_string(),
907 category: TechnologyCategory::Library(LibraryType::UI),
908 confidence: 0.90,
909 dependency_patterns: vec!["vue".to_string()],
910 requires: vec![],
911 conflicts_with: vec![],
912 is_primary_indicator: false,
913 alternative_names: vec!["vuejs".to_string()],
914 file_indicators: vec![],
915 },
916 TechnologyRule {
917 name: "SolidJS".to_string(),
918 category: TechnologyCategory::Library(LibraryType::UI),
919 confidence: 0.95,
920 dependency_patterns: vec!["solid-js".to_string()],
921 requires: vec![],
922 conflicts_with: vec![],
923 is_primary_indicator: false,
924 alternative_names: vec!["solid".to_string()],
925 file_indicators: vec![],
926 },
927 TechnologyRule {
928 name: "HTMX".to_string(),
929 category: TechnologyCategory::Library(LibraryType::UI),
930 confidence: 0.95,
931 dependency_patterns: vec!["htmx.org".to_string()],
932 requires: vec![],
933 conflicts_with: vec![],
934 is_primary_indicator: false,
935 alternative_names: vec!["htmx".to_string()],
936 file_indicators: vec![],
937 },
938
939 TechnologyRule {
941 name: "Express.js".to_string(),
942 category: TechnologyCategory::BackendFramework,
943 confidence: 0.95,
944 dependency_patterns: vec!["express".to_string()],
945 requires: vec![],
946 conflicts_with: vec![],
947 is_primary_indicator: true,
948 alternative_names: vec!["express".to_string()],
949 file_indicators: vec![],
950 },
951 TechnologyRule {
952 name: "Fastify".to_string(),
953 category: TechnologyCategory::BackendFramework,
954 confidence: 0.95,
955 dependency_patterns: vec!["fastify".to_string()],
956 requires: vec![],
957 conflicts_with: vec![],
958 is_primary_indicator: true,
959 alternative_names: vec![],
960 file_indicators: vec![],
961 },
962 TechnologyRule {
963 name: "Nest.js".to_string(),
964 category: TechnologyCategory::BackendFramework,
965 confidence: 0.95,
966 dependency_patterns: vec!["@nestjs/core".to_string()],
967 requires: vec![],
968 conflicts_with: vec![],
969 is_primary_indicator: true,
970 alternative_names: vec!["nestjs".to_string()],
971 file_indicators: vec![],
972 },
973 TechnologyRule {
974 name: "Hono".to_string(),
975 category: TechnologyCategory::BackendFramework,
976 confidence: 0.95,
977 dependency_patterns: vec!["hono".to_string()],
978 requires: vec![],
979 conflicts_with: vec![],
980 is_primary_indicator: true,
981 alternative_names: vec![],
982 file_indicators: vec![],
983 },
984 TechnologyRule {
985 name: "Elysia".to_string(),
986 category: TechnologyCategory::BackendFramework,
987 confidence: 0.95,
988 dependency_patterns: vec!["elysia".to_string()],
989 requires: vec![],
990 conflicts_with: vec![],
991 is_primary_indicator: true,
992 alternative_names: vec![],
993 file_indicators: vec![],
994 },
995 TechnologyRule {
996 name: "Encore".to_string(),
997 category: TechnologyCategory::BackendFramework,
998 confidence: 0.95,
999 dependency_patterns: vec!["encore.dev".to_string(), "encore".to_string()],
1000 requires: vec![],
1001 conflicts_with: vec!["Next.js".to_string()],
1002 is_primary_indicator: true,
1003 alternative_names: vec!["encore-ts-starter".to_string()],
1004 file_indicators: vec!["encore.app".to_string(), "encore.service.ts".to_string(), "encore.service.js".to_string()],
1005 },
1006
1007 TechnologyRule {
1009 name: "Vite".to_string(),
1010 category: TechnologyCategory::BuildTool,
1011 confidence: 0.80,
1012 dependency_patterns: vec!["vite".to_string()],
1013 requires: vec![],
1014 conflicts_with: vec![],
1015 is_primary_indicator: false,
1016 alternative_names: vec![],
1017 file_indicators: vec![],
1018 },
1019 TechnologyRule {
1020 name: "Webpack".to_string(),
1021 category: TechnologyCategory::BuildTool,
1022 confidence: 0.80,
1023 dependency_patterns: vec!["webpack".to_string()],
1024 requires: vec![],
1025 conflicts_with: vec![],
1026 is_primary_indicator: false,
1027 alternative_names: vec![],
1028 file_indicators: vec![],
1029 },
1030
1031 TechnologyRule {
1033 name: "Prisma".to_string(),
1034 category: TechnologyCategory::Database,
1035 confidence: 0.90,
1036 dependency_patterns: vec!["prisma".to_string(), "@prisma/client".to_string()],
1037 requires: vec![],
1038 conflicts_with: vec![],
1039 is_primary_indicator: false,
1040 alternative_names: vec![],
1041 file_indicators: vec![],
1042 },
1043 TechnologyRule {
1044 name: "Drizzle ORM".to_string(),
1045 category: TechnologyCategory::Database,
1046 confidence: 0.90,
1047 dependency_patterns: vec!["drizzle-orm".to_string(), "drizzle-kit".to_string()],
1048 requires: vec![],
1049 conflicts_with: vec![],
1050 is_primary_indicator: false,
1051 alternative_names: vec!["drizzle".to_string()],
1052 file_indicators: vec![],
1053 },
1054 TechnologyRule {
1055 name: "Sequelize".to_string(),
1056 category: TechnologyCategory::Database,
1057 confidence: 0.90,
1058 dependency_patterns: vec!["sequelize".to_string()],
1059 requires: vec![],
1060 conflicts_with: vec![],
1061 is_primary_indicator: false,
1062 alternative_names: vec![],
1063 file_indicators: vec![],
1064 },
1065 TechnologyRule {
1066 name: "TypeORM".to_string(),
1067 category: TechnologyCategory::Database,
1068 confidence: 0.90,
1069 dependency_patterns: vec!["typeorm".to_string()],
1070 requires: vec![],
1071 conflicts_with: vec![],
1072 is_primary_indicator: false,
1073 alternative_names: vec![],
1074 file_indicators: vec![],
1075 },
1076 TechnologyRule {
1077 name: "MikroORM".to_string(),
1078 category: TechnologyCategory::Database,
1079 confidence: 0.90,
1080 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()],
1081 requires: vec![],
1082 conflicts_with: vec![],
1083 is_primary_indicator: false,
1084 alternative_names: vec!["mikro-orm".to_string()],
1085 file_indicators: vec![],
1086 },
1087 TechnologyRule {
1088 name: "Mongoose".to_string(),
1089 category: TechnologyCategory::Database,
1090 confidence: 0.95,
1091 dependency_patterns: vec!["mongoose".to_string()],
1092 requires: vec![],
1093 conflicts_with: vec![],
1094 is_primary_indicator: false,
1095 alternative_names: vec![],
1096 file_indicators: vec![],
1097 },
1098 TechnologyRule {
1099 name: "Typegoose".to_string(),
1100 category: TechnologyCategory::Database,
1101 confidence: 0.90,
1102 dependency_patterns: vec!["@typegoose/typegoose".to_string()],
1103 requires: vec!["Mongoose".to_string()],
1104 conflicts_with: vec![],
1105 is_primary_indicator: false,
1106 alternative_names: vec![],
1107 file_indicators: vec![],
1108 },
1109 TechnologyRule {
1110 name: "Objection.js".to_string(),
1111 category: TechnologyCategory::Database,
1112 confidence: 0.90,
1113 dependency_patterns: vec!["objection".to_string()],
1114 requires: vec!["Knex.js".to_string()],
1115 conflicts_with: vec![],
1116 is_primary_indicator: false,
1117 alternative_names: vec!["objectionjs".to_string()],
1118 file_indicators: vec![],
1119 },
1120 TechnologyRule {
1121 name: "Bookshelf".to_string(),
1122 category: TechnologyCategory::Database,
1123 confidence: 0.85,
1124 dependency_patterns: vec!["bookshelf".to_string()],
1125 requires: vec!["Knex.js".to_string()],
1126 conflicts_with: vec![],
1127 is_primary_indicator: false,
1128 alternative_names: vec![],
1129 file_indicators: vec![],
1130 },
1131 TechnologyRule {
1132 name: "Waterline".to_string(),
1133 category: TechnologyCategory::Database,
1134 confidence: 0.85,
1135 dependency_patterns: vec!["waterline".to_string(), "sails-mysql".to_string(), "sails-postgresql".to_string(), "sails-disk".to_string()],
1136 requires: vec![],
1137 conflicts_with: vec![],
1138 is_primary_indicator: false,
1139 alternative_names: vec![],
1140 file_indicators: vec![],
1141 },
1142 TechnologyRule {
1143 name: "Knex.js".to_string(),
1144 category: TechnologyCategory::Database,
1145 confidence: 0.85,
1146 dependency_patterns: vec!["knex".to_string()],
1147 requires: vec![],
1148 conflicts_with: vec![],
1149 is_primary_indicator: false,
1150 alternative_names: vec!["knexjs".to_string()],
1151 file_indicators: vec![],
1152 },
1153
1154 TechnologyRule {
1156 name: "Node.js".to_string(),
1157 category: TechnologyCategory::Runtime,
1158 confidence: 0.90,
1159 dependency_patterns: vec!["node".to_string()], requires: vec![],
1161 conflicts_with: vec![],
1162 is_primary_indicator: false,
1163 alternative_names: vec!["nodejs".to_string()],
1164 file_indicators: vec![],
1165 },
1166 TechnologyRule {
1167 name: "Bun".to_string(),
1168 category: TechnologyCategory::Runtime,
1169 confidence: 0.95,
1170 dependency_patterns: vec!["bun".to_string()], requires: vec![],
1172 conflicts_with: vec![],
1173 is_primary_indicator: false,
1174 alternative_names: vec![],
1175 file_indicators: vec![],
1176 },
1177 TechnologyRule {
1178 name: "Deno".to_string(),
1179 category: TechnologyCategory::Runtime,
1180 confidence: 0.95,
1181 dependency_patterns: vec!["@deno/core".to_string(), "deno".to_string()],
1182 requires: vec![],
1183 conflicts_with: vec![],
1184 is_primary_indicator: false,
1185 alternative_names: vec![],
1186 file_indicators: vec![],
1187 },
1188 TechnologyRule {
1189 name: "WinterJS".to_string(),
1190 category: TechnologyCategory::Runtime,
1191 confidence: 0.95,
1192 dependency_patterns: vec!["winterjs".to_string(), "winter-js".to_string()],
1193 requires: vec![],
1194 conflicts_with: vec![],
1195 is_primary_indicator: false,
1196 alternative_names: vec!["winter.js".to_string()],
1197 file_indicators: vec![],
1198 },
1199 TechnologyRule {
1200 name: "Cloudflare Workers".to_string(),
1201 category: TechnologyCategory::Runtime,
1202 confidence: 0.90,
1203 dependency_patterns: vec!["@cloudflare/workers-types".to_string(), "@cloudflare/vitest-pool-workers".to_string(), "wrangler".to_string()],
1204 requires: vec![],
1205 conflicts_with: vec![],
1206 is_primary_indicator: false,
1207 alternative_names: vec!["cloudflare-workers".to_string()],
1208 file_indicators: vec![],
1209 },
1210 TechnologyRule {
1211 name: "Vercel Edge Runtime".to_string(),
1212 category: TechnologyCategory::Runtime,
1213 confidence: 0.90,
1214 dependency_patterns: vec!["@vercel/edge-runtime".to_string(), "@edge-runtime/vm".to_string()],
1215 requires: vec![],
1216 conflicts_with: vec![],
1217 is_primary_indicator: false,
1218 alternative_names: vec!["vercel-edge".to_string()],
1219 file_indicators: vec![],
1220 },
1221 TechnologyRule {
1222 name: "Hermes".to_string(),
1223 category: TechnologyCategory::Runtime,
1224 confidence: 0.85,
1225 dependency_patterns: vec!["hermes-engine".to_string()],
1226 requires: vec!["React Native".to_string()],
1227 conflicts_with: vec![],
1228 is_primary_indicator: false,
1229 alternative_names: vec![],
1230 file_indicators: vec![],
1231 },
1232 TechnologyRule {
1233 name: "Electron".to_string(),
1234 category: TechnologyCategory::Runtime,
1235 confidence: 0.95,
1236 dependency_patterns: vec!["electron".to_string()],
1237 requires: vec![],
1238 conflicts_with: vec![],
1239 is_primary_indicator: false,
1240 alternative_names: vec![],
1241 file_indicators: vec![],
1242 },
1243 TechnologyRule {
1244 name: "Tauri".to_string(),
1245 category: TechnologyCategory::Runtime,
1246 confidence: 0.95,
1247 dependency_patterns: vec!["@tauri-apps/cli".to_string(), "@tauri-apps/api".to_string()],
1248 requires: vec![],
1249 conflicts_with: vec!["Electron".to_string()],
1250 is_primary_indicator: false,
1251 alternative_names: vec![],
1252 file_indicators: vec![],
1253 },
1254 TechnologyRule {
1255 name: "QuickJS".to_string(),
1256 category: TechnologyCategory::Runtime,
1257 confidence: 0.85,
1258 dependency_patterns: vec!["quickjs".to_string(), "quickjs-emscripten".to_string()],
1259 requires: vec![],
1260 conflicts_with: vec![],
1261 is_primary_indicator: false,
1262 alternative_names: vec![],
1263 file_indicators: vec![],
1264 },
1265
1266 TechnologyRule {
1268 name: "Jest".to_string(),
1269 category: TechnologyCategory::Testing,
1270 confidence: 0.85,
1271 dependency_patterns: vec!["jest".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: "Vitest".to_string(),
1280 category: TechnologyCategory::Testing,
1281 confidence: 0.85,
1282 dependency_patterns: vec!["vitest".to_string()],
1283 requires: vec![],
1284 conflicts_with: vec![],
1285 is_primary_indicator: false,
1286 alternative_names: vec![],
1287 file_indicators: vec![],
1288 },
1289 ]
1290}