├── .gitignore ├── .prettierrc ├── LICENSE ├── README.md ├── bin └── create-expo-app.js ├── package-lock.json ├── package.json └── template ├── .eslintignore ├── .eslintrc.js ├── .github └── workflows │ └── main.yml ├── .gitignore ├── .prettierignore ├── .prettierrc ├── App.js ├── jest.config.js ├── scripts └── env.js ├── src ├── components │ ├── App.tsx │ ├── AppNavigator.tsx │ ├── common │ │ └── Heading.tsx │ └── home │ │ └── HomeScreen.tsx ├── constants │ ├── env.ts │ ├── fonts.ts │ ├── images.ts │ ├── metrics.ts │ ├── routes.ts │ └── theme.ts ├── jest │ └── setup.ts ├── types │ └── index.ts └── utils │ ├── __tests__ │ └── text.test.ts │ └── text.ts ├── tsconfig.json └── typings └── index.d.ts /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | -------------------------------------------------------------------------------- /.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } 5 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2019 App & Flow 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # create-expo-app 2 | 3 | A somewhat opinionated way to create React Native apps using Expo. 4 | 5 | It provides configuration and a reasonable set of rules, it ensures that tooling and developer dependencies work well together (e.g. TypeScript, ESLint, Prettier, Jest, ...), and lastly, it creates a basic project structure. 6 | 7 | ## Usage 8 | 9 | ```sh 10 | npx https://github.com/AppAndFlow/create-expo-app 11 | ``` 12 | 13 | ## Prerequisites 14 | 15 | - [Expo CLI](https://www.npmjs.com/package/expo-cli) 16 | 17 | ## Includes 18 | 19 | - Expo 20 | - TypeScript 21 | - ESLint 22 | - Prettier 23 | - Jest 24 | - React Navigation 25 | - react-native-testing-library 26 | - react-native-kondo 27 | - Preloading & caching of local assets on app startup 28 | - Environment variables script 29 | - CI workflow (via GitHub Actions) to lint, type check & test 30 | - Minimal project structure 31 | -------------------------------------------------------------------------------- /bin/create-expo-app.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | 3 | const path = require('path'); 4 | const fs = require('fs-extra'); 5 | const program = require('commander'); 6 | const execa = require('execa'); 7 | 8 | program 9 | .version('1.0.0') 10 | .usage('[options] ') 11 | .arguments('') 12 | .action(create) 13 | .parse(process.argv); 14 | 15 | const dependencies = ['react-native-kondo', 'react-navigation-stack']; 16 | const devDependencies = [ 17 | '@types/jest', 18 | '@types/react-native', 19 | '@types/react-test-renderer', 20 | '@types/react', 21 | '@typescript-eslint/eslint-plugin', 22 | '@typescript-eslint/parser', 23 | 'eslint-plugin-import', 24 | 'eslint-plugin-react-hooks', 25 | 'eslint-plugin-react', 26 | 'eslint', 27 | 'fs-extra', 28 | 'jest-expo', 29 | 'jest-fetch-mock', 30 | 'jest', 31 | 'lodash.merge', 32 | 'prettier', 33 | 'react-native-testing-library', 34 | 'react-test-renderer', 35 | 'ts-jest', 36 | 'typescript', 37 | ]; 38 | const expoDependencies = [ 39 | 'expo-asset', 40 | 'expo-constants', 41 | 'expo-constants', 42 | 'expo-font', 43 | 'react-native-gesture-handler', 44 | 'react-native-reanimated', 45 | 'react-native-screens', 46 | 'react-navigation', 47 | ]; 48 | const scripts = { 49 | lint: 'eslint . --ext .js,.jsx,.ts,.tsx', 50 | prettier: "prettier --write '**/*'", 51 | start: 'expo start', 52 | test: 'jest', 53 | tsc: 'tsc', 54 | }; 55 | 56 | async function create(appName) { 57 | try { 58 | const targetDirectory = path.resolve(process.cwd(), appName); 59 | const execaOpts = { stdio: 'inherit', cwd: targetDirectory }; 60 | const forwardedOpts = process.argv.slice(2, -1); 61 | 62 | await execa( 63 | 'expo', 64 | ['init', '--npm', '--template', 'blank', ...forwardedOpts, appName], 65 | { ...execaOpts, cwd: process.cwd() }, 66 | ); 67 | console.log('Installing additional dependencies...'); 68 | await execa('expo', ['install', ...expoDependencies], execaOpts); 69 | await execa('npm', ['i', ...dependencies], execaOpts); 70 | await execa('npm', ['i', '-D', ...devDependencies], execaOpts); 71 | console.log('Setting up project structure...'); 72 | await fs.copy( 73 | path.resolve(__dirname, '../template'), 74 | path.resolve(targetDirectory), 75 | ); 76 | 77 | const pkg = await fs.readJSON( 78 | path.resolve(targetDirectory, 'package.json'), 79 | ); 80 | 81 | await fs.writeJSON( 82 | path.resolve(targetDirectory, 'package.json'), 83 | { ...pkg, scripts }, 84 | { spaces: 2 }, 85 | ); 86 | console.log('Done!'); 87 | } catch (e) { 88 | console.error(e.stderr || e); 89 | process.exit(1); 90 | } 91 | } 92 | -------------------------------------------------------------------------------- /package-lock.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-expo-app", 3 | "version": "1.0.0", 4 | "lockfileVersion": 1, 5 | "requires": true, 6 | "dependencies": { 7 | "commander": { 8 | "version": "3.0.2", 9 | "resolved": "https://registry.npmjs.org/commander/-/commander-3.0.2.tgz", 10 | "integrity": "sha512-Gar0ASD4BDyKC4hl4DwHqDrmvjoxWKZigVnAbn5H1owvm4CxCPdb0HQDehwNYMJpla5+M2tPmPARzhtYuwpHow==" 11 | }, 12 | "cross-spawn": { 13 | "version": "7.0.1", 14 | "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.1.tgz", 15 | "integrity": "sha512-u7v4o84SwFpD32Z8IIcPZ6z1/ie24O6RU3RbtL5Y316l3KuHVPx9ItBgWQ6VlfAFnRnTtMUrsQ9MUUTuEZjogg==", 16 | "requires": { 17 | "path-key": "^3.1.0", 18 | "shebang-command": "^2.0.0", 19 | "which": "^2.0.1" 20 | } 21 | }, 22 | "end-of-stream": { 23 | "version": "1.4.4", 24 | "resolved": "https://registry.npmjs.org/end-of-stream/-/end-of-stream-1.4.4.tgz", 25 | "integrity": "sha512-+uw1inIHVPQoaVuHzRyXd21icM+cnt4CzD5rW+NC1wjOUSTOs+Te7FOv7AhN7vS9x/oIyhLP5PR1H+phQAHu5Q==", 26 | "requires": { 27 | "once": "^1.4.0" 28 | } 29 | }, 30 | "execa": { 31 | "version": "2.1.0", 32 | "resolved": "https://registry.npmjs.org/execa/-/execa-2.1.0.tgz", 33 | "integrity": "sha512-Y/URAVapfbYy2Xp/gb6A0E7iR8xeqOCXsuuaoMn7A5PzrXUK84E1gyiEfq0wQd/GHA6GsoHWwhNq8anb0mleIw==", 34 | "requires": { 35 | "cross-spawn": "^7.0.0", 36 | "get-stream": "^5.0.0", 37 | "is-stream": "^2.0.0", 38 | "merge-stream": "^2.0.0", 39 | "npm-run-path": "^3.0.0", 40 | "onetime": "^5.1.0", 41 | "p-finally": "^2.0.0", 42 | "signal-exit": "^3.0.2", 43 | "strip-final-newline": "^2.0.0" 44 | } 45 | }, 46 | "fs-extra": { 47 | "version": "8.1.0", 48 | "resolved": "https://registry.npmjs.org/fs-extra/-/fs-extra-8.1.0.tgz", 49 | "integrity": "sha512-yhlQgA6mnOJUKOsRUFsgJdQCvkKhcz8tlZG5HBQfReYZy46OwLcY+Zia0mtdHsOo9y/hP+CxMN0TU9QxoOtG4g==", 50 | "requires": { 51 | "graceful-fs": "^4.2.0", 52 | "jsonfile": "^4.0.0", 53 | "universalify": "^0.1.0" 54 | } 55 | }, 56 | "get-stream": { 57 | "version": "5.1.0", 58 | "resolved": "https://registry.npmjs.org/get-stream/-/get-stream-5.1.0.tgz", 59 | "integrity": "sha512-EXr1FOzrzTfGeL0gQdeFEvOMm2mzMOglyiOXSTpPC+iAjAKftbr3jpCMWynogwYnM+eSj9sHGc6wjIcDvYiygw==", 60 | "requires": { 61 | "pump": "^3.0.0" 62 | } 63 | }, 64 | "graceful-fs": { 65 | "version": "4.2.2", 66 | "resolved": "https://registry.npmjs.org/graceful-fs/-/graceful-fs-4.2.2.tgz", 67 | "integrity": "sha512-IItsdsea19BoLC7ELy13q1iJFNmd7ofZH5+X/pJr90/nRoPEX0DJo1dHDbgtYWOhJhcCgMDTOw84RZ72q6lB+Q==" 68 | }, 69 | "is-stream": { 70 | "version": "2.0.0", 71 | "resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.0.tgz", 72 | "integrity": "sha512-XCoy+WlUr7d1+Z8GgSuXmpuUFC9fOhRXglJMx+dwLKTkL44Cjd4W1Z5P+BQZpr+cR93aGP4S/s7Ftw6Nd/kiEw==" 73 | }, 74 | "isexe": { 75 | "version": "2.0.0", 76 | "resolved": "https://registry.npmjs.org/isexe/-/isexe-2.0.0.tgz", 77 | "integrity": "sha1-6PvzdNxVb/iUehDcsFctYz8s+hA=" 78 | }, 79 | "jsonfile": { 80 | "version": "4.0.0", 81 | "resolved": "https://registry.npmjs.org/jsonfile/-/jsonfile-4.0.0.tgz", 82 | "integrity": "sha1-h3Gq4HmbZAdrdmQPygWPnBDjPss=", 83 | "requires": { 84 | "graceful-fs": "^4.1.6" 85 | } 86 | }, 87 | "merge-stream": { 88 | "version": "2.0.0", 89 | "resolved": "https://registry.npmjs.org/merge-stream/-/merge-stream-2.0.0.tgz", 90 | "integrity": "sha512-abv/qOcuPfk3URPfDzmZU1LKmuw8kT+0nIHvKrKgFrwifol/doWcdA4ZqsWQ8ENrFKkd67Mfpo/LovbIUsbt3w==" 91 | }, 92 | "mimic-fn": { 93 | "version": "2.1.0", 94 | "resolved": "https://registry.npmjs.org/mimic-fn/-/mimic-fn-2.1.0.tgz", 95 | "integrity": "sha512-OqbOk5oEQeAZ8WXWydlu9HJjz9WVdEIvamMCcXmuqUYjTknH/sqsWvhQ3vgwKFRR1HpjvNBKQ37nbJgYzGqGcg==" 96 | }, 97 | "npm-run-path": { 98 | "version": "3.1.0", 99 | "resolved": "https://registry.npmjs.org/npm-run-path/-/npm-run-path-3.1.0.tgz", 100 | "integrity": "sha512-Dbl4A/VfiVGLgQv29URL9xshU8XDY1GeLy+fsaZ1AA8JDSfjvr5P5+pzRbWqRSBxk6/DW7MIh8lTM/PaGnP2kg==", 101 | "requires": { 102 | "path-key": "^3.0.0" 103 | } 104 | }, 105 | "once": { 106 | "version": "1.4.0", 107 | "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", 108 | "integrity": "sha1-WDsap3WWHUsROsF9nFC6753Xa9E=", 109 | "requires": { 110 | "wrappy": "1" 111 | } 112 | }, 113 | "onetime": { 114 | "version": "5.1.0", 115 | "resolved": "https://registry.npmjs.org/onetime/-/onetime-5.1.0.tgz", 116 | "integrity": "sha512-5NcSkPHhwTVFIQN+TUqXoS5+dlElHXdpAWu9I0HP20YOtIi+aZ0Ct82jdlILDxjLEAWwvm+qj1m6aEtsDVmm6Q==", 117 | "requires": { 118 | "mimic-fn": "^2.1.0" 119 | } 120 | }, 121 | "p-finally": { 122 | "version": "2.0.1", 123 | "resolved": "https://registry.npmjs.org/p-finally/-/p-finally-2.0.1.tgz", 124 | "integrity": "sha512-vpm09aKwq6H9phqRQzecoDpD8TmVyGw70qmWlyq5onxY7tqyTTFVvxMykxQSQKILBSFlbXpypIw2T1Ml7+DDtw==" 125 | }, 126 | "path-key": { 127 | "version": "3.1.0", 128 | "resolved": "https://registry.npmjs.org/path-key/-/path-key-3.1.0.tgz", 129 | "integrity": "sha512-8cChqz0RP6SHJkMt48FW0A7+qUOn+OsnOsVtzI59tZ8m+5bCSk7hzwET0pulwOM2YMn9J1efb07KB9l9f30SGg==" 130 | }, 131 | "prettier": { 132 | "version": "1.18.2", 133 | "resolved": "https://registry.npmjs.org/prettier/-/prettier-1.18.2.tgz", 134 | "integrity": "sha512-OeHeMc0JhFE9idD4ZdtNibzY0+TPHSpSSb9h8FqtP+YnoZZ1sl8Vc9b1sasjfymH3SonAF4QcA2+mzHPhMvIiw==", 135 | "dev": true 136 | }, 137 | "pump": { 138 | "version": "3.0.0", 139 | "resolved": "https://registry.npmjs.org/pump/-/pump-3.0.0.tgz", 140 | "integrity": "sha512-LwZy+p3SFs1Pytd/jYct4wpv49HiYCqd9Rlc5ZVdk0V+8Yzv6jR5Blk3TRmPL1ft69TxP0IMZGJ+WPFU2BFhww==", 141 | "requires": { 142 | "end-of-stream": "^1.1.0", 143 | "once": "^1.3.1" 144 | } 145 | }, 146 | "shebang-command": { 147 | "version": "2.0.0", 148 | "resolved": "https://registry.npmjs.org/shebang-command/-/shebang-command-2.0.0.tgz", 149 | "integrity": "sha512-kHxr2zZpYtdmrN1qDjrrX/Z1rR1kG8Dx+gkpK1G4eXmvXswmcE1hTWBWYUzlraYw1/yZp6YuDY77YtvbN0dmDA==", 150 | "requires": { 151 | "shebang-regex": "^3.0.0" 152 | } 153 | }, 154 | "shebang-regex": { 155 | "version": "3.0.0", 156 | "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", 157 | "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" 158 | }, 159 | "signal-exit": { 160 | "version": "3.0.2", 161 | "resolved": "https://registry.npmjs.org/signal-exit/-/signal-exit-3.0.2.tgz", 162 | "integrity": "sha1-tf3AjxKH6hF4Yo5BXiUTK3NkbG0=" 163 | }, 164 | "strip-final-newline": { 165 | "version": "2.0.0", 166 | "resolved": "https://registry.npmjs.org/strip-final-newline/-/strip-final-newline-2.0.0.tgz", 167 | "integrity": "sha512-BrpvfNAE3dcvq7ll3xVumzjKjZQ5tI1sEUIKr3Uoks0XUl45St3FlatVqef9prk4jRDzhW6WZg+3bk93y6pLjA==" 168 | }, 169 | "universalify": { 170 | "version": "0.1.2", 171 | "resolved": "https://registry.npmjs.org/universalify/-/universalify-0.1.2.tgz", 172 | "integrity": "sha512-rBJeI5CXAlmy1pV+617WB9J63U6XcazHHF2f2dbJix4XzpUF0RS3Zbj0FGIOCAva5P/d/GBOYaACQ1w+0azUkg==" 173 | }, 174 | "which": { 175 | "version": "2.0.1", 176 | "resolved": "https://registry.npmjs.org/which/-/which-2.0.1.tgz", 177 | "integrity": "sha512-N7GBZOTswtB9lkQBZA4+zAXrjEIWAUOB93AvzUiudRzRxhUdLURQ7D/gAIMY1gatT/LTbmbcv8SiYazy3eYB7w==", 178 | "requires": { 179 | "isexe": "^2.0.0" 180 | } 181 | }, 182 | "wrappy": { 183 | "version": "1.0.2", 184 | "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", 185 | "integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8=" 186 | } 187 | } 188 | } 189 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "create-expo-app", 3 | "version": "1.0.0", 4 | "description": "A somewhat opinionated way to create React Native apps using Expo", 5 | "bin": { 6 | "create-expo-app": "bin/create-expo-app.js" 7 | }, 8 | "repository": { 9 | "type": "git", 10 | "url": "git+https://github.com/AppAndFlow/create-expo-app.git" 11 | }, 12 | "author": "AppAndFlow", 13 | "license": "MIT", 14 | "bugs": { 15 | "url": "https://github.com/AppAndFlow/create-expo-app/issues" 16 | }, 17 | "homepage": "https://github.com/AppAndFlow/create-expo-app#readme", 18 | "dependencies": { 19 | "commander": "^3.0.2", 20 | "execa": "^2.1.0", 21 | "fs-extra": "^8.1.0" 22 | }, 23 | "devDependencies": { 24 | "prettier": "^1.18.2" 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /template/.eslintignore: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppAndFlow/create-expo-app/9fbbeb72d43b24c3e98dfaac6af2f84130228671/template/.eslintignore -------------------------------------------------------------------------------- /template/.eslintrc.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | env: { browser: true, es6: true, jest: true, node: true }, 3 | extends: [ 4 | 'eslint:recommended', 5 | 'plugin:@typescript-eslint/recommended', 6 | 'plugin:react/recommended', 7 | ], 8 | globals: { __DEV__: 'readonly' }, 9 | parser: '@typescript-eslint/parser', 10 | parserOptions: { 11 | ecmaFeatures: { jsx: true }, 12 | ecmaVersion: 6, 13 | project: 'tsconfig.json', 14 | sourceType: 'module', 15 | }, 16 | plugins: ['@typescript-eslint', 'import', 'react-hooks'], 17 | rules: { 18 | '@typescript-eslint/ban-ts-ignore': 'off', 19 | '@typescript-eslint/explicit-function-return-type': 'off', 20 | '@typescript-eslint/explicit-member-accessibility': 'off', 21 | '@typescript-eslint/no-explicit-any': 'off', 22 | '@typescript-eslint/no-non-null-assertion': 'off', 23 | '@typescript-eslint/no-use-before-define': 'off', 24 | 'import/order': [ 25 | 'error', 26 | { 27 | groups: [['builtin', 'external', 'internal']], 28 | 'newlines-between': 'always', 29 | }, 30 | ], 31 | 'react-hooks/exhaustive-deps': 'warn', 32 | 'react-hooks/rules-of-hooks': 'error', 33 | }, 34 | settings: { react: { version: 'detect' } }, 35 | }; 36 | -------------------------------------------------------------------------------- /template/.github/workflows/main.yml: -------------------------------------------------------------------------------- 1 | name: CI 2 | on: push 3 | jobs: 4 | main: 5 | runs-on: ubuntu-latest 6 | steps: 7 | - uses: actions/checkout@v1 8 | - run: npm ci 9 | - run: npm run lint 10 | - run: npm run tsc 11 | - run: npm test 12 | env: 13 | CI: true 14 | -------------------------------------------------------------------------------- /template/.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .expo 3 | node_modules 4 | -------------------------------------------------------------------------------- /template/.prettierignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | .eslintignore 3 | .expo 4 | .gitignore 5 | .prettierignore 6 | .watchmanconfig 7 | assets 8 | -------------------------------------------------------------------------------- /template/.prettierrc: -------------------------------------------------------------------------------- 1 | { 2 | "singleQuote": true, 3 | "trailingComma": "all" 4 | } 5 | -------------------------------------------------------------------------------- /template/App.js: -------------------------------------------------------------------------------- 1 | import { UIManager } from 'react-native'; 2 | 3 | import App from './src/components/App'; 4 | import metrics from './src/constants/metrics'; 5 | 6 | if (metrics.isAndroid) { 7 | if (UIManager.setLayoutAnimationEnabledExperimental) { 8 | UIManager.setLayoutAnimationEnabledExperimental(true); 9 | } 10 | } 11 | 12 | export default App; 13 | -------------------------------------------------------------------------------- /template/jest.config.js: -------------------------------------------------------------------------------- 1 | // eslint-disable-next-line @typescript-eslint/no-var-requires 2 | const { defaults } = require('ts-jest/presets'); 3 | 4 | module.exports = { 5 | ...defaults, 6 | preset: 'jest-expo', 7 | setupFiles: ['./src/jest/setup.ts'], 8 | transformIgnorePatterns: [ 9 | 'node_modules/(?!((jest-)?react-native|expo(nent)?|@expo(nent)?/.*|@unimodules|react-navigation|sentry-expo))', 10 | ], 11 | }; 12 | -------------------------------------------------------------------------------- /template/scripts/env.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | /* eslint-disable @typescript-eslint/no-var-requires */ 3 | 4 | const path = require('path'); 5 | const fs = require('fs-extra'); 6 | const { execSync } = require('child_process'); 7 | const merge = require('lodash.merge'); 8 | 9 | (async () => { 10 | const envs = ['dev', 'prod']; 11 | const overrides = { 12 | dev: { 13 | expo: { 14 | ios: {}, 15 | android: {}, 16 | extra: { 17 | env: { 18 | name: 'dev', 19 | }, 20 | }, 21 | }, 22 | }, 23 | prod: { 24 | expo: { 25 | ios: {}, 26 | android: {}, 27 | extra: { 28 | env: { 29 | name: 'prod', 30 | }, 31 | }, 32 | }, 33 | }, 34 | }; 35 | 36 | try { 37 | const env = process.argv[2]; 38 | const appPath = path.resolve(__dirname, '..', 'app.json'); 39 | 40 | if (!envs.includes(env)) { 41 | throw new Error(`Please specify a valid env (${[...envs].join(' ')})`); 42 | } 43 | 44 | const app = await fs.readJSON(appPath); 45 | const updatedApp = merge({}, app, overrides[env]); 46 | 47 | await fs.writeJSON(appPath, updatedApp, { spaces: 2 }); 48 | execSync('./node_modules/.bin/prettier --write app.json', { 49 | stdio: 'inherit', 50 | }); 51 | } catch (e) { 52 | console.error(e); 53 | process.exit(1); 54 | } 55 | })(); 56 | -------------------------------------------------------------------------------- /template/src/components/App.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { AppLoading } from 'expo'; 3 | import { Asset } from 'expo-asset'; 4 | import * as Font from 'expo-font'; 5 | 6 | import images from '../constants/images'; 7 | import { fontsMap } from '../constants/fonts'; 8 | import AppNavigator from './AppNavigator'; 9 | 10 | const App = () => { 11 | const [ready, setReady] = React.useState(false); 12 | 13 | const loadResources = () => { 14 | return Promise.all([ 15 | Asset.loadAsync(Object.values(images)), 16 | Font.loadAsync({ 17 | ...fontsMap, 18 | }), 19 | ]) as Promise; 20 | }; 21 | 22 | const onFinish = () => setReady(true); 23 | 24 | return ready ? ( 25 | 26 | ) : ( 27 | 28 | ); 29 | }; 30 | 31 | export default App; 32 | -------------------------------------------------------------------------------- /template/src/components/AppNavigator.tsx: -------------------------------------------------------------------------------- 1 | import { createAppContainer } from 'react-navigation'; 2 | import { createStackNavigator } from 'react-navigation-stack'; 3 | 4 | import routes from '../constants/routes'; 5 | import HomeScreen from './home/HomeScreen'; 6 | 7 | const MainStackNavigator = createStackNavigator({ 8 | [routes.HOME]: HomeScreen, 9 | }); 10 | 11 | export default createAppContainer(MainStackNavigator); 12 | -------------------------------------------------------------------------------- /template/src/components/common/Heading.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Text, TextProps } from 'react-native-kondo'; 3 | 4 | const Heading = (props: TextProps) => ( 5 | 6 | ); 7 | 8 | export default Heading; 9 | -------------------------------------------------------------------------------- /template/src/components/home/HomeScreen.tsx: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { Box } from 'react-native-kondo'; 3 | 4 | import Heading from '../common/Heading'; 5 | 6 | const HomeScreen = () => ( 7 | 8 | Edit HomeScreen.tsx 9 | 10 | ); 11 | 12 | HomeScreen.navigationOptions = { 13 | header: null, 14 | }; 15 | 16 | export default HomeScreen; 17 | -------------------------------------------------------------------------------- /template/src/constants/env.ts: -------------------------------------------------------------------------------- 1 | import Constants from 'expo-constants'; 2 | 3 | import { Env } from '../types'; 4 | 5 | export default Constants.manifest.extra.env as Env; 6 | -------------------------------------------------------------------------------- /template/src/constants/fonts.ts: -------------------------------------------------------------------------------- 1 | export const fontsMap = { 2 | // regular: require('../../assets/fonts/regular.otf'), 3 | }; 4 | 5 | export default { 6 | // regular: 'regular', 7 | }; 8 | -------------------------------------------------------------------------------- /template/src/constants/images.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | // close: require('../../assets/images/close.png'), 3 | }; 4 | -------------------------------------------------------------------------------- /template/src/constants/metrics.ts: -------------------------------------------------------------------------------- 1 | import { Dimensions, Platform } from 'react-native'; 2 | import Constants from 'expo-constants'; 3 | 4 | const { width: windowWidth, height: windowHeight } = Dimensions.get('window'); 5 | const createHitSlop = (size: number) => ({ 6 | top: size, 7 | right: size, 8 | bottom: size, 9 | left: size, 10 | }); 11 | const createCircle = (diameter: number) => ({ 12 | height: diameter, 13 | width: diameter, 14 | borderRadius: diameter / 2, 15 | }); 16 | 17 | export default { 18 | isAndroid: Platform.OS === 'android', 19 | isIos: Platform.OS === 'ios', 20 | isIphoneX: 21 | Platform.OS === 'ios' && (windowHeight === 812 || windowHeight === 896), 22 | isSmallDevice: windowWidth < 375, 23 | createCircle, 24 | createHitSlop, 25 | statusBarHeight: Constants.statusBarHeight, 26 | windowHeight, 27 | windowWidth, 28 | }; 29 | -------------------------------------------------------------------------------- /template/src/constants/routes.ts: -------------------------------------------------------------------------------- 1 | export default { 2 | HOME: 'Home', 3 | }; 4 | -------------------------------------------------------------------------------- /template/src/constants/theme.ts: -------------------------------------------------------------------------------- 1 | export default {}; 2 | -------------------------------------------------------------------------------- /template/src/jest/setup.ts: -------------------------------------------------------------------------------- 1 | import fetch from 'jest-fetch-mock'; 2 | 3 | // @ts-ignore 4 | global.fetch = fetch; 5 | -------------------------------------------------------------------------------- /template/src/types/index.ts: -------------------------------------------------------------------------------- 1 | export interface Env { 2 | name: string; 3 | } 4 | -------------------------------------------------------------------------------- /template/src/utils/__tests__/text.test.ts: -------------------------------------------------------------------------------- 1 | import { truncate } from '../text'; 2 | 3 | describe('text', () => { 4 | it('truncates string when it exceeds length', () => { 5 | expect(truncate('this should be truncated', 20)).toBe( 6 | 'this should be trunc...', 7 | ); 8 | }); 9 | 10 | it('does not truncate string when it does not exceed length', () => { 11 | expect(truncate('not truncated', 20)).toBe('not truncated'); 12 | }); 13 | }); 14 | -------------------------------------------------------------------------------- /template/src/utils/text.ts: -------------------------------------------------------------------------------- 1 | const truncate = (text: string, maxLength: number) => { 2 | return text.length > maxLength ? text.substring(0, maxLength) + '...' : text; 3 | }; 4 | 5 | export { truncate }; 6 | -------------------------------------------------------------------------------- /template/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "esModuleInterop": true, 4 | "jsx": "react", 5 | "lib": ["dom", "esnext"], 6 | "module": "es2015", 7 | "moduleResolution": "node", 8 | "noEmit": true, 9 | "skipLibCheck": true, 10 | "strict": true, 11 | "target": "es6" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /template/typings/index.d.ts: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/AppAndFlow/create-expo-app/9fbbeb72d43b24c3e98dfaac6af2f84130228671/template/typings/index.d.ts --------------------------------------------------------------------------------