@@ -247,6 +247,12 @@ actor ProjectScanner {
247247 private func detectProject( at path: String , files: [ String ] ) -> ProjectInfo ? {
248248 var projectType : String ?
249249 var tags : Set < String > = [ ]
250+ let isReactNative = isReactNativeProject ( at: path, files: files)
251+
252+ if isReactNative {
253+ tags. insert ( " React Native " )
254+ tags. insert ( " Mobile " )
255+ }
250256
251257 for file in files {
252258 let ext = ( file as NSString ) . pathExtension
@@ -268,6 +274,12 @@ actor ProjectScanner {
268274 }
269275 }
270276
277+ if isReactNative {
278+ projectType = " React Native "
279+ tags. remove ( " JavaScript " )
280+ tags. remove ( " Node.js " )
281+ }
282+
271283 guard let type = projectType else { return nil }
272284
273285 let name = ( path as NSString ) . lastPathComponent
@@ -303,7 +315,7 @@ actor ProjectScanner {
303315 " React " , " Next.js " , " Vue " , " Angular " , " Vite " , " Node.js " , " PHP " , " Ruby " , " Go "
304316 ]
305317 let desktopTypes : Set < String > = [ " .NET " , " .NET Solution " , " F# " , " VB.NET " ]
306- let mobileTypes : Set < String > = [ " iOS " , " Flutter " , " Xcode " , " Xcode Workspace " ]
318+ let mobileTypes : Set < String > = [ " iOS " , " Flutter " , " Xcode " , " Xcode Workspace " , " React Native " ]
307319 let cloudTypes : Set < String > = [ " Docker " ]
308320
309321 if mobileTypes. contains ( type) { return . mobile }
@@ -361,6 +373,7 @@ actor ProjectScanner {
361373 private func generateIconColor( type: String ) -> String {
362374 let typeColors : [ String : String ] = [
363375 " Node.js " : " 68A063 " ,
376+ " React Native " : " 61DAFB " ,
364377 " React " : " 61DAFB " ,
365378 " Next.js " : " 000000 " ,
366379 " Vue " : " 42B883 " ,
@@ -386,4 +399,85 @@ actor ProjectScanner {
386399 ]
387400 return typeColors [ type] ?? " 6B7280 "
388401 }
402+
403+ // MARK: - React Native Detection
404+
405+ private func isReactNativeProject( at path: String , files: [ String ] ) -> Bool {
406+ let normalizedFiles = Set ( files. map { $0. lowercased ( ) } )
407+ guard normalizedFiles. contains ( " package.json " ) else { return false }
408+
409+ let reactNativeMarkerFiles : Set < String > = [
410+ " app.json " ,
411+ " app.config.js " ,
412+ " app.config.ts " ,
413+ " react-native.config.js " ,
414+ " metro.config.js " ,
415+ " metro.config.cjs " ,
416+ " eas.json "
417+ ]
418+
419+ if !reactNativeMarkerFiles. isDisjoint ( with: normalizedFiles) {
420+ return true
421+ }
422+
423+ let hasPlatformFolders = normalizedFiles. contains ( " ios " ) && normalizedFiles. contains ( " android " )
424+
425+ let packagePath = ( path as NSString ) . appendingPathComponent ( " package.json " )
426+ guard let data = try ? Data ( contentsOf: URL ( fileURLWithPath: packagePath) ) ,
427+ let json = try ? JSONSerialization . jsonObject ( with: data) as? [ String : Any ] else {
428+ return hasPlatformFolders
429+ }
430+
431+ if hasReactNativeDependency ( in: json) {
432+ return true
433+ }
434+
435+ if let scripts = json [ " scripts " ] as? [ String : Any ] {
436+ let scriptKeys = scripts. keys. map { $0. lowercased ( ) }
437+ let scriptValues = scripts. values
438+ . compactMap { $0 as? String }
439+ . map { $0. lowercased ( ) }
440+
441+ if scriptKeys. contains ( where: { $0. contains ( " react-native " ) || $0. contains ( " expo " ) } ) {
442+ return true
443+ }
444+
445+ if scriptValues. contains ( where: { $0. contains ( " react-native " ) || $0. contains ( " expo " ) } ) {
446+ return true
447+ }
448+ }
449+
450+ if let keywords = json [ " keywords " ] as? [ String ] {
451+ let loweredKeywords = keywords. map { $0. lowercased ( ) }
452+ if loweredKeywords. contains ( where: { $0. contains ( " react-native " ) || $0 == " expo " } ) {
453+ return true
454+ }
455+ }
456+
457+ if let name = ( json [ " name " ] as? String ) ? . lowercased ( ) , name. contains ( " react-native " ) {
458+ return true
459+ }
460+
461+ return hasPlatformFolders
462+ }
463+
464+ private func hasReactNativeDependency( in packageJson: [ String : Any ] ) -> Bool {
465+ let dependencyKeys = [ " dependencies " , " devDependencies " , " peerDependencies " ]
466+ for key in dependencyKeys {
467+ guard let deps = packageJson [ key] as? [ String : Any ] else { continue }
468+ let depNames = deps. keys. map { $0. lowercased ( ) }
469+
470+ if depNames. contains ( where: {
471+ $0 == " react-native " ||
472+ $0. hasPrefix ( " @react-native/ " ) ||
473+ $0. hasPrefix ( " react-native- " ) ||
474+ $0 == " expo " ||
475+ $0. hasPrefix ( " @expo/ " ) ||
476+ $0. hasPrefix ( " expo- " )
477+ } ) {
478+ return true
479+ }
480+ }
481+ return false
482+ }
389483}
0 commit comments