From 6477fc481b1451e1f4ceb3dbbe698fe2d561467e Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sat, 16 Feb 2019 22:18:23 +0100 Subject: [PATCH 01/12] feat: rewrite upgrade to use rn-diff-purge --- packages/cli/package.json | 1 + packages/cli/src/cliEntry.js | 1 + packages/cli/src/upgrade/upgrade.js | 274 +++++++++++++++++----------- yarn.lock | 1 + 4 files changed, 172 insertions(+), 105 deletions(-) diff --git a/packages/cli/package.json b/packages/cli/package.json index 3aeb28ab6..703668a96 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -27,6 +27,7 @@ "envinfo": "^5.7.0", "errorhandler": "^1.5.0", "escape-string-regexp": "^1.0.5", + "execa": "^1.0.0", "fs-extra": "^1.0.0", "glob": "^7.1.1", "graceful-fs": "^4.1.3", diff --git a/packages/cli/src/cliEntry.js b/packages/cli/src/cliEntry.js index 072c0e9c2..c2b083844 100644 --- a/packages/cli/src/cliEntry.js +++ b/packages/cli/src/cliEntry.js @@ -34,6 +34,7 @@ const defaultOptParser = val => val; const handleError = err => { logger.error(err.message); + console.log('\n', err); process.exit(1); }; diff --git a/packages/cli/src/upgrade/upgrade.js b/packages/cli/src/upgrade/upgrade.js index cd601b784..6c83e8625 100644 --- a/packages/cli/src/upgrade/upgrade.js +++ b/packages/cli/src/upgrade/upgrade.js @@ -4,151 +4,215 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * - * @format * @flow */ -import fs from 'fs'; +import https from 'https'; import path from 'path'; +import fs from 'fs'; import semver from 'semver'; +import execa from 'execa'; import type { ContextT } from '../core/types.flow'; import logger from '../util/logger'; -import copyProjectTemplateAndReplace from '../generator/copyProjectTemplateAndReplace'; +import PackageManager from '../util/PackageManager'; + +const fetch = (url: string) => + new Promise((resolve, reject) => { + const request = https.get(url, response => { + if (response.statusCode < 200 || response.statusCode > 299) { + reject( + new Error(`Failed to load page, status code: ${response.statusCode}`) + ); + } + const body = []; + response.on('data', chunk => body.push(chunk)); + response.on('end', () => resolve(body.join(''))); + }); + request.on('error', err => reject(err)); + }); + +const getLatestRNVersion = async (): Promise => { + logger.info('No version passed. Fetching latest...'); + const { stdout } = await execa('npm', ['info', 'react-native', 'version']); + return stdout; +}; -/** - * Migrate application to a new version of React Native. - * See http://facebook.github.io/react-native/docs/upgrading.html - */ -function validateAndUpgrade(argv: Array, ctx: ContextT) { - const projectDir = ctx.root; +const getRNPeerDeps = async ( + version: string +): Promise<{ [key: string]: string }> => { + const { stdout } = await execa('npm', [ + 'info', + `react-native@${version}`, + 'peerDependencies', + '--json', + ]); + + return JSON.parse(stdout); +}; - const packageJSON = JSON.parse( - fs.readFileSync(path.resolve(projectDir, 'package.json'), 'utf8') - ); +const getPatch = async (currentVersion, newVersion, projectDir) => { + let patch; + const rnDiffPurgeUrl = 'https://github.com/pvinis/rn-diff-purge'; + const rnDiffAppName = 'RnDiffApp'; + const { name } = require(path.join(projectDir, 'package.json')); - warn( - 'You should consider using the new upgrade tool based on Git. It ' + - 'makes upgrades easier by resolving most conflicts automatically.\n' + - 'To use it:\n' + - '- Go back to the old version of React Native\n' + - '- Run "npm install -g react-native-git-upgrade"\n' + - '- Run "react-native-git-upgrade"\n' + - 'See https://facebook.github.io/react-native/docs/upgrading.html' - ); + logger.info(`Fetching diff between v${currentVersion} and v${newVersion}...`); - const projectName = packageJSON.name; - if (!projectName) { - warn( - 'Your project needs to have a name, declared in package.json, ' + - 'such as "name": "AwesomeApp". Please add a project name. Aborting.' + try { + patch = await fetch( + `${rnDiffPurgeUrl}/compare/version/${currentVersion}...version/${newVersion}.diff` ); - return; - } - - const version = packageJSON.dependencies['react-native']; - if (!version) { - warn( - 'Your "package.json" file doesn\'t seem to declare "react-native" as ' + - 'a dependency. Nothing to upgrade. Aborting.' + } catch (error) { + logger.error( + `Failed to fetch diff for react-native@${newVersion}. Maybe it's not released yet?` ); - return; - } - - if (version === 'latest' || version === '*') { - warn( - 'Some major releases introduce breaking changes.\n' + - 'Please use a caret version number in your "package.json" file \n' + - 'to avoid breakage. Use e.g. react-native: ^0.38.0. Aborting.' + logger.info( + `For available releases to diff see: https://github.com/pvinis/rn-diff-purge#version-changes` ); - return; + return null; } - const installed = JSON.parse( - fs.readFileSync( - path.resolve(projectDir, 'node_modules/react-native/package.json'), - 'utf8' - ) - ); - - if (!semver.satisfies(installed.version, version)) { - warn( - 'react-native version in "package.json" doesn\'t match ' + - 'the installed version in "node_modules".\n' + - 'Try running "npm install" to fix this. Aborting.' - ); - return; - } + return patch + .replace(new RegExp(rnDiffAppName, 'g'), name) + .replace(new RegExp(rnDiffAppName.toLowerCase(), 'g'), name.toLowerCase()); +}; - const v = version.replace(/^(~|\^|=)/, '').replace(/x/i, '0'); +const getVersionToUpgradeTo = async (argv, currentVersion, projectDir) => { + const newVersion = argv[0] + ? semver.valid(argv[0]) || semver.coerce(argv[0]).version + : await getLatestRNVersion(); - if (!semver.valid(v)) { - warn( - "A valid version number for 'react-native' is not specified in your " + - "'package.json' file. Aborting." + if (!newVersion) { + logger.error( + `Provided version "${newVersion}" is not allowed. Please pass a valid semver version` ); - return; + return null; } - logger.info( - `Upgrading project to react-native v${installed.version}\n` + - `Check out the release notes and breaking changes: ` + - `https://github.com/facebook/react-native/releases/tag/v${semver.major( - v - )}.${semver.minor(v)}.0` - ); - - // >= v0.21.0, we require react to be a peer dependency - if (semver.gte(v, '0.21.0') && !packageJSON.dependencies.react) { - warn( - 'Your "package.json" file doesn\'t seem to have "react" as a dependency.\n' + - '"react" was changed from a dependency to a peer dependency in react-native v0.21.0.\n' + - 'Therefore, it\'s necessary to include "react" in your project\'s dependencies.\n' + - 'Please run "npm install --save react", then re-run "react-native upgrade".\n' + if (currentVersion > newVersion) { + logger.error( + `Trying to upgrade from newer version "${currentVersion}" to older "${newVersion}"` ); - return; + return null; } - if (semver.satisfies(v, '~0.26.0')) { - warn( - 'React Native 0.26 introduced some breaking changes to the native files on iOS. You can\n' + - 'perform them manually by checking the release notes or use "rnpm" ' + - 'to do it automatically.\n' + - 'Just run:\n' + - '"npm install -g rnpm && npm install rnpm-plugin-upgrade@0.26 --save-dev", ' + - 'then run "rnpm upgrade".' + if (currentVersion === newVersion) { + const { + dependencies: { 'react-native': version }, + } = require(path.join(projectDir, 'package.json')); + if (semver.satisfies(newVersion, version)) { + logger.warn( + `Specified version "${newVersion}" is already installed in node_modules and it satisfies "${version}" semver range. No need to upgrade` + ); + return null; + } + logger.error( + `Dependency mismatch. Specified version "${newVersion}" is already installed in node_modules and it doesn't satisfy "${version}" semver range of your "react-native" dependency. Please re-install your dependencies` ); + return null; } - upgradeProjectFiles(projectDir, projectName); + return newVersion; +}; +const installDeps = async (newVersion, projectDir) => { logger.info( - `Successfully upgraded this project to react-native v${installed.version}` + `Installing react-native@${newVersion} and its peer dependencies...` ); -} + const peerDeps = await getRNPeerDeps(newVersion); + const pm = new PackageManager({ projectDir }); + const deps = [ + `react-native@${newVersion}`, + ...Object.entries(peerDeps).map( + // $FlowFixMe - Object.entries type definition is poor + ([module, version]) => `${module}@${version}` + ), + ]; + pm.install(deps); +}; /** - * Once all checks passed, upgrade the project files. + * Upgrade application to a new version of React Native. */ -function upgradeProjectFiles(projectDir, projectName) { - // Just overwrite - copyProjectTemplateAndReplace( - path.dirname(require.resolve('react-native/template')), +async function upgrade(argv: Array, ctx: ContextT) { + const rnDiffGitAddress = `https://github.com/pvinis/rn-diff-purge.git`; + const tmpRemote = 'tmp-rn-diff-purge'; + const tmpPatchFile = 'tmp-upgrade-rn.patch'; + const projectDir = ctx.root; + const { version: currentVersion } = require(path.join( projectDir, - projectName, - { upgrade: true } + 'node_modules/react-native/package.json' + )); + + const newVersion = await getVersionToUpgradeTo( + argv, + currentVersion, + projectDir ); -} -function warn(message) { - logger.warn(message); + if (!newVersion) { + return; + } + + const patch = await getPatch(currentVersion, newVersion, projectDir); + + if (patch === null) { + return; + } + + if (patch === '') { + // Yay, nothing to diff! + await installDeps(newVersion, projectDir); + logger.success( + `Upgraded React Native to v${newVersion} 🎉. Now you can review and commit the changes` + ); + return; + } + + try { + fs.writeFileSync(tmpPatchFile, patch); + await execa('git', ['remote', 'add', tmpRemote, rnDiffGitAddress]); + await execa('git', ['fetch', tmpRemote]); + + try { + logger.info('Applying diff...'); + await execa( + 'git', + ['apply', tmpPatchFile, '--exclude=package.json', '-p2', '--3way'], + { stdio: 'inherit' } + ); + } catch (error) { + logger.error( + `Applying diff failed. Please review the conflicts and resolve them.` + ); + logger.info( + `You may find release notes helpful: https://github.com/facebook/react-native/releases/tag/v${newVersion}` + ); + return; + } + + await installDeps(newVersion, projectDir); + } catch (error) { + throw new Error(error.stderr || error); + } finally { + try { + fs.unlinkSync(tmpPatchFile); + } catch (e) { + // ignore + } + await execa('git', ['remote', 'remove', tmpRemote]); + } + + logger.success( + `Upgraded React Native to v${newVersion} 🎉. Now you can review and commit the changes` + ); } const upgradeCommand = { - name: 'upgrade', + name: 'upgrade [version]', description: - "upgrade your app's template files to the latest version; run this after " + - 'updating the react-native version in your package.json and running npm install', - func: validateAndUpgrade, + "Upgrade your app's template files to the specified or latest npm version using `rn-diff-purge` project. Only valid semver versions are allowed.", + func: upgrade, }; export default upgradeCommand; diff --git a/yarn.lock b/yarn.lock index c6132c147..01cf937f7 100644 --- a/yarn.lock +++ b/yarn.lock @@ -3293,6 +3293,7 @@ execa@^0.7.0: execa@^1.0.0: version "1.0.0" resolved "https://registry.yarnpkg.com/execa/-/execa-1.0.0.tgz#c6236a5bb4df6d6f15e88e7f017798216749ddd8" + integrity sha512-adbxcyWV46qiHyvSp50TKt05tB4tK3HcmF7/nxfAdhnox83seTDbwnaqKO4sXRy7roHAIFqJP/Rw/AuEbX61LA== dependencies: cross-spawn "^6.0.0" get-stream "^4.0.0" From 5cc7abf30e4b7ce750e7cf800d34335fdbeabf76 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sat, 16 Feb 2019 23:00:49 +0100 Subject: [PATCH 02/12] fix: use Object.keys instead of Object.entries --- packages/cli/src/upgrade/upgrade.js | 5 +---- 1 file changed, 1 insertion(+), 4 deletions(-) diff --git a/packages/cli/src/upgrade/upgrade.js b/packages/cli/src/upgrade/upgrade.js index 6c83e8625..06ab2b23b 100644 --- a/packages/cli/src/upgrade/upgrade.js +++ b/packages/cli/src/upgrade/upgrade.js @@ -123,10 +123,7 @@ const installDeps = async (newVersion, projectDir) => { const pm = new PackageManager({ projectDir }); const deps = [ `react-native@${newVersion}`, - ...Object.entries(peerDeps).map( - // $FlowFixMe - Object.entries type definition is poor - ([module, version]) => `${module}@${version}` - ), + ...Object.keys(peerDeps).map(module => `${module}@${peerDeps[module]}`), ]; pm.install(deps); }; From 1d4122594e28dbd53623f28092549dd1ccbe710b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sat, 16 Feb 2019 23:15:26 +0100 Subject: [PATCH 03/12] chore: move new upgrade behind --improved flag --- packages/cli/src/cliEntry.js | 1 - packages/cli/src/upgrade/newUpgrade.js | 215 +++++++++++++++++++ packages/cli/src/upgrade/upgrade.js | 281 +++++++++++-------------- 3 files changed, 333 insertions(+), 164 deletions(-) create mode 100644 packages/cli/src/upgrade/newUpgrade.js diff --git a/packages/cli/src/cliEntry.js b/packages/cli/src/cliEntry.js index c2b083844..072c0e9c2 100644 --- a/packages/cli/src/cliEntry.js +++ b/packages/cli/src/cliEntry.js @@ -34,7 +34,6 @@ const defaultOptParser = val => val; const handleError = err => { logger.error(err.message); - console.log('\n', err); process.exit(1); }; diff --git a/packages/cli/src/upgrade/newUpgrade.js b/packages/cli/src/upgrade/newUpgrade.js new file mode 100644 index 000000000..06ab2b23b --- /dev/null +++ b/packages/cli/src/upgrade/newUpgrade.js @@ -0,0 +1,215 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @flow + */ + +import https from 'https'; +import path from 'path'; +import fs from 'fs'; +import semver from 'semver'; +import execa from 'execa'; +import type { ContextT } from '../core/types.flow'; +import logger from '../util/logger'; +import PackageManager from '../util/PackageManager'; + +const fetch = (url: string) => + new Promise((resolve, reject) => { + const request = https.get(url, response => { + if (response.statusCode < 200 || response.statusCode > 299) { + reject( + new Error(`Failed to load page, status code: ${response.statusCode}`) + ); + } + const body = []; + response.on('data', chunk => body.push(chunk)); + response.on('end', () => resolve(body.join(''))); + }); + request.on('error', err => reject(err)); + }); + +const getLatestRNVersion = async (): Promise => { + logger.info('No version passed. Fetching latest...'); + const { stdout } = await execa('npm', ['info', 'react-native', 'version']); + return stdout; +}; + +const getRNPeerDeps = async ( + version: string +): Promise<{ [key: string]: string }> => { + const { stdout } = await execa('npm', [ + 'info', + `react-native@${version}`, + 'peerDependencies', + '--json', + ]); + + return JSON.parse(stdout); +}; + +const getPatch = async (currentVersion, newVersion, projectDir) => { + let patch; + const rnDiffPurgeUrl = 'https://github.com/pvinis/rn-diff-purge'; + const rnDiffAppName = 'RnDiffApp'; + const { name } = require(path.join(projectDir, 'package.json')); + + logger.info(`Fetching diff between v${currentVersion} and v${newVersion}...`); + + try { + patch = await fetch( + `${rnDiffPurgeUrl}/compare/version/${currentVersion}...version/${newVersion}.diff` + ); + } catch (error) { + logger.error( + `Failed to fetch diff for react-native@${newVersion}. Maybe it's not released yet?` + ); + logger.info( + `For available releases to diff see: https://github.com/pvinis/rn-diff-purge#version-changes` + ); + return null; + } + + return patch + .replace(new RegExp(rnDiffAppName, 'g'), name) + .replace(new RegExp(rnDiffAppName.toLowerCase(), 'g'), name.toLowerCase()); +}; + +const getVersionToUpgradeTo = async (argv, currentVersion, projectDir) => { + const newVersion = argv[0] + ? semver.valid(argv[0]) || semver.coerce(argv[0]).version + : await getLatestRNVersion(); + + if (!newVersion) { + logger.error( + `Provided version "${newVersion}" is not allowed. Please pass a valid semver version` + ); + return null; + } + + if (currentVersion > newVersion) { + logger.error( + `Trying to upgrade from newer version "${currentVersion}" to older "${newVersion}"` + ); + return null; + } + + if (currentVersion === newVersion) { + const { + dependencies: { 'react-native': version }, + } = require(path.join(projectDir, 'package.json')); + if (semver.satisfies(newVersion, version)) { + logger.warn( + `Specified version "${newVersion}" is already installed in node_modules and it satisfies "${version}" semver range. No need to upgrade` + ); + return null; + } + logger.error( + `Dependency mismatch. Specified version "${newVersion}" is already installed in node_modules and it doesn't satisfy "${version}" semver range of your "react-native" dependency. Please re-install your dependencies` + ); + return null; + } + + return newVersion; +}; + +const installDeps = async (newVersion, projectDir) => { + logger.info( + `Installing react-native@${newVersion} and its peer dependencies...` + ); + const peerDeps = await getRNPeerDeps(newVersion); + const pm = new PackageManager({ projectDir }); + const deps = [ + `react-native@${newVersion}`, + ...Object.keys(peerDeps).map(module => `${module}@${peerDeps[module]}`), + ]; + pm.install(deps); +}; + +/** + * Upgrade application to a new version of React Native. + */ +async function upgrade(argv: Array, ctx: ContextT) { + const rnDiffGitAddress = `https://github.com/pvinis/rn-diff-purge.git`; + const tmpRemote = 'tmp-rn-diff-purge'; + const tmpPatchFile = 'tmp-upgrade-rn.patch'; + const projectDir = ctx.root; + const { version: currentVersion } = require(path.join( + projectDir, + 'node_modules/react-native/package.json' + )); + + const newVersion = await getVersionToUpgradeTo( + argv, + currentVersion, + projectDir + ); + + if (!newVersion) { + return; + } + + const patch = await getPatch(currentVersion, newVersion, projectDir); + + if (patch === null) { + return; + } + + if (patch === '') { + // Yay, nothing to diff! + await installDeps(newVersion, projectDir); + logger.success( + `Upgraded React Native to v${newVersion} 🎉. Now you can review and commit the changes` + ); + return; + } + + try { + fs.writeFileSync(tmpPatchFile, patch); + await execa('git', ['remote', 'add', tmpRemote, rnDiffGitAddress]); + await execa('git', ['fetch', tmpRemote]); + + try { + logger.info('Applying diff...'); + await execa( + 'git', + ['apply', tmpPatchFile, '--exclude=package.json', '-p2', '--3way'], + { stdio: 'inherit' } + ); + } catch (error) { + logger.error( + `Applying diff failed. Please review the conflicts and resolve them.` + ); + logger.info( + `You may find release notes helpful: https://github.com/facebook/react-native/releases/tag/v${newVersion}` + ); + return; + } + + await installDeps(newVersion, projectDir); + } catch (error) { + throw new Error(error.stderr || error); + } finally { + try { + fs.unlinkSync(tmpPatchFile); + } catch (e) { + // ignore + } + await execa('git', ['remote', 'remove', tmpRemote]); + } + + logger.success( + `Upgraded React Native to v${newVersion} 🎉. Now you can review and commit the changes` + ); +} + +const upgradeCommand = { + name: 'upgrade [version]', + description: + "Upgrade your app's template files to the specified or latest npm version using `rn-diff-purge` project. Only valid semver versions are allowed.", + func: upgrade, +}; + +export default upgradeCommand; diff --git a/packages/cli/src/upgrade/upgrade.js b/packages/cli/src/upgrade/upgrade.js index 06ab2b23b..9e1d8b764 100644 --- a/packages/cli/src/upgrade/upgrade.js +++ b/packages/cli/src/upgrade/upgrade.js @@ -4,212 +4,167 @@ * This source code is licensed under the MIT license found in the * LICENSE file in the root directory of this source tree. * + * @format * @flow */ +/* eslint-disable consistent-return */ -import https from 'https'; -import path from 'path'; import fs from 'fs'; +import path from 'path'; import semver from 'semver'; -import execa from 'execa'; import type { ContextT } from '../core/types.flow'; import logger from '../util/logger'; -import PackageManager from '../util/PackageManager'; - -const fetch = (url: string) => - new Promise((resolve, reject) => { - const request = https.get(url, response => { - if (response.statusCode < 200 || response.statusCode > 299) { - reject( - new Error(`Failed to load page, status code: ${response.statusCode}`) - ); - } - const body = []; - response.on('data', chunk => body.push(chunk)); - response.on('end', () => resolve(body.join(''))); - }); - request.on('error', err => reject(err)); - }); - -const getLatestRNVersion = async (): Promise => { - logger.info('No version passed. Fetching latest...'); - const { stdout } = await execa('npm', ['info', 'react-native', 'version']); - return stdout; -}; +import copyProjectTemplateAndReplace from '../generator/copyProjectTemplateAndReplace'; +import newUpgrade from './newUpgrade'; -const getRNPeerDeps = async ( - version: string -): Promise<{ [key: string]: string }> => { - const { stdout } = await execa('npm', [ - 'info', - `react-native@${version}`, - 'peerDependencies', - '--json', - ]); - - return JSON.parse(stdout); +type FlagsT = { + improved: boolean, }; -const getPatch = async (currentVersion, newVersion, projectDir) => { - let patch; - const rnDiffPurgeUrl = 'https://github.com/pvinis/rn-diff-purge'; - const rnDiffAppName = 'RnDiffApp'; - const { name } = require(path.join(projectDir, 'package.json')); - - logger.info(`Fetching diff between v${currentVersion} and v${newVersion}...`); - - try { - patch = await fetch( - `${rnDiffPurgeUrl}/compare/version/${currentVersion}...version/${newVersion}.diff` - ); - } catch (error) { - logger.error( - `Failed to fetch diff for react-native@${newVersion}. Maybe it's not released yet?` - ); - logger.info( - `For available releases to diff see: https://github.com/pvinis/rn-diff-purge#version-changes` - ); - return null; +/** + * Migrate application to a new version of React Native. + * See http://facebook.github.io/react-native/docs/upgrading.html + */ +function validateAndUpgrade(argv: Array, ctx: ContextT, args: FlagsT) { + if (args.improved) { + return newUpgrade.func(argv, ctx); } + const projectDir = ctx.root; - return patch - .replace(new RegExp(rnDiffAppName, 'g'), name) - .replace(new RegExp(rnDiffAppName.toLowerCase(), 'g'), name.toLowerCase()); -}; + const packageJSON = JSON.parse( + fs.readFileSync(path.resolve(projectDir, 'package.json'), 'utf8') + ); -const getVersionToUpgradeTo = async (argv, currentVersion, projectDir) => { - const newVersion = argv[0] - ? semver.valid(argv[0]) || semver.coerce(argv[0]).version - : await getLatestRNVersion(); + warn( + 'You should consider using the new upgrade tool based on Git. It ' + + 'makes upgrades easier by resolving most conflicts automatically.\n' + + 'To use it:\n' + + '- Go back to the old version of React Native\n' + + '- Run "npm install -g react-native-git-upgrade"\n' + + '- Run "react-native-git-upgrade"\n' + + 'See https://facebook.github.io/react-native/docs/upgrading.html' + ); - if (!newVersion) { - logger.error( - `Provided version "${newVersion}" is not allowed. Please pass a valid semver version` + const projectName = packageJSON.name; + if (!projectName) { + warn( + 'Your project needs to have a name, declared in package.json, ' + + 'such as "name": "AwesomeApp". Please add a project name. Aborting.' ); - return null; + return; } - if (currentVersion > newVersion) { - logger.error( - `Trying to upgrade from newer version "${currentVersion}" to older "${newVersion}"` + const version = packageJSON.dependencies['react-native']; + if (!version) { + warn( + 'Your "package.json" file doesn\'t seem to declare "react-native" as ' + + 'a dependency. Nothing to upgrade. Aborting.' ); - return null; + return; } - if (currentVersion === newVersion) { - const { - dependencies: { 'react-native': version }, - } = require(path.join(projectDir, 'package.json')); - if (semver.satisfies(newVersion, version)) { - logger.warn( - `Specified version "${newVersion}" is already installed in node_modules and it satisfies "${version}" semver range. No need to upgrade` - ); - return null; - } - logger.error( - `Dependency mismatch. Specified version "${newVersion}" is already installed in node_modules and it doesn't satisfy "${version}" semver range of your "react-native" dependency. Please re-install your dependencies` + if (version === 'latest' || version === '*') { + warn( + 'Some major releases introduce breaking changes.\n' + + 'Please use a caret version number in your "package.json" file \n' + + 'to avoid breakage. Use e.g. react-native: ^0.38.0. Aborting.' ); - return null; + return; } - return newVersion; -}; - -const installDeps = async (newVersion, projectDir) => { - logger.info( - `Installing react-native@${newVersion} and its peer dependencies...` - ); - const peerDeps = await getRNPeerDeps(newVersion); - const pm = new PackageManager({ projectDir }); - const deps = [ - `react-native@${newVersion}`, - ...Object.keys(peerDeps).map(module => `${module}@${peerDeps[module]}`), - ]; - pm.install(deps); -}; - -/** - * Upgrade application to a new version of React Native. - */ -async function upgrade(argv: Array, ctx: ContextT) { - const rnDiffGitAddress = `https://github.com/pvinis/rn-diff-purge.git`; - const tmpRemote = 'tmp-rn-diff-purge'; - const tmpPatchFile = 'tmp-upgrade-rn.patch'; - const projectDir = ctx.root; - const { version: currentVersion } = require(path.join( - projectDir, - 'node_modules/react-native/package.json' - )); - - const newVersion = await getVersionToUpgradeTo( - argv, - currentVersion, - projectDir + const installed = JSON.parse( + fs.readFileSync( + path.resolve(projectDir, 'node_modules/react-native/package.json'), + 'utf8' + ) ); - if (!newVersion) { + if (!semver.satisfies(installed.version, version)) { + warn( + 'react-native version in "package.json" doesn\'t match ' + + 'the installed version in "node_modules".\n' + + 'Try running "npm install" to fix this. Aborting.' + ); return; } - const patch = await getPatch(currentVersion, newVersion, projectDir); + const v = version.replace(/^(~|\^|=)/, '').replace(/x/i, '0'); - if (patch === null) { + if (!semver.valid(v)) { + warn( + "A valid version number for 'react-native' is not specified in your " + + "'package.json' file. Aborting." + ); return; } - if (patch === '') { - // Yay, nothing to diff! - await installDeps(newVersion, projectDir); - logger.success( - `Upgraded React Native to v${newVersion} 🎉. Now you can review and commit the changes` + logger.info( + `Upgrading project to react-native v${installed.version}\n` + + `Check out the release notes and breaking changes: ` + + `https://github.com/facebook/react-native/releases/tag/v${semver.major( + v + )}.${semver.minor(v)}.0` + ); + + // >= v0.21.0, we require react to be a peer dependency + if (semver.gte(v, '0.21.0') && !packageJSON.dependencies.react) { + warn( + 'Your "package.json" file doesn\'t seem to have "react" as a dependency.\n' + + '"react" was changed from a dependency to a peer dependency in react-native v0.21.0.\n' + + 'Therefore, it\'s necessary to include "react" in your project\'s dependencies.\n' + + 'Please run "npm install --save react", then re-run "react-native upgrade".\n' ); return; } - try { - fs.writeFileSync(tmpPatchFile, patch); - await execa('git', ['remote', 'add', tmpRemote, rnDiffGitAddress]); - await execa('git', ['fetch', tmpRemote]); - - try { - logger.info('Applying diff...'); - await execa( - 'git', - ['apply', tmpPatchFile, '--exclude=package.json', '-p2', '--3way'], - { stdio: 'inherit' } - ); - } catch (error) { - logger.error( - `Applying diff failed. Please review the conflicts and resolve them.` - ); - logger.info( - `You may find release notes helpful: https://github.com/facebook/react-native/releases/tag/v${newVersion}` - ); - return; - } - - await installDeps(newVersion, projectDir); - } catch (error) { - throw new Error(error.stderr || error); - } finally { - try { - fs.unlinkSync(tmpPatchFile); - } catch (e) { - // ignore - } - await execa('git', ['remote', 'remove', tmpRemote]); + if (semver.satisfies(v, '~0.26.0')) { + warn( + 'React Native 0.26 introduced some breaking changes to the native files on iOS. You can\n' + + 'perform them manually by checking the release notes or use "rnpm" ' + + 'to do it automatically.\n' + + 'Just run:\n' + + '"npm install -g rnpm && npm install rnpm-plugin-upgrade@0.26 --save-dev", ' + + 'then run "rnpm upgrade".' + ); } - logger.success( - `Upgraded React Native to v${newVersion} 🎉. Now you can review and commit the changes` + upgradeProjectFiles(projectDir, projectName); + + logger.info( + `Successfully upgraded this project to react-native v${installed.version}` ); } +/** + * Once all checks passed, upgrade the project files. + */ +function upgradeProjectFiles(projectDir, projectName) { + // Just overwrite + copyProjectTemplateAndReplace( + path.dirname(require.resolve('react-native/template')), + projectDir, + projectName, + { upgrade: true } + ); +} + +function warn(message) { + logger.warn(message); +} + const upgradeCommand = { - name: 'upgrade [version]', + name: 'upgrade', description: - "Upgrade your app's template files to the specified or latest npm version using `rn-diff-purge` project. Only valid semver versions are allowed.", - func: upgrade, + "upgrade your app's template files to the latest version; run this after " + + 'updating the react-native version in your package.json and running npm install', + func: validateAndUpgrade, + options: [ + { + command: '--improved', + description: + 'Improved implementation using rn-diff-purge project for diffing. This is going to be the default in next major and the old way of upgrading will be deprecated', + }, + ], }; export default upgradeCommand; From e60b64bc7bf963c053810ec35815df311d7e3953 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Sun, 17 Feb 2019 12:15:22 +0100 Subject: [PATCH 04/12] test: add tests --- packages/cli/package.json | 3 + .../__snapshots__/newUpgrade.test.js.snap | 27 +++ .../src/upgrade/__tests__/newUpgrade.test.js | 171 ++++++++++++++++++ .../cli/src/upgrade/__tests__/sample.patch | 51 ++++++ packages/cli/src/upgrade/helpers.js | 17 ++ packages/cli/src/upgrade/newUpgrade.js | 38 +--- yarn.lock | 10 + 7 files changed, 288 insertions(+), 29 deletions(-) create mode 100644 packages/cli/src/upgrade/__tests__/__snapshots__/newUpgrade.test.js.snap create mode 100644 packages/cli/src/upgrade/__tests__/newUpgrade.test.js create mode 100644 packages/cli/src/upgrade/__tests__/sample.patch create mode 100644 packages/cli/src/upgrade/helpers.js diff --git a/packages/cli/package.json b/packages/cli/package.json index 703668a96..4ca402fd5 100644 --- a/packages/cli/package.json +++ b/packages/cli/package.json @@ -56,5 +56,8 @@ }, "peerDependencies": { "react-native": "^0.57.0" + }, + "devDependencies": { + "snapshot-diff": "^0.5.0" } } diff --git a/packages/cli/src/upgrade/__tests__/__snapshots__/newUpgrade.test.js.snap b/packages/cli/src/upgrade/__tests__/__snapshots__/newUpgrade.test.js.snap new file mode 100644 index 000000000..13c7caaa8 --- /dev/null +++ b/packages/cli/src/upgrade/__tests__/__snapshots__/newUpgrade.test.js.snap @@ -0,0 +1,27 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`fetches regular patch, adds remote, applies patch, installs deps, removes remote,: RnDiffApp is replaced with app name (TestApp) 1`] = ` +"Snapshot Diff: +- First value ++ Second value + +@@ -1,5 +1,5 @@ +- diff --git a/RnDiffApp/android/build.gradle b/RnDiffApp/android/build.gradle ++ diff --git a/TestApp/android/build.gradle b/TestApp/android/build.gradle + index 85d8f2f8..a1e80854 100644 +- --- a/RnDiffApp/android/build.gradle +- +++ b/RnDiffApp/android/build.gradle ++ --- a/TestApp/android/build.gradle ++ +++ b/TestApp/android/build.gradle + @@ -9,8 +9,8 @@ buildscript { +@@ -28,6 +28,6 @@ + +- diff --git a/RnDiffApp/package.json b/RnDiffApp/package.json ++ diff --git a/TestApp/package.json b/TestApp/package.json + index 4e617645..c82829bd 100644 +- --- a/RnDiffApp/package.json +- +++ b/RnDiffApp/package.json ++ --- a/TestApp/package.json ++ +++ b/TestApp/package.json + @@ -7,14 +7,14 @@" +`; diff --git a/packages/cli/src/upgrade/__tests__/newUpgrade.test.js b/packages/cli/src/upgrade/__tests__/newUpgrade.test.js new file mode 100644 index 000000000..ef29b4063 --- /dev/null +++ b/packages/cli/src/upgrade/__tests__/newUpgrade.test.js @@ -0,0 +1,171 @@ +// @flow +import execa from 'execa'; +import path from 'path'; +import fs from 'fs'; +import snapshotDiff from 'snapshot-diff'; +import * as upgrade from '../newUpgrade'; +import { fetch } from '../helpers'; +import logger from '../../util/logger'; + +jest.mock('https'); +jest.mock('fs'); +jest.mock('path'); +jest.mock('execa', () => { + const module = jest.fn((command, args) => { + mockPushLog('$', 'execa', command, args); + if (command === 'npm' && args[3] === '--json') { + return Promise.resolve({ + stdout: '{"react": "16.6.3"}', + }); + } + return Promise.resolve({ stdout: '' }); + }); + return module; +}); +jest.mock( + '/project/root/node_modules/react-native/package.json', + () => ({ name: 'react-native', version: '0.57.8' }), + { virtual: true } +); +jest.mock( + '/project/root/package.json', + () => ({ name: 'TestApp', dependencies: { 'react-native': '^0.57.8' } }), + { virtual: true } +); +jest.mock('../../util/PackageManager', () => + jest.fn(() => ({ + install: args => { + mockPushLog('$ yarn add', ...args); + }, + })) +); +jest.mock('../helpers', () => ({ + ...jest.requireActual('../helpers'), + fetch: jest.fn(() => Promise.resolve('patch')), +})); +jest.mock('../../util/logger', () => ({ + info: jest.fn((...args) => mockPushLog('info', args)), + error: jest.fn((...args) => mockPushLog('error', args)), + warn: jest.fn((...args) => mockPushLog('warn', args)), + success: jest.fn((...args) => mockPushLog('success', args)), +})); + +const currentVersion = '0.57.8'; +const newVersion = '0.58.4'; +const olderVersion = '0.56.0'; +const ctx = { + root: '/project/root', + reactNativePath: '', +}; + +const samplePatch = jest + .requireActual('fs') + .readFileSync(path.join(__dirname, './sample.patch'), 'utf8'); + +let logs = []; +const mockPushLog = (...args) => + logs.push(args.map(x => (Array.isArray(x) ? x.join(' ') : x)).join(' ')); +const flushOutput = () => logs.join('\n'); + +beforeEach(() => { + jest.clearAllMocks(); + // $FlowFixMe + fs.writeFileSync = jest.fn(filename => mockPushLog('[fs] write', filename)); + // $FlowFixMe + fs.unlinkSync = jest.fn((...args) => mockPushLog('[fs] unlink', args)); + logs = []; +}); + +test('uses latest version of react-native when none passed', async () => { + await upgrade.default.func([], ctx); + expect(execa).toBeCalledWith('npm', ['info', 'react-native', 'version']); +}); + +test('errors when invalid version passed', async () => { + await upgrade.default.func(['next'], ctx); + expect(logger.error).toBeCalledWith( + 'Provided version "next" is not allowed. Please pass a valid semver version' + ); +}); + +test('errors when older version passed', async () => { + await upgrade.default.func([olderVersion], ctx); + expect(logger.error).toBeCalledWith( + `Trying to upgrade from newer version "${currentVersion}" to older "${olderVersion}"` + ); +}); + +test('warns when dependency upgrade version is in semver range', async () => { + await upgrade.default.func([currentVersion], ctx); + expect(logger.warn).toBeCalledWith( + `Specified version "${currentVersion}" is already installed in node_modules and it satisfies "^0.57.8" semver range. No need to upgrade` + ); +}); + +test('fetches empty patch and installs deps', async () => { + (fetch: any).mockImplementation(() => Promise.resolve('')); + await upgrade.default.func([newVersion], ctx); + expect(flushOutput()).toMatchInlineSnapshot(` +"info Fetching diff between v0.57.8 and v0.58.4... +info Diff has no changes to apply, proceeding further +info Installing react-native@0.58.4 and its peer dependencies... +$ execa npm info react-native@0.58.4 peerDependencies --json +$ yarn add react-native@0.58.4 react@16.6.3 +success Upgraded React Native to v0.58.4 🎉. Now you can review and commit the changes" +`); +}); + +test('fetches regular patch, adds remote, applies patch, installs deps, removes remote,', async () => { + (fetch: any).mockImplementation(() => Promise.resolve(samplePatch)); + await upgrade.default.func([newVersion], ctx); + expect(flushOutput()).toMatchInlineSnapshot(` +"info Fetching diff between v0.57.8 and v0.58.4... +[fs] write tmp-upgrade-rn.patch +$ execa git remote add tmp-rn-diff-purge https://github.com/pvinis/rn-diff-purge.git +$ execa git fetch tmp-rn-diff-purge +info Applying diff... +$ execa git apply tmp-upgrade-rn.patch --exclude=package.json -p2 --3way +info Installing react-native@0.58.4 and its peer dependencies... +$ execa npm info react-native@0.58.4 peerDependencies --json +$ yarn add react-native@0.58.4 react@16.6.3 +[fs] unlink tmp-upgrade-rn.patch +$ execa git remote remove tmp-rn-diff-purge +success Upgraded React Native to v0.58.4 🎉. Now you can review and commit the changes" +`); + + expect( + snapshotDiff(samplePatch, fs.writeFileSync.mock.calls[0][1], { + contextLines: 1, + }) + ).toMatchSnapshot('RnDiffApp is replaced with app name (TestApp)'); +}); + +test('cleans up if patching fails,', async () => { + (fetch: any).mockImplementation(() => Promise.resolve(samplePatch)); + (execa: any).mockImplementation((command, args) => { + mockPushLog('$', 'execa', command, args); + if (command === 'npm' && args[3] === '--json') { + return Promise.resolve({ + stdout: '{"react": "16.6.3"}', + }); + } + if (command === 'git' && args[0] === 'apply') { + throw new Error({ code: 1, stderr: 'error patching' }); + } + return Promise.resolve({ stdout: '' }); + }); + + await upgrade.default.func([newVersion], ctx); + expect(flushOutput()).toMatchInlineSnapshot(` +"info Fetching diff between v0.57.8 and v0.58.4... +[fs] write tmp-upgrade-rn.patch +$ execa git remote add tmp-rn-diff-purge https://github.com/pvinis/rn-diff-purge.git +$ execa git fetch tmp-rn-diff-purge +info Applying diff... +$ execa git apply tmp-upgrade-rn.patch --exclude=package.json -p2 --3way +error Applying diff failed. Please review the conflicts and resolve them. +info You may find release notes helpful: https://github.com/facebook/react-native/releases/tag/v0.58.4 +[fs] unlink tmp-upgrade-rn.patch +$ execa git remote remove tmp-rn-diff-purge" +`); +}); diff --git a/packages/cli/src/upgrade/__tests__/sample.patch b/packages/cli/src/upgrade/__tests__/sample.patch new file mode 100644 index 000000000..f5f8ef685 --- /dev/null +++ b/packages/cli/src/upgrade/__tests__/sample.patch @@ -0,0 +1,51 @@ +diff --git a/RnDiffApp/android/build.gradle b/RnDiffApp/android/build.gradle +index 85d8f2f8..a1e80854 100644 +--- a/RnDiffApp/android/build.gradle ++++ b/RnDiffApp/android/build.gradle +@@ -9,8 +9,8 @@ buildscript { + supportLibVersion = "27.1.1" + } + repositories { +- jcenter() + google() ++ jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:3.1.4' +@@ -23,12 +23,12 @@ buildscript { + allprojects { + repositories { + mavenLocal() ++ google() + jcenter() + maven { + // All of React Native (JS, Obj-C sources, Android binaries) is installed from npm + url "$rootDir/../node_modules/react-native/android" + } +- google() + } + } + +diff --git a/RnDiffApp/package.json b/RnDiffApp/package.json +index 4e617645..c82829bd 100644 +--- a/RnDiffApp/package.json ++++ b/RnDiffApp/package.json +@@ -7,14 +7,14 @@ + "test": "jest" + }, + "dependencies": { +- "react": "16.5.0", +- "react-native": "0.57.0" ++ "react": "16.6.1", ++ "react-native": "0.57.7" + }, + "devDependencies": { + "babel-jest": "23.6.0", + "jest": "23.6.0", +- "metro-react-native-babel-preset": "0.47.1", +- "react-test-renderer": "16.5.0" ++ "metro-react-native-babel-preset": "0.49.2", ++ "react-test-renderer": "16.6.1" + }, + "jest": { + "preset": "react-native" diff --git a/packages/cli/src/upgrade/helpers.js b/packages/cli/src/upgrade/helpers.js new file mode 100644 index 000000000..e40353588 --- /dev/null +++ b/packages/cli/src/upgrade/helpers.js @@ -0,0 +1,17 @@ +// @flow +import https from 'https'; + +export const fetch = (url: string) => + new Promise((resolve, reject) => { + const request = https.get(url, response => { + if (response.statusCode < 200 || response.statusCode > 299) { + reject( + new Error(`Failed to load page, status code: ${response.statusCode}`) + ); + } + const body = []; + response.on('data', chunk => body.push(chunk)); + response.on('end', () => resolve(body.join(''))); + }); + request.on('error', err => reject(err)); + }); diff --git a/packages/cli/src/upgrade/newUpgrade.js b/packages/cli/src/upgrade/newUpgrade.js index 06ab2b23b..97ab70829 100644 --- a/packages/cli/src/upgrade/newUpgrade.js +++ b/packages/cli/src/upgrade/newUpgrade.js @@ -1,13 +1,4 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @flow - */ - -import https from 'https'; +// @flow import path from 'path'; import fs from 'fs'; import semver from 'semver'; @@ -15,21 +6,7 @@ import execa from 'execa'; import type { ContextT } from '../core/types.flow'; import logger from '../util/logger'; import PackageManager from '../util/PackageManager'; - -const fetch = (url: string) => - new Promise((resolve, reject) => { - const request = https.get(url, response => { - if (response.statusCode < 200 || response.statusCode > 299) { - reject( - new Error(`Failed to load page, status code: ${response.statusCode}`) - ); - } - const body = []; - response.on('data', chunk => body.push(chunk)); - response.on('end', () => resolve(body.join(''))); - }); - request.on('error', err => reject(err)); - }); +import { fetch } from './helpers'; const getLatestRNVersion = async (): Promise => { logger.info('No version passed. Fetching latest...'); @@ -79,12 +56,15 @@ const getPatch = async (currentVersion, newVersion, projectDir) => { const getVersionToUpgradeTo = async (argv, currentVersion, projectDir) => { const newVersion = argv[0] - ? semver.valid(argv[0]) || semver.coerce(argv[0]).version + ? semver.valid(argv[0]) || + (semver.coerce(argv[0]) ? semver.coerce(argv[0]).version : null) : await getLatestRNVersion(); if (!newVersion) { logger.error( - `Provided version "${newVersion}" is not allowed. Please pass a valid semver version` + `Provided version "${ + argv[0] + }" is not allowed. Please pass a valid semver version` ); return null; } @@ -95,11 +75,11 @@ const getVersionToUpgradeTo = async (argv, currentVersion, projectDir) => { ); return null; } - if (currentVersion === newVersion) { const { dependencies: { 'react-native': version }, } = require(path.join(projectDir, 'package.json')); + if (semver.satisfies(newVersion, version)) { logger.warn( `Specified version "${newVersion}" is already installed in node_modules and it satisfies "${version}" semver range. No need to upgrade` @@ -158,7 +138,7 @@ async function upgrade(argv: Array, ctx: ContextT) { } if (patch === '') { - // Yay, nothing to diff! + logger.info('Diff has no changes to apply, proceeding further'); await installDeps(newVersion, projectDir); logger.success( `Upgraded React Native to v${newVersion} 🎉. Now you can review and commit the changes` diff --git a/yarn.lock b/yarn.lock index 01cf937f7..157ce3e37 100644 --- a/yarn.lock +++ b/yarn.lock @@ -7367,6 +7367,16 @@ snapdragon@^0.8.1: source-map-resolve "^0.5.0" use "^3.1.0" +snapshot-diff@^0.5.0: + version "0.5.0" + resolved "https://registry.yarnpkg.com/snapshot-diff/-/snapshot-diff-0.5.0.tgz#f67f4441f0fed806ad759f937112cda8ba78ff2b" + integrity sha512-mDCiZCCPQb4JP8iD8+WRNo5snbcveQqmcm0uRiKedPn+8aIKhp1gvu8BQ3KE28XFT9fI0FChkMIr5zuQdYHlRw== + dependencies: + jest-diff "^24.0.0" + jest-snapshot "^24.0.0" + pretty-format "^24.0.0" + strip-ansi "^5.0.0" + socks-proxy-agent@^4.0.0: version "4.0.1" resolved "https://registry.yarnpkg.com/socks-proxy-agent/-/socks-proxy-agent-4.0.1.tgz#5936bf8b707a993079c6f37db2091821bffa6473" From be7681635f7a96083671c7469d5a2007839873a0 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Mon, 18 Feb 2019 07:54:41 +0100 Subject: [PATCH 05/12] chore: use upgrade by default and add --legacy flag --- ...rade.test.js.snap => upgrade.test.js.snap} | 0 .../{newUpgrade.test.js => upgrade.test.js} | 19 +- packages/cli/src/upgrade/legacyUpgrade.js | 154 ++++++++++ packages/cli/src/upgrade/newUpgrade.js | 195 ------------ packages/cli/src/upgrade/upgrade.js | 282 ++++++++++-------- 5 files changed, 327 insertions(+), 323 deletions(-) rename packages/cli/src/upgrade/__tests__/__snapshots__/{newUpgrade.test.js.snap => upgrade.test.js.snap} (100%) rename packages/cli/src/upgrade/__tests__/{newUpgrade.test.js => upgrade.test.js} (93%) create mode 100644 packages/cli/src/upgrade/legacyUpgrade.js delete mode 100644 packages/cli/src/upgrade/newUpgrade.js diff --git a/packages/cli/src/upgrade/__tests__/__snapshots__/newUpgrade.test.js.snap b/packages/cli/src/upgrade/__tests__/__snapshots__/upgrade.test.js.snap similarity index 100% rename from packages/cli/src/upgrade/__tests__/__snapshots__/newUpgrade.test.js.snap rename to packages/cli/src/upgrade/__tests__/__snapshots__/upgrade.test.js.snap diff --git a/packages/cli/src/upgrade/__tests__/newUpgrade.test.js b/packages/cli/src/upgrade/__tests__/upgrade.test.js similarity index 93% rename from packages/cli/src/upgrade/__tests__/newUpgrade.test.js rename to packages/cli/src/upgrade/__tests__/upgrade.test.js index ef29b4063..b339124bc 100644 --- a/packages/cli/src/upgrade/__tests__/newUpgrade.test.js +++ b/packages/cli/src/upgrade/__tests__/upgrade.test.js @@ -3,7 +3,7 @@ import execa from 'execa'; import path from 'path'; import fs from 'fs'; import snapshotDiff from 'snapshot-diff'; -import * as upgrade from '../newUpgrade'; +import upgrade from '../upgrade'; import { fetch } from '../helpers'; import logger from '../../util/logger'; @@ -57,6 +57,9 @@ const ctx = { root: '/project/root', reactNativePath: '', }; +const opts = { + legacy: false, +}; const samplePatch = jest .requireActual('fs') @@ -77,26 +80,26 @@ beforeEach(() => { }); test('uses latest version of react-native when none passed', async () => { - await upgrade.default.func([], ctx); + await upgrade.func([], ctx, opts); expect(execa).toBeCalledWith('npm', ['info', 'react-native', 'version']); }); test('errors when invalid version passed', async () => { - await upgrade.default.func(['next'], ctx); + await upgrade.func(['next'], ctx, opts); expect(logger.error).toBeCalledWith( 'Provided version "next" is not allowed. Please pass a valid semver version' ); }); test('errors when older version passed', async () => { - await upgrade.default.func([olderVersion], ctx); + await upgrade.func([olderVersion], ctx, opts); expect(logger.error).toBeCalledWith( `Trying to upgrade from newer version "${currentVersion}" to older "${olderVersion}"` ); }); test('warns when dependency upgrade version is in semver range', async () => { - await upgrade.default.func([currentVersion], ctx); + await upgrade.func([currentVersion], ctx, opts); expect(logger.warn).toBeCalledWith( `Specified version "${currentVersion}" is already installed in node_modules and it satisfies "^0.57.8" semver range. No need to upgrade` ); @@ -104,7 +107,7 @@ test('warns when dependency upgrade version is in semver range', async () => { test('fetches empty patch and installs deps', async () => { (fetch: any).mockImplementation(() => Promise.resolve('')); - await upgrade.default.func([newVersion], ctx); + await upgrade.func([newVersion], ctx, opts); expect(flushOutput()).toMatchInlineSnapshot(` "info Fetching diff between v0.57.8 and v0.58.4... info Diff has no changes to apply, proceeding further @@ -117,7 +120,7 @@ success Upgraded React Native to v0.58.4 🎉. Now you can review and commit the test('fetches regular patch, adds remote, applies patch, installs deps, removes remote,', async () => { (fetch: any).mockImplementation(() => Promise.resolve(samplePatch)); - await upgrade.default.func([newVersion], ctx); + await upgrade.func([newVersion], ctx, opts); expect(flushOutput()).toMatchInlineSnapshot(` "info Fetching diff between v0.57.8 and v0.58.4... [fs] write tmp-upgrade-rn.patch @@ -155,7 +158,7 @@ test('cleans up if patching fails,', async () => { return Promise.resolve({ stdout: '' }); }); - await upgrade.default.func([newVersion], ctx); + await upgrade.func([newVersion], ctx, opts); expect(flushOutput()).toMatchInlineSnapshot(` "info Fetching diff between v0.57.8 and v0.58.4... [fs] write tmp-upgrade-rn.patch diff --git a/packages/cli/src/upgrade/legacyUpgrade.js b/packages/cli/src/upgrade/legacyUpgrade.js new file mode 100644 index 000000000..cd601b784 --- /dev/null +++ b/packages/cli/src/upgrade/legacyUpgrade.js @@ -0,0 +1,154 @@ +/** + * Copyright (c) Facebook, Inc. and its affiliates. + * + * This source code is licensed under the MIT license found in the + * LICENSE file in the root directory of this source tree. + * + * @format + * @flow + */ + +import fs from 'fs'; +import path from 'path'; +import semver from 'semver'; +import type { ContextT } from '../core/types.flow'; +import logger from '../util/logger'; +import copyProjectTemplateAndReplace from '../generator/copyProjectTemplateAndReplace'; + +/** + * Migrate application to a new version of React Native. + * See http://facebook.github.io/react-native/docs/upgrading.html + */ +function validateAndUpgrade(argv: Array, ctx: ContextT) { + const projectDir = ctx.root; + + const packageJSON = JSON.parse( + fs.readFileSync(path.resolve(projectDir, 'package.json'), 'utf8') + ); + + warn( + 'You should consider using the new upgrade tool based on Git. It ' + + 'makes upgrades easier by resolving most conflicts automatically.\n' + + 'To use it:\n' + + '- Go back to the old version of React Native\n' + + '- Run "npm install -g react-native-git-upgrade"\n' + + '- Run "react-native-git-upgrade"\n' + + 'See https://facebook.github.io/react-native/docs/upgrading.html' + ); + + const projectName = packageJSON.name; + if (!projectName) { + warn( + 'Your project needs to have a name, declared in package.json, ' + + 'such as "name": "AwesomeApp". Please add a project name. Aborting.' + ); + return; + } + + const version = packageJSON.dependencies['react-native']; + if (!version) { + warn( + 'Your "package.json" file doesn\'t seem to declare "react-native" as ' + + 'a dependency. Nothing to upgrade. Aborting.' + ); + return; + } + + if (version === 'latest' || version === '*') { + warn( + 'Some major releases introduce breaking changes.\n' + + 'Please use a caret version number in your "package.json" file \n' + + 'to avoid breakage. Use e.g. react-native: ^0.38.0. Aborting.' + ); + return; + } + + const installed = JSON.parse( + fs.readFileSync( + path.resolve(projectDir, 'node_modules/react-native/package.json'), + 'utf8' + ) + ); + + if (!semver.satisfies(installed.version, version)) { + warn( + 'react-native version in "package.json" doesn\'t match ' + + 'the installed version in "node_modules".\n' + + 'Try running "npm install" to fix this. Aborting.' + ); + return; + } + + const v = version.replace(/^(~|\^|=)/, '').replace(/x/i, '0'); + + if (!semver.valid(v)) { + warn( + "A valid version number for 'react-native' is not specified in your " + + "'package.json' file. Aborting." + ); + return; + } + + logger.info( + `Upgrading project to react-native v${installed.version}\n` + + `Check out the release notes and breaking changes: ` + + `https://github.com/facebook/react-native/releases/tag/v${semver.major( + v + )}.${semver.minor(v)}.0` + ); + + // >= v0.21.0, we require react to be a peer dependency + if (semver.gte(v, '0.21.0') && !packageJSON.dependencies.react) { + warn( + 'Your "package.json" file doesn\'t seem to have "react" as a dependency.\n' + + '"react" was changed from a dependency to a peer dependency in react-native v0.21.0.\n' + + 'Therefore, it\'s necessary to include "react" in your project\'s dependencies.\n' + + 'Please run "npm install --save react", then re-run "react-native upgrade".\n' + ); + return; + } + + if (semver.satisfies(v, '~0.26.0')) { + warn( + 'React Native 0.26 introduced some breaking changes to the native files on iOS. You can\n' + + 'perform them manually by checking the release notes or use "rnpm" ' + + 'to do it automatically.\n' + + 'Just run:\n' + + '"npm install -g rnpm && npm install rnpm-plugin-upgrade@0.26 --save-dev", ' + + 'then run "rnpm upgrade".' + ); + } + + upgradeProjectFiles(projectDir, projectName); + + logger.info( + `Successfully upgraded this project to react-native v${installed.version}` + ); +} + +/** + * Once all checks passed, upgrade the project files. + */ +function upgradeProjectFiles(projectDir, projectName) { + // Just overwrite + copyProjectTemplateAndReplace( + path.dirname(require.resolve('react-native/template')), + projectDir, + projectName, + { upgrade: true } + ); +} + +function warn(message) { + logger.warn(message); +} + +const upgradeCommand = { + name: 'upgrade', + description: + "upgrade your app's template files to the latest version; run this after " + + 'updating the react-native version in your package.json and running npm install', + func: validateAndUpgrade, +}; + +export default upgradeCommand; diff --git a/packages/cli/src/upgrade/newUpgrade.js b/packages/cli/src/upgrade/newUpgrade.js deleted file mode 100644 index 97ab70829..000000000 --- a/packages/cli/src/upgrade/newUpgrade.js +++ /dev/null @@ -1,195 +0,0 @@ -// @flow -import path from 'path'; -import fs from 'fs'; -import semver from 'semver'; -import execa from 'execa'; -import type { ContextT } from '../core/types.flow'; -import logger from '../util/logger'; -import PackageManager from '../util/PackageManager'; -import { fetch } from './helpers'; - -const getLatestRNVersion = async (): Promise => { - logger.info('No version passed. Fetching latest...'); - const { stdout } = await execa('npm', ['info', 'react-native', 'version']); - return stdout; -}; - -const getRNPeerDeps = async ( - version: string -): Promise<{ [key: string]: string }> => { - const { stdout } = await execa('npm', [ - 'info', - `react-native@${version}`, - 'peerDependencies', - '--json', - ]); - - return JSON.parse(stdout); -}; - -const getPatch = async (currentVersion, newVersion, projectDir) => { - let patch; - const rnDiffPurgeUrl = 'https://github.com/pvinis/rn-diff-purge'; - const rnDiffAppName = 'RnDiffApp'; - const { name } = require(path.join(projectDir, 'package.json')); - - logger.info(`Fetching diff between v${currentVersion} and v${newVersion}...`); - - try { - patch = await fetch( - `${rnDiffPurgeUrl}/compare/version/${currentVersion}...version/${newVersion}.diff` - ); - } catch (error) { - logger.error( - `Failed to fetch diff for react-native@${newVersion}. Maybe it's not released yet?` - ); - logger.info( - `For available releases to diff see: https://github.com/pvinis/rn-diff-purge#version-changes` - ); - return null; - } - - return patch - .replace(new RegExp(rnDiffAppName, 'g'), name) - .replace(new RegExp(rnDiffAppName.toLowerCase(), 'g'), name.toLowerCase()); -}; - -const getVersionToUpgradeTo = async (argv, currentVersion, projectDir) => { - const newVersion = argv[0] - ? semver.valid(argv[0]) || - (semver.coerce(argv[0]) ? semver.coerce(argv[0]).version : null) - : await getLatestRNVersion(); - - if (!newVersion) { - logger.error( - `Provided version "${ - argv[0] - }" is not allowed. Please pass a valid semver version` - ); - return null; - } - - if (currentVersion > newVersion) { - logger.error( - `Trying to upgrade from newer version "${currentVersion}" to older "${newVersion}"` - ); - return null; - } - if (currentVersion === newVersion) { - const { - dependencies: { 'react-native': version }, - } = require(path.join(projectDir, 'package.json')); - - if (semver.satisfies(newVersion, version)) { - logger.warn( - `Specified version "${newVersion}" is already installed in node_modules and it satisfies "${version}" semver range. No need to upgrade` - ); - return null; - } - logger.error( - `Dependency mismatch. Specified version "${newVersion}" is already installed in node_modules and it doesn't satisfy "${version}" semver range of your "react-native" dependency. Please re-install your dependencies` - ); - return null; - } - - return newVersion; -}; - -const installDeps = async (newVersion, projectDir) => { - logger.info( - `Installing react-native@${newVersion} and its peer dependencies...` - ); - const peerDeps = await getRNPeerDeps(newVersion); - const pm = new PackageManager({ projectDir }); - const deps = [ - `react-native@${newVersion}`, - ...Object.keys(peerDeps).map(module => `${module}@${peerDeps[module]}`), - ]; - pm.install(deps); -}; - -/** - * Upgrade application to a new version of React Native. - */ -async function upgrade(argv: Array, ctx: ContextT) { - const rnDiffGitAddress = `https://github.com/pvinis/rn-diff-purge.git`; - const tmpRemote = 'tmp-rn-diff-purge'; - const tmpPatchFile = 'tmp-upgrade-rn.patch'; - const projectDir = ctx.root; - const { version: currentVersion } = require(path.join( - projectDir, - 'node_modules/react-native/package.json' - )); - - const newVersion = await getVersionToUpgradeTo( - argv, - currentVersion, - projectDir - ); - - if (!newVersion) { - return; - } - - const patch = await getPatch(currentVersion, newVersion, projectDir); - - if (patch === null) { - return; - } - - if (patch === '') { - logger.info('Diff has no changes to apply, proceeding further'); - await installDeps(newVersion, projectDir); - logger.success( - `Upgraded React Native to v${newVersion} 🎉. Now you can review and commit the changes` - ); - return; - } - - try { - fs.writeFileSync(tmpPatchFile, patch); - await execa('git', ['remote', 'add', tmpRemote, rnDiffGitAddress]); - await execa('git', ['fetch', tmpRemote]); - - try { - logger.info('Applying diff...'); - await execa( - 'git', - ['apply', tmpPatchFile, '--exclude=package.json', '-p2', '--3way'], - { stdio: 'inherit' } - ); - } catch (error) { - logger.error( - `Applying diff failed. Please review the conflicts and resolve them.` - ); - logger.info( - `You may find release notes helpful: https://github.com/facebook/react-native/releases/tag/v${newVersion}` - ); - return; - } - - await installDeps(newVersion, projectDir); - } catch (error) { - throw new Error(error.stderr || error); - } finally { - try { - fs.unlinkSync(tmpPatchFile); - } catch (e) { - // ignore - } - await execa('git', ['remote', 'remove', tmpRemote]); - } - - logger.success( - `Upgraded React Native to v${newVersion} 🎉. Now you can review and commit the changes` - ); -} - -const upgradeCommand = { - name: 'upgrade [version]', - description: - "Upgrade your app's template files to the specified or latest npm version using `rn-diff-purge` project. Only valid semver versions are allowed.", - func: upgrade, -}; - -export default upgradeCommand; diff --git a/packages/cli/src/upgrade/upgrade.js b/packages/cli/src/upgrade/upgrade.js index 9e1d8b764..538b4db38 100644 --- a/packages/cli/src/upgrade/upgrade.js +++ b/packages/cli/src/upgrade/upgrade.js @@ -1,168 +1,210 @@ -/** - * Copyright (c) Facebook, Inc. and its affiliates. - * - * This source code is licensed under the MIT license found in the - * LICENSE file in the root directory of this source tree. - * - * @format - * @flow - */ +// @flow /* eslint-disable consistent-return */ - -import fs from 'fs'; import path from 'path'; +import fs from 'fs'; import semver from 'semver'; +import execa from 'execa'; import type { ContextT } from '../core/types.flow'; import logger from '../util/logger'; -import copyProjectTemplateAndReplace from '../generator/copyProjectTemplateAndReplace'; -import newUpgrade from './newUpgrade'; +import PackageManager from '../util/PackageManager'; +import { fetch } from './helpers'; +import legacyUpgrade from './legacyUpgrade'; type FlagsT = { - improved: boolean, + legacy: boolean, }; -/** - * Migrate application to a new version of React Native. - * See http://facebook.github.io/react-native/docs/upgrading.html - */ -function validateAndUpgrade(argv: Array, ctx: ContextT, args: FlagsT) { - if (args.improved) { - return newUpgrade.func(argv, ctx); - } - const projectDir = ctx.root; +const getLatestRNVersion = async (): Promise => { + logger.info('No version passed. Fetching latest...'); + const { stdout } = await execa('npm', ['info', 'react-native', 'version']); + return stdout; +}; - const packageJSON = JSON.parse( - fs.readFileSync(path.resolve(projectDir, 'package.json'), 'utf8') - ); +const getRNPeerDeps = async ( + version: string +): Promise<{ [key: string]: string }> => { + const { stdout } = await execa('npm', [ + 'info', + `react-native@${version}`, + 'peerDependencies', + '--json', + ]); + + return JSON.parse(stdout); +}; - warn( - 'You should consider using the new upgrade tool based on Git. It ' + - 'makes upgrades easier by resolving most conflicts automatically.\n' + - 'To use it:\n' + - '- Go back to the old version of React Native\n' + - '- Run "npm install -g react-native-git-upgrade"\n' + - '- Run "react-native-git-upgrade"\n' + - 'See https://facebook.github.io/react-native/docs/upgrading.html' - ); +const getPatch = async (currentVersion, newVersion, projectDir) => { + let patch; + const rnDiffPurgeUrl = 'https://github.com/pvinis/rn-diff-purge'; + const rnDiffAppName = 'RnDiffApp'; + const { name } = require(path.join(projectDir, 'package.json')); - const projectName = packageJSON.name; - if (!projectName) { - warn( - 'Your project needs to have a name, declared in package.json, ' + - 'such as "name": "AwesomeApp". Please add a project name. Aborting.' + logger.info(`Fetching diff between v${currentVersion} and v${newVersion}...`); + + try { + patch = await fetch( + `${rnDiffPurgeUrl}/compare/version/${currentVersion}...version/${newVersion}.diff` ); - return; + } catch (error) { + logger.error( + `Failed to fetch diff for react-native@${newVersion}. Maybe it's not released yet?` + ); + logger.info( + `For available releases to diff see: https://github.com/pvinis/rn-diff-purge#version-changes` + ); + return null; } - const version = packageJSON.dependencies['react-native']; - if (!version) { - warn( - 'Your "package.json" file doesn\'t seem to declare "react-native" as ' + - 'a dependency. Nothing to upgrade. Aborting.' + return patch + .replace(new RegExp(rnDiffAppName, 'g'), name) + .replace(new RegExp(rnDiffAppName.toLowerCase(), 'g'), name.toLowerCase()); +}; + +const getVersionToUpgradeTo = async (argv, currentVersion, projectDir) => { + const newVersion = argv[0] + ? semver.valid(argv[0]) || + (semver.coerce(argv[0]) ? semver.coerce(argv[0]).version : null) + : await getLatestRNVersion(); + + if (!newVersion) { + logger.error( + `Provided version "${ + argv[0] + }" is not allowed. Please pass a valid semver version` ); - return; + return null; } - if (version === 'latest' || version === '*') { - warn( - 'Some major releases introduce breaking changes.\n' + - 'Please use a caret version number in your "package.json" file \n' + - 'to avoid breakage. Use e.g. react-native: ^0.38.0. Aborting.' + if (currentVersion > newVersion) { + logger.error( + `Trying to upgrade from newer version "${currentVersion}" to older "${newVersion}"` ); - return; + return null; + } + if (currentVersion === newVersion) { + const { + dependencies: { 'react-native': version }, + } = require(path.join(projectDir, 'package.json')); + + if (semver.satisfies(newVersion, version)) { + logger.warn( + `Specified version "${newVersion}" is already installed in node_modules and it satisfies "${version}" semver range. No need to upgrade` + ); + return null; + } + logger.error( + `Dependency mismatch. Specified version "${newVersion}" is already installed in node_modules and it doesn't satisfy "${version}" semver range of your "react-native" dependency. Please re-install your dependencies` + ); + return null; } - const installed = JSON.parse( - fs.readFileSync( - path.resolve(projectDir, 'node_modules/react-native/package.json'), - 'utf8' - ) + return newVersion; +}; + +const installDeps = async (newVersion, projectDir) => { + logger.info( + `Installing react-native@${newVersion} and its peer dependencies...` ); + const peerDeps = await getRNPeerDeps(newVersion); + const pm = new PackageManager({ projectDir }); + const deps = [ + `react-native@${newVersion}`, + ...Object.keys(peerDeps).map(module => `${module}@${peerDeps[module]}`), + ]; + pm.install(deps); +}; - if (!semver.satisfies(installed.version, version)) { - warn( - 'react-native version in "package.json" doesn\'t match ' + - 'the installed version in "node_modules".\n' + - 'Try running "npm install" to fix this. Aborting.' - ); - return; +/** + * Upgrade application to a new version of React Native. + */ +async function upgrade(argv: Array, ctx: ContextT, args: FlagsT) { + if (args.legacy) { + return legacyUpgrade.func(argv, ctx); } + const rnDiffGitAddress = `https://github.com/pvinis/rn-diff-purge.git`; + const tmpRemote = 'tmp-rn-diff-purge'; + const tmpPatchFile = 'tmp-upgrade-rn.patch'; + const projectDir = ctx.root; + const { version: currentVersion } = require(path.join( + projectDir, + 'node_modules/react-native/package.json' + )); - const v = version.replace(/^(~|\^|=)/, '').replace(/x/i, '0'); + const newVersion = await getVersionToUpgradeTo( + argv, + currentVersion, + projectDir + ); - if (!semver.valid(v)) { - warn( - "A valid version number for 'react-native' is not specified in your " + - "'package.json' file. Aborting." - ); + if (!newVersion) { return; } - logger.info( - `Upgrading project to react-native v${installed.version}\n` + - `Check out the release notes and breaking changes: ` + - `https://github.com/facebook/react-native/releases/tag/v${semver.major( - v - )}.${semver.minor(v)}.0` - ); + const patch = await getPatch(currentVersion, newVersion, projectDir); - // >= v0.21.0, we require react to be a peer dependency - if (semver.gte(v, '0.21.0') && !packageJSON.dependencies.react) { - warn( - 'Your "package.json" file doesn\'t seem to have "react" as a dependency.\n' + - '"react" was changed from a dependency to a peer dependency in react-native v0.21.0.\n' + - 'Therefore, it\'s necessary to include "react" in your project\'s dependencies.\n' + - 'Please run "npm install --save react", then re-run "react-native upgrade".\n' - ); + if (patch === null) { return; } - if (semver.satisfies(v, '~0.26.0')) { - warn( - 'React Native 0.26 introduced some breaking changes to the native files on iOS. You can\n' + - 'perform them manually by checking the release notes or use "rnpm" ' + - 'to do it automatically.\n' + - 'Just run:\n' + - '"npm install -g rnpm && npm install rnpm-plugin-upgrade@0.26 --save-dev", ' + - 'then run "rnpm upgrade".' + if (patch === '') { + logger.info('Diff has no changes to apply, proceeding further'); + await installDeps(newVersion, projectDir); + logger.success( + `Upgraded React Native to v${newVersion} 🎉. Now you can review and commit the changes` ); + return; } - upgradeProjectFiles(projectDir, projectName); - - logger.info( - `Successfully upgraded this project to react-native v${installed.version}` - ); -} + try { + fs.writeFileSync(tmpPatchFile, patch); + await execa('git', ['remote', 'add', tmpRemote, rnDiffGitAddress]); + await execa('git', ['fetch', tmpRemote]); + + try { + logger.info('Applying diff...'); + await execa( + 'git', + ['apply', tmpPatchFile, '--exclude=package.json', '-p2', '--3way'], + { stdio: 'inherit' } + ); + } catch (error) { + logger.error( + `Applying diff failed. Please review the conflicts and resolve them.` + ); + logger.info( + `You may find release notes helpful: https://github.com/facebook/react-native/releases/tag/v${newVersion}` + ); + return; + } + + await installDeps(newVersion, projectDir); + } catch (error) { + throw new Error(error.stderr || error); + } finally { + try { + fs.unlinkSync(tmpPatchFile); + } catch (e) { + // ignore + } + await execa('git', ['remote', 'remove', tmpRemote]); + } -/** - * Once all checks passed, upgrade the project files. - */ -function upgradeProjectFiles(projectDir, projectName) { - // Just overwrite - copyProjectTemplateAndReplace( - path.dirname(require.resolve('react-native/template')), - projectDir, - projectName, - { upgrade: true } + logger.success( + `Upgraded React Native to v${newVersion} 🎉. Now you can review and commit the changes` ); } -function warn(message) { - logger.warn(message); -} - const upgradeCommand = { - name: 'upgrade', + name: 'upgrade [version]', description: - "upgrade your app's template files to the latest version; run this after " + - 'updating the react-native version in your package.json and running npm install', - func: validateAndUpgrade, + "Upgrade your app's template files to the specified or latest npm version using `rn-diff-purge` project. Only valid semver versions are allowed.", + func: upgrade, options: [ { - command: '--improved', + command: '--legacy', description: - 'Improved implementation using rn-diff-purge project for diffing. This is going to be the default in next major and the old way of upgrading will be deprecated', + "Legacy implementation. Upgrade your app's template files to the latest version; run this after " + + 'updating the react-native version in your package.json and running npm install', }, ], }; From c48134ffd24d75890e2c8e2c4931f850123c41a5 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Mon, 18 Feb 2019 10:46:41 +0100 Subject: [PATCH 06/12] feat: add status and install deps unconditionally --- .../cli/src/upgrade/__tests__/upgrade.test.js | 27 ++++++-- packages/cli/src/upgrade/upgrade.js | 61 ++++++++++++++++--- 2 files changed, 73 insertions(+), 15 deletions(-) diff --git a/packages/cli/src/upgrade/__tests__/upgrade.test.js b/packages/cli/src/upgrade/__tests__/upgrade.test.js index b339124bc..96a2c28dc 100644 --- a/packages/cli/src/upgrade/__tests__/upgrade.test.js +++ b/packages/cli/src/upgrade/__tests__/upgrade.test.js @@ -48,6 +48,7 @@ jest.mock('../../util/logger', () => ({ error: jest.fn((...args) => mockPushLog('error', args)), warn: jest.fn((...args) => mockPushLog('warn', args)), success: jest.fn((...args) => mockPushLog('success', args)), + log: jest.fn((...args) => mockPushLog(args)), })); const currentVersion = '0.57.8'; @@ -126,12 +127,15 @@ test('fetches regular patch, adds remote, applies patch, installs deps, removes [fs] write tmp-upgrade-rn.patch $ execa git remote add tmp-rn-diff-purge https://github.com/pvinis/rn-diff-purge.git $ execa git fetch tmp-rn-diff-purge +$ execa git apply --check tmp-upgrade-rn.patch --exclude=package.json -p2 --3way info Applying diff... $ execa git apply tmp-upgrade-rn.patch --exclude=package.json -p2 --3way +[fs] unlink tmp-upgrade-rn.patch info Installing react-native@0.58.4 and its peer dependencies... $ execa npm info react-native@0.58.4 peerDependencies --json $ yarn add react-native@0.58.4 react@16.6.3 -[fs] unlink tmp-upgrade-rn.patch +info Running \\"git status\\" to check what changed... +$ execa git status $ execa git remote remove tmp-rn-diff-purge success Upgraded React Native to v0.58.4 🎉. Now you can review and commit the changes" `); @@ -153,7 +157,11 @@ test('cleans up if patching fails,', async () => { }); } if (command === 'git' && args[0] === 'apply') { - throw new Error({ code: 1, stderr: 'error patching' }); + // eslint-disable-next-line prefer-promise-reject-errors + return Promise.reject({ + code: 1, + stderr: 'error: .flowconfig: does not exist in index\n', + }); } return Promise.resolve({ stdout: '' }); }); @@ -164,11 +172,20 @@ test('cleans up if patching fails,', async () => { [fs] write tmp-upgrade-rn.patch $ execa git remote add tmp-rn-diff-purge https://github.com/pvinis/rn-diff-purge.git $ execa git fetch tmp-rn-diff-purge -info Applying diff... -$ execa git apply tmp-upgrade-rn.patch --exclude=package.json -p2 --3way -error Applying diff failed. Please review the conflicts and resolve them. +$ execa git apply --check tmp-upgrade-rn.patch --exclude=package.json -p2 --3way +info Applying diff (excluding: package.json, .flowconfig)... +$ execa git apply tmp-upgrade-rn.patch --exclude=package.json --exclude=.flowconfig -p2 --3way +error: .flowconfig: does not exist in index + +error Automatically applying diff failed. Please run \\"git diff\\", review the conflicts and resolve them +info Here's the diff we tried to apply: https://github.com/pvinis/rn-diff-purge/compare/version/0.57.8...version/0.58.4 info You may find release notes helpful: https://github.com/facebook/react-native/releases/tag/v0.58.4 [fs] unlink tmp-upgrade-rn.patch +info Installing react-native@0.58.4 and its peer dependencies... +$ execa npm info react-native@0.58.4 peerDependencies --json +$ yarn add react-native@0.58.4 react@16.6.3 +info Running \\"git status\\" to check what changed... +$ execa git status $ execa git remote remove tmp-rn-diff-purge" `); }); diff --git a/packages/cli/src/upgrade/upgrade.js b/packages/cli/src/upgrade/upgrade.js index 538b4db38..01b214de6 100644 --- a/packages/cli/src/upgrade/upgrade.js +++ b/packages/cli/src/upgrade/upgrade.js @@ -2,6 +2,7 @@ /* eslint-disable consistent-return */ import path from 'path'; import fs from 'fs'; +import chalk from 'chalk'; import semver from 'semver'; import execa from 'execa'; import type { ContextT } from '../core/types.flow'; @@ -14,6 +15,8 @@ type FlagsT = { legacy: boolean, }; +const rnDiffPurgeUrl = 'https://github.com/pvinis/rn-diff-purge'; + const getLatestRNVersion = async (): Promise => { logger.info('No version passed. Fetching latest...'); const { stdout } = await execa('npm', ['info', 'react-native', 'version']); @@ -35,7 +38,7 @@ const getRNPeerDeps = async ( const getPatch = async (currentVersion, newVersion, projectDir) => { let patch; - const rnDiffPurgeUrl = 'https://github.com/pvinis/rn-diff-purge'; + const rnDiffAppName = 'RnDiffApp'; const { name } = require(path.join(projectDir, 'package.json')); @@ -156,28 +159,63 @@ async function upgrade(argv: Array, ctx: ContextT, args: FlagsT) { } try { + let filesToExclude = ['package.json']; + fs.writeFileSync(tmpPatchFile, patch); await execa('git', ['remote', 'add', tmpRemote, rnDiffGitAddress]); await execa('git', ['fetch', tmpRemote]); try { - logger.info('Applying diff...'); - await execa( - 'git', - ['apply', tmpPatchFile, '--exclude=package.json', '-p2', '--3way'], - { stdio: 'inherit' } - ); + try { + const excludes = filesToExclude.map(e => `--exclude=${e}`); + await execa('git', [ + 'apply', + '--check', + tmpPatchFile, + ...excludes, + '-p2', + '--3way', + ]); + logger.info(`Applying diff...`); + } catch (error) { + filesToExclude = [ + ...filesToExclude, + ...error.stderr + .split('\n') + .filter(x => x.includes('does not exist in index')) + .map(x => + x.replace(/^error: (.*): does not exist in index$/, '$1') + ), + ].filter(Boolean); + + logger.info( + `Applying diff (excluding: ${filesToExclude.join(', ')})...` + ); + } finally { + const excludes = filesToExclude.map(e => `--exclude=${e}`); + await execa('git', [ + 'apply', + tmpPatchFile, + ...excludes, + '-p2', + '--3way', + ]); + } } catch (error) { + if (error.stderr) { + logger.log(chalk.dim(error.stderr)); + } logger.error( - `Applying diff failed. Please review the conflicts and resolve them.` + 'Automatically applying diff failed. Please run "git diff", review the conflicts and resolve them' + ); + logger.info( + `Here's the diff we tried to apply: ${rnDiffPurgeUrl}/compare/version/${currentVersion}...version/${newVersion}` ); logger.info( `You may find release notes helpful: https://github.com/facebook/react-native/releases/tag/v${newVersion}` ); return; } - - await installDeps(newVersion, projectDir); } catch (error) { throw new Error(error.stderr || error); } finally { @@ -186,6 +224,9 @@ async function upgrade(argv: Array, ctx: ContextT, args: FlagsT) { } catch (e) { // ignore } + await installDeps(newVersion, projectDir); + logger.info('Running "git status" to check what changed...'); + await execa('git', ['status'], { stdio: 'inherit' }); await execa('git', ['remote', 'remove', tmpRemote]); } From f8c5e86b914ce052e5848ad5b6bf345540e2167b Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Mon, 18 Feb 2019 11:06:48 +0100 Subject: [PATCH 07/12] feat: add package.json and lockfiles to git stage --- packages/cli/src/upgrade/__tests__/upgrade.test.js | 9 +++++++++ packages/cli/src/upgrade/upgrade.js | 13 ++++++++++++- 2 files changed, 21 insertions(+), 1 deletion(-) diff --git a/packages/cli/src/upgrade/__tests__/upgrade.test.js b/packages/cli/src/upgrade/__tests__/upgrade.test.js index 96a2c28dc..4549bb7d2 100644 --- a/packages/cli/src/upgrade/__tests__/upgrade.test.js +++ b/packages/cli/src/upgrade/__tests__/upgrade.test.js @@ -115,6 +115,9 @@ info Diff has no changes to apply, proceeding further info Installing react-native@0.58.4 and its peer dependencies... $ execa npm info react-native@0.58.4 peerDependencies --json $ yarn add react-native@0.58.4 react@16.6.3 +$ execa git add package.json +$ execa git add yarn.lock +$ execa git add package-lock.json success Upgraded React Native to v0.58.4 🎉. Now you can review and commit the changes" `); }); @@ -134,6 +137,9 @@ $ execa git apply tmp-upgrade-rn.patch --exclude=package.json -p2 --3way info Installing react-native@0.58.4 and its peer dependencies... $ execa npm info react-native@0.58.4 peerDependencies --json $ yarn add react-native@0.58.4 react@16.6.3 +$ execa git add package.json +$ execa git add yarn.lock +$ execa git add package-lock.json info Running \\"git status\\" to check what changed... $ execa git status $ execa git remote remove tmp-rn-diff-purge @@ -184,6 +190,9 @@ info You may find release notes helpful: https://github.com/facebook/react-nativ info Installing react-native@0.58.4 and its peer dependencies... $ execa npm info react-native@0.58.4 peerDependencies --json $ yarn add react-native@0.58.4 react@16.6.3 +$ execa git add package.json +$ execa git add yarn.lock +$ execa git add package-lock.json info Running \\"git status\\" to check what changed... $ execa git status $ execa git remote remove tmp-rn-diff-purge" diff --git a/packages/cli/src/upgrade/upgrade.js b/packages/cli/src/upgrade/upgrade.js index 01b214de6..d1eb0d510 100644 --- a/packages/cli/src/upgrade/upgrade.js +++ b/packages/cli/src/upgrade/upgrade.js @@ -114,7 +114,18 @@ const installDeps = async (newVersion, projectDir) => { `react-native@${newVersion}`, ...Object.keys(peerDeps).map(module => `${module}@${peerDeps[module]}`), ]; - pm.install(deps); + await pm.install(deps); + await execa('git', ['add', 'package.json']); + try { + await execa('git', ['add', 'yarn.lock']); + } catch (error) { + // ignore + } + try { + await execa('git', ['add', 'package-lock.json']); + } catch (error) { + // ignore + } }; /** From 2fff90e3a70c80198a2dbe02ca91fab625b491a4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Mon, 18 Feb 2019 19:48:30 +0100 Subject: [PATCH 08/12] fix: use fetch --no-tags --- packages/cli/src/upgrade/upgrade.js | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/cli/src/upgrade/upgrade.js b/packages/cli/src/upgrade/upgrade.js index d1eb0d510..d68cb744d 100644 --- a/packages/cli/src/upgrade/upgrade.js +++ b/packages/cli/src/upgrade/upgrade.js @@ -174,7 +174,7 @@ async function upgrade(argv: Array, ctx: ContextT, args: FlagsT) { fs.writeFileSync(tmpPatchFile, patch); await execa('git', ['remote', 'add', tmpRemote, rnDiffGitAddress]); - await execa('git', ['fetch', tmpRemote]); + await execa('git', ['fetch', '--no-tags', tmpRemote]); try { try { From 9bd837f60cb275c15744d9ae185f8007fe054db6 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Mon, 18 Feb 2019 19:52:09 +0100 Subject: [PATCH 09/12] fix: add logger.success back --- packages/cli/src/upgrade/__tests__/upgrade.test.js | 4 ++-- packages/cli/src/util/logger.js | 7 +++++-- 2 files changed, 7 insertions(+), 4 deletions(-) diff --git a/packages/cli/src/upgrade/__tests__/upgrade.test.js b/packages/cli/src/upgrade/__tests__/upgrade.test.js index 4549bb7d2..81477081e 100644 --- a/packages/cli/src/upgrade/__tests__/upgrade.test.js +++ b/packages/cli/src/upgrade/__tests__/upgrade.test.js @@ -129,7 +129,7 @@ test('fetches regular patch, adds remote, applies patch, installs deps, removes "info Fetching diff between v0.57.8 and v0.58.4... [fs] write tmp-upgrade-rn.patch $ execa git remote add tmp-rn-diff-purge https://github.com/pvinis/rn-diff-purge.git -$ execa git fetch tmp-rn-diff-purge +$ execa git fetch --no-tags tmp-rn-diff-purge $ execa git apply --check tmp-upgrade-rn.patch --exclude=package.json -p2 --3way info Applying diff... $ execa git apply tmp-upgrade-rn.patch --exclude=package.json -p2 --3way @@ -177,7 +177,7 @@ test('cleans up if patching fails,', async () => { "info Fetching diff between v0.57.8 and v0.58.4... [fs] write tmp-upgrade-rn.patch $ execa git remote add tmp-rn-diff-purge https://github.com/pvinis/rn-diff-purge.git -$ execa git fetch tmp-rn-diff-purge +$ execa git fetch --no-tags tmp-rn-diff-purge $ execa git apply --check tmp-upgrade-rn.patch --exclude=package.json -p2 --3way info Applying diff (excluding: package.json, .flowconfig)... $ execa git apply tmp-upgrade-rn.patch --exclude=package.json --exclude=.flowconfig -p2 --3way diff --git a/packages/cli/src/util/logger.js b/packages/cli/src/util/logger.js index 1f0442612..06e1d899f 100644 --- a/packages/cli/src/util/logger.js +++ b/packages/cli/src/util/logger.js @@ -24,6 +24,10 @@ const error = (...messages: Array) => { console.error(`${chalk.red.bold('error')} ${formatMessages(messages)}`); }; +const success = (...messages: Array) => { + console.log(`${chalk.green.bold('success')} ${formatMessages(messages)}`); +}; + const debug = (...messages: Array) => { console.log(`${chalk.gray.bold('debug')} ${formatMessages(messages)}`); }; @@ -37,8 +41,7 @@ export default { info, warn, error, + success, debug, log, }; - -// export { info, warn, error, debug }; From 26aa0326b95f4e3b287861a0243d05858241468d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Mon, 18 Feb 2019 23:31:20 +0100 Subject: [PATCH 10/12] feat: add silent mode to PackageManager --- packages/cli/src/util/PackageManager.js | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/packages/cli/src/util/PackageManager.js b/packages/cli/src/util/PackageManager.js index 3c3d963aa..a586dd3c9 100644 --- a/packages/cli/src/util/PackageManager.js +++ b/packages/cli/src/util/PackageManager.js @@ -14,8 +14,10 @@ export default class PackageManager { this.options = options; } - executeCommand(command: string) { - return execSync(command, { stdio: 'inherit' }); + executeCommand(command: string, options?: { silent: boolean }) { + return execSync(command, { + stdio: options && options.silent ? 'pipe' : 'inherit', + }); } shouldCallYarn() { @@ -26,11 +28,12 @@ export default class PackageManager { ); } - install(packageNames: Array) { + install(packageNames: Array, options?: { silent: boolean }) { return this.shouldCallYarn() - ? this.executeCommand(`yarn add ${packageNames.join(' ')}`) + ? this.executeCommand(`yarn add ${packageNames.join(' ')}`, options) : this.executeCommand( - `npm install ${packageNames.join(' ')} --save --save-exact` + `npm install ${packageNames.join(' ')} --save --save-exact`, + options ); } From 4773afac6b12e6e4436a3b80e3312a44a41a5872 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Mon, 18 Feb 2019 23:32:54 +0100 Subject: [PATCH 11/12] feat: rearrange the output a bit --- .../cli/src/upgrade/__tests__/upgrade.test.js | 19 ++- packages/cli/src/upgrade/upgrade.js | 125 ++++++++++-------- 2 files changed, 84 insertions(+), 60 deletions(-) diff --git a/packages/cli/src/upgrade/__tests__/upgrade.test.js b/packages/cli/src/upgrade/__tests__/upgrade.test.js index 81477081e..71054941f 100644 --- a/packages/cli/src/upgrade/__tests__/upgrade.test.js +++ b/packages/cli/src/upgrade/__tests__/upgrade.test.js @@ -112,6 +112,7 @@ test('fetches empty patch and installs deps', async () => { expect(flushOutput()).toMatchInlineSnapshot(` "info Fetching diff between v0.57.8 and v0.58.4... info Diff has no changes to apply, proceeding further +warn Continuing after failure. Most of the files are upgraded but you will need to deal with some conflicts manually info Installing react-native@0.58.4 and its peer dependencies... $ execa npm info react-native@0.58.4 peerDependencies --json $ yarn add react-native@0.58.4 react@16.6.3 @@ -172,7 +173,14 @@ test('cleans up if patching fails,', async () => { return Promise.resolve({ stdout: '' }); }); - await upgrade.func([newVersion], ctx, opts); + try { + await upgrade.func([newVersion], ctx, opts); + } catch (error) { + expect(error.message).toBe( + 'Upgrade failed. Please see the messages above for details' + ); + } + expect(flushOutput()).toMatchInlineSnapshot(` "info Fetching diff between v0.57.8 and v0.58.4... [fs] write tmp-upgrade-rn.patch @@ -181,12 +189,12 @@ $ execa git fetch --no-tags tmp-rn-diff-purge $ execa git apply --check tmp-upgrade-rn.patch --exclude=package.json -p2 --3way info Applying diff (excluding: package.json, .flowconfig)... $ execa git apply tmp-upgrade-rn.patch --exclude=package.json --exclude=.flowconfig -p2 --3way -error: .flowconfig: does not exist in index - -error Automatically applying diff failed. Please run \\"git diff\\", review the conflicts and resolve them +error: .flowconfig: does not exist in index +error Automatically applying diff failed info Here's the diff we tried to apply: https://github.com/pvinis/rn-diff-purge/compare/version/0.57.8...version/0.58.4 info You may find release notes helpful: https://github.com/facebook/react-native/releases/tag/v0.58.4 [fs] unlink tmp-upgrade-rn.patch +warn Continuing after failure. Most of the files are upgraded but you will need to deal with some conflicts manually info Installing react-native@0.58.4 and its peer dependencies... $ execa npm info react-native@0.58.4 peerDependencies --json $ yarn add react-native@0.58.4 react@16.6.3 @@ -195,6 +203,7 @@ $ execa git add yarn.lock $ execa git add package-lock.json info Running \\"git status\\" to check what changed... $ execa git status -$ execa git remote remove tmp-rn-diff-purge" +$ execa git remote remove tmp-rn-diff-purge +warn Please run \\"git diff\\" to review the conflicts and resolve them" `); }); diff --git a/packages/cli/src/upgrade/upgrade.js b/packages/cli/src/upgrade/upgrade.js index d68cb744d..e5f7e863c 100644 --- a/packages/cli/src/upgrade/upgrade.js +++ b/packages/cli/src/upgrade/upgrade.js @@ -104,7 +104,12 @@ const getVersionToUpgradeTo = async (argv, currentVersion, projectDir) => { return newVersion; }; -const installDeps = async (newVersion, projectDir) => { +const installDeps = async (newVersion, projectDir, patchSuccess) => { + if (!patchSuccess) { + logger.warn( + 'Continuing after failure. Most of the files are upgraded but you will need to deal with some conflicts manually' + ); + } logger.info( `Installing react-native@${newVersion} and its peer dependencies...` ); @@ -114,7 +119,7 @@ const installDeps = async (newVersion, projectDir) => { `react-native@${newVersion}`, ...Object.keys(peerDeps).map(module => `${module}@${peerDeps[module]}`), ]; - await pm.install(deps); + await pm.install(deps, { silent: true }); await execa('git', ['add', 'package.json']); try { await execa('git', ['add', 'yarn.lock']); @@ -128,6 +133,54 @@ const installDeps = async (newVersion, projectDir) => { } }; +const applyPatch = async ( + currentVersion: string, + newVersion: string, + tmpPatchFile: string +) => { + let filesToExclude = ['package.json']; + try { + try { + const excludes = filesToExclude.map(e => `--exclude=${e}`); + await execa('git', [ + 'apply', + '--check', + tmpPatchFile, + ...excludes, + '-p2', + '--3way', + ]); + logger.info(`Applying diff...`); + } catch (error) { + filesToExclude = [ + ...filesToExclude, + ...error.stderr + .split('\n') + .filter(x => x.includes('does not exist in index')) + .map(x => x.replace(/^error: (.*): does not exist in index$/, '$1')), + ].filter(Boolean); + + logger.info(`Applying diff (excluding: ${filesToExclude.join(', ')})...`); + } finally { + const excludes = filesToExclude.map(e => `--exclude=${e}`); + await execa('git', ['apply', tmpPatchFile, ...excludes, '-p2', '--3way']); + } + } catch (error) { + if (error.stderr) { + logger.log(`${chalk.dim(error.stderr.trim())}`); + } + logger.error('Automatically applying diff failed'); + logger.info( + `Here's the diff we tried to apply: ${rnDiffPurgeUrl}/compare/version/${currentVersion}...version/${newVersion}` + ); + logger.info( + `You may find release notes helpful: https://github.com/facebook/react-native/releases/tag/v${newVersion}` + ); + return false; + } + return true; +}; + /** * Upgrade application to a new version of React Native. */ @@ -169,62 +222,14 @@ async function upgrade(argv: Array, ctx: ContextT, args: FlagsT) { return; } - try { - let filesToExclude = ['package.json']; + let patchSuccess; + try { fs.writeFileSync(tmpPatchFile, patch); await execa('git', ['remote', 'add', tmpRemote, rnDiffGitAddress]); await execa('git', ['fetch', '--no-tags', tmpRemote]); - - try { - try { - const excludes = filesToExclude.map(e => `--exclude=${e}`); - await execa('git', [ - 'apply', - '--check', - tmpPatchFile, - ...excludes, - '-p2', - '--3way', - ]); - logger.info(`Applying diff...`); - } catch (error) { - filesToExclude = [ - ...filesToExclude, - ...error.stderr - .split('\n') - .filter(x => x.includes('does not exist in index')) - .map(x => - x.replace(/^error: (.*): does not exist in index$/, '$1') - ), - ].filter(Boolean); - - logger.info( - `Applying diff (excluding: ${filesToExclude.join(', ')})...` - ); - } finally { - const excludes = filesToExclude.map(e => `--exclude=${e}`); - await execa('git', [ - 'apply', - tmpPatchFile, - ...excludes, - '-p2', - '--3way', - ]); - } - } catch (error) { - if (error.stderr) { - logger.log(chalk.dim(error.stderr)); - } - logger.error( - 'Automatically applying diff failed. Please run "git diff", review the conflicts and resolve them' - ); - logger.info( - `Here's the diff we tried to apply: ${rnDiffPurgeUrl}/compare/version/${currentVersion}...version/${newVersion}` - ); - logger.info( - `You may find release notes helpful: https://github.com/facebook/react-native/releases/tag/v${newVersion}` - ); + patchSuccess = await applyPatch(currentVersion, newVersion, tmpPatchFile); + if (!patchSuccess) { return; } } catch (error) { @@ -235,10 +240,20 @@ async function upgrade(argv: Array, ctx: ContextT, args: FlagsT) { } catch (e) { // ignore } - await installDeps(newVersion, projectDir); + await installDeps(newVersion, projectDir, patchSuccess); logger.info('Running "git status" to check what changed...'); await execa('git', ['status'], { stdio: 'inherit' }); await execa('git', ['remote', 'remove', tmpRemote]); + + if (!patchSuccess) { + logger.warn( + 'Please run "git diff" to review the conflicts and resolve them' + ); + // eslint-disable-next-line no-unsafe-finally + throw new Error( + 'Upgrade failed. Please see the messages above for details' + ); + } } logger.success( From 8bd3b8b2827633b11fe7f155d6fa190e6581ba71 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Micha=C5=82=20Pierzcha=C5=82a?= Date: Tue, 19 Feb 2019 14:13:57 +0100 Subject: [PATCH 12/12] fix: remove extra success logger --- packages/cli/src/util/logger.js | 5 ----- 1 file changed, 5 deletions(-) diff --git a/packages/cli/src/util/logger.js b/packages/cli/src/util/logger.js index 06e1d899f..35aeff681 100644 --- a/packages/cli/src/util/logger.js +++ b/packages/cli/src/util/logger.js @@ -24,10 +24,6 @@ const error = (...messages: Array) => { console.error(`${chalk.red.bold('error')} ${formatMessages(messages)}`); }; -const success = (...messages: Array) => { - console.log(`${chalk.green.bold('success')} ${formatMessages(messages)}`); -}; - const debug = (...messages: Array) => { console.log(`${chalk.gray.bold('debug')} ${formatMessages(messages)}`); }; @@ -41,7 +37,6 @@ export default { info, warn, error, - success, debug, log, };