@@ -155,7 +155,7 @@ fn detect_by_config_files(language: &DetectedLanguage, rules: &[TechnologyRule])
155155 }
156156 }
157157 // Check for Next.js config files
158- else if file_name == "next.config.js" || file_name == " next.config.ts" {
158+ else if file_name. starts_with ( " next.config." ) {
159159 if let Some ( nextjs_rule) = rules. iter ( ) . find ( |r| r. name == "Next.js" ) {
160160 detected. push ( DetectedTechnology {
161161 name : nextjs_rule. name . clone ( ) ,
@@ -221,27 +221,29 @@ fn detect_by_project_structure(language: &DetectedLanguage, rules: &[TechnologyR
221221 let mut has_encore_service_files = false ;
222222 let mut has_app_json = false ;
223223 let mut has_app_js_ts = false ;
224-
224+ let mut has_next_config = false ;
225+ let mut has_tanstack_config = false ;
226+
225227 // Check project directories
226228 for file_path in & language. files {
227229 if let Some ( parent) = file_path. parent ( ) {
228230 let path_str = parent. to_string_lossy ( ) ;
229231 let file_name = file_path. file_name ( ) . and_then ( |n| n. to_str ( ) ) . unwrap_or ( "" ) ;
230-
232+
231233 // Check for React Native structure
232234 if path_str. contains ( "android" ) {
233235 has_android_dir = true ;
234236 } else if path_str. contains ( "ios" ) {
235237 has_ios_dir = true ;
236238 }
237239 // Check for Next.js structure
238- else if path_str . contains ( "pages" ) {
240+ else if has_path_component ( parent , "pages" ) {
239241 has_pages_dir = true ;
240- } else if path_str . contains ( "app" ) && !path_str . contains ( "app.config" ) && !path_str . contains ( "encore.app" ) {
242+ } else if has_path_component ( parent , "app" ) && !file_name . contains ( "app.config" ) && !file_name . contains ( "encore.app" ) {
241243 has_app_dir = true ;
242244 }
243245 // Check for TanStack Start structure
244- else if path_str . contains ( "app/routes" ) {
246+ else if has_app_routes ( parent ) {
245247 has_app_routes_dir = true ;
246248 }
247249 // Check for Encore structure
@@ -256,12 +258,22 @@ fn detect_by_project_structure(language: &DetectedLanguage, rules: &[TechnologyR
256258 } else if file_name == "App.js" || file_name == "App.tsx" {
257259 has_app_js_ts = true ;
258260 }
261+
262+ // Configs (need to be recorded so structure checks can require them)
263+ 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+ }
259269 }
260270 }
261-
271+
262272 // Check if we have Expo dependencies
263273 let has_expo_deps = language. main_dependencies . iter ( ) . any ( |dep| dep == "expo" || dep == "react-native" ) ;
264-
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+
265277 // Determine frameworks based on structure
266278 if has_encore_app_file || has_encore_service_files {
267279 // Likely Encore
@@ -277,7 +289,7 @@ fn detect_by_project_structure(language: &DetectedLanguage, rules: &[TechnologyR
277289 file_indicators : encore_rule. file_indicators . clone ( ) ,
278290 } ) ;
279291 }
280- } else if has_app_routes_dir {
292+ } else if has_app_routes_dir && ( has_tanstack_dep || has_tanstack_config ) {
281293 // Likely TanStack Start
282294 if let Some ( tanstack_rule) = rules. iter ( ) . find ( |r| r. name == "Tanstack Start" ) {
283295 detected. push ( DetectedTechnology {
@@ -291,7 +303,7 @@ fn detect_by_project_structure(language: &DetectedLanguage, rules: &[TechnologyR
291303 file_indicators : tanstack_rule. file_indicators . clone ( ) ,
292304 } ) ;
293305 }
294- } else if has_pages_dir || has_app_dir {
306+ } else if ( has_pages_dir || has_app_dir) && ( has_next_dep || has_next_config ) {
295307 // Likely Next.js
296308 if let Some ( nextjs_rule) = rules. iter ( ) . find ( |r| r. name == "Next.js" ) {
297309 detected. push ( DetectedTechnology {
@@ -342,6 +354,21 @@ fn detect_by_project_structure(language: &DetectedLanguage, rules: &[TechnologyR
342354 }
343355}
344356
357+ /// Returns true if any path component exactly matches the target (avoids substring false positives like "apps/")
358+ fn has_path_component ( path : & Path , target : & str ) -> bool {
359+ path. components ( )
360+ . any ( |c| c. as_os_str ( ) . to_string_lossy ( ) == target)
361+ }
362+
363+ /// Detects the canonical TanStack Start layout app/routes (component-level, not substring)
364+ fn 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+
345372/// New: Detect frameworks by analyzing source code patterns
346373fn detect_by_source_patterns ( language : & DetectedLanguage , rules : & [ TechnologyRule ] ) -> Option < Vec < DetectedTechnology > > {
347374 let mut detected = Vec :: new ( ) ;
@@ -1287,4 +1314,4 @@ fn get_js_technology_rules() -> Vec<TechnologyRule> {
12871314 file_indicators: vec![ ] ,
12881315 } ,
12891316 ]
1290- }
1317+ }
0 commit comments