├── index.ts ├── src ├── assets │ ├── fonts │ │ ├── __tests__ │ │ │ └── font.mock.ts │ │ ├── MADECarvingSoft-Bold.otf │ │ ├── MADECarvingSoft-Thin.otf │ │ ├── MADECarvingSoft-Black.otf │ │ ├── MADECarvingSoft-Light.otf │ │ ├── MADECarvingSoft-Medium.otf │ │ ├── MADECarvingSoft-Regular.otf │ │ ├── MADECarvingSoft-SemiBold.otf │ │ ├── MADECarvingSoft-ExtraLight.otf │ │ ├── MADECarvingSoftOutline-Bold.otf │ │ ├── MADECarvingSoftOutline-Thin.otf │ │ ├── MADECarvingSoftOutline-Black.otf │ │ ├── MADECarvingSoftOutline-Light.otf │ │ ├── MADECarvingSoftOutline-Medium.otf │ │ ├── MADECarvingSoftOutline-Regular.otf │ │ ├── MADECarvingSoftOutline-SemiBold.otf │ │ └── MADECarvingSoftOutline-ExtraLight.otf │ └── images │ │ ├── __tests__ │ │ └── image.mock.ts │ │ ├── splash.png │ │ └── app-icon.png ├── utils │ ├── __tests__ │ │ └── dimensions.test.ts │ └── dimensions.ts ├── components │ ├── Spinner.tsx │ ├── ScreenLayout.tsx │ ├── __tests__ │ │ ├── Spinner.test.tsx │ │ └── LinkButton.test.tsx │ └── LinkButton.tsx ├── hooks │ ├── __tests__ │ │ └── useCacheAssets.test.ts │ └── useCacheAssets.ts ├── app │ ├── _layout.tsx │ ├── index.tsx │ ├── second │ │ └── index.tsx │ └── +html.tsx └── config │ └── theme.ts ├── eas.json ├── .gitignore ├── .prettierrc ├── babel.config.js ├── global.d.ts ├── tsconfig.json ├── eslint.config.mjs ├── app.json ├── README.md ├── package.json └── jest.config.js /index.ts: -------------------------------------------------------------------------------- 1 | import 'expo-router/entry' 2 | -------------------------------------------------------------------------------- /src/assets/fonts/__tests__/font.mock.ts: -------------------------------------------------------------------------------- 1 | export default { type: 'font', mocked: true } 2 | -------------------------------------------------------------------------------- /src/assets/images/__tests__/image.mock.ts: -------------------------------------------------------------------------------- 1 | export default { type: 'image', mocked: true } 2 | -------------------------------------------------------------------------------- /eas.json: -------------------------------------------------------------------------------- 1 | { 2 | "build": { "preview": { "ios": { "simulator": true } }, "production": {} } 3 | } 4 | -------------------------------------------------------------------------------- /src/assets/images/splash.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedevenvironment/expo-router-typescript/HEAD/src/assets/images/splash.png -------------------------------------------------------------------------------- /src/assets/images/app-icon.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedevenvironment/expo-router-typescript/HEAD/src/assets/images/app-icon.png -------------------------------------------------------------------------------- /src/assets/fonts/MADECarvingSoft-Bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedevenvironment/expo-router-typescript/HEAD/src/assets/fonts/MADECarvingSoft-Bold.otf -------------------------------------------------------------------------------- /src/assets/fonts/MADECarvingSoft-Thin.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedevenvironment/expo-router-typescript/HEAD/src/assets/fonts/MADECarvingSoft-Thin.otf -------------------------------------------------------------------------------- /src/assets/fonts/MADECarvingSoft-Black.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedevenvironment/expo-router-typescript/HEAD/src/assets/fonts/MADECarvingSoft-Black.otf -------------------------------------------------------------------------------- /src/assets/fonts/MADECarvingSoft-Light.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedevenvironment/expo-router-typescript/HEAD/src/assets/fonts/MADECarvingSoft-Light.otf -------------------------------------------------------------------------------- /src/assets/fonts/MADECarvingSoft-Medium.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedevenvironment/expo-router-typescript/HEAD/src/assets/fonts/MADECarvingSoft-Medium.otf -------------------------------------------------------------------------------- /src/assets/fonts/MADECarvingSoft-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedevenvironment/expo-router-typescript/HEAD/src/assets/fonts/MADECarvingSoft-Regular.otf -------------------------------------------------------------------------------- /src/assets/fonts/MADECarvingSoft-SemiBold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedevenvironment/expo-router-typescript/HEAD/src/assets/fonts/MADECarvingSoft-SemiBold.otf -------------------------------------------------------------------------------- /src/assets/fonts/MADECarvingSoft-ExtraLight.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedevenvironment/expo-router-typescript/HEAD/src/assets/fonts/MADECarvingSoft-ExtraLight.otf -------------------------------------------------------------------------------- /src/assets/fonts/MADECarvingSoftOutline-Bold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedevenvironment/expo-router-typescript/HEAD/src/assets/fonts/MADECarvingSoftOutline-Bold.otf -------------------------------------------------------------------------------- /src/assets/fonts/MADECarvingSoftOutline-Thin.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedevenvironment/expo-router-typescript/HEAD/src/assets/fonts/MADECarvingSoftOutline-Thin.otf -------------------------------------------------------------------------------- /src/assets/fonts/MADECarvingSoftOutline-Black.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedevenvironment/expo-router-typescript/HEAD/src/assets/fonts/MADECarvingSoftOutline-Black.otf -------------------------------------------------------------------------------- /src/assets/fonts/MADECarvingSoftOutline-Light.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedevenvironment/expo-router-typescript/HEAD/src/assets/fonts/MADECarvingSoftOutline-Light.otf -------------------------------------------------------------------------------- /src/assets/fonts/MADECarvingSoftOutline-Medium.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedevenvironment/expo-router-typescript/HEAD/src/assets/fonts/MADECarvingSoftOutline-Medium.otf -------------------------------------------------------------------------------- /src/assets/fonts/MADECarvingSoftOutline-Regular.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedevenvironment/expo-router-typescript/HEAD/src/assets/fonts/MADECarvingSoftOutline-Regular.otf -------------------------------------------------------------------------------- /src/assets/fonts/MADECarvingSoftOutline-SemiBold.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedevenvironment/expo-router-typescript/HEAD/src/assets/fonts/MADECarvingSoftOutline-SemiBold.otf -------------------------------------------------------------------------------- /src/assets/fonts/MADECarvingSoftOutline-ExtraLight.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/thedevenvironment/expo-router-typescript/HEAD/src/assets/fonts/MADECarvingSoftOutline-ExtraLight.otf -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules/ 2 | .expo/ 3 | .expo-shared/ 4 | dist/ 5 | npm-debug.* 6 | *.jks 7 | *.p8 8 | *.p12 9 | *.key 10 | *.mobileprovision 11 | *.orig.* 12 | web-build/ 13 | build-*.tar.gz 14 | coverage 15 | 16 | # macOS 17 | .DS_Store 18 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "arrowParens": "always", 3 | "bracketSameLine": true, 4 | "bracketSpacing": true, 5 | "endOfLine": "lf", 6 | "jsxSingleQuote": false, 7 | "printWidth": 200, 8 | "semi": false, 9 | "singleQuote": true, 10 | "tabWidth": 2, 11 | "trailingComma": "none", 12 | "useTabs": false 13 | } 14 | -------------------------------------------------------------------------------- /src/utils/__tests__/dimensions.test.ts: -------------------------------------------------------------------------------- 1 | import { dimensions } from 'src/utils/dimensions' 2 | 3 | jest.mock('react-native', () => ({ Dimensions: { get: jest.fn(() => ({ height: 1000, width: 1000 })) } })) 4 | 5 | describe('src/assets/styles/dimensions', () => { 6 | it('should return 100px', () => { 7 | const value = dimensions(100, 'px') 8 | 9 | expect(value).toBe('117px') 10 | }) 11 | }) 12 | -------------------------------------------------------------------------------- /babel.config.js: -------------------------------------------------------------------------------- 1 | module.exports = function (api) { 2 | api.cache(true) 3 | return { 4 | presets: ['babel-preset-expo'], 5 | plugins: [ 6 | // Compile Exported Namespaces 7 | '@babel/plugin-proposal-export-namespace-from', 8 | // React Native Reanimated 9 | 'react-native-reanimated/plugin', 10 | // Use Absolute Imports 11 | ['module-resolver', { alias: { src: './src' } }] 12 | ] 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /global.d.ts: -------------------------------------------------------------------------------- 1 | /* Asset Extensions */ 2 | declare module '*.png' 3 | declare module '*.svg' 4 | declare module '*.jpeg' 5 | declare module '*.jpg' 6 | declare module '*.otf' 7 | 8 | /* Styled Components */ 9 | declare module 'styled-components' { 10 | import type { CSSProp } from 'styled-components' 11 | import { appTheme } from 'src/config/theme' 12 | type ThemeType = typeof appTheme 13 | export interface DefaultTheme extends ThemeType {} 14 | } 15 | -------------------------------------------------------------------------------- /src/components/Spinner.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native' 2 | import { ActivityIndicator } from 'react-native' 3 | import { appTheme } from 'src/config/theme' 4 | 5 | export default function Spinner() { 6 | return ( 7 | 8 | 9 | 10 | ) 11 | } 12 | 13 | const S = { 14 | Spinner: styled.View` 15 | background-color: ${appTheme.background}; 16 | height: 100%; 17 | width: 100%; 18 | display: flex; 19 | align-items: center; 20 | justify-content: center; 21 | ` 22 | } 23 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "$schema": "https://json.schemastore.org/tsconfig", 3 | "display": "Expo", 4 | "compilerOptions": { 5 | "types": ["./global.d.ts", ], 6 | "allowJs": true, 7 | "esModuleInterop": true, 8 | "jsx": "react-native", 9 | "lib": ["DOM", "ESNext"], 10 | "moduleResolution": "node", 11 | "noImplicitThis": false, 12 | "noEmit": true, 13 | "resolveJsonModule": true, 14 | "skipLibCheck": true, 15 | "target": "ESNext", 16 | "baseUrl": ".", 17 | "paths": { 18 | "src/*": ["src/*"] 19 | }, 20 | "strict": true 21 | }, 22 | "exclude": ["jest.config.js"], 23 | "extends": "expo/tsconfig.base" 24 | } 25 | -------------------------------------------------------------------------------- /src/hooks/__tests__/useCacheAssets.test.ts: -------------------------------------------------------------------------------- 1 | import useCacheAssets from 'src/hooks/useCacheAssets' 2 | 3 | jest.mock('expo-font', () => ({ 4 | useFonts: jest.fn(() => [true, true]) 5 | })) 6 | 7 | jest.mock('react-native', () => ({ 8 | Dimensions: { get: jest.fn(() => ({ height: 1000, width: 1000 })) }, 9 | Platform: { OS: 'ios' } 10 | })) 11 | 12 | jest.mock('react', () => ({ 13 | useEffect: jest.fn(), 14 | useMemo: jest.fn(() => true), 15 | useState: jest.fn(() => [true, jest.fn()]) 16 | })) 17 | 18 | describe('src/hooks/useCacheAssets', () => { 19 | it('should return appLoaded', () => { 20 | const areAssetsCached = useCacheAssets() 21 | 22 | expect(areAssetsCached).toBe(true) 23 | }) 24 | }) 25 | -------------------------------------------------------------------------------- /src/utils/dimensions.ts: -------------------------------------------------------------------------------- 1 | import { Dimensions } from 'react-native' 2 | 3 | export const { height, width } = Dimensions.get('window') 4 | 5 | /** 6 | * Will Get Window Height between 600 - 1100 7 | * - 8 | */ 9 | function getWindowHeight() { 10 | let numerator 11 | const denominator = 850 12 | if (height < 600) numerator = 600 13 | else if (height > 1100) numerator = 1100 14 | else numerator = height 15 | return Math.floor((numerator / denominator) * 100) / 100 16 | } 17 | 18 | /** 19 | * Will Return A Dynamic Value Based On Window Size. 20 | * - 21 | */ 22 | export function dimensions(value: number, suffix: string): string { 23 | const size = value * getWindowHeight() 24 | return size + suffix 25 | } 26 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import globals from 'globals' 2 | import pluginJs from '@eslint/js' 3 | import tseslint from 'typescript-eslint' 4 | import pluginReact from 'eslint-plugin-react' 5 | 6 | /** @type {import('eslint').Linter.Config[]} */ 7 | export default [ 8 | { files: ['**/*.{js,mjs,cjs,ts,jsx,tsx}'] }, 9 | { languageOptions: { globals: { ...globals.browser, ...globals.node } } }, 10 | pluginJs.configs.recommended, 11 | ...tseslint.configs.recommended, 12 | pluginReact.configs.flat.recommended, 13 | { 14 | plugins: { react: pluginReact }, 15 | settings: { react: { version: '^18.3.1' } }, 16 | rules: { 17 | 'react/react-in-jsx-scope': 'off', 18 | '@typescript-eslint/no-empty-object-type': 'off', 19 | '@typescript-eslint/no-require-imports': 'off' 20 | } 21 | } 22 | ] 23 | -------------------------------------------------------------------------------- /src/app/_layout.tsx: -------------------------------------------------------------------------------- 1 | import 'expo-dev-client' 2 | import { ThemeProvider as NavProvider } from '@react-navigation/native' 3 | import { Slot } from 'expo-router' 4 | import { StatusBar } from 'expo-status-bar' 5 | import styled, { ThemeProvider } from 'styled-components/native' 6 | import { appTheme, navTheme } from 'src/config/theme' 7 | 8 | export default function AppLayout() { 9 | return ( 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | ) 19 | } 20 | 21 | const S = { 22 | AppWrapper: styled.SafeAreaView` 23 | flex: 1; 24 | flex-direction: column; 25 | background-color: ${appTheme.background}; 26 | ` 27 | } 28 | -------------------------------------------------------------------------------- /src/hooks/useCacheAssets.ts: -------------------------------------------------------------------------------- 1 | import { useFonts } from 'expo-font' 2 | 3 | /** 4 | * Use Cache Assets Before Render 5 | * - 6 | */ 7 | export default function useCacheAssets() { 8 | const [fontsLoaded] = useFonts({ 9 | madeBlack: require('src/assets/fonts/MADECarvingSoft-Black.otf'), 10 | madeBold: require('src/assets/fonts/MADECarvingSoft-Bold.otf'), 11 | madeSemiBold: require('src/assets/fonts/MADECarvingSoft-SemiBold.otf'), 12 | madeRegular: require('src/assets/fonts/MADECarvingSoft-Regular.otf'), 13 | madeMedium: require('src/assets/fonts/MADECarvingSoft-Medium.otf'), 14 | madeLight: require('src/assets/fonts/MADECarvingSoft-Light.otf'), 15 | madeExtraLight: require('src/assets/fonts/MADECarvingSoft-ExtraLight.otf'), 16 | madeThin: require('src/assets/fonts/MADECarvingSoft-Thin.otf') 17 | }) 18 | 19 | return fontsLoaded 20 | } 21 | -------------------------------------------------------------------------------- /src/config/theme.ts: -------------------------------------------------------------------------------- 1 | import type { Theme as NavTheme } from '@react-navigation/native' 2 | import type { DefaultTheme } from 'styled-components/native' 3 | import { dimensions, height, width } from 'src/utils/dimensions' 4 | 5 | /** 6 | * Theme For Styled Components 7 | * - 8 | */ 9 | export const appTheme = { 10 | background: '#003C4F', 11 | primary: '#FFF', 12 | secondary: '#CCC', 13 | highlight: '#F35570', 14 | size: dimensions, 15 | windowHeight: `${height}px`, 16 | windowWidth: `${width}px` 17 | } as DefaultTheme 18 | 19 | /** 20 | * Theme For Expo Navigation Header 21 | * - 22 | */ 23 | export const navTheme = { 24 | dark: false, 25 | colors: { 26 | background: appTheme.background, 27 | border: appTheme.secondary, 28 | card: appTheme.background, 29 | notification: appTheme.highlight, 30 | primary: appTheme.primary, 31 | text: appTheme.primary 32 | } 33 | } as NavTheme 34 | -------------------------------------------------------------------------------- /src/components/ScreenLayout.tsx: -------------------------------------------------------------------------------- 1 | import Head from 'expo-router/head' 2 | import { Stack } from 'expo-router' 3 | import styled from 'styled-components/native' 4 | import Spinner from 'src/components/Spinner' 5 | import useCacheAssets from 'src/hooks/useCacheAssets' 6 | 7 | interface Props { 8 | children: React.ReactNode 9 | desc?: string 10 | title: string | undefined 11 | } 12 | 13 | export default function ScreenLayout({ children, desc, title }: Props) { 14 | const areAssetsCached = useCacheAssets() 15 | 16 | return ( 17 | 18 | 19 | 20 | {title} 21 | 22 | 23 | 24 | {areAssetsCached ? children : } 25 | 26 | ) 27 | } 28 | 29 | const S = { 30 | Wrapper: styled.View` 31 | flex: 1; 32 | ` 33 | } 34 | -------------------------------------------------------------------------------- /src/components/__tests__/Spinner.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { create, type ReactTestRendererJSON } from 'react-test-renderer' 3 | import { ThemeProvider } from 'styled-components/native' 4 | import { appTheme } from 'src/config/theme' 5 | import Spinner from 'src/components/Spinner' 6 | 7 | jest.mock('expo-router', () => ({ Link: 'Link' })) 8 | 9 | describe('src/components/Spinner', () => { 10 | const SpinnerComponent = ( 11 | 12 | 13 | 14 | ) 15 | 16 | it('renders correctly', () => { 17 | const spinner = create(SpinnerComponent).toJSON() as ReactTestRendererJSON 18 | const activityIndicator = spinner.children![0] as ReactTestRendererJSON 19 | 20 | expect(spinner.type).toBe('View') 21 | expect(spinner.props.testID).toBe('spinner') 22 | 23 | expect(activityIndicator.type).toBe('ActivityIndicator') 24 | expect(activityIndicator!.props.testID).toBe('activity-indicator') 25 | expect(activityIndicator!.props.size).toBe('large') 26 | }) 27 | }) 28 | -------------------------------------------------------------------------------- /src/app/index.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native' 2 | import { Stack } from 'expo-router' 3 | import LinkButton from 'src/components/LinkButton' 4 | import ScreenLayout from 'src/components/ScreenLayout' 5 | 6 | export default function HomeScreen() { 7 | return ( 8 | 9 | 10 | 11 | 12 | 🌉 13 | Go to app/index.tsx to edit 14 | 15 | 16 | 17 | 18 | ) 19 | } 20 | 21 | const S = { 22 | Content: styled.View` 23 | flex: 1; 24 | align-items: center; 25 | justify-content: center; 26 | gap: 10px; 27 | `, 28 | Title: styled.Text` 29 | font-size: ${(p) => p.theme.size(150, 'px')}; 30 | `, 31 | Text: styled.Text` 32 | color: ${(p) => p.theme.primary}; 33 | font-family: madeRegular; 34 | font-size: ${(p) => p.theme.size(15, 'px')}; 35 | ` 36 | } 37 | -------------------------------------------------------------------------------- /src/app/second/index.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native' 2 | import { Stack } from 'expo-router' 3 | import LinkButton from 'src/components/LinkButton' 4 | import ScreenLayout from 'src/components/ScreenLayout' 5 | 6 | export default function SecondScreen() { 7 | return ( 8 | 9 | 10 | 11 | 12 | 🥈 13 | Go to app/second/index.tsx to edit 14 | 15 | 16 | 17 | 18 | ) 19 | } 20 | 21 | const S = { 22 | Content: styled.View` 23 | flex: 1; 24 | align-items: center; 25 | justify-content: center; 26 | gap: 10px; 27 | `, 28 | Title: styled.Text` 29 | font-size: ${(p) => p.theme.size(150, 'px')}; 30 | `, 31 | Text: styled.Text` 32 | color: ${(p) => p.theme.primary}; 33 | font-family: madeRegular; 34 | font-size: ${(p) => p.theme.size(15, 'px')}; 35 | ` 36 | } 37 | -------------------------------------------------------------------------------- /src/components/__tests__/LinkButton.test.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react' 2 | import { create, type ReactTestRendererJSON } from 'react-test-renderer' 3 | import { ThemeProvider } from 'styled-components/native' 4 | import { appTheme } from 'src/config/theme' 5 | import LinkButton from 'src/components/LinkButton' 6 | 7 | jest.mock('expo-router', () => ({ Link: 'Link' })) 8 | 9 | describe('src/components/LinkButton', () => { 10 | const LinkButtonComponent = ( 11 | 12 | 13 | 14 | ) 15 | 16 | it('renders correctly', () => { 17 | const linkButton = create(LinkButtonComponent).toJSON() as ReactTestRendererJSON 18 | const linkButtonText = linkButton.children![0] as ReactTestRendererJSON 19 | 20 | expect(linkButton.type).toBe('Link') 21 | expect(linkButton.props.testID).toBe('link-button') 22 | expect(linkButton!.props.href).toBe('/') 23 | 24 | expect(linkButtonText.type).toBe('Text') 25 | expect(linkButtonText!.children![0]).toBe('test') 26 | expect(linkButtonText!.props.testID).toBe('link-button-text') 27 | }) 28 | }) 29 | -------------------------------------------------------------------------------- /app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "name": "Expo Router Typescript", 4 | "slug": "expo-router-typescript", 5 | "scheme": "app", 6 | "version": "1.0.0", 7 | "orientation": "portrait", 8 | "icon": "./src/assets/images/app-icon.png", 9 | "userInterfaceStyle": "automatic", 10 | "newArchEnabled": true, 11 | "splash": { 12 | "image": "./src/assets/images/splash.png", 13 | "backgroundColor": "#222" 14 | }, 15 | "experiments": { 16 | "reactServerFunctions": false 17 | }, 18 | "assetBundlePatterns": ["assets/*"], 19 | "ios": { 20 | "supportsTablet": false 21 | }, 22 | "android": { 23 | "adaptiveIcon": { 24 | "foregroundImage": "./src/assets/images/app-icon.png", 25 | "backgroundColor": "#222" 26 | }, 27 | "package": "com.haytherecharlie.exporoutertypescript" 28 | }, 29 | "web": { 30 | "favicon": "./src/assets/images/app-icon.png", 31 | "output": "static", 32 | "bundler": "metro" 33 | }, 34 | "plugins": ["expo-router", "expo-font"], 35 | "extra": { 36 | "router": { 37 | "origin": false 38 | } 39 | } 40 | } 41 | } 42 | -------------------------------------------------------------------------------- /src/components/LinkButton.tsx: -------------------------------------------------------------------------------- 1 | import styled from 'styled-components/native' 2 | import { Link } from 'expo-router' 3 | import { openURL } from 'expo-linking' 4 | 5 | interface Props { 6 | href: string 7 | text: string 8 | } 9 | 10 | export default function LinkButton({ href, text }: Props) { 11 | return href.substring(0, 1) === '/' ? ( 12 | 13 | {text} 14 | 15 | ) : ( 16 | openURL(href)}> 17 | {text} 18 | 19 | ) 20 | } 21 | 22 | const S = { 23 | ExternalLink: styled.TouchableOpacity` 24 | padding: ${(p) => p.theme.size(10, 'px')} ${(p) => p.theme.size(20, 'px')}; 25 | border-color: ${(p) => p.theme.highlight}; 26 | border-width: ${(p) => p.theme.size(1, 'px')}; 27 | border-radius: ${(p) => p.theme.size(5, 'px')}; 28 | background-color: transparent; 29 | `, 30 | InternalLink: styled(Link)` 31 | padding: ${(p) => p.theme.size(10, 'px')} ${(p) => p.theme.size(20, 'px')}; 32 | border-color: ${(p) => p.theme.highlight}; 33 | border-width: ${(p) => p.theme.size(1, 'px')}; 34 | border-radius: ${(p) => p.theme.size(5, 'px')}; 35 | background-color: transparent; 36 | `, 37 | LinkText: styled.Text` 38 | color: ${(p) => p.theme.highlight}; 39 | font-family: madeBold; 40 | ` 41 | } 42 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | ![Expo Router Typescript](https://thedevenvironment.com/expo-router-typescript.png) 2 | 3 | --- 4 | 5 | **INTRODUCTION** 6 | 7 | This repo is a scaffolding of an Expo application that uses Expo Router and it's file based routing capibilities. It has all the necessary packages needed to just start the application and begin adding routes. 8 | 9 | The reason it exists is to mitigate the amount of work needed to add typescript, jest, absolute imports and eslint into the traditional quickstart that the "create-expo-app" CLI command provides. 10 | 11 | Two sample screens have been created, so everything will run out of the box. Also a number of useful scripts have been created in the package.json that allow the ability to run, build, test, lint and serve the application. 12 | 13 | --- 14 | 15 | **FOLDER STRUCTURE** 16 | 17 | - `src`: The main directory of the application. 18 | 19 | - `app`: Folder based routing directory. 20 | 21 | - `assets`: Images, fonts, sounds, etc. 22 | 23 | - `components`: Reusable React components. 24 | 25 | - `config`: Shared configuration values. 26 | 27 | - `hooks`: Reusable hooks. 28 | 29 | - `utils`: Helpers and reusable methods. 30 | 31 | --- 32 | 33 | **GETTING STARTED** 34 | 35 | ```bash 36 | # Clone Repo 37 | git clone git@github.com:thedevenvironment/expo-router-typescript.git 38 | ``` 39 | 40 | ```bash 41 | # Install Dependencies 42 | npm run setup 43 | ``` 44 | 45 | ```bash 46 | # Start The Dev Server 47 | npm run dev 48 | 49 | # Press 's' to switch to dev build 50 | # Press 'a' to open Android simulator 51 | # Press 'i' to open iOS simulator 52 | # Press 'w' to open web browser 53 | ``` 54 | 55 | --- 56 | 57 | **LEARN MORE** 58 | 59 | [App.json Documentation](https://docs.expo.dev/versions/latest/config/app/) 60 | 61 | [Expo Documentation](https://docs.expo.dev/tutorial/introduction/) 62 | 63 | [Expo Router Documentation](https://expo.github.io/router/docs/) 64 | 65 | [React Navigation Documentation](https://reactnavigation.org/docs/getting-started) 66 | -------------------------------------------------------------------------------- /src/app/+html.tsx: -------------------------------------------------------------------------------- 1 | import type { PropsWithChildren } from 'react' 2 | import { ScrollViewStyleReset } from 'expo-router/html' 3 | 4 | export default function Root({ children }: PropsWithChildren) { 5 | return ( 6 | 7 | 8 | 9 | 10 | 11 | Expo Router Typescript 12 | 13 | 14 | 15 | 16 | 17 | 18 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | {children} 39 | 40 | ) 41 | } 42 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "expo-router-typescript", 3 | "scripts": { 4 | "build:android": "npx eas-cli build -p android --profile preview --local", 5 | "build:ios": "npx eas-cli build -p ios --profile preview --local", 6 | "build:web": "npx expo export --platform web -c --output-dir dist", 7 | "dev": "npm run start", 8 | "format": "npx prettier src --write", 9 | "lint": "eslint src/**/*.tsx src/**/*.ts", 10 | "lint:fix": "npm run lint -- --fix", 11 | "offline": "expo start --offline", 12 | "publish:expo": "npx eas-cli update", 13 | "publish:web": "firebase deploy --only hosting", 14 | "serve:app": "expo start --dev-client", 15 | "serve:web": "npx serve dist", 16 | "setup": "npm install expo && expo install", 17 | "start": "expo start --go", 18 | "test": "jest", 19 | "upgrade": "expo upgrade", 20 | "web": "expo start --web --no-dev --host localhost --port 3000" 21 | }, 22 | "overrides": { 23 | "cookie": "^0.7.0" 24 | }, 25 | "dependencies": { 26 | "@react-navigation/native": "^7.0.0", 27 | "expo": "~52.0.7", 28 | "expo-dev-client": "~5.0.1", 29 | "expo-font": "~13.0.1", 30 | "expo-linking": "~7.0.2", 31 | "expo-router": "~4.0.5", 32 | "expo-status-bar": "~2.0.0", 33 | "react": "18.3.1", 34 | "react-dom": "18.3.1", 35 | "react-native": "0.76.6", 36 | "react-native-animatable": "^1.4.0", 37 | "react-native-gesture-handler": "~2.20.2", 38 | "react-native-reanimated": "~3.16.1", 39 | "react-native-safe-area-context": "4.12.0", 40 | "react-native-screens": "~4.4.0", 41 | "react-native-web": "~0.19.13", 42 | "styled-components": "^6.1.13" 43 | }, 44 | "devDependencies": { 45 | "@babel/core": "^7.26.0", 46 | "@babel/plugin-proposal-export-namespace-from": "^7.18.9", 47 | "@eslint/js": "^9.14.0", 48 | "@testing-library/jest-native": "^5.4.3", 49 | "@testing-library/react-native": "^12.8.1", 50 | "@types/jest": "^29.5.14", 51 | "@types/react": "~18.3.12", 52 | "@types/react-test-renderer": "^18.3.0", 53 | "@types/styled-components": "^5.1.34", 54 | "@types/styled-components-react-native": "^5.2.5", 55 | "@typescript-eslint/eslint-plugin": "^8.14.0", 56 | "@typescript-eslint/parser": "^8.14.0", 57 | "ajv": "^8.17.1", 58 | "babel-plugin-module-resolver": "^5.0.2", 59 | "eslint": "^9.14.0", 60 | "eslint-config-prettier": "^9.1.0", 61 | "eslint-plugin-react": "^7.37.2", 62 | "globals": "^15.12.0", 63 | "jest": "^29.7.0", 64 | "jest-expo": "~52.0.1", 65 | "prettier": "^3.3.3", 66 | "react-test-renderer": "^18.3.1", 67 | "typescript": "^5.6.3", 68 | "typescript-eslint": "^8.14.0" 69 | }, 70 | "expo": { 71 | "doctor": { 72 | "reactNativeDirectoryCheck": { 73 | "listUnknownPackages": false 74 | } 75 | } 76 | } 77 | } 78 | -------------------------------------------------------------------------------- /jest.config.js: -------------------------------------------------------------------------------- 1 | /* 2 | * For a detailed explanation regarding each configuration property, visit: 3 | * https://jestjs.io/docs/configuration 4 | */ 5 | 6 | module.exports = { 7 | // All imported modules in your tests should be mocked automatically 8 | // automock: false, 9 | 10 | // Stop running tests after `n` failures 11 | bail: 5, 12 | 13 | // The directory where Jest should store its cached dependency information 14 | // cacheDirectory: "/private/var/folders/0k/2mdd_0f172zcn7scm2rktt1r0000gn/T/jest_dx", 15 | 16 | // Automatically clear mock calls, instances, contexts and results before every test 17 | clearMocks: true, 18 | 19 | // Indicates whether the coverage information should be collected while executing the test 20 | collectCoverage: true, 21 | 22 | // An array of glob patterns indicating a set of files for which coverage information should be collected 23 | // collectCoverageFrom: undefined, 24 | 25 | // The directory where Jest should output its coverage files 26 | coverageDirectory: 'coverage', 27 | 28 | // An array of regexp pattern strings used to skip coverage collection 29 | coveragePathIgnorePatterns: ['/node_modules/'], 30 | 31 | // Indicates which provider should be used to instrument code for coverage 32 | // coverageProvider: "babel", 33 | 34 | // A list of reporter names that Jest uses when writing coverage reports 35 | coverageReporters: ['html'], 36 | 37 | // An object that configures minimum threshold enforcement for coverage results 38 | // coverageThreshold: undefined, 39 | 40 | // A path to a custom dependency extractor 41 | // dependencyExtractor: undefined, 42 | 43 | // Make calling deprecated APIs throw helpful error messages 44 | // errorOnDeprecated: false, 45 | 46 | // The default configuration for fake timers 47 | // fakeTimers: { 48 | // "enableGlobally": false 49 | // }, 50 | 51 | // Force coverage collection from ignored files using an array of glob patterns 52 | // forceCoverageMatch: [], 53 | 54 | // A path to a module which exports an async function that is triggered once before all test suites 55 | // globalSetup: undefined, 56 | 57 | // A path to a module which exports an async function that is triggered once after all test suites 58 | // globalTeardown: undefined, 59 | 60 | // A set of global variables that need to be available in all test environments 61 | // globals: {}, 62 | 63 | // The maximum amount of workers used to run your tests. Can be specified as % or a number. E.g. maxWorkers: 10% will use 10% of your CPU amount + 1 as the maximum worker number. maxWorkers: 2 will use a maximum of 2 workers. 64 | // maxWorkers: "50%", 65 | 66 | // An array of directory names to be searched recursively up from the requiring module's location 67 | // moduleDirectories: [ 68 | // "node_modules" 69 | // ], 70 | 71 | // An array of file extensions your modules use 72 | // moduleFileExtensions: [ 73 | // "js", 74 | // "mjs", 75 | // "cjs", 76 | // "jsx", 77 | // "ts", 78 | // "tsx", 79 | // "json", 80 | // "node" 81 | // ], 82 | 83 | // A map from regular expressions to module names or to arrays of module names that allow to stub out resources with a single module 84 | moduleNameMapper: { 85 | '\\.(wav|mp3|m4a|aac|oga)$': '/src/assets/audio/__tests__/audio.mock.ts', 86 | '\\.(eot|otf|webp|svg|ttf|woff|woff2)$': '/src/assets/fonts/__tests__/font.mock.ts', 87 | '\\.(jpg|ico|jpeg|png|gif)$': '/src/assets/images/__tests__/image.mock.ts', 88 | '\\.(css|less)$': '/src/assets/styles/__tests__/style.mock.ts', 89 | '\\.(mp4|webm)$': '/src/assets/videos/__tests__/video.mock.ts' 90 | }, 91 | 92 | // An array of regexp pattern strings, matched against all module paths before considered 'visible' to the module loader 93 | // modulePathIgnorePatterns: [], 94 | 95 | // Activates notifications for test results 96 | // notify: false, 97 | 98 | // An enum that specifies notification mode. Requires { notify: true } 99 | // notifyMode: "failure-change", 100 | 101 | // A preset that is used as a base for Jest's configuration 102 | preset: 'jest-expo', 103 | 104 | // Run tests from one or more projects 105 | // projects: undefined, 106 | 107 | // Use this configuration option to add custom reporters to Jest 108 | // reporters: undefined, 109 | 110 | // Automatically reset mock state before every test 111 | // resetMocks: false, 112 | 113 | // Reset the module registry before running each individual test 114 | // resetModules: false, 115 | 116 | // A path to a custom resolver 117 | // resolver: undefined, 118 | 119 | // Automatically restore mock state and implementation before every test 120 | // restoreMocks: false, 121 | 122 | // The root directory that Jest should scan for tests and modules within 123 | // rootDir: undefined, 124 | 125 | // A list of paths to directories that Jest should use to search for files in 126 | // roots: [ 127 | // "" 128 | // ], 129 | 130 | // Allows you to use a custom runner instead of Jest's default test runner 131 | // runner: "jest-runner", 132 | 133 | // The paths to modules that run some code to configure or set up the testing environment before each test 134 | // setupFiles: [], 135 | 136 | // A list of paths to modules that run some code to configure or set up the testing framework before each test 137 | setupFilesAfterEnv: ['@testing-library/jest-native/extend-expect'], 138 | 139 | // The number of seconds after which a test is considered as slow and reported as such in the results. 140 | // slowTestThreshold: 5, 141 | 142 | // A list of paths to snapshot serializer modules Jest should use for snapshot testing 143 | // snapshotSerializers: [], 144 | 145 | // The test environment that will be used for testing 146 | testEnvironment: 'node', 147 | 148 | // Options that will be passed to the testEnvironment 149 | // testEnvironmentOptions: {}, 150 | 151 | // Adds a location field to test results 152 | // testLocationInResults: false, 153 | 154 | // The glob patterns Jest uses to detect test files 155 | testMatch: ['**/__tests__/**/*.test.[jt]s?(x)'], 156 | 157 | // An array of regexp pattern strings that are matched against all test paths, matched tests are skipped 158 | testPathIgnorePatterns: ['/node_modules/'], 159 | 160 | // The regexp pattern or array of patterns that Jest uses to detect test files 161 | // testRegex: [], 162 | 163 | // This option allows the use of a custom results processor 164 | // testResultsProcessor: undefined, 165 | 166 | // This option allows use of a custom test runner 167 | // testRunner: "jest-circus/runner", 168 | 169 | // A map from regular expressions to paths to transformers 170 | // transform: undefined, 171 | 172 | // An array of regexp pattern strings that are matched against all source file paths, matched files will skip transformation 173 | transformIgnorePatterns: [ 174 | 'node_modules/(?!((jest-)?react-native|@react-native(-community)?)|expo(nent)?|@expo(nent)?/.*|@expo-google-fonts/.*|react-navigation|@react-navigation/.*|@unimodules/.*|unimodules|sentry-expo|native-base|react-native-svg)' 175 | ], 176 | 177 | // An array of regexp pattern strings that are matched against all modules before the module loader will automatically return a mock for them 178 | // unmockedModulePathPatterns: undefined, 179 | 180 | // Indicates whether each individual test should be reported during the run 181 | verbose: true 182 | 183 | // An array of regexp patterns that are matched against all source file paths before re-running tests in watch mode 184 | // watchPathIgnorePatterns: [], 185 | 186 | // Whether to use watchman for file crawling 187 | // watchman: true, 188 | } 189 | --------------------------------------------------------------------------------