|
| 1 | +#!/usr/bin/env node |
| 2 | + |
| 3 | +// @flow |
| 4 | + |
| 5 | +// DON'T MODIFY THIS FILE |
| 6 | +// IF AT ALL POSSIBLE, MAKE ANY CHANGES IN THE SCRIPTS PACKAGE |
| 7 | + |
| 8 | +import fsp from 'fs-promise'; |
| 9 | +import chalk from 'chalk'; |
| 10 | +import minimist from 'minimist'; |
| 11 | +import path from 'path'; |
| 12 | +import pathExists from 'path-exists'; |
| 13 | +import semver from 'semver'; |
| 14 | +import spawn from 'cross-spawn'; |
| 15 | + |
| 16 | +const argv = minimist(process.argv.slice(2)); |
| 17 | + |
| 18 | +/** |
| 19 | + * Arguments: |
| 20 | + * --version - to print current version |
| 21 | + * --verbose - to print npm logs during init |
| 22 | + * --scripts-version <alternative package> |
| 23 | + * Example of valid values: |
| 24 | + * - a specific npm version: "0.22.0-rc1" |
| 25 | + * - a .tgz archive from npm: "https://registry.npmjs.org/react-native-scripts/-/react-native-scripts-0.20.0.tgz" |
| 26 | + * - a package from `tasks/clean_pack.sh`: "/home/adam/create-react-native-app/react-native-scripts-0.22.0.tgz" |
| 27 | + */ |
| 28 | +const commands = argv._; |
| 29 | +if (commands.length === 0) { |
| 30 | + if (argv.version) { |
| 31 | + const version = require('../package.json').version; |
| 32 | + console.log(`create-react-native-app version: ${version}`); |
| 33 | + process.exit(); |
| 34 | + } |
| 35 | + console.error( |
| 36 | + 'Usage: create-react-native-app <project-directory> [--verbose]' |
| 37 | + ); |
| 38 | + process.exit(1); |
| 39 | +} |
| 40 | + |
| 41 | +createApp(commands[0], !!argv.verbose, argv['scripts-version']).then(() => {}); |
| 42 | + |
| 43 | +async function createApp(name: string, verbose: boolean, version: ?string): Promise<void> { |
| 44 | + const root = path.resolve(name); |
| 45 | + const appName = path.basename(root); |
| 46 | + |
| 47 | + const packageToInstall = getInstallPackage(version); |
| 48 | + const packageName = getPackageName(packageToInstall); |
| 49 | + checkAppName(appName, packageName); |
| 50 | + |
| 51 | + if (!await pathExists(name)) { |
| 52 | + await fsp.mkdir(root); |
| 53 | + } else if (!await isSafeToCreateProjectIn(root)) { |
| 54 | + console.log(`The directory \`${name}\` contains file(s) that could conflict. Aborting.`); |
| 55 | + process.exit(1); |
| 56 | + } |
| 57 | + |
| 58 | + console.log(`Creating a new React Native app in ${root}.`); |
| 59 | + console.log(); |
| 60 | + |
| 61 | + const packageJson = { |
| 62 | + name: appName, |
| 63 | + version: '0.1.0', |
| 64 | + private: true, |
| 65 | + }; |
| 66 | + await fsp.writeFile( |
| 67 | + path.join(root, 'package.json'), |
| 68 | + JSON.stringify(packageJson, null, 2) |
| 69 | + ); |
| 70 | + process.chdir(root); |
| 71 | + |
| 72 | + console.log('Installing packages. This might take a couple minutes.'); |
| 73 | + console.log('Installing react-native-scripts...'); |
| 74 | + console.log(); |
| 75 | + |
| 76 | + await run(root, appName, version, verbose, packageToInstall, packageName); |
| 77 | +} |
| 78 | + |
| 79 | +function install(packageToInstall: string, verbose: boolean, |
| 80 | + callback: (code: number, command: string, args: Array<string>) => Promise<void> |
| 81 | + ): void { |
| 82 | + let args = [ |
| 83 | + 'add', |
| 84 | + '--dev', |
| 85 | + '--exact', |
| 86 | + packageToInstall, |
| 87 | + ]; |
| 88 | + const proc = spawn('yarnpkg', args, {stdio: 'inherit'}); |
| 89 | + |
| 90 | + let yarnExists = true; |
| 91 | + proc.on('error', function(err) { |
| 92 | + if (err.code === 'ENOENT') { |
| 93 | + yarnExists = false; |
| 94 | + } |
| 95 | + }); |
| 96 | + |
| 97 | + proc.on('close', function(code) { |
| 98 | + if (yarnExists) { |
| 99 | + callback(code, 'yarnpkg', args).then(() => {}, (e) => { throw e; }); |
| 100 | + return; |
| 101 | + } |
| 102 | + // No Yarn installed, continuing with npm. |
| 103 | + args = ['install']; |
| 104 | + |
| 105 | + if (verbose) { |
| 106 | + args.push('--verbose'); |
| 107 | + } |
| 108 | + |
| 109 | + args = args.concat([ |
| 110 | + '--save-dev', |
| 111 | + '--save-exact', |
| 112 | + packageToInstall, |
| 113 | + ]); |
| 114 | + |
| 115 | + const npmProc = spawn('npm', args, {stdio: 'inherit'}); |
| 116 | + npmProc.on('close', function(code) { |
| 117 | + callback(code, 'npm', args).then(() => {}, (e) => { throw e; });; |
| 118 | + }); |
| 119 | + }); |
| 120 | +} |
| 121 | + |
| 122 | +async function run(root: string, appName: string, version: ?string, verbose: boolean, |
| 123 | + packageToInstall: string, packageName: string): Promise<void> { |
| 124 | + |
| 125 | + install(packageToInstall, verbose, async (code: number, command: string, args: Array<string>) => { |
| 126 | + if (code !== 0) { |
| 127 | + console.error(`\`${command} ${args.join(' ')}\` failed`); |
| 128 | + return; |
| 129 | + } |
| 130 | + |
| 131 | + await checkNodeVersion(packageName); |
| 132 | + |
| 133 | + const scriptsPath = path.resolve( |
| 134 | + process.cwd(), |
| 135 | + 'node_modules', |
| 136 | + packageName, |
| 137 | + 'build', |
| 138 | + 'scripts', |
| 139 | + 'init.js' |
| 140 | + ); |
| 141 | + |
| 142 | + // $FlowFixMe (dikaiosune) maybe there's a way to convince flow this is legit? |
| 143 | + const init = require(scriptsPath); |
| 144 | + await init(root, appName, verbose); |
| 145 | + }); |
| 146 | +} |
| 147 | + |
| 148 | +function getInstallPackage(version: ?string): string { |
| 149 | + let packageToInstall = 'react-native-scripts'; |
| 150 | + const validSemver = semver.valid(version); |
| 151 | + if (validSemver) { |
| 152 | + packageToInstall += '@' + validSemver; |
| 153 | + } else if (version) { |
| 154 | + // for tar.gz or alternative paths |
| 155 | + packageToInstall = version; |
| 156 | + } |
| 157 | + return packageToInstall; |
| 158 | +} |
| 159 | + |
| 160 | +// Extract package name from tarball url or path. |
| 161 | +function getPackageName(installPackage: string): string { |
| 162 | + if (installPackage.indexOf('.tgz') > -1) { |
| 163 | + // The package name could be with or without semver version, e.g. react-scripts-0.2.0-alpha.1.tgz |
| 164 | + // However, this function returns package name only wihout semver version. |
| 165 | + const matches = installPackage.match(/^.+\/(.+?)(?:-\d+.+)?\.tgz$/); |
| 166 | + if (matches && matches.length >= 2) { |
| 167 | + return matches[1]; |
| 168 | + } else { |
| 169 | + throw new Error(`Provided scripts package (${installPackage}) doesn't have a valid filename.`); |
| 170 | + } |
| 171 | + } else if (installPackage.indexOf('@') > 0) { |
| 172 | + // Do not match @scope/ when stripping off @version or @tag |
| 173 | + return installPackage.charAt(0) + installPackage.substr(1).split('@')[0]; |
| 174 | + } |
| 175 | + return installPackage; |
| 176 | +} |
| 177 | + |
| 178 | +async function checkNodeVersion(packageName: string): Promise<void> { |
| 179 | + const packageJsonPath = path.resolve( |
| 180 | + process.cwd(), |
| 181 | + 'node_modules', |
| 182 | + packageName, |
| 183 | + 'package.json' |
| 184 | + ); |
| 185 | + |
| 186 | + const packageJson = JSON.parse(await fsp.readFile(packageJsonPath)); |
| 187 | + if (!packageJson.engines || !packageJson.engines.node) { |
| 188 | + return; |
| 189 | + } |
| 190 | + |
| 191 | + if (!semver.satisfies(process.version, packageJson.engines.node)) { |
| 192 | + console.error( |
| 193 | + chalk.red( |
| 194 | + 'You are currently running Node %s but create-react-native-app requires %s.' + |
| 195 | + ' Please use a supported version of Node.\n' |
| 196 | + ), |
| 197 | + process.version, |
| 198 | + packageJson.engines.node |
| 199 | + ); |
| 200 | + process.exit(1); |
| 201 | + } |
| 202 | +} |
| 203 | + |
| 204 | +function checkAppName(appName: string, packageName: string): void { |
| 205 | + const allDependencies = ['react-native-scripts', 'exponent', 'vector-icons', 'react', 'react-native']; |
| 206 | + |
| 207 | + if (allDependencies.indexOf(appName) >= 0) { |
| 208 | + console.error( |
| 209 | + chalk.red( |
| 210 | + 'We cannot create a project called `' + appName + '` because a dependency with the same name exists.\n' + |
| 211 | + 'Due to the way npm works, the following names are not allowed:\n\n' |
| 212 | + ) + |
| 213 | + chalk.cyan( |
| 214 | + allDependencies.map((depName) => { |
| 215 | + return ' ' + depName; |
| 216 | + }).join('\n') |
| 217 | + ) + |
| 218 | + chalk.red('\n\nPlease choose a different project name.') |
| 219 | + ); |
| 220 | + process.exit(1); |
| 221 | + } |
| 222 | +} |
| 223 | + |
| 224 | +// If project only contains files generated by GH, it’s safe |
| 225 | +async function isSafeToCreateProjectIn(root: string): Promise<boolean> { |
| 226 | + const validFiles = ['.DS_Store', 'Thumbs.db', '.git', '.gitignore', 'README.md', 'LICENSE']; |
| 227 | + return (await fsp.readdir(root)) |
| 228 | + .every((file) => { |
| 229 | + return validFiles.indexOf(file) >= 0; |
| 230 | + }); |
| 231 | +} |
0 commit comments