react-native-expo — independently scanned and version-tracked by SaferSkills.
SaferSkills independently audited react-native-expo (Agent Skill) and scored it 100/100 (green). The audit ran 55 deterministic rules across Security, Supply Chain, Maintenance, Transparency, and Community; it found 0 high-severity and 0 lower-severity findings. The full rule-by-rule trace and per-finding evidence are below. Free, methodology-open.
Findings & checks · 0 flagged
Every scanned point with the score it earned and what moved between them.
First recorded scan — no prior version to compare against.
The primary manifest — the file an agent reads to learn what this artifact does.
Expo Router v4 → File-based navigation (app/ directory)
NativeWind v4 → Tailwind CSS via className prop
React Native Reusables → Pre-built UI components (shadcn/ui port)
Reanimated 3 → 60fps animations on UI thread
Gesture Handler 2 → Pan, pinch, tap gestures
TypeScript → Required for all componentsmy-app/
├── app/
│ ├── _layout.tsx ← Root layout (fonts, providers, theme)
│ ├── index.tsx ← Entry/splash screen
│ ├── (auth)/
│ │ ├── _layout.tsx
│ │ ├── login.tsx
│ │ └── register.tsx
│ └── (tabs)/
│ ├── _layout.tsx ← Tab bar definition
│ ├── index.tsx ← Home tab
│ ├── search.tsx
│ └── profile.tsx
├── components/
│ ├── ui/ ← RNR components live here
│ │ ├── button.tsx
│ │ ├── dialog.tsx
│ │ └── ...
│ └── [feature-name]/ ← Feature-specific components
├── lib/
│ └── utils.ts ← cn() helper and utilities
├── constants/
│ └── colors.ts ← Design tokens
├── global.css ← Tailwind directives
├── tailwind.config.js
└── nativewind-env.d.tsconst { hairlineWidth } = require("nativewind/theme");
/** @type {import('tailwindcss').Config} */
module.exports = {
darkMode: "class",
content: ["./app/**/*.{js,jsx,ts,tsx}", "./components/**/*.{js,jsx,ts,tsx}"],
presets: [require("nativewind/preset")],
theme: {
extend: {
colors: {
border: "hsl(var(--border))",
input: "hsl(var(--input))",
ring: "hsl(var(--ring))",
background: "hsl(var(--background))",
foreground: "hsl(var(--foreground))",
primary: {
DEFAULT: "hsl(var(--primary))",
foreground: "hsl(var(--primary-foreground))",
},
secondary: {
DEFAULT: "hsl(var(--secondary))",
foreground: "hsl(var(--secondary-foreground))",
},
destructive: {
DEFAULT: "hsl(var(--destructive))",
foreground: "hsl(var(--destructive-foreground))",
},
muted: {
DEFAULT: "hsl(var(--muted))",
foreground: "hsl(var(--muted-foreground))",
},
accent: {
DEFAULT: "hsl(var(--accent))",
foreground: "hsl(var(--accent-foreground))",
},
card: {
DEFAULT: "hsl(var(--card))",
foreground: "hsl(var(--card-foreground))",
},
},
borderWidth: {
hairline: hairlineWidth(),
},
},
},
plugins: [],
};@tailwind base;
@tailwind components;
@tailwind utilities;
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 222.2 84% 4.9%;
--primary: 221.2 83.2% 53.3%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 221.2 83.2% 53.3%;
}
.dark:root {
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--primary: 217.2 91.2% 59.8%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 224.3 76.3% 48%;
}
}/// <reference types="nativewind/types" />import { clsx, type ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export function cn(...inputs: ClassValue[]) {
return twMerge(clsx(inputs));
}classNamedark: prefix: className="bg-white dark:bg-gray-900"className="ios:pt-safe android:pt-0"active:opacity-80, disabled:opacity-50, focus:border-primarygroup to parent, use group-active:scale-95 on childrencn() helper, never string concatenationstyle={{ height: dynamicHeight }})// ✅ Correct
<View className="flex-1 bg-background px-4 py-6">
<Text className="text-foreground text-xl font-bold dark:text-white">Title</Text>
</View>
// ❌ Wrong — never do this
<View style={{ flex: 1, backgroundColor: '#fff', paddingHorizontal: 16 }}># Setup (one-time)
npx @react-native-reusables/cli@latest init
# Add individual components
npx @react-native-reusables/cli@latest add button
npx @react-native-reusables/cli@latest add dialog
npx @react-native-reusables/cli@latest add bottom-sheet
npx @react-native-reusables/cli@latest add input
npx @react-native-reusables/cli@latest add card
npx @react-native-reusables/cli@latest add select
npx @react-native-reusables/cli@latest add avatar
npx @react-native-reusables/cli@latest add badge
npx @react-native-reusables/cli@latest add checkbox
npx @react-native-reusables/cli@latest add progress
npx @react-native-reusables/cli@latest add skeleton
npx @react-native-reusables/cli@latest add toast
npx @react-native-reusables/cli@latest add alert-dialog
npx @react-native-reusables/cli@latest add dropdown-menu
npx @react-native-reusables/cli@latest add label
npx @react-native-reusables/cli@latest add separator
npx @react-native-reusables/cli@latest add switch
npx @react-native-reusables/cli@latest add tabs
npx @react-native-reusables/cli@latest add tooltipshadcn MCP (alternative): Add registry to components.json:
{
"registries": {
"@rnr": "https://reactnativereusables.com/r/nativewind/{name}.json"
}
}// Button
import { Button } from '~/components/ui/button';
<Button variant="default" size="lg" onPress={handlePress}>
<Text>Submit</Text>
</Button>
// variants: default | destructive | outline | secondary | ghost | link
// sizes: default | sm | lg | icon
// Input
import { Input } from '~/components/ui/input';
<Input placeholder="Email" value={email} onChangeText={setEmail}
keyboardType="email-address" className="mb-4" />
// Card
import { Card, CardHeader, CardTitle, CardContent } from '~/components/ui/card';
<Card className="mx-4">
<CardHeader><CardTitle>Title</CardTitle></CardHeader>
<CardContent><Text>Content</Text></CardContent>
</Card>
// Dialog (Modal)
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '~/components/ui/dialog';
<Dialog>
<DialogTrigger asChild>
<Button><Text>Open</Text></Button>
</DialogTrigger>
<DialogContent>
<DialogHeader><DialogTitle>Dialog Title</DialogTitle></DialogHeader>
<Text>Dialog content here</Text>
</DialogContent>
</Dialog>
// Badge
import { Badge } from '~/components/ui/badge';
<Badge variant="secondary"><Text>New</Text></Badge>
// Avatar
import { Avatar, AvatarImage, AvatarFallback } from '~/components/ui/avatar';
<Avatar>
<AvatarImage source={{ uri: user.avatar }} />
<AvatarFallback><Text>JD</Text></AvatarFallback>
</Avatar>// Root _layout.tsx
import { Stack } from 'expo-router';
import { StatusBar } from 'expo-status-bar';
import '../global.css';
export default function RootLayout() {
return (
<>
<StatusBar style="auto" />
<Stack screenOptions={{ headerShown: false }}>
<Stack.Screen name="(auth)" />
<Stack.Screen name="(tabs)" />
</Stack>
</>
);
}
// Tab layout: app/(tabs)/_layout.tsx
import { Tabs } from 'expo-router';
import { Home, Search, User } from 'lucide-react-native';
export default function TabLayout() {
return (
<Tabs screenOptions={{
tabBarActiveTintColor: 'hsl(var(--primary))',
tabBarShowLabel: false,
}}>
<Tabs.Screen name="index" options={{
title: 'Home',
tabBarIcon: ({ color, size }) => <Home color={color} size={size} />,
}} />
<Tabs.Screen name="search" options={{
title: 'Search',
tabBarIcon: ({ color, size }) => <Search color={color} size={size} />,
}} />
</Tabs>
);
}
// Navigation
import { useRouter, Link } from 'expo-router';
const router = useRouter();
router.push('/profile/123');
router.replace('/(auth)/login');
router.back();
// Dynamic routes: app/profile/[id].tsx
import { useLocalSearchParams } from 'expo-router';
const { id } = useLocalSearchParams<{ id: string }>();style={{}} only for dynamic/computed values.className prop or editing the copied source.className={cn("base-classes", isActive && "active-classes", variant === 'primary' && "primary-classes")}[param].tsx for dynamic, (group)/ for logical groups, +not-found.tsx for 404.<SafeAreaProvider>, use useSafeAreaInsets() for custom padding.useColorScheme() from RNR or NativeWind. Set className on root View: className={isDark ? 'dark' : ''}.{items.map(...)} inside ScrollView for lists of 10+ items.React.memo for list items, useCallback for handlers passed to list items, Reanimated for 60fps.import { View, Text, ScrollView } from 'react-native';
import { SafeAreaView } from 'react-native-safe-area-context';
import { Button } from '~/components/ui/button';
import { Card, CardContent } from '~/components/ui/card';
export default function HomeScreen() {
return (
<SafeAreaView className="flex-1 bg-background">
<ScrollView className="flex-1" contentContainerClassName="px-4 py-6 gap-4">
<View className="flex-row items-center justify-between mb-2">
<Text className="text-2xl font-bold text-foreground">Dashboard</Text>
<Button variant="ghost" size="icon">
<Text>⚙️</Text>
</Button>
</View>
<Card>
<CardContent className="pt-4">
<Text className="text-muted-foreground text-sm">Revenue</Text>
<Text className="text-3xl font-bold text-foreground mt-1">$12,480</Text>
</CardContent>
</Card>
</ScrollView>
</SafeAreaView>
);
}references/nativewind-patterns.md — NativeWind v4 complete patternsreferences/rnr-components.md — All RNR component APIsreferences/expo-router-patterns.md — Navigation patternsreferences/reanimated-patterns.md — Animation patternsreferences/styling-patterns.md — Theming, dark mode, responsive~30 seconds. Free. No account. Every finding cites a rule and a line of evidence.