From ca403f0a5f6723b6aae8ea0c131e45f689ea017e Mon Sep 17 00:00:00 2001 From: Martin Konicek Date: Fri, 2 Dec 2016 00:04:37 -0800 Subject: [PATCH] Use Yarn if available Summary: **Motivation** If the project is using yarn to manage its dependencies, we should be running 'yarn add' to upgrade the React Native dependency, rather than 'npm install'. **Test plan (required)** Running in a project that's in a bad state: Error: react-native version in "package.json" (0.29.2) doesn't match the installed version in "node_modules" (0.38.0). Try running "yarn" to fix this. Removed yarn.lock file, ran again: Error: react-native version in "package.json" (0.29.2) doesn't match the installed version in "node_modules" (0.38.0). Try running "npm install" to fix this. Running inside a folder that doesn't contain a `package.json` file: Error: Unable to find "/Users/mkonicek/Zero29App/package.json" or "/Users/mkonicek/Zero29App/node_modules/react-native/package.json". Make sure you ran "yarn" and that you are inside a React Native project. Removed yarn.lock file, ran again: Error: Unable to find "/Users/mkonicek/Zero29App/package.json" or "/Users/ Closes https://github.com/facebook/react-native/pull/11225 Differential Revision: D4261102 Pulled By: mkonicek fbshipit-source-id: b44ae91fe46f2c81b14616ca2868cc171692c895 --- react-native-git-upgrade/checks.js | 6 +- react-native-git-upgrade/cliEntry.js | 95 +++++++++++++++++----------- react-native-git-upgrade/index.js | 3 +- react-native-git-upgrade/yarn.js | 58 +++++++++++++++++ 4 files changed, 121 insertions(+), 41 deletions(-) create mode 100644 react-native-git-upgrade/yarn.js diff --git a/react-native-git-upgrade/checks.js b/react-native-git-upgrade/checks.js index 2d89f84a0cddd7..181ddb0e2c882f 100644 --- a/react-native-git-upgrade/checks.js +++ b/react-native-git-upgrade/checks.js @@ -19,12 +19,14 @@ function checkDeclaredVersion(declaredVersion) { } } -function checkMatchingVersions(currentVersion, declaredVersion) { +function checkMatchingVersions(currentVersion, declaredVersion, useYarn) { if (!semver.satisfies(currentVersion, declaredVersion)) { throw new Error( 'react-native version in "package.json" (' + declaredVersion + ') doesn\'t match ' + 'the installed version in "node_modules" (' + currentVersion + ').\n' + - 'Try running "npm install" to fix this.' + (useYarn ? + 'Try running "yarn" to fix this.' : + 'Try running "npm install" to fix this.') ); } } diff --git a/react-native-git-upgrade/cliEntry.js b/react-native-git-upgrade/cliEntry.js index 4d8f7bd1b97779..73567aad54c7b0 100644 --- a/react-native-git-upgrade/cliEntry.js +++ b/react-native-git-upgrade/cliEntry.js @@ -18,6 +18,7 @@ const TerminalAdapter = require('yeoman-environment/lib/adapter'); const log = require('npmlog'); const rimraf = require('rimraf'); const semver = require('semver'); +const yarn = require('./yarn'); const { checkDeclaredVersion, @@ -31,8 +32,7 @@ log.heading = 'git-upgrade'; /** * Promisify the callback-based shelljs function exec - * @param command - * @param logOutput + * @param logOutput If true, log the stdout of the command. * @returns {Promise} */ function exec(command, logOutput) { @@ -62,42 +62,37 @@ stdout: ${stdout}`)); }) } -/** -+ * Returns two objects: -+ * - Parsed node_modules/react-native/package.json -+ * - Parsed package.json -+ */ -function readPackageFiles() { +function parseJsonFile(path, useYarn) { + const installHint = useYarn ? + 'Make sure you ran "yarn" and that you are inside a React Native project.' : + 'Make sure you ran "npm install" and that you are inside a React Native project.'; + let fileContents; + try { + fileContents = fs.readFileSync(path, 'utf8'); + } catch (err) { + throw new Error('Cannot find "' + path + '". ' + installHint); + } + try { + return JSON.parse(fileContents); + } catch (err) { + throw new Error('Cannot parse "' + path + '": ' + err.message); + } +} + +function readPackageFiles(useYarn) { const reactNativeNodeModulesPakPath = path.resolve( - process.cwd(), - 'node_modules', - 'react-native', - 'package.json' + process.cwd(), 'node_modules', 'react-native', 'package.json' ); - const reactNodeModulesPakPath = path.resolve( - process.cwd(), - 'node_modules', - 'react', - 'package.json' + process.cwd(), 'node_modules', 'react', 'package.json' ); - const pakPath = path.resolve( - process.cwd(), - 'package.json' + process.cwd(), 'package.json' ); - - try { - const reactNativeNodeModulesPak = JSON.parse(fs.readFileSync(reactNativeNodeModulesPakPath, 'utf8')); - const reactNodeModulesPak = JSON.parse(fs.readFileSync(reactNodeModulesPakPath, 'utf8')); - const pak = JSON.parse(fs.readFileSync(pakPath, 'utf8')); - - return {reactNativeNodeModulesPak, reactNodeModulesPak, pak}; - } catch (err) { - throw new Error( - 'Unable to find one of "' + pakPath + '", "' + rnPakPath + '" or "' + reactPakPath + '". ' + - 'Make sure you ran "npm install" and that you are inside a React Native project.' - ) + return { + reactNativeNodeModulesPak: parseJsonFile(reactNativeNodeModulesPakPath), + reactNodeModulesPak: parseJsonFile(reactNodeModulesPakPath), + pak: parseJsonFile(pakPath) } } @@ -191,6 +186,21 @@ async function checkForUpdates() { } } +/** + * If true, use yarn instead of the npm client to upgrade the project. + */ +function shouldUseYarn(cliArgs, projectDir) { + if (cliArgs && cliArgs.npm) { + return false; + } + const yarnVersion = yarn.getYarnVersionIfAvailable(); + if (yarnVersion && yarn.isProjectUsingYarn(projectDir)) { + log.info('Using yarn ' + yarnVersion); + return true; + } + return false; +} + /** * @param requestedVersion The version argument, e.g. 'react-native-git-upgrade 0.38'. * `undefined` if no argument passed. @@ -204,8 +214,10 @@ async function run(requestedVersion, cliArgs) { try { await checkForUpdates(); + const useYarn = shouldUseYarn(cliArgs, path.resolve(process.cwd())); + log.info('Read package.json files'); - const {reactNativeNodeModulesPak, reactNodeModulesPak, pak} = readPackageFiles(); + const {reactNativeNodeModulesPak, reactNodeModulesPak, pak} = readPackageFiles(useYarn); const appName = pak.name; const currentVersion = reactNativeNodeModulesPak.version; const currentReactVersion = reactNodeModulesPak.version; @@ -218,7 +230,7 @@ async function run(requestedVersion, cliArgs) { checkDeclaredVersion(declaredVersion); log.info('Check matching versions'); - checkMatchingVersions(currentVersion, declaredVersion); + checkMatchingVersions(currentVersion, declaredVersion, useYarn); log.info('Check React peer dependency'); checkReactPeerDependency(currentVersion, declaredReactVersion); @@ -231,6 +243,8 @@ async function run(requestedVersion, cliArgs) { const viewOutput = await exec(viewCommand, verbose).then(JSON.parse); const newVersion = viewOutput.version; const newReactVersionRange = viewOutput['peerDependencies.react']; + // Print which versions we're upgrading to + log.info('Upgrading to React Native ' + newVersion + (newReactVersionRange ? ', React ' + newReactVersionRange : '')); log.info('Check new version'); checkNewVersionValid(newVersion, requestedVersion); @@ -264,9 +278,14 @@ async function run(requestedVersion, cliArgs) { await exec('git commit -m "Old version" --allow-empty', verbose); log.info('Install the new version'); - let installCommand = 'npm install --save --color=always'; + let installCommand; + if (useYarn) { + installCommand = 'yarn add'; + } else { + installCommand = 'npm install --save --color=always'; + } installCommand += ' react-native@' + newVersion; - if (!semver.satisfies(currentReactVersion, newReactVersionRange)) { + if (newReactVersionRange && !semver.satisfies(currentReactVersion, newReactVersionRange)) { // Install React as well to avoid unmet peer dependency installCommand += ' react@' + newReactVersionRange; } @@ -296,8 +315,8 @@ async function run(requestedVersion, cliArgs) { await exec(`git apply --3way ${patchPath}`, true); } catch (err) { log.warn( - 'The upgrade process succeeded but there might be conflicts to be resolved.\n' + - 'See above for the list of files that had merge conflicts.'); + 'The upgrade process succeeded but there might be conflicts to be resolved. ' + + 'See above for the list of files that have merge conflicts.'); } finally { log.info('Upgrade done'); if (cliArgs.verbose) { diff --git a/react-native-git-upgrade/index.js b/react-native-git-upgrade/index.js index f8e1294869fd2f..c676a7a65ee93b 100644 --- a/react-native-git-upgrade/index.js +++ b/react-native-git-upgrade/index.js @@ -27,7 +27,8 @@ if (argv._.length === 0 && (argv.h || argv.help)) { '', ' -h, --help output usage information', ' -v, --version output the version number', - ' --verbose output', + ' --verbose output debugging info', + ' --npm force using the npm client even if your project uses yarn', '', ].join('\n')); process.exit(0); diff --git a/react-native-git-upgrade/yarn.js b/react-native-git-upgrade/yarn.js new file mode 100644 index 00000000000000..41db31ab38e798 --- /dev/null +++ b/react-native-git-upgrade/yarn.js @@ -0,0 +1,58 @@ +/** + * Copyright (c) 2015-present, Facebook, Inc. + * All rights reserved. + * + * This source code is licensed under the BSD-style license found in the + * LICENSE file in the root directory of this source tree. An additional grant + * of patent rights can be found in the PATENTS file in the same directory. + */ +'use strict'; + +const fs = require('fs'); +const path = require('path'); +const semver = require('semver'); + +/** + * Use Yarn if available, it's much faster than the npm client. + * Return the version of yarn installed on the system, null if yarn is not available. + */ +function getYarnVersionIfAvailable() { + let yarnVersion; + try { + // execSync returns a Buffer -> convert to string + if (process.platform.startsWith('win')) { + yarnVersion = (execSync('yarn --version').toString() || '').trim(); + } else { + yarnVersion = (execSync('yarn --version 2>/dev/null').toString() || '').trim(); + } + } catch (error) { + return null; + } + // yarn < 0.16 has a 'missing manifest' bug + try { + if (semver.gte(yarnVersion, '0.16.0')) { + return yarnVersion; + } else { + return null; + } + } catch (error) { + console.error('Cannot parse yarn version: ' + yarnVersion); + return null; + } +} + +/** + * Check that 'react-native init' itself used yarn to install React Native. + * When using an old global react-native-cli@1.0.0 (or older), we don't want + * to install React Native with npm, and React + Jest with yarn. + * Let's be safe and not mix yarn and npm in a single project. + * @param projectDir e.g. /Users/martin/AwesomeApp + */ +function isProjectUsingYarn(projectDir) { + return fs.existsSync(path.join(projectDir, 'yarn.lock')); +} + +module.exports = { + getYarnVersionIfAvailable: getYarnVersionIfAvailable, + isProjectUsingYarn: isProjectUsingYarn, +};