├── .babelrc ├── .eslintrc ├── .flowconfig ├── .gitignore ├── LICENSE ├── README.md ├── package.json ├── src ├── bin │ ├── crna-entry.js │ └── react-native-scripts.js ├── scripts │ ├── android.js │ ├── eject.js │ ├── init.js │ ├── ios.js │ └── start.js └── util │ ├── bsb.js │ ├── clearConsole.js │ ├── expo.js │ ├── install.js │ ├── log.js │ └── packager.js ├── taskfile.js ├── template ├── .babelrc ├── .watchmanconfig ├── README.md ├── app.json ├── bsconfig.json ├── gitignore └── src │ └── App.re └── yarn.lock /.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "sourceMaps": true, 3 | "presets": ["es2015", "stage-1"], 4 | "plugins": ["transform-runtime", "add-module-exports", "transform-flow-strip-types"] 5 | } 6 | -------------------------------------------------------------------------------- /.eslintrc: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "exponent/react", 3 | "plugins": [ 4 | "flowtype", 5 | ], 6 | "globals": { 7 | "document": false, 8 | }, 9 | "rules": { 10 | "flowtype/define-flow-type": 1, 11 | "flowtype/use-flow-type": 1, 12 | }, 13 | } 14 | -------------------------------------------------------------------------------- /.flowconfig: -------------------------------------------------------------------------------- 1 | [ignore] 2 | 3 | [include] 4 | 5 | [libs] 6 | 7 | [options] 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | build/ 3 | yarn-error.log 4 | *.tgz 5 | .DS_Store 6 | *.log 7 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | Copyright (c) 2017, Create React Native App Contributors 2 | All rights reserved. 3 | 4 | Redistribution and use in source and binary forms, with or without 5 | modification, are permitted provided that the following conditions are met: 6 | 7 | * Redistributions of source code must retain the above copyright notice, this 8 | list of conditions and the following disclaimer. 9 | 10 | * Redistributions in binary form must reproduce the above copyright notice, 11 | this list of conditions and the following disclaimer in the documentation 12 | and/or other materials provided with the distribution. 13 | 14 | * Neither the name of the copyright holder nor the names of its 15 | contributors may be used to endorse or promote products derived from 16 | this software without specific prior written permission. 17 | 18 | THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" 19 | AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE 20 | IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE 21 | DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE 22 | FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL 23 | DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR 24 | SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER 25 | CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, 26 | OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE 27 | OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. 28 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # reason-react-native-scripts 2 | 3 | Early, experimental version of BuckleScript / ReasonReact / bs-react-native support integrated into [Create React Native App](https://github.com/react-community/create-react-native-app) to make starting a Reason project with React Native dead simple. 4 | 5 | ## Use it 6 | 7 | ``` 8 | yarn global add create-react-native-app 9 | create-react-native-app HelloWorldRe --scripts-version reason-react-native-scripts 10 | cd HelloWorldRe 11 | yarn start 12 | ``` 13 | 14 | You will see some peerDependencies warnings, don't worry about it. I told you this was early and experimental, right? 😅 15 | 16 | ## Development 17 | 18 | `yarn && yarn start` will start a watcher that will build artifacts and place them in the build directory. 19 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "reason-react-native-scripts", 3 | "version": "0.1.0", 4 | "description": "Configuration and scripts for Create React Native App using ReasonML.", 5 | "license": "BSD-3-Clause", 6 | "keywords": [ 7 | "react-native", 8 | "react" 9 | ], 10 | "homepage": "https://github.com/react-community/reason-react-native-scripts", 11 | "bugs": "https://github.com/react-community/reason-react-native-scripts", 12 | "engines": { 13 | "node": ">=6" 14 | }, 15 | "files": [ 16 | "build", 17 | "template" 18 | ], 19 | "bin": { 20 | "react-native-scripts": "./build/bin/react-native-scripts.js" 21 | }, 22 | "scripts": { 23 | "start": "taskr", 24 | "build": "taskr build" 25 | }, 26 | "dependencies": { 27 | "@expo/bunyan": "3.0.2", 28 | "babel-runtime": "^6.9.2", 29 | "chalk": "^2.4.1", 30 | "cross-spawn": "^6.0.5", 31 | "execa": "^1.0.0", 32 | "fs-extra": "^7.0.0", 33 | "indent-string": "^3.0.0", 34 | "inquirer": "^6.2.0", 35 | "lodash": "^4.17.10", 36 | "match-require": "^2.0.0", 37 | "minimist": "^1.2.0", 38 | "path-exists": "^3.0.0", 39 | "progress": "^2.0.0", 40 | "qrcode-terminal": "^0.12.0", 41 | "rimraf": "^2.6.1", 42 | "split": "^1.0.1", 43 | "xdl": "48.3.0" 44 | }, 45 | "devDependencies": { 46 | "@taskr/babel": "^1.0.6", 47 | "@taskr/clear": "^1.0.6", 48 | "@taskr/esnext": "^1.0.0", 49 | "@taskr/shell": "^1.0.6", 50 | "@taskr/watch": "^1.0.6", 51 | "babel-plugin-add-module-exports": "^0.2.1", 52 | "babel-plugin-transform-flow-strip-types": "^6.8.0", 53 | "babel-plugin-transform-runtime": "^6.9.0", 54 | "babel-preset-es2015": "^6.9.0", 55 | "babel-preset-stage-1": "^6.5.0", 56 | "taskr": "^1.0.6" 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/bin/crna-entry.js: -------------------------------------------------------------------------------- 1 | import Expo from 'expo'; 2 | import React, { Component } from 'react'; 3 | import { View } from 'react-native'; 4 | 5 | const { app } = require('../../../../lib/js/src/App'); 6 | 7 | // we don't want this to require transformation 8 | class AwakeInDevApp extends Component { 9 | render() { 10 | return React.createElement( 11 | View, 12 | { 13 | style: { 14 | flex: 1, 15 | }, 16 | }, 17 | React.createElement(app, this.props), 18 | React.createElement(process.env.NODE_ENV === 'development' ? Expo.KeepAwake : View) 19 | ); 20 | } 21 | } 22 | 23 | Expo.registerRootComponent(AwakeInDevApp); 24 | -------------------------------------------------------------------------------- /src/bin/react-native-scripts.js: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env node 2 | // @flow 3 | import spawn from 'cross-spawn'; 4 | 5 | const script = process.argv[2]; 6 | const args = process.argv.slice(3); 7 | 8 | const validCommands = ['eject', 'android', 'ios', 'start', 'test']; 9 | 10 | if (validCommands.indexOf(script) !== -1) { 11 | // the command is valid 12 | const result = spawn.sync( 13 | 'node', 14 | ['--no-deprecation', require.resolve('../scripts/' + script)].concat(args), 15 | { stdio: 'inherit' } 16 | ); 17 | process.exit(result.status); 18 | } else { 19 | console.log( 20 | `Invalid command '${script}'. Please check if you need to update react-native-scripts.` 21 | ); 22 | } 23 | -------------------------------------------------------------------------------- /src/scripts/android.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Android, Config, ProjectSettings, UrlUtils } from 'xdl'; 4 | 5 | import chalk from 'chalk'; 6 | import indent from 'indent-string'; 7 | import path from 'path'; 8 | import pathExists from 'path-exists'; 9 | import qr from 'qrcode-terminal'; 10 | import log from '../util/log'; 11 | 12 | import packager from '../util/packager'; 13 | 14 | Config.validation.reactNativeVersionWarnings = false; 15 | Config.developerTool = 'crna'; 16 | Config.offline = true; 17 | 18 | packager.run(startAndroidAndPrintInfo); 19 | 20 | // print a nicely formatted message with setup information 21 | async function startAndroidAndPrintInfo() { 22 | const address = await UrlUtils.constructManifestUrlAsync(process.cwd()); 23 | log.withTimestamp('Starting Android...'); 24 | 25 | const { success, error } = await Android.openProjectAsync(process.cwd()); 26 | 27 | qr.generate(address, qrCode => { 28 | log.withTimestamp(`${chalk.green('Packager started!')}`); 29 | log( 30 | ` 31 | To view your app with live reloading, point the Expo app to this QR code. 32 | You'll find the QR scanner on the Projects tab of the app. 33 | 34 | ${indent(qrCode, 2)} 35 | 36 | Or enter this address in the Expo app's search bar: 37 | 38 | ${chalk.underline(chalk.cyan(address))} 39 | 40 | Your phone will need to be on the same local network as this computer. 41 | For links to install the Expo app, please visit ${chalk.underline(chalk.cyan('https://expo.io'))}. 42 | 43 | Logs from serving your app will appear here. Press Ctrl+C at any time to stop. 44 | ` 45 | ); 46 | }); 47 | 48 | if (!success) { 49 | log(chalk.red(error.message)); 50 | } 51 | } 52 | -------------------------------------------------------------------------------- /src/scripts/eject.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import chalk from 'chalk'; 4 | import fse from 'fs-extra'; 5 | import inquirer from 'inquirer'; 6 | import matchRequire from 'match-require'; 7 | import path from 'path'; 8 | import rimraf from 'rimraf'; 9 | import spawn from 'cross-spawn'; 10 | import log from '../util/log'; 11 | 12 | import { detach } from '../util/expo'; 13 | 14 | async function eject() { 15 | try { 16 | const filesWithExpo = await filesUsingExpoSdk(); 17 | const usingExpo = filesWithExpo.length > 0; 18 | 19 | let expoSdkWarning; 20 | if (usingExpo) { 21 | expoSdkWarning = `${chalk.bold('Warning!')} We found at least one file where your project imports the Expo SDK: 22 | `; 23 | 24 | for (let filename of filesWithExpo) { 25 | expoSdkWarning += ` ${chalk.cyan(filename)}\n`; 26 | } 27 | 28 | expoSdkWarning += ` 29 | ${chalk.yellow.bold('If you choose the "plain" React Native option below, these imports will stop working.')}`; 30 | } else { 31 | expoSdkWarning = `\ 32 | We didn't find any uses of the Expo SDK in your project, so you should be fine to eject to 33 | "Plain" React Native. (This check isn't very sophisticated, though.)`; 34 | } 35 | 36 | log( 37 | ` 38 | ${expoSdkWarning} 39 | 40 | We ${chalk.italic('strongly')} recommend that you read this document before you proceed: 41 | ${chalk.cyan('https://github.com/react-community/create-react-native-app/blob/master/EJECTING.md')} 42 | 43 | Ejecting is permanent! Please be careful with your selection. 44 | ` 45 | ); 46 | 47 | let reactNativeOptionMessage = "React Native: I'd like a regular React Native project."; 48 | 49 | if (usingExpo) { 50 | reactNativeOptionMessage = chalk.italic( 51 | "(WARNING: See above message for why this option may break your project's build)\n " 52 | ) + reactNativeOptionMessage; 53 | } 54 | 55 | const questions = [ 56 | { 57 | type: 'list', 58 | name: 'ejectMethod', 59 | message: 'How would you like to eject from create-react-native-app?', 60 | default: usingExpo ? 'expoKit' : 'raw', 61 | choices: [ 62 | { 63 | name: reactNativeOptionMessage, 64 | value: 'raw', 65 | }, 66 | { 67 | name: "ExpoKit: I'll create or log in with an Expo account to use React Native and the Expo SDK.", 68 | value: 'expoKit', 69 | }, 70 | { 71 | name: "Cancel: I'll continue with my current project structure.", 72 | value: 'cancel', 73 | }, 74 | ], 75 | }, 76 | ]; 77 | 78 | const { ejectMethod } = await inquirer.prompt(questions); 79 | 80 | if (ejectMethod === 'raw') { 81 | const useYarn = await fse.exists(path.resolve('yarn.lock')); 82 | const npmOrYarn = useYarn ? 'yarn' : 'npm'; 83 | const appJson = JSON.parse(await fse.readFile(path.resolve('app.json'))); 84 | const pkgJson = JSON.parse(await fse.readFile(path.resolve('package.json'))); 85 | let { 86 | name: newName, 87 | displayName: newDisplayName, 88 | expo: { name: expName }, 89 | } = appJson; 90 | 91 | // we ask user to provide a project name (default is package name stripped of dashes) 92 | // but we want to infer some good default choices, especially if they've set them up in app.json 93 | if (!newName) { 94 | newName = stripDashes(pkgJson.name); 95 | } 96 | 97 | if (!newDisplayName && expName) { 98 | newDisplayName = expName; 99 | } 100 | 101 | log("We have a couple of questions to ask you about how you'd like to name your app:"); 102 | const { enteredName, enteredDisplayname } = await inquirer.prompt([ 103 | { 104 | name: 'enteredDisplayname', 105 | message: "What should your app appear as on a user's home screen?", 106 | default: newDisplayName, 107 | validate: s => { 108 | return s.length > 0; 109 | }, 110 | }, 111 | { 112 | name: 'enteredName', 113 | message: 'What should your Android Studio and Xcode projects be called?', 114 | default: newName, 115 | validate: s => { 116 | return s.length > 0 && s.indexOf('-') === -1 && s.indexOf(' ') === -1; 117 | }, 118 | }, 119 | ]); 120 | 121 | appJson.name = enteredName; 122 | appJson.displayName = enteredDisplayname; 123 | 124 | log('Writing your selections to app.json...'); 125 | // write the updated app.json file 126 | await fse.writeFile(path.resolve('app.json'), JSON.stringify(appJson, null, 2)); 127 | log(chalk.green('Wrote to app.json, please update it manually in the future.')); 128 | 129 | const ejectCommand = 'node'; 130 | const ejectArgs = [ 131 | path.resolve('node_modules', 'react-native', 'local-cli', 'cli.js'), 132 | 'eject', 133 | ]; 134 | 135 | const { status } = spawn.sync(ejectCommand, ejectArgs, { 136 | stdio: 'inherit', 137 | }); 138 | 139 | if (status !== 0) { 140 | log(chalk.red(`Eject failed with exit code ${status}, see above output for any issues.`)); 141 | log(chalk.yellow('You may want to delete the `ios` and/or `android` directories.')); 142 | process.exit(1); 143 | } else { 144 | log('Successfully copied template native code.'); 145 | } 146 | 147 | const newDevDependencies = []; 148 | // Try to replace the Babel preset. 149 | try { 150 | const projectBabelPath = path.resolve('.babelrc'); 151 | // If .babelrc doesn't exist, the app is using the default config and 152 | // editing the config is not necessary. 153 | if (await fse.exists(projectBabelPath)) { 154 | const projectBabelRc = (await fse.readFile(projectBabelPath)).toString(); 155 | 156 | // We assume the .babelrc is valid JSON. If we can't parse it (e.g. if 157 | // it's JSON5) the error is caught and a message asking to change it 158 | // manually gets printed. 159 | const babelRcJson = JSON.parse(projectBabelRc); 160 | if (babelRcJson.presets && babelRcJson.presets.includes('babel-preset-expo')) { 161 | babelRcJson.presets = babelRcJson.presets.map( 162 | preset => 163 | preset === 'babel-preset-expo' 164 | ? 'babel-preset-react-native-stage-0/decorator-support' 165 | : preset 166 | ); 167 | await fse.writeFile(projectBabelPath, JSON.stringify(babelRcJson, null, 2)); 168 | newDevDependencies.push('babel-preset-react-native-stage-0'); 169 | log( 170 | chalk.green( 171 | `Babel preset changed to \`babel-preset-react-native-stage-0/decorator-support\`.` 172 | ) 173 | ); 174 | } 175 | } 176 | } catch (e) { 177 | log( 178 | chalk.yellow( 179 | `We had an issue preparing your .babelrc for ejection. 180 | If you have a .babelrc in your project, make sure to change the preset 181 | from \`babel-preset-expo\` to \`babel-preset-react-native-stage-0/decorator-support\`.` 182 | ) 183 | ); 184 | log(chalk.red(e)); 185 | } 186 | 187 | delete pkgJson.main; 188 | 189 | // NOTE: expo won't work after performing a raw eject, so we should delete this 190 | // it will be a better error message for the module to not be found than for whatever problems 191 | // missing native modules will cause 192 | delete pkgJson.dependencies.expo; 193 | delete pkgJson.devDependencies['react-native-scripts']; 194 | 195 | pkgJson.scripts.start = 'react-native start'; 196 | pkgJson.scripts.ios = 'react-native run-ios'; 197 | pkgJson.scripts.android = 'react-native run-android'; 198 | 199 | // no longer relevant to an ejected project (maybe build is?) 200 | delete pkgJson.scripts.eject; 201 | 202 | log(`Updating your ${npmOrYarn} scripts in package.json...`); 203 | 204 | await fse.writeFile(path.resolve('package.json'), JSON.stringify(pkgJson, null, 2)); 205 | 206 | log(chalk.green('Your package.json is up to date!')); 207 | 208 | log(`Adding entry point...`); 209 | 210 | const lolThatsSomeComplexCode = `import { AppRegistry } from 'react-native'; 211 | import App from './App'; 212 | AppRegistry.registerComponent('${newName}', () => App); 213 | `; 214 | 215 | await fse.writeFile(path.resolve('index.js'), lolThatsSomeComplexCode); 216 | 217 | log(chalk.green('Added new entry points!')); 218 | 219 | log( 220 | ` 221 | Note that using \`${npmOrYarn} start\` will now require you to run Xcode and/or 222 | Android Studio to build the native code for your project.` 223 | ); 224 | 225 | log('Removing node_modules...'); 226 | rimraf.sync(path.resolve('node_modules')); 227 | if (useYarn) { 228 | log('Installing packages with yarn...'); 229 | const args = newDevDependencies.length > 0 ? ['add', '--dev', ...newDevDependencies] : []; 230 | spawn.sync('yarnpkg', args, { stdio: 'inherit' }); 231 | } else { 232 | // npm prints the whole package tree to stdout unless we ignore it. 233 | const stdio = [process.stdin, 'ignore', process.stderr]; 234 | 235 | log('Installing existing packages with npm...'); 236 | spawn.sync('npm', ['install'], { stdio }); 237 | 238 | if (newDevDependencies.length > 0) { 239 | log('Installing new packages with npm...'); 240 | spawn.sync('npm', ['install', '--save-dev', ...newDevDependencies], { 241 | stdio, 242 | }); 243 | } 244 | } 245 | } else if (ejectMethod === 'expoKit') { 246 | await detach(); 247 | } else { 248 | // we don't want to print the survey for cancellations 249 | log('OK! If you change your mind you can run this command again.'); 250 | return; 251 | } 252 | 253 | log( 254 | `${chalk.green('Ejected successfully!')} 255 | Please consider letting us know why you ejected in this survey: 256 | ${chalk.cyan('https://goo.gl/forms/iD6pl218r7fn9N0d2')}` 257 | ); 258 | } catch (e) { 259 | console.error(chalk.red(`Error running eject: ${e}`)); 260 | } 261 | } 262 | 263 | async function filesUsingExpoSdk(): Promise> { 264 | const projectJsFiles = await findJavaScriptProjectFilesInRoot(process.cwd()); 265 | 266 | const jsFileContents = (await Promise.all( 267 | projectJsFiles.map(f => fse.readFile(f)) 268 | )).map((buf, i) => { 269 | return { 270 | filename: projectJsFiles[i], 271 | contents: buf.toString(), 272 | }; 273 | }); 274 | 275 | const filesUsingExpo = []; 276 | 277 | for (let { filename, contents } of jsFileContents) { 278 | const requires = matchRequire.findAll(contents); 279 | 280 | if (requires.includes('expo')) { 281 | filesUsingExpo.push(filename); 282 | } 283 | } 284 | 285 | filesUsingExpo.sort(); 286 | 287 | return filesUsingExpo; 288 | } 289 | 290 | function stripDashes(s: string): string { 291 | let ret = ''; 292 | 293 | for (let c of s) { 294 | if (c !== ' ' && c !== '-') { 295 | ret += c; 296 | } 297 | } 298 | 299 | return ret; 300 | } 301 | 302 | async function findJavaScriptProjectFilesInRoot(root: string): Promise> { 303 | // ignore node_modules 304 | if (root.includes('node_modules')) { 305 | return []; 306 | } 307 | 308 | const stats = await fse.stat(root); 309 | 310 | if (stats.isFile()) { 311 | if (root.endsWith('.js')) { 312 | return [root]; 313 | } else { 314 | return []; 315 | } 316 | } else if (stats.isDirectory()) { 317 | const children = await fse.readdir(root); 318 | 319 | // we want to do this concurrently in large project folders 320 | const jsFilesInChildren = await Promise.all( 321 | children.map(f => findJavaScriptProjectFilesInRoot(path.join(root, f))) 322 | ); 323 | 324 | return [].concat.apply([], jsFilesInChildren); 325 | } else { 326 | // lol it's not a file or directory, we can't return a honey badger, 'cause it don't give a 327 | return []; 328 | } 329 | } 330 | 331 | eject() 332 | .then(() => { 333 | // the expo local github auth server leaves a setTimeout for 5 minutes 334 | // so we need to explicitly exit (for now, this will be resolved in the nearish future) 335 | process.exit(0); 336 | }) 337 | .catch(e => { 338 | console.error(`Problem running eject: ${e}`); 339 | process.exit(1); 340 | }); 341 | -------------------------------------------------------------------------------- /src/scripts/init.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import chalk from 'chalk'; 4 | import fse from 'fs-extra'; 5 | import path from 'path'; 6 | import pathExists from 'path-exists'; 7 | import spawn from 'cross-spawn'; 8 | import log from '../util/log'; 9 | import install from '../util/install'; 10 | 11 | // UPDATE DEPENDENCY VERSIONS HERE 12 | const DEFAULT_DEPENDENCIES = { 13 | expo: '^29.0.0', 14 | react: '16.4.2', 15 | 'react-native': '0.55.4', 16 | 'bs-platform': '^4.0.5', 17 | 'bs-react-native': '~0.9.0', 18 | 'reason-react': '~0.5.3', 19 | }; 20 | 21 | // TODO figure out how this interacts with ejection 22 | const DEFAULT_DEV_DEPENDENCIES = { 23 | }; 24 | 25 | module.exports = async ( 26 | appPath: string, 27 | appName: string, 28 | verbose: boolean, 29 | cwd: string = '' 30 | ) => { 31 | const ownPackageName: string = require('../../package.json').name; 32 | const ownPath: string = path.join(appPath, 'node_modules', ownPackageName); 33 | const useYarn: boolean = await pathExists(path.join(appPath, 'yarn.lock')); 34 | const npmOrYarn = useYarn ? 'yarn' : 'npm'; 35 | 36 | // FIXME(perry) remove when npm 5 is supported 37 | if (!useYarn) { 38 | let npmVersion = spawn 39 | .sync('npm', ['--version']) 40 | .stdout.toString() 41 | .trim(); 42 | 43 | if (npmVersion.match(/\d+/)[0] === '5') { 44 | console.log( 45 | chalk.yellow( 46 | ` 47 | ******************************************************************************* 48 | ERROR: npm 5 is not supported yet 49 | ******************************************************************************* 50 | 51 | It looks like you're using npm 5 which was recently released. 52 | 53 | Create React Native App doesn't work with npm 5 yet, unfortunately. We 54 | recommend using npm 4 or yarn until some bugs are resolved. 55 | 56 | You can follow the known issues with npm 5 at: 57 | https://github.com/npm/npm/issues/16991 58 | 59 | ******************************************************************************* 60 | ` 61 | ) 62 | ); 63 | process.exit(1); 64 | } 65 | } 66 | 67 | const readmeExists: boolean = await pathExists( 68 | path.join(appPath, 'README.md') 69 | ); 70 | if (readmeExists) { 71 | await fse.rename( 72 | path.join(appPath, 'README.md'), 73 | path.join(appPath, 'README.old.md') 74 | ); 75 | } 76 | 77 | const appPackagePath: string = path.join(appPath, 'package.json'); 78 | const appPackage = JSON.parse(await fse.readFile(appPackagePath)); 79 | 80 | // mutate the default package.json in any ways we need to 81 | appPackage.main = 82 | './node_modules/reason-react-native-scripts/build/bin/crna-entry.js'; 83 | appPackage.scripts = { 84 | start: 'react-native-scripts start', 85 | eject: 'react-native-scripts eject', 86 | android: 'react-native-scripts android', 87 | ios: 'react-native-scripts ios', 88 | test: 'node node_modules/jest/bin/jest.js --watch', 89 | }; 90 | 91 | // TODO figure out integration with jest 92 | // appPackage.jest = { 93 | // preset: 'jest-expo', 94 | // }; 95 | 96 | if (!appPackage.dependencies) { 97 | appPackage.dependencies = {}; 98 | } 99 | 100 | if (!appPackage.devDependencies) { 101 | appPackage.devDependencies = {}; 102 | } 103 | 104 | // react-native-scripts is already in the package.json devDependencies 105 | // so we need to merge instead of assigning 106 | Object.assign(appPackage.dependencies, DEFAULT_DEPENDENCIES); 107 | 108 | // TODO figure out integration with jest 109 | // Object.assign(appPackage.devDependencies, DEFAULT_DEV_DEPENDENCIES); 110 | 111 | // Write the new appPackage after copying so that we can include any existing 112 | await fse.writeFile(appPackagePath, JSON.stringify(appPackage, null, 2)); 113 | 114 | // Copy the files for the user 115 | await fse.copy(path.join(ownPath, 'template'), appPath); 116 | 117 | // Rename gitignore after the fact to prevent npm from renaming it to .npmignore 118 | try { 119 | await fse.rename( 120 | path.join(appPath, 'gitignore'), 121 | path.join(appPath, '.gitignore') 122 | ); 123 | } catch (err) { 124 | // Append if there's already a `.gitignore` file there 125 | if (err.code === 'EEXIST') { 126 | const data = await fse.readFile(path.join(appPath, 'gitignore')); 127 | await fse.appendFile(path.join(appPath, '.gitignore'), data); 128 | await fse.unlink(path.join(appPath, 'gitignore')); 129 | } else { 130 | throw err; 131 | } 132 | } 133 | const { code, command, args } = await install(appPath); 134 | if (code !== 0) { 135 | console.error('Failed to install'); 136 | // console.error(`\`${command} ${args.join(' ')}\` failed`); 137 | return; 138 | } 139 | 140 | // display the cleanest way to get to the app dir 141 | // if the cwd + appName is equal to the full path, then just cd into appName 142 | let cdpath; 143 | if (path.resolve(cwd, appName) === appPath) { 144 | cdpath = appName; 145 | } else { 146 | cdpath = appPath; 147 | } 148 | 149 | log( 150 | ` 151 | 152 | Success! Created ${appName} at ${appPath} 153 | Inside that directory, you can run several commands: 154 | 155 | ${chalk.cyan(npmOrYarn + ' start')} 156 | Starts the development server so you can open your app in the Expo 157 | app on your phone. 158 | 159 | ${chalk.cyan(npmOrYarn + ' run ios')} 160 | (Mac only, requires Xcode) 161 | Starts the development server and loads your app in an iOS simulator. 162 | 163 | ${chalk.cyan(npmOrYarn + ' run android')} 164 | (Requires Android build tools) 165 | Starts the development server and loads your app on a connected Android 166 | device or emulator. 167 | 168 | ${chalk.cyan(npmOrYarn + ' test')} 169 | Starts the test runner. 170 | 171 | ${chalk.cyan(npmOrYarn + ' run eject')} 172 | Removes this tool and copies build dependencies, configuration files 173 | and scripts into the app directory. If you do this, you can’t go back! 174 | 175 | We suggest that you begin by typing: 176 | 177 | ${chalk.cyan('cd ' + cdpath)} 178 | ${chalk.cyan(npmOrYarn + ' start')}` 179 | ); 180 | 181 | if (readmeExists) { 182 | log( 183 | ` 184 | ${chalk.yellow('You had a `README.md` file, we renamed it to `README.old.md`')}` 185 | ); 186 | } 187 | 188 | log(); 189 | log('Happy hacking!'); 190 | }; 191 | -------------------------------------------------------------------------------- /src/scripts/ios.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Config, ProjectSettings, Simulator, UrlUtils } from 'xdl'; 4 | 5 | import chalk from 'chalk'; 6 | import indent from 'indent-string'; 7 | import path from 'path'; 8 | import pathExists from 'path-exists'; 9 | import qr from 'qrcode-terminal'; 10 | import log from '../util/log'; 11 | 12 | import packager from '../util/packager'; 13 | 14 | Config.validation.reactNativeVersionWarnings = false; 15 | Config.developerTool = 'crna'; 16 | Config.offline = true; 17 | 18 | const command: string = pathExists.sync(path.join(process.cwd(), 'yarn.lock')) ? 'yarnpkg' : 'npm'; 19 | 20 | if (!Simulator.isPlatformSupported()) { 21 | log( 22 | chalk.red( 23 | '\nThis command only works on macOS computers with Xcode and the iOS simulator installed.' 24 | ) 25 | ); 26 | log( 27 | chalk.yellow( 28 | `If you run \`${chalk.cyan(command + ' start')}\` then you can view your app on a physical device.\n` 29 | ) 30 | ); 31 | process.exit(1); 32 | } 33 | 34 | packager.run(startSimulatorAndPrintInfo); 35 | 36 | // print a nicely formatted message with setup information 37 | async function startSimulatorAndPrintInfo() { 38 | const address = await UrlUtils.constructManifestUrlAsync(process.cwd()); 39 | const localAddress = await UrlUtils.constructManifestUrlAsync(process.cwd(), { 40 | hostType: 'localhost', 41 | }); 42 | 43 | log.withTimestamp('Starting simulator...'); 44 | const { success, msg } = await Simulator.openUrlInSimulatorSafeAsync(localAddress); 45 | 46 | if (success) { 47 | qr.generate(address, qrCode => { 48 | log.withTimestamp(`${chalk.green('Packager started!')}`); 49 | log( 50 | ` 51 | To view your app with live reloading, point the Expo app to this QR code. 52 | You'll find the QR scanner on the Projects tab of the app. 53 | 54 | ${indent(qrCode, 2)} 55 | 56 | Or enter this address in the Expo app's search bar: 57 | 58 | ${chalk.underline(chalk.cyan(address))} 59 | 60 | Your phone will need to be on the same local network as this computer. 61 | For links to install the Expo app, please visit ${chalk.underline(chalk.cyan('https://expo.io'))}. 62 | 63 | Logs from serving your app will appear here. Press Ctrl+C at any time to stop. 64 | 65 | If you restart the simulator or change the simulated hardware, you may need to restart this process. 66 | ` 67 | ); 68 | }); 69 | } else { 70 | log.withTimestamp( 71 | `${chalk.red('Failed to start simulator:')} 72 | 73 | ${msg} 74 | 75 | ${chalk.red('Exiting...')}` 76 | ); 77 | process.exit(0); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/scripts/start.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { Android, Config, Project, ProjectSettings, Simulator, UrlUtils } from 'xdl'; 4 | 5 | import chalk from 'chalk'; 6 | import indent from 'indent-string'; 7 | import qr from 'qrcode-terminal'; 8 | import minimist from 'minimist'; 9 | import log from '../util/log'; 10 | import clearConsole from '../util/clearConsole'; 11 | 12 | import packager from '../util/packager'; 13 | 14 | Config.validation.reactNativeVersionWarnings = false; 15 | Config.developerTool = 'crna'; 16 | Config.offline = true; 17 | 18 | const args = minimist(process.argv.slice(2), { 19 | boolean: ['reset-cache', 'interactive'], 20 | default: { interactive: true }, 21 | }); 22 | let dev = true; 23 | 24 | const options = {}; 25 | if (args['reset-cache']) { 26 | options.reset = true; 27 | log('Asking packager to reset its cache...'); 28 | } 29 | 30 | let isInteractive = false; 31 | const { stdin } = process; 32 | if (args.interactive && typeof stdin.setRawMode === 'function') { 33 | stdin.setRawMode(true); 34 | stdin.resume(); 35 | stdin.setEncoding('utf8'); 36 | stdin.on('data', handleKeypress); 37 | isInteractive = true; 38 | } 39 | 40 | packager.run(onReady, options, isInteractive); 41 | 42 | function onReady() { 43 | log(chalk.green('Packager started!\n')); 44 | printServerInfo(); 45 | } 46 | 47 | // print a nicely formatted message with setup information 48 | async function printServerInfo() { 49 | const settings = await ProjectSettings.readPackagerInfoAsync(process.cwd()); 50 | // who knows why qrcode-terminal takes a callback instead of just returning a string 51 | const address = await UrlUtils.constructManifestUrlAsync(process.cwd()); 52 | qr.generate(address, qrCode => { 53 | log( 54 | `To view your app with live reloading, point the Expo app to this QR code. 55 | You'll find the QR scanner on the Projects tab of the app. 56 | 57 | ${indent(qrCode, 2)} 58 | 59 | Or enter this address in the Expo app's search bar: 60 | 61 | ${chalk.underline(chalk.cyan(address))} 62 | 63 | Your phone will need to be on the same local network as this computer. 64 | For links to install the Expo app, please visit ${chalk.underline(chalk.cyan('https://expo.io'))}. 65 | 66 | Logs from serving your app will appear here. Press Ctrl+C at any time to stop.` 67 | ); 68 | printUsage(); 69 | }); 70 | } 71 | 72 | function printUsage() { 73 | if (!isInteractive) { 74 | return; 75 | } 76 | const { dim, bold } = chalk; 77 | const devMode = dev ? 'development' : 'production'; 78 | const iosInfo = process.platform === 'win32' 79 | ? dim('.') 80 | : `${dim(`, or`)} i ${dim(`to open iOS emulator.`)}`; 81 | log( 82 | ` 83 | ${dim(`\u203A Press`)} a ${dim(`to open Android device or emulator`)}${iosInfo} 84 | ${dim(`\u203A Press`)} q ${dim(`to display QR code.`)} 85 | ${dim(`\u203A Press`)} r ${dim(`to restart packager, or`)} R ${dim(`to restart packager and clear cache.`)} 86 | ${dim(`\u203A Press`)} d ${dim(`to toggle development mode. (current mode: ${bold(devMode)}${chalk.reset.dim(')')}`)} 87 | ` 88 | ); 89 | } 90 | 91 | const CTRL_C = '\u0003'; 92 | const CTRL_D = '\u0004'; 93 | 94 | async function handleKeypress(key) { 95 | switch (key) { 96 | case CTRL_C: 97 | case CTRL_D: 98 | process.emit('SIGINT'); 99 | return; 100 | case 'a': { 101 | clearConsole(); 102 | log.withTimestamp('Starting Android...'); 103 | const { success, error } = await Android.openProjectAsync(process.cwd()); 104 | if (!success) { 105 | log(chalk.red(error.message)); 106 | } 107 | printUsage(); 108 | return; 109 | } 110 | case 'i': { 111 | clearConsole(); 112 | log.withTimestamp('Starting iOS...'); 113 | const localAddress = await UrlUtils.constructManifestUrlAsync(process.cwd(), { 114 | hostType: 'localhost', 115 | }); 116 | const { success, msg } = await Simulator.openUrlInSimulatorSafeAsync(localAddress); 117 | if (!success) { 118 | log(chalk.red(msg)); 119 | } 120 | printUsage(); 121 | return; 122 | } 123 | case 'q': 124 | clearConsole(); 125 | await printServerInfo(); 126 | return; 127 | case 'r': 128 | case 'R': { 129 | clearConsole(); 130 | const reset = key === 'R'; 131 | if (reset) { 132 | log.withTimestamp('Asking packager to reset its cache...'); 133 | } 134 | log.withTimestamp('Restarting packager...'); 135 | Project.startAsync(process.cwd(), { reset }); 136 | return; 137 | } 138 | case 'd': 139 | clearConsole(); 140 | dev = !dev; 141 | await ProjectSettings.setAsync(process.cwd(), { dev }); 142 | log( 143 | `Packager now running in ${chalk.bold(dev ? 'development' : 'production')}${chalk.reset(` mode.`)} 144 | 145 | Please close and reopen the project in the Expo app for the 146 | change to take effect.` 147 | ); 148 | printUsage(); 149 | return; 150 | } 151 | } 152 | -------------------------------------------------------------------------------- /src/util/bsb.js: -------------------------------------------------------------------------------- 1 | const stream = require('stream'); 2 | const execa = require('execa'); 3 | const Writable = require('stream').Writable; 4 | const split = require('split'); 5 | const stdout = new Writable(); 6 | const stderr = new Writable(); 7 | import log from './log'; 8 | 9 | let _resolveOnInitialize; 10 | 11 | stdout._write = (data, encoding, next) => { 12 | if (data.toString().includes('> Finish compiling')) { 13 | if (_resolveOnInitialize) { 14 | _resolveOnInitialize(); 15 | _resolveOnInitialize = null; 16 | } 17 | } 18 | log.withTimestamp(`[bsb] ${data.toString()}`); 19 | next(); 20 | }; 21 | 22 | stderr._write = (data, encoding, next) => { 23 | if (data.toString().includes('refmt version missing')) { 24 | next(); 25 | } else { 26 | log.withTimestamp(`[bsb] ${data.toString()}`); 27 | next(); 28 | } 29 | }; 30 | 31 | export async function spawnBsbWatcherAsync() { 32 | let cp; 33 | await new Promise(resolve => { 34 | _resolveOnInitialize = resolve; 35 | 36 | cp = execa( 37 | 'bsb', 38 | ['-make-world', '-clean-world', '-w'], 39 | ['pipe', 'pipe', null] 40 | ); 41 | 42 | cp.stdout.pipe(split()).pipe(stdout); 43 | cp.stderr.pipe(split()).pipe(stderr); 44 | }); 45 | 46 | return { bsbChildProcess: cp }; 47 | } 48 | -------------------------------------------------------------------------------- /src/util/clearConsole.js: -------------------------------------------------------------------------------- 1 | export default function clearConsole() { 2 | process.stdout.write(process.platform === 'win32' ? '\x1Bc' : '\x1B[2J\x1B[3J\x1B[H'); 3 | } 4 | -------------------------------------------------------------------------------- /src/util/expo.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import chalk from 'chalk'; 4 | import fse from 'fs-extra'; 5 | import inquirer from 'inquirer'; 6 | import path from 'path'; 7 | import install from '../util/install'; 8 | 9 | import { Detach, User as UserManager, Versions } from 'xdl'; 10 | 11 | import type { User } from 'xdl/build/User'; 12 | 13 | const AUTH_CLIENT_ID = 'MGQh3rK3WZFWhJ91BShagHggMOhrE6nR'; 14 | UserManager.initialize(AUTH_CLIENT_ID); 15 | 16 | export async function detach() { 17 | const user = await loginOrRegister(); 18 | 19 | const appJsonPath = path.join(process.cwd(), 'app.json'); 20 | const appJson = JSON.parse(await fse.readFile(appJsonPath)); 21 | 22 | if ((!appJson.expo.ios || !appJson.expo.ios.bundleIdentifier) && process.platform === 'darwin') { 23 | console.log( 24 | ` 25 | You'll need to specify an iOS bundle identifier. It must be unique on the App Store if you want to 26 | publish it there. See this StackOverflow question for more information: 27 | ${chalk.cyan('https://stackoverflow.com/questions/11347470/what-does-bundle-identifier-mean-in-the-ios-project')} 28 | ` 29 | ); 30 | const { iosBundleIdentifier } = await inquirer.prompt([ 31 | { 32 | name: 'iosBundleIdentifier', 33 | message: 'What would you like your iOS bundle identifier to be?', 34 | }, 35 | ]); 36 | 37 | appJson.expo.ios = appJson.expo.ios || {}; 38 | appJson.expo.ios.bundleIdentifier = iosBundleIdentifier; 39 | } 40 | 41 | // check for android.package field, prompt interactively 42 | if (!appJson.expo.android || !appJson.expo.android.package) { 43 | console.log( 44 | ` 45 | You'll need to specify an Android package name. It must be unique on the Play Store if you want to 46 | publish it there. See this StackOverflow question for more information: 47 | ${chalk.cyan('https://stackoverflow.com/questions/6273892/android-package-name-convention')} 48 | ` 49 | ); 50 | 51 | const { androidPackage } = await inquirer.prompt([ 52 | { 53 | name: 'androidPackage', 54 | message: 'What would you like your Android package name to be?', 55 | }, 56 | ]); 57 | 58 | appJson.expo.android = appJson.expo.android || {}; 59 | appJson.expo.android.package = androidPackage; 60 | } 61 | 62 | // update app.json file with new contents 63 | await fse.writeFile(appJsonPath, JSON.stringify(appJson, null, 2)); 64 | 65 | await Detach.detachAsync(process.cwd()); 66 | // yesno lib doesn't properly shut down. without this the command won't exit 67 | process.stdin.pause(); 68 | 69 | const pkgJson = JSON.parse((await fse.readFile(path.resolve('package.json'))).toString()); 70 | 71 | pkgJson.main = 'node_modules/expo/AppEntry.js'; 72 | 73 | delete pkgJson.devDependencies['react-native-scripts']; 74 | delete pkgJson.scripts.start; 75 | delete pkgJson.scripts.build; 76 | delete pkgJson.scripts.eject; 77 | delete pkgJson.scripts.android; 78 | delete pkgJson.scripts.ios; 79 | 80 | const versions = await Versions.versionsAsync(); 81 | const sdkTag = versions.sdkVersions[appJson.expo.sdkVersion].expoReactNativeTag; 82 | 83 | await fse.writeFile('package.json', JSON.stringify(pkgJson, null, 2)); 84 | 85 | console.log('Installing the Expo fork of react-native...'); 86 | const reactNativeVersion = `https://github.com/expo/react-native/archive/${sdkTag}.tar.gz`; 87 | const { 88 | code, 89 | } = await install(process.cwd(), 'react-native', reactNativeVersion, { 90 | silent: true, 91 | }); 92 | 93 | if (code === 0) { 94 | console.log(`${chalk.green('Successfully set up ExpoKit!')}`); 95 | } else { 96 | console.warn( 97 | ` 98 | ${chalk.yellow('Unable to install the Expo fork of react-native.')} 99 | ${chalk.yellow(`Please install react-native@${reactNativeVersion} before continuing.`)} 100 | ` 101 | ); 102 | } 103 | 104 | console.log( 105 | ` 106 | You'll need to use Expo's XDE to run this project: 107 | ${chalk.cyan('https://docs.expo.io/versions/latest/introduction/installation.html')} 108 | 109 | For further instructions, please read ExpoKit's build documentation: 110 | ${chalk.cyan('https://docs.expo.io/versions/latest/guides/expokit.html')} 111 | ` 112 | ); 113 | } 114 | 115 | async function loginOrRegister(): Promise { 116 | console.log(chalk.yellow('\nAn Expo account is required to proceed.\n')); 117 | const currentUser = await UserManager.getCurrentUserAsync(); 118 | 119 | if (currentUser) { 120 | const loggedInQuestions = [ 121 | { 122 | type: 'list', 123 | name: 'stayLoggedIn', 124 | message: `It appears you're already logged in to Expo as \ 125 | ${chalk.green(currentUser.nickname)}, would you like to continue with this account?`, 126 | choices: [ 127 | { 128 | name: `Yes, continue as ${currentUser.nickname}.`, 129 | value: true, 130 | }, 131 | { 132 | name: "No, I'd like to start a new session.", 133 | value: false, 134 | }, 135 | ], 136 | }, 137 | ]; 138 | 139 | const { stayLoggedIn } = await inquirer.prompt(loggedInQuestions); 140 | 141 | if (stayLoggedIn) { 142 | return currentUser; 143 | } else { 144 | await UserManager.logoutAsync(); 145 | console.log(chalk.green('\nSuccessfully logged out!\n')); 146 | } 147 | } 148 | 149 | const questions = [ 150 | { 151 | type: 'list', 152 | name: 'action', 153 | message: 'How would you like to authenticate?', 154 | choices: [ 155 | { 156 | name: 'Make a new Expo account', 157 | value: 'register', 158 | }, 159 | { 160 | name: 'Log in with an existing Expo account', 161 | value: 'existingUser', 162 | }, 163 | { 164 | name: 'Cancel', 165 | value: 'cancel', 166 | }, 167 | ], 168 | }, 169 | ]; 170 | 171 | const { action } = await inquirer.prompt(questions); 172 | 173 | if (action === 'github') { 174 | return await githubAuthAsync(); 175 | } else if (action === 'register') { 176 | return await registerAsync(); 177 | } else if (action === 'existingUser') { 178 | return await usernamePasswordAuthAsync(); 179 | } else { 180 | return null; 181 | } 182 | } 183 | 184 | async function githubAuthAsync(): Promise { 185 | let user = await UserManager.loginAsync('github'); 186 | if (user) { 187 | console.log(chalk.green(`\nSuccessfully logged in as ${user.nickname} with GitHub!`)); 188 | return user; 189 | } else { 190 | throw new Error('Unexpected Error: No user returned from the API'); 191 | } 192 | } 193 | 194 | function validator(val: string): boolean { 195 | if (val.trim() === '') { 196 | return false; 197 | } 198 | return true; 199 | } 200 | 201 | async function usernamePasswordAuthAsync(): Promise { 202 | const questions = [ 203 | { 204 | type: 'input', 205 | name: 'username', 206 | message: 'Username/Email Address:', 207 | validate: validator, 208 | }, 209 | { 210 | type: 'password', 211 | name: 'password', 212 | message: 'Password:', 213 | validate: validator, 214 | }, 215 | ]; 216 | 217 | const answers = await inquirer.prompt(questions); 218 | 219 | const data = { 220 | username: answers.username, 221 | password: answers.password, 222 | }; 223 | 224 | let user = await UserManager.loginAsync('user-pass', data); 225 | 226 | if (user) { 227 | console.log(chalk.green(`\nSuccessfully logged in as ${user.nickname}!`)); 228 | return user; 229 | } else { 230 | throw new Error('Unexpected Error: No user returned from the Expo API'); 231 | } 232 | } 233 | 234 | async function registerAsync(): Promise { 235 | console.log( 236 | ` 237 | Thanks for signing up for Expo! 238 | Just a few questions: 239 | ` 240 | ); 241 | 242 | const questions = [ 243 | { 244 | type: 'input', 245 | name: 'givenName', 246 | message: 'First (Given) Name:', 247 | validate: validator, 248 | }, 249 | { 250 | type: 'input', 251 | name: 'familyName', 252 | message: 'Last (Family) Name:', 253 | validate: validator, 254 | }, 255 | { 256 | type: 'input', 257 | name: 'username', 258 | message: 'Username:', 259 | validate: validator, 260 | }, 261 | { 262 | type: 'input', 263 | name: 'email', 264 | message: 'Email Address:', 265 | validate: validator, 266 | }, 267 | { 268 | type: 'password', 269 | name: 'password', 270 | message: 'Password:', 271 | validate: validator, 272 | }, 273 | { 274 | type: 'password', 275 | name: 'passwordRepeat', 276 | message: 'Password Repeat:', 277 | validate(val, answers) { 278 | if (val.trim() === '') { 279 | return false; 280 | } 281 | if (val.trim() !== answers.password.trim()) { 282 | return `Passwords don't match!`; 283 | } 284 | return true; 285 | }, 286 | }, 287 | ]; 288 | 289 | const answers = await inquirer.prompt(questions); 290 | 291 | const registeredUser = await UserManager.registerAsync({ 292 | ...answers, 293 | }); 294 | 295 | console.log(chalk.green('\nRegistration successful!')); 296 | 297 | return registeredUser; 298 | } 299 | -------------------------------------------------------------------------------- /src/util/install.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import spawn from 'cross-spawn'; 4 | import pathExists from 'path-exists'; 5 | import path from 'path'; 6 | import log from '../util/log'; 7 | 8 | type InstallResult = { 9 | code: number, 10 | command: string, 11 | args: Array, 12 | }; 13 | 14 | export default (async function install( 15 | appPath: string, 16 | packageName?: string, 17 | packageVersion?: string, 18 | options?: any = {} 19 | ): Promise { 20 | const useYarn: boolean = await pathExists(path.join(appPath, 'yarn.lock')); 21 | 22 | let command = ''; 23 | let args = []; 24 | 25 | if (useYarn) { 26 | command = 'yarnpkg'; 27 | if (packageName) { 28 | args = ['add']; 29 | } 30 | } else { 31 | command = 'npm'; 32 | args = ['install', '--save']; 33 | 34 | // if (verbose) { 35 | // args.push('--verbose'); 36 | // } 37 | } 38 | 39 | let pkg = packageName; 40 | if (pkg) { 41 | if (packageVersion) { 42 | pkg = `${pkg}@${packageVersion}`; 43 | } 44 | 45 | args.push(pkg); 46 | } 47 | 48 | const npmOrYarn = useYarn ? 'yarn' : 'npm'; 49 | log(`Installing ${pkg ? pkg : 'dependencies'} using ${npmOrYarn}...`); 50 | log(); 51 | 52 | let spawnOpts = {}; 53 | if (options.silent) { 54 | spawnOpts.silent = true; 55 | } else { 56 | spawnOpts.stdio = 'inherit'; 57 | } 58 | 59 | const proc = spawn(command, args, spawnOpts); 60 | return new Promise(resolve => { 61 | proc.on('close', code => resolve({ code, command, args })); 62 | }); 63 | }); 64 | -------------------------------------------------------------------------------- /src/util/log.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import chalk from 'chalk'; 4 | 5 | function log(data: any) { 6 | const args = Array.prototype.slice.call(arguments, 0); 7 | 8 | respectProgressBars(() => { 9 | console.log(...args); 10 | }); 11 | } 12 | 13 | log.withTimestamp = function(data: any) { 14 | const prefix = chalk.dim(new Date().toLocaleTimeString()) + ':'; 15 | const args = [prefix].concat(Array.prototype.slice.call(arguments, 0)); 16 | 17 | respectProgressBars(() => { 18 | console.log(...args); 19 | }); 20 | }; 21 | 22 | let _bundleProgressBar; 23 | log.setBundleProgressBar = function(bundleProgressBar) { 24 | _bundleProgressBar = bundleProgressBar; 25 | }; 26 | 27 | function respectProgressBars(commitLogs) { 28 | if (_bundleProgressBar) { 29 | _bundleProgressBar.terminate(); 30 | _bundleProgressBar.lastDraw = ''; 31 | commitLogs(); 32 | _bundleProgressBar.render(); 33 | } else { 34 | commitLogs(); 35 | } 36 | } 37 | 38 | export default log; 39 | -------------------------------------------------------------------------------- /src/util/packager.js: -------------------------------------------------------------------------------- 1 | // @flow 2 | 3 | import { 4 | PackagerLogsStream, 5 | Project, 6 | ProjectSettings, 7 | ProjectUtils, 8 | } from 'xdl'; 9 | 10 | import _ from 'lodash'; 11 | import spawn from 'cross-spawn'; 12 | import ProgressBar from 'progress'; 13 | import bunyan from '@expo/bunyan'; 14 | import chalk from 'chalk'; 15 | import { spawnBsbWatcherAsync } from './bsb'; 16 | 17 | import log from './log'; 18 | 19 | function installExitHooks(projectDir, isInteractive) { 20 | if (!isInteractive && process.platform === 'win32') { 21 | require('readline') 22 | .createInterface({ 23 | input: process.stdin, 24 | output: process.stdout, 25 | }) 26 | .on('SIGINT', () => { 27 | process.emit('SIGINT'); 28 | }); 29 | } 30 | 31 | process.on('SIGINT', () => { 32 | log.withTimestamp('Stopping packager...'); 33 | cleanUpPackager(projectDir).then(() => { 34 | // TODO: this shows up after process exits, fix it 35 | log.withTimestamp(chalk.green('Packager stopped.')); 36 | process.exit(); 37 | }); 38 | }); 39 | } 40 | 41 | async function cleanUpPackager(projectDir) { 42 | const result = await Promise.race([ 43 | Project.stopAsync(projectDir), 44 | new Promise((resolve, reject) => setTimeout(resolve, 1000, 'stopFailed')), 45 | ]); 46 | 47 | if (result === 'stopFailed') { 48 | // find RN packager pid, attempt to kill manually 49 | try { 50 | const { packagerPid } = await ProjectSettings.readPackagerInfoAsync( 51 | projectDir 52 | ); 53 | process.kill(packagerPid); 54 | } catch (e) { 55 | process.exit(1); 56 | } 57 | } 58 | } 59 | 60 | function shouldIgnoreMsg(msg) { 61 | return msg.indexOf('Duplicate module name: bser') >= 0 || 62 | msg.indexOf('Duplicate module name: fb-watchman') >= 0 || 63 | msg.indexOf('Warning: React.createClass is no longer supported') >= 0 || 64 | msg.indexOf('Warning: PropTypes has been moved to a separate package') >= 0; 65 | } 66 | 67 | async function run( 68 | onReady: () => ?any, 69 | options: Object = {}, 70 | isInteractive?: boolean = false 71 | ) { 72 | // first, run bsb 73 | log.withTimestamp('Starting BuckleScript watcher for building Reason code'); 74 | let { bsbChildProcess } = await spawnBsbWatcherAsync(); 75 | log.withTimestamp(chalk.green('BuckleScript watcher started')); 76 | 77 | let packagerReady = false; 78 | let needsClear = false; 79 | let logBuffer = ''; 80 | let progressBar; 81 | const projectDir = process.cwd(); 82 | 83 | if (process.platform !== 'win32') { 84 | const watchmanExists = spawn.sync('which', ['watchman']).status === 0; 85 | 86 | if (process.platform === 'darwin' && !watchmanExists) { 87 | const watcherDetails = spawn 88 | .sync('sysctl', ['kern.maxfiles']) 89 | .stdout.toString(); 90 | if (parseInt(watcherDetails.split(':')[1].trim()) < 5242880) { 91 | log.withTimestamp( 92 | `${chalk.red(`Unable to start server`)} 93 | See https://git.io/v5vcn for more information, either install watchman or run the following snippet: 94 | ${chalk.cyan(` sudo sysctl -w kern.maxfiles=5242880 95 | sudo sysctl -w kern.maxfilesperproc=524288`)} 96 | ` 97 | ); 98 | process.exit(1); 99 | } 100 | } else if (!watchmanExists) { 101 | try { 102 | const watcherDetails = spawn 103 | .sync('sysctl', ['fs.inotify.max_user_watches']) 104 | .stdout.toString(); 105 | if (parseInt(watcherDetails.split('=')[1].trim()) < 12288) { 106 | log.withTimestamp( 107 | `${chalk.red(`Unable to start server`)} 108 | See https://git.io/v5vcn for more information, either install watchman or run the following snippet: 109 | ${chalk.cyan(` sudo sysctl -w fs.inotify.max_user_instances=1024 110 | sudo sysctl -w fs.inotify.max_user_watches=12288`)}` 111 | ); 112 | process.exit(1); 113 | } 114 | } catch (e) { 115 | // note(brentvatne): I'm not sure why stdout is null for some OS's 116 | // https://github.com/react-community/create-react-native-app/issues/391 117 | log.withTimestamp( 118 | 'Warning: Unable to run `sysctl fs.inotify.max_user_watches`. If you encounter issues, please refer to https://git.io/v5vcn' 119 | ); 120 | } 121 | } 122 | } 123 | 124 | const handleLogChunk = chunk => { 125 | // pig, meet lipstick 126 | // 1. https://github.com/facebook/react-native/issues/14620 127 | // 2. https://github.com/facebook/react-native/issues/14610 128 | // 3. https://github.com/react-community/create-react-native-app/issues/229#issuecomment-308654303 129 | // @ide is investigating 3), the first two are upstream issues that will 130 | // likely be resolved by others 131 | if (shouldIgnoreMsg(chunk.msg)) { 132 | return; 133 | } 134 | 135 | // we don't need to print the entire manifest when loading the app 136 | if (chunk.msg.indexOf(' with appParams: ') >= 0) { 137 | if (needsClear) { 138 | // this is set when we previously encountered an error 139 | // TODO clearConsole(); 140 | } 141 | let devEnabled = chunk.msg.includes('__DEV__ === true'); 142 | log.withTimestamp( 143 | `Running app on ${chunk.deviceName} in ${devEnabled ? 'development' : 'production'} mode\n` 144 | ); 145 | return; 146 | } 147 | 148 | if (chunk.msg === 'Dependency graph loaded.') { 149 | packagerReady = true; 150 | onReady(); 151 | return; 152 | } 153 | 154 | if (packagerReady) { 155 | const message = `${chunk.msg.trim()}\n`; 156 | logWithLevel(chunk); 157 | 158 | if (chunk.level === bunyan.ERROR) { 159 | // if you run into a syntax error then we should clear log output on reload 160 | needsClear = message.indexOf('SyntaxError') >= 0; 161 | } 162 | } else { 163 | if (chunk.level >= bunyan.ERROR) { 164 | log(chalk.yellow('***ERROR STARTING PACKAGER***')); 165 | log(logBuffer); 166 | log(chalk.red(chunk.msg)); 167 | logBuffer = ''; 168 | } else { 169 | logBuffer += chunk.msg + '\n'; 170 | } 171 | } 172 | }; 173 | 174 | // Subscribe to packager/server logs 175 | let packagerLogsStream = new PackagerLogsStream({ 176 | projectRoot: projectDir, 177 | onStartBuildBundle: () => { 178 | progressBar = new ProgressBar( 179 | 'Building JavaScript bundle [:bar] :percent', 180 | { 181 | total: 100, 182 | clear: true, 183 | complete: '=', 184 | incomplete: ' ', 185 | } 186 | ); 187 | 188 | log.setBundleProgressBar(progressBar); 189 | }, 190 | onProgressBuildBundle: percent => { 191 | if (!progressBar || progressBar.complete) return; 192 | let ticks = percent - progressBar.curr; 193 | ticks > 0 && progressBar.tick(ticks); 194 | }, 195 | onFinishBuildBundle: (err, startTime, endTime) => { 196 | if (progressBar && !progressBar.complete) { 197 | progressBar.tick(100 - progressBar.curr); 198 | } 199 | 200 | if (progressBar) { 201 | log.setBundleProgressBar(null); 202 | progressBar = null; 203 | 204 | if (err) { 205 | log.withTimestamp(chalk.red(`Failed building JavaScript bundle`)); 206 | } else { 207 | let duration = endTime - startTime; 208 | log.withTimestamp( 209 | chalk.green(`Finished building JavaScript bundle in ${duration}ms`) 210 | ); 211 | } 212 | } 213 | }, 214 | updateLogs: updater => { 215 | let newLogChunks = updater([]); 216 | 217 | if (progressBar) { 218 | // Restarting watchman causes `onFinishBuildBundle` to not fire. Until 219 | // this is handled upstream in xdl, reset progress bar with error here. 220 | newLogChunks.forEach(chunk => { 221 | if (chunk.msg === 'Restarted watchman.') { 222 | progressBar.tick(100 - progressBar.curr); 223 | log.setBundleProgressBar(null); 224 | progressBar = null; 225 | log.withTimestamp(chalk.red('Failed building JavaScript bundle')); 226 | } 227 | }); 228 | } 229 | 230 | newLogChunks.map(handleLogChunk); 231 | }, 232 | }); 233 | 234 | // Subscribe to device updates separately from packager/server updates 235 | ProjectUtils.attachLoggerStream(projectDir, { 236 | stream: { 237 | write: chunk => { 238 | if (chunk.tag === 'device') { 239 | handleLogChunk(chunk); 240 | } 241 | }, 242 | }, 243 | type: 'raw', 244 | }); 245 | 246 | installExitHooks(projectDir, isInteractive); 247 | log.withTimestamp('Starting packager...'); 248 | 249 | Project.startAsync(projectDir, options).then( 250 | () => {}, 251 | reason => { 252 | log.withTimestamp(chalk.red(`Error starting packager: ${reason.stack}`)); 253 | process.exit(1); 254 | } 255 | ); 256 | } 257 | 258 | const logStackTrace = (chunk, logFn, nestedLogFn, colorFn) => { 259 | let traceInfo; 260 | try { 261 | traceInfo = JSON.parse(chunk.msg); 262 | } catch (e) { 263 | return logFn(colorFn(chunk.msg)); 264 | } 265 | 266 | let { message, stack } = traceInfo; 267 | logFn(colorFn(chalk.bold(message))); 268 | 269 | const isLibraryFrame = line => { 270 | return line.startsWith('node_modules'); 271 | }; 272 | 273 | let stackFrames = _.compact(stack.split('\n')); 274 | let lastAppCodeFrameIndex = _.findLastIndex(stackFrames, line => { 275 | return !isLibraryFrame(line); 276 | }); 277 | let lastFrameIndexToLog = Math.min( 278 | stackFrames.length - 1, 279 | lastAppCodeFrameIndex + 2 // show max two more frames after last app code frame 280 | ); 281 | let unloggedFrames = stackFrames.length - lastFrameIndexToLog; 282 | 283 | // If we're only going to exclude one frame, just log them all 284 | if (unloggedFrames === 1) { 285 | lastFrameIndexToLog = stackFrames.length - 1; 286 | unloggedFrames = 0; 287 | } 288 | 289 | for (let i = 0; i <= lastFrameIndexToLog; i++) { 290 | let line = stackFrames[i]; 291 | if (!line) { 292 | continue; 293 | } else if (line.match(/react-native\/.*YellowBox.js/)) { 294 | continue; 295 | } 296 | 297 | if (line.startsWith('node_modules')) { 298 | nestedLogFn(colorFn('- ' + line)); 299 | } else { 300 | nestedLogFn(colorFn('* ' + line)); 301 | } 302 | } 303 | 304 | if (unloggedFrames > 0) { 305 | nestedLogFn( 306 | colorFn( 307 | `- ... ${unloggedFrames} more stack frames from framework internals` 308 | ) 309 | ); 310 | } 311 | }; 312 | 313 | const logWithLevel = chunk => { 314 | if (!chunk.msg) { 315 | return; 316 | } 317 | 318 | let colorFn = (str) => str; 319 | if (chunk.level === bunyan.WARN) { 320 | colorFn = chalk.yellow; 321 | } else if (chunk.level === bunyan.ERROR) { 322 | colorFn = chalk.red; 323 | } 324 | 325 | if (chunk.includesStack) { 326 | logStackTrace(chunk, log.withTimestamp, log, colorFn); 327 | } else { 328 | logLines(chunk.msg, log.withTimestamp, colorFn); 329 | } 330 | }; 331 | 332 | const logLines = (msg, logFn, colorFn) => { 333 | for (let line of msg.split('\n')) { 334 | logFn(colorFn(line)); 335 | } 336 | }; 337 | 338 | export default { run }; 339 | -------------------------------------------------------------------------------- /taskfile.js: -------------------------------------------------------------------------------- 1 | const { join } = require('path'); 2 | 3 | const paths = { 4 | build: 'build', 5 | source: 'src/**/*.js', 6 | sourceRoot: join(__dirname, 'src'), 7 | }; 8 | 9 | export default async function (fly) { 10 | await fly.watch(paths.source, 'babel'); 11 | } 12 | 13 | export async function babel(fly, opts) { 14 | await fly.source(opts.src || paths.source).babel().target(paths.build); 15 | } 16 | 17 | export async function clean(fly) { 18 | await fly.clear(paths.build); 19 | } 20 | 21 | export async function build(fly, opts) { 22 | await fly.serial(['clean', 'babel'], opts); 23 | } 24 | -------------------------------------------------------------------------------- /template/.babelrc: -------------------------------------------------------------------------------- 1 | { 2 | "presets": ["babel-preset-expo"], 3 | "env": { 4 | "development": { 5 | "plugins": ["transform-react-jsx-source"] 6 | } 7 | } 8 | } 9 | -------------------------------------------------------------------------------- /template/.watchmanconfig: -------------------------------------------------------------------------------- 1 | {} 2 | -------------------------------------------------------------------------------- /template/README.md: -------------------------------------------------------------------------------- 1 | This project was bootstrapped with [Create React Native App](https://github.com/react-community/create-react-native-app) and[Reason React Native Scripts](https://github.com/react-community/reason-react-native-scripts). 2 | 3 | Below you'll find information about performing common tasks. The most recent version of this guide is available [here](https://github.com/react-community/create-react-native-app/blob/master/react-native-scripts/template/README.md). 4 | 5 | ## Table of Contents 6 | 7 | * [Updating to New Releases](#updating-to-new-releases) 8 | * [Available Scripts](#available-scripts) 9 | * [npm start](#npm-start) 10 | * [npm test](#npm-test) 11 | * [npm run ios](#npm-run-ios) 12 | * [npm run android](#npm-run-android) 13 | * [npm run eject](#npm-run-eject) 14 | * [Writing and Running Tests](#writing-and-running-tests) 15 | * [Environment Variables](#environment-variables) 16 | * [Configuring Packager IP Address](#configuring-packager-ip-address) 17 | * [Adding Flow](#adding-flow) 18 | * [Customizing App Display Name and Icon](#customizing-app-display-name-and-icon) 19 | * [Sharing and Deployment](#sharing-and-deployment) 20 | * [Publishing to Expo's React Native Community](#publishing-to-expos-react-native-community) 21 | * [Building an Expo "standalone" app](#building-an-expo-standalone-app) 22 | * [Ejecting from Create React Native App](#ejecting-from-create-react-native-app) 23 | * [Build Dependencies (Xcode & Android Studio)](#build-dependencies-xcode-android-studio) 24 | * [Should I Use ExpoKit?](#should-i-use-expokit) 25 | * [Troubleshooting](#troubleshooting) 26 | * [Networking](#networking) 27 | * [iOS Simulator won't open](#ios-simulator-wont-open) 28 | * [QR Code does not scan](#qr-code-does-not-scan) 29 | 30 | ## Updating to New Releases 31 | 32 | You should only need to update the global installation of `create-react-native-app` very rarely, ideally never. 33 | 34 | Updating the `react-native-scripts` dependency of your app should be as simple as bumping the version number in `package.json` and reinstalling your project's dependencies. 35 | 36 | Upgrading to a new version of React Native requires updating the `react-native`, `react`, and `expo` package versions, and setting the correct `sdkVersion` in `app.json`. See the [versioning guide](https://github.com/react-community/create-react-native-app/blob/master/VERSIONS.md) for up-to-date information about package version compatibility. 37 | 38 | ## Available Scripts 39 | 40 | If Yarn was installed when the project was initialized, then dependencies will have been installed via Yarn, and you should probably use it to run these commands as well. Unlike dependency installation, command running syntax is identical for Yarn and NPM at the time of this writing. 41 | 42 | ### `npm start` 43 | 44 | Runs your app in development mode. 45 | 46 | Open it in the [Expo app](https://expo.io) on your phone to view it. It will reload if you save edits to your files, and you will see build errors and logs in the terminal. 47 | 48 | Sometimes you may need to reset or clear the React Native packager's cache. To do so, you can pass the `--reset-cache` flag to the start script: 49 | 50 | ``` 51 | npm start -- --reset-cache 52 | # or 53 | yarn start -- --reset-cache 54 | ``` 55 | 56 | #### `npm test` 57 | 58 | Runs the [jest](https://github.com/facebook/jest) test runner on your tests. 59 | 60 | #### `npm run ios` 61 | 62 | Like `npm start`, but also attempts to open your app in the iOS Simulator if you're on a Mac and have it installed. 63 | 64 | #### `npm run android` 65 | 66 | Like `npm start`, but also attempts to open your app on a connected Android device or emulator. Requires an installation of Android build tools (see [React Native docs](https://facebook.github.io/react-native/docs/getting-started.html) for detailed setup). We also recommend installing Genymotion as your Android emulator. Once you've finished setting up the native build environment, there are two options for making the right copy of `adb` available to Create React Native App: 67 | 68 | ##### Using Android Studio's `adb` 69 | 70 | 1. Make sure that you can run adb from your terminal. 71 | 2. Open Genymotion and navigate to `Settings -> ADB`. Select “Use custom Android SDK tools” and update with your [Android SDK directory](https://stackoverflow.com/questions/25176594/android-sdk-location). 72 | 73 | ##### Using Genymotion's `adb` 74 | 75 | 1. Find Genymotion’s copy of adb. On macOS for example, this is normally `/Applications/Genymotion.app/Contents/MacOS/tools/`. 76 | 2. Add the Genymotion tools directory to your path (instructions for [Mac](http://osxdaily.com/2014/08/14/add-new-path-to-path-command-line/), [Linux](http://www.computerhope.com/issues/ch001647.htm), and [Windows](https://www.howtogeek.com/118594/how-to-edit-your-system-path-for-easy-command-line-access/)). 77 | 3. Make sure that you can run adb from your terminal. 78 | 79 | #### `npm run eject` 80 | 81 | This will start the process of "ejecting" from Create React Native App's build scripts. You'll be asked a couple of questions about how you'd like to build your project. 82 | 83 | **Warning:** Running eject is a permanent action (aside from whatever version control system you use). An ejected app will require you to have an [Xcode and/or Android Studio environment](https://facebook.github.io/react-native/docs/getting-started.html) set up. 84 | 85 | ## Customizing App Display Name and Icon 86 | 87 | You can edit `app.json` to include [configuration keys](https://docs.expo.io/versions/latest/guides/configuration.html) under the `expo` key. 88 | 89 | To change your app's display name, set the `expo.name` key in `app.json` to an appropriate string. 90 | 91 | To set an app icon, set the `expo.icon` key in `app.json` to be either a local path or a URL. It's recommended that you use a 512x512 png file with transparency. 92 | 93 | ## Writing and Running Tests 94 | 95 | This project is set up to use [jest](https://facebook.github.io/jest/) for tests. You can configure whatever testing strategy you like, but jest works out of the box. Create test files in directories called `__tests__` or with the `.test` extension to have the files loaded by jest. See the [the template project](https://github.com/react-community/create-react-native-app/blob/master/react-native-scripts/template/App.test.js) for an example test. The [jest documentation](https://facebook.github.io/jest/docs/en/getting-started.html) is also a wonderful resource, as is the [React Native testing tutorial](https://facebook.github.io/jest/docs/en/tutorial-react-native.html). 96 | 97 | ## Environment Variables 98 | 99 | You can configure some of Create React Native App's behavior using environment variables. 100 | 101 | ### Configuring Packager IP Address 102 | 103 | When starting your project, you'll see something like this for your project URL: 104 | 105 | ``` 106 | exp://192.168.0.2:19000 107 | ``` 108 | 109 | The "manifest" at that URL tells the Expo app how to retrieve and load your app's JavaScript bundle, so even if you load it in the app via a URL like `exp://localhost:19000`, the Expo client app will still try to retrieve your app at the IP address that the start script provides. 110 | 111 | In some cases, this is less than ideal. This might be the case if you need to run your project inside of a virtual machine and you have to access the packager via a different IP address than the one which prints by default. In order to override the IP address or hostname that is detected by Create React Native App, you can specify your own hostname via the `REACT_NATIVE_PACKAGER_HOSTNAME` environment variable: 112 | 113 | Mac and Linux: 114 | 115 | ``` 116 | REACT_NATIVE_PACKAGER_HOSTNAME='my-custom-ip-address-or-hostname' npm start 117 | ``` 118 | 119 | Windows: 120 | ``` 121 | set REACT_NATIVE_PACKAGER_HOSTNAME='my-custom-ip-address-or-hostname' 122 | npm start 123 | ``` 124 | 125 | The above example would cause the development server to listen on `exp://my-custom-ip-address-or-hostname:19000`. 126 | 127 | ## Adding Flow 128 | 129 | Flow is a static type checker that helps you write code with fewer bugs. Check out this [introduction to using static types in JavaScript](https://medium.com/@preethikasireddy/why-use-static-types-in-javascript-part-1-8382da1e0adb) if you are new to this concept. 130 | 131 | React Native works with [Flow](http://flowtype.org/) out of the box, as long as your Flow version matches the one used in the version of React Native. 132 | 133 | To add a local dependency to the correct Flow version to a Create React Native App project, follow these steps: 134 | 135 | 1. Find the Flow `[version]` at the bottom of the included [.flowconfig](.flowconfig) 136 | 2. Run `npm install --save-dev flow-bin@x.y.z` (or `yarn add --dev flow-bin@x.y.z`), where `x.y.z` is the .flowconfig version number. 137 | 3. Add `"flow": "flow"` to the `scripts` section of your `package.json`. 138 | 4. Add `// @flow` to any files you want to type check (for example, to `App.js`). 139 | 140 | Now you can run `npm run flow` (or `yarn flow`) to check the files for type errors. 141 | You can optionally use a [plugin for your IDE or editor](https://flow.org/en/docs/editors/) for a better integrated experience. 142 | 143 | To learn more about Flow, check out [its documentation](https://flow.org/). 144 | 145 | ## Sharing and Deployment 146 | 147 | Create React Native App does a lot of work to make app setup and development simple and straightforward, but it's very difficult to do the same for deploying to Apple's App Store or Google's Play Store without relying on a hosted service. 148 | 149 | ### Publishing to Expo's React Native Community 150 | 151 | Expo provides free hosting for the JS-only apps created by CRNA, allowing you to share your app through the Expo client app. This requires registration for an Expo account. 152 | 153 | Install the `exp` command-line tool, and run the publish command: 154 | 155 | ``` 156 | $ npm i -g exp 157 | $ exp publish 158 | ``` 159 | 160 | ### Building an Expo "standalone" app 161 | 162 | You can also use a service like [Expo's standalone builds](https://docs.expo.io/versions/latest/guides/building-standalone-apps.html) if you want to get an IPA/APK for distribution without having to build the native code yourself. 163 | 164 | ### Ejecting from Create React Native App 165 | 166 | If you want to build and deploy your app yourself, you'll need to eject from CRNA and use Xcode and Android Studio. 167 | 168 | This is usually as simple as running `npm run eject` in your project, which will walk you through the process. Make sure to install `react-native-cli` and follow the [native code getting started guide for React Native](https://facebook.github.io/react-native/docs/getting-started.html). 169 | 170 | #### Should I Use ExpoKit? 171 | 172 | If you have made use of Expo APIs while working on your project, then those API calls will stop working if you eject to a regular React Native project. If you want to continue using those APIs, you can eject to "React Native + ExpoKit" which will still allow you to build your own native code and continue using the Expo APIs. See the [ejecting guide](https://github.com/react-community/create-react-native-app/blob/master/EJECTING.md) for more details about this option. 173 | 174 | ## Troubleshooting 175 | 176 | ### Networking 177 | 178 | If you're unable to load your app on your phone due to a network timeout or a refused connection, a good first step is to verify that your phone and computer are on the same network and that they can reach each other. Create React Native App needs access to ports 19000 and 19001 so ensure that your network and firewall settings allow access from your device to your computer on both of these ports. 179 | 180 | Try opening a web browser on your phone and opening the URL that the packager script prints, replacing `exp://` with `http://`. So, for example, if underneath the QR code in your terminal you see: 181 | 182 | ``` 183 | exp://192.168.0.1:19000 184 | ``` 185 | 186 | Try opening Safari or Chrome on your phone and loading 187 | 188 | ``` 189 | http://192.168.0.1:19000 190 | ``` 191 | 192 | and 193 | 194 | ``` 195 | http://192.168.0.1:19001 196 | ``` 197 | 198 | If this works, but you're still unable to load your app by scanning the QR code, please open an issue on the [Create React Native App repository](https://github.com/react-community/create-react-native-app) with details about these steps and any other error messages you may have received. 199 | 200 | If you're not able to load the `http` URL in your phone's web browser, try using the tethering/mobile hotspot feature on your phone (beware of data usage, though), connecting your computer to that WiFi network, and restarting the packager. 201 | 202 | ### iOS Simulator won't open 203 | 204 | If you're on a Mac, there are a few errors that users sometimes see when attempting to `npm run ios`: 205 | 206 | * "non-zero exit code: 107" 207 | * "You may need to install Xcode" but it is already installed 208 | * and others 209 | 210 | There are a few steps you may want to take to troubleshoot these kinds of errors: 211 | 212 | 1. Make sure Xcode is installed and open it to accept the license agreement if it prompts you. You can install it from the Mac App Store. 213 | 2. Open Xcode's Preferences, the Locations tab, and make sure that the `Command Line Tools` menu option is set to something. Sometimes when the CLI tools are first installed by Homebrew this option is left blank, which can prevent Apple utilities from finding the simulator. Make sure to re-run `npm/yarn run ios` after doing so. 214 | 3. If that doesn't work, open the Simulator, and under the app menu select `Reset Contents and Settings...`. After that has finished, quit the Simulator, and re-run `npm/yarn run ios`. 215 | 216 | ### QR Code does not scan 217 | 218 | If you're not able to scan the QR code, make sure your phone's camera is focusing correctly, and also make sure that the contrast on the two colors in your terminal is high enough. For example, WebStorm's default themes may [not have enough contrast](https://github.com/react-community/create-react-native-app/issues/49) for terminal QR codes to be scannable with the system barcode scanners that the Expo app uses. 219 | 220 | If this causes problems for you, you may want to try changing your terminal's color theme to have more contrast, or running Create React Native App from a different terminal. You can also manually enter the URL printed by the packager script in the Expo app's search bar to load it manually. 221 | -------------------------------------------------------------------------------- /template/app.json: -------------------------------------------------------------------------------- 1 | { 2 | "expo": { 3 | "sdkVersion": "29.0.0" 4 | } 5 | } 6 | -------------------------------------------------------------------------------- /template/bsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "my-first-reason-app", 3 | "reason": { 4 | "react-jsx": 2 5 | }, 6 | "bsc-flags": ["-bs-super-errors"], 7 | "bs-dependencies": ["bs-react-native", "reason-react"], 8 | "sources": [ 9 | { 10 | "dir": "src" 11 | } 12 | ], 13 | "refmt": 3 14 | } 15 | -------------------------------------------------------------------------------- /template/gitignore: -------------------------------------------------------------------------------- 1 | # See https://help.github.com/ignore-files/ for more about ignoring files. 2 | 3 | # expo 4 | .expo/ 5 | 6 | # dependencies 7 | /node_modules 8 | 9 | # misc 10 | .env.local 11 | .env.development.local 12 | .env.test.local 13 | .env.production.local 14 | 15 | npm-debug.log* 16 | yarn-debug.log* 17 | yarn-error.log* 18 | -------------------------------------------------------------------------------- /template/src/App.re: -------------------------------------------------------------------------------- 1 | open BsReactNative; 2 | 3 | let app = () => 4 | 5 | 6 | ; 7 | --------------------------------------------------------------------------------