diff --git a/packages/cli/package.json b/packages/cli/package.json index 3aeb28ab6..4ca402fd5 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", @@ -55,5 +56,8 @@ }, "peerDependencies": { "react-native": "^0.57.0" + }, + "devDependencies": { + "snapshot-diff": "^0.5.0" } } diff --git a/packages/cli/src/upgrade/__tests__/__snapshots__/upgrade.test.js.snap b/packages/cli/src/upgrade/__tests__/__snapshots__/upgrade.test.js.snap new file mode 100644 index 000000000..13c7caaa8 --- /dev/null +++ b/packages/cli/src/upgrade/__tests__/__snapshots__/upgrade.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__/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/__tests__/upgrade.test.js b/packages/cli/src/upgrade/__tests__/upgrade.test.js new file mode 100644 index 000000000..71054941f --- /dev/null +++ b/packages/cli/src/upgrade/__tests__/upgrade.test.js @@ -0,0 +1,209 @@ +// @flow +import execa from 'execa'; +import path from 'path'; +import fs from 'fs'; +import snapshotDiff from 'snapshot-diff'; +import upgrade from '../upgrade'; +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)), + log: jest.fn((...args) => mockPushLog(args)), +})); + +const currentVersion = '0.57.8'; +const newVersion = '0.58.4'; +const olderVersion = '0.56.0'; +const ctx = { + root: '/project/root', + reactNativePath: '', +}; +const opts = { + legacy: false, +}; + +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.func([], ctx, opts); + expect(execa).toBeCalledWith('npm', ['info', 'react-native', 'version']); +}); + +test('errors when invalid version passed', async () => { + 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.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.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` + ); +}); + +test('fetches empty patch and installs deps', async () => { + (fetch: any).mockImplementation(() => Promise.resolve('')); + 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 +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 +$ 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" +`); +}); + +test('fetches regular patch, adds remote, applies patch, installs deps, removes remote,', async () => { + (fetch: any).mockImplementation(() => Promise.resolve(samplePatch)); + 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 +$ execa git remote add tmp-rn-diff-purge https://github.com/pvinis/rn-diff-purge.git +$ 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 +[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 +$ 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 +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') { + // 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: '' }); + }); + + 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 +$ execa git remote add tmp-rn-diff-purge https://github.com/pvinis/rn-diff-purge.git +$ 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 +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 +$ 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 +warn Please run \\"git diff\\" to review the conflicts and resolve them" +`); +}); 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/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/upgrade.js b/packages/cli/src/upgrade/upgrade.js index cd601b784..e5f7e863c 100644 --- a/packages/cli/src/upgrade/upgrade.js +++ b/packages/cli/src/upgrade/upgrade.js @@ -1,154 +1,279 @@ -/** - * 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'; +// @flow +/* 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'; import logger from '../util/logger'; -import copyProjectTemplateAndReplace from '../generator/copyProjectTemplateAndReplace'; +import PackageManager from '../util/PackageManager'; +import { fetch } from './helpers'; +import legacyUpgrade from './legacyUpgrade'; -/** - * 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; +type FlagsT = { + legacy: boolean, +}; - const packageJSON = JSON.parse( - fs.readFileSync(path.resolve(projectDir, 'package.json'), 'utf8') - ); +const rnDiffPurgeUrl = 'https://github.com/pvinis/rn-diff-purge'; - 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 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 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.' + 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` ); - 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')); - 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.' + 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; + return null; } - const v = version.replace(/^(~|\^|=)/, '').replace(/x/i, '0'); + return newVersion; +}; - if (!semver.valid(v)) { - warn( - "A valid version number for 'react-native' is not specified in your " + - "'package.json' file. Aborting." +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' ); - 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` + `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]}`), + ]; + await pm.install(deps, { silent: true }); + 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 + } +}; + +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); - // >= 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' + 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. + */ +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 newVersion = await getVersionToUpgradeTo( + argv, + currentVersion, + projectDir + ); + + if (!newVersion) { 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".' + 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; } - upgradeProjectFiles(projectDir, projectName); + let patchSuccess; - 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', '--no-tags', tmpRemote]); + patchSuccess = await applyPatch(currentVersion, newVersion, tmpPatchFile); + if (!patchSuccess) { + return; + } + } catch (error) { + throw new Error(error.stderr || error); + } finally { + try { + fs.unlinkSync(tmpPatchFile); + } catch (e) { + // ignore + } + 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]); -/** - * 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 } - ); -} + 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' + ); + } + } -function warn(message) { - logger.warn(message); + 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, + options: [ + { + command: '--legacy', + description: + "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', + }, + ], }; export default upgradeCommand; 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 ); } diff --git a/packages/cli/src/util/logger.js b/packages/cli/src/util/logger.js index 1f0442612..35aeff681 100644 --- a/packages/cli/src/util/logger.js +++ b/packages/cli/src/util/logger.js @@ -40,5 +40,3 @@ export default { debug, log, }; - -// export { info, warn, error, debug }; diff --git a/yarn.lock b/yarn.lock index c6132c147..157ce3e37 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" @@ -7366,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"