New upgrading process, relying on Git

Summary:
The upgrading process based on Yeoman is a pain. For each file, Yeoman (or the brand new copyAndReplace solution a477aec) compares the newly generated content with the existing one and prompts the user if it differs, with very basic options: overwrite or skip.

I have digged into this problem and came with [rn-diff](https://github.com/ncuillery/rn-diff) (you may have read [this article](https://medium.com/ncuillery/easier-react-native-upgrades-with-rn-diff-5020b5c3de2d#.llvy2dym5)). This repository helps people to upgrade RN on their projects. An alternative upgrading process using `git apply` instead of Yeoman is described [here](https://github.com/ncuillery/rn-diff/blob/master/USAGE.md).

This PR is the integration of this process into the core. I got rid of the drawbacks mentioned in the link below in order to make it a clean, elegant, one-step operation.

This process is based on some Shell operations that:
- Generate the blank sources of both old and new versions
Closes https://github.com/facebook/react-native/pull/11110

Differential Revision: D4237107

Pulled By: mkonicek

fbshipit-source-id: 15e82e030b762415c925ccb2a62ddb354a6e18b9
This commit is contained in:
Nicolas Cuillery 2016-11-28 08:17:51 -08:00 committed by Facebook Github Bot
parent f520bb9a66
commit 7d89b773a7
7 changed files with 538 additions and 5 deletions

View File

@ -14,7 +14,80 @@ iOS project and a JavaScript project, all combined under an npm package, upgradi
tricky. But we try to make it easy for you. Here's what you need to do to upgrade from an older
version of React Native:
## 1. Upgrade the `react-native` dependency
## Upgrade based on Git
**IMPORTANT:** You don't have to install the new version of React Native, it will be installed automatically.
The module `react-native-git-upgrade` provides a one-step operation to upgrade the source files with
a minimum of conflicts. Under the hood, it consists in 2 phases:
* First, it computes a Git patch between both old and new template files,
* Then, the patch is applied on the user's sources.
### 1. Install Git
Your project doesn't have to be handled by the Git versioning sytem (could be Mercurial, SVN or none)
but Git has to be installed and available in the `PATH`. You can download Git here:
https://git-scm.com/downloads
### 2. Install the `react-native-git-upgrade` module
It's a CLI tool and must be installed globally:
```sh
$ npm install -g react-native-git-upgrade
```
### 3. Run the command
Run the command to start the process:
```sh
$ react-native-git-upgrade
# Upgrade React Native to the latest version
# Or:
$ react-native-git-upgrade X.Y.Z
# Upgrade React Native to the X.Y.Z version
```
The templates are upgraded in a optimized way. You still may encounter conflicts but only where the Git
3-way merge have failed, depending on the version and how you modified your sources.
### 4. Resolve the conflicts
Conflicted files include delimiters which make very clear where the changes come from. For example:
```
13B07F951A680F5B00A75B9A /* Release */ = {
isa = XCBuildConfiguration;
buildSettings = {
ASSETCATALOG_COMPILER_APPICON_NAME = AppIcon;
<<<<<<< ours
CODE_SIGN_IDENTITY = "iPhone Developer";
FRAMEWORK_SEARCH_PATHS = (
"$(inherited)",
"$(PROJECT_DIR)/HockeySDK.embeddedframework",
"$(PROJECT_DIR)/HockeySDK-iOS/HockeySDK.embeddedframework",
);
=======
CURRENT_PROJECT_VERSION = 1;
>>>>>>> theirs
HEADER_SEARCH_PATHS = (
"$(inherited)",
/Applications/Xcode.app/Contents/Developer/Toolchains/XcodeDefault.xctoolchain/usr/include,
"$(SRCROOT)/../node_modules/react-native/React/**",
"$(SRCROOT)/../node_modules/react-native-code-push/ios/CodePush/**",
);
```
You can think of "ours" as "your team" and "theirs" as "the React Native dev team".
## Alternative
Use this only in case the above didn't work.
### 1. Upgrade the `react-native` dependency
Note the latest version of the `react-native` npm package from here (or use `npm info react-native` to check):
@ -23,7 +96,7 @@ Note the latest version of the `react-native` npm package from here (or use `npm
Now install that version of `react-native` in your project with `npm install --save`:
```sh
$ npm install --save react-native@X.Y
$ npm install --save react-native@X.Y
# where X.Y is the semantic version you are upgrading to
npm WARN peerDependencies The peer dependency react@~R included from react-native...
```
@ -34,7 +107,7 @@ $ npm install --save react@R
# where R is the new version of react from the peerDependency warning you saw
```
## 2. Upgrade your project templates
### 2. Upgrade your project templates
The new npm package may contain updates to the files that are normally generated when you
run `react-native init`, like the iOS and the Android sub-projects.
@ -53,8 +126,7 @@ This will check your files against the latest template and perform the following
* If there is a new file in the template, it is simply created.
* If a file in the template is identical to your file, it is skipped.
* If a file is different in your project than the template, you will be prompted; you have options
to view a diff between your file and the template file, keep your file or overwrite it with the
template version. If you are unsure, press `h` to get a list of possible commands.
to keep your file or overwrite it with the template version.
# Manual Upgrades

View File

@ -32,6 +32,16 @@ function validateAndUpgrade() {
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(

72
react-native-git-upgrade/checks.js vendored Normal file
View File

@ -0,0 +1,72 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
const {execSync} = require('child_process');
const semver = require('semver');
function checkDeclaredVersion(declaredVersion) {
if (!declaredVersion) {
throw new Error(
'Your "package.json" file doesn\'t seem to have "react-native" as a dependency.'
);
}
}
function checkMatchingVersions(currentVersion, declaredVersion) {
if (!semver.satisfies(currentVersion, declaredVersion)) {
throw new Error(
'react-native version in "package.json" doesn\'t match ' +
'the installed version in "node_modules".\n' +
'Try running "npm install" to fix this.'
);
}
}
function checkReactPeerDependency(currentVersion, declaredReactVersion) {
if (semver.lt(currentVersion, '0.21.0') && !declaredReactVersion) {
throw new Error(
'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".'
);
}
}
function checkGitAvailable() {
try {
execSync('git --version');
} catch (error) {
throw new Error(
'"react-native-git-upgrade" requires "git" to be available in path. ' +
'Please install Git (https://git-scm.com)"'
);
}
}
function checkNewVersion(newVersion, requiredVersion) {
if (!semver.valid(newVersion) && requiredVersion) {
throw new Error(
'The specified version of React Native ' + requiredVersion + ' doesn\'t exist.\n' +
'Re-run the react-native-git-upgrade command with an existing version,\n' +
'for example: "react-native-git-upgrade 0.38.0",\n' +
'or without arguments to upgrade to the latest: "react-native-git-upgrade".'
);
}
}
module.exports = {
checkDeclaredVersion,
checkMatchingVersions,
checkReactPeerDependency,
checkGitAvailable,
checkNewVersion,
};

22
react-native-git-upgrade/cli.js vendored Normal file
View File

@ -0,0 +1,22 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
require('babel-register')({
presets: [
require('babel-preset-es2015-node'),
require('babel-preset-stage-3')
],
// Enable transpiling for react-native-git-upgrade AND the generator, just like the upgrade CLI command does
only: /(react-native-git-upgrade\/(?!(node_modules)))|(local-cli\/generator)/
});
var cliEntry = require('./cliEntry');
module.exports = cliEntry;

283
react-native-git-upgrade/cliEntry.js vendored Normal file
View File

@ -0,0 +1,283 @@
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
'use strict';
const fs = require('fs');
const os = require('os');
const path = require('path');
const shell = require('shelljs');
const Promise = require('promise');
const yeoman = require('yeoman-environment');
const TerminalAdapter = require('yeoman-environment/lib/adapter');
const log = require('npmlog');
const rimraf = require('rimraf');
const semver = require('semver');
const {
checkDeclaredVersion,
checkMatchingVersions,
checkReactPeerDependency,
checkGitAvailable,
checkNewVersion
} = require('./checks');
log.heading = 'git-upgrade';
/**
* Promisify the callback-based shelljs function exec
* @param command
* @param logOutput
* @returns {Promise}
*/
function exec(command, logOutput) {
return new Promise((resolve, reject) => {
let stderr, stdout = '';
const child = shell.exec(command, {async: true, silent: true});
child.stdout.on('data', data => {
stdout += data;
if (logOutput) {
process.stdout.write(data);
}
});
child.stderr.on('data', data => {
stderr += data;
process.stderr.write(data);
});
child.on('exit', code => {
(code === 0)
? resolve(stdout)
: reject(new Error(`Command '${command}' exited with code ${code}:
stderr: ${stderr}
stdout: ${stdout}`));
});
})
}
function readPackageFiles() {
const rnPakPath = path.resolve(
process.cwd(),
'node_modules',
'react-native',
'package.json'
);
const pakPath = path.resolve(
process.cwd(),
'package.json'
);
try {
const rnPak = JSON.parse(fs.readFileSync(rnPakPath, 'utf8'));
const pak = JSON.parse(fs.readFileSync(pakPath, 'utf8'));
return {rnPak, pak};
} catch (err) {
throw new Error(
'Unable to find "' + pakPath + '" or "' + rnPakPath + '". Make sure that you have run ' +
'"npm install" and that you are inside a React Native project.'
)
}
}
function setupWorkingDir(tmpDir) {
return new Promise((resolve, reject) => {
rimraf(tmpDir, err => {
if (err) {
reject(err);
} else {
fs.mkdirSync(tmpDir);
resolve();
}
});
});
}
function configureGitEnv(tmpDir) {
/*
* The workflow inits a temporary Git repository. We don't want to interfere
* with an existing user's Git repository.
* Thanks to Git env vars, we could address an different directory from the
* default ".git". See https://git-scm.com/book/tr/v2/Git-Internals-Environment-Variables
*/
process.env.GIT_DIR = path.resolve(tmpDir, '.gitrn');
process.env.GIT_WORK_TREE = '.';
}
function generateTemplates(generatorDir, appName, verbose) {
try {
const yeomanGeneratorEntryPoint = path.resolve(generatorDir, 'index.js');
// Try requiring the index.js (entry-point of Yeoman generators)
fs.accessSync(yeomanGeneratorEntryPoint);
return runYeomanGenerators(generatorDir, appName, verbose);
} catch(err) {
return runCopyAndReplace(generatorDir, appName);
}
}
function runCopyAndReplace(generatorDir, appName) {
const copyProjectTemplateAndReplacePath = path.resolve(generatorDir, 'copyProjectTemplateAndReplace');
/*
* This module is required twice during the process: for both old and new version
* of React Native.
* This file could have changed between these 2 versions. When generating the new template,
* we don't want to load the old version of the generator from the cache
*/
delete require.cache[copyProjectTemplateAndReplacePath];
const copyProjectTemplateAndReplace = require(copyProjectTemplateAndReplacePath);
copyProjectTemplateAndReplace(
path.resolve(generatorDir, '..', 'templates', 'HelloWorld'),
process.cwd(),
appName,
{upgrade: true, force: true}
);
}
function runYeomanGenerators(generatorDir, appName, verbose) {
if (!verbose) {
// Yeoman output needs monkey-patching (no silent option)
TerminalAdapter.prototype.log = () => {};
TerminalAdapter.prototype.log.force = () => {};
TerminalAdapter.prototype.log.create = () => {};
}
const env = yeoman.createEnv();
env.register(generatorDir, 'react:app');
const generatorArgs = ['react:app', appName];
return new Promise((resolve) => env.run(generatorArgs, {upgrade: true, force: true}, resolve));
}
async function run(requiredVersion, cliArgs) {
const context = {
tmpDir: path.resolve(os.tmpdir(), 'react-native-git-upgrade'),
generatorDir: path.resolve(process.cwd(), 'node_modules', 'react-native', 'local-cli', 'generator'),
requiredVersion,
cliArgs,
};
try {
log.info('Check for react-native-git-upgrade updates');
const lastGitUpgradeVersion = await exec('npm view react-native-git-upgrade@latest version');
const current = require('./package').version;
const latest = semver.clean(lastGitUpgradeVersion);
if (current !== latest) {
log.warn(
'A more recent version of "react-native-git-upgrade" has been found.\n' +
`Current: ${current}\n` +
`Latest: ${latest}\n` +
'Please run "npm install -g react-native-git-upgrade"'
);
}
log.info('Read package.json files');
const {rnPak, pak} = readPackageFiles();
context.appName = pak.name;
context.currentVersion = rnPak.version;
context.declaredVersion = pak.dependencies['react-native'];
context.declaredReactVersion = pak.dependencies.react;
const verbose = context.cliArgs.verbose;
log.info('Check declared version');
checkDeclaredVersion(context.declaredVersion);
log.info('Check matching versions');
checkMatchingVersions(context.currentVersion, context.declaredVersion);
log.info('Check React peer dependency');
checkReactPeerDependency(context.currentVersion, context.declaredReactVersion);
log.info('Check Git installation');
checkGitAvailable();
log.info('Get react-native version from NPM registry');
const versionOutput = await exec('npm view react-native@' + (context.requiredVersion || 'latest') + ' version', verbose);
context.newVersion = semver.clean(versionOutput);
log.info('Check new version');
checkNewVersion(context.newVersion, context.requiredVersion);
log.info('Setup temporary working directory');
await setupWorkingDir(context.tmpDir);
log.info('Configure Git environment');
configureGitEnv(context.tmpDir);
log.info('Init Git repository');
await exec('git init', verbose);
log.info('Add all files to commit');
await exec('git add .', verbose);
log.info('Commit pristine sources');
await exec('git commit -m "Project snapshot"', verbose);
log.info ('Create a tag before updating sources');
await exec('git tag project-snapshot', verbose);
context.sourcesUpdated = true;
log.info('Generate old version template');
await generateTemplates(context.generatorDir, context.appName, verbose);
log.info('Add updated files to commit');
await exec('git add .', verbose);
log.info('Commit old version template');
await exec('git commit -m "Old version" --allow-empty', verbose);
log.info('Install the new version');
await exec('npm install --save react-native@' + context.newVersion + ' --color=always', verbose);
log.info('Generate new version template');
await generateTemplates(context.generatorDir, context.appName, verbose);
log.info('Add updated files to commit');
await exec('git add .', verbose);
log.info('Commit new version template');
await exec('git commit -m "New version" --allow-empty', verbose);
log.info('Generate the patch between the 2 versions');
const diffOutput = await exec('git diff HEAD~1 HEAD', verbose);
log.info('Save the patch in temp directory');
context.patchPath = path.resolve(context.tmpDir, `upgrade_${context.currentVersion}_${context.newVersion}.patch`);
fs.writeFileSync(context.patchPath, diffOutput);
log.info('Reset the 2 temporary commits');
await exec('git reset HEAD~2 --hard', verbose);
try {
log.info('Apply the patch');
await exec(`git apply --3way ${context.patchPath}`, true);
} catch (err) {
log.warn('The upgrade process succeeded but you may have conflicts to solve');
} finally {
log.info('Upgrade done');
if (context.cliArgs.verbose) {
log.info(`Temporary working directory: ${context.tmpDir}`);
}
}
} catch (err) {
log.error('An error occurred during upgrade:');
log.error(err.stack);
if (context.sourcesUpdated) {
log.error('Restore initial sources');
await exec('git checkout project-snapshot', true);
}
}
}
module.exports = {
run: run,
};

45
react-native-git-upgrade/index.js vendored Normal file
View File

@ -0,0 +1,45 @@
#!/usr/bin/env node
/**
* Copyright (c) 2015-present, Facebook, Inc.
* All rights reserved.
*
* This source code is licensed under the BSD-style license found in the
* LICENSE file in the root directory of this source tree. An additional grant
* of patent rights can be found in the PATENTS file in the same directory.
*/
var argv = require('minimist')(process.argv.slice(2));
var cli = require('./cli');
if (argv._.length === 0 && (argv.h || argv.help)) {
console.log([
'',
' Usage: react-native-git-upgrade [version] [options]',
'',
'',
' Commands:',
'',
' [Version] upgrades React Native and app templates to the desired version',
' (latest, if not specified)',
'',
' Options:',
'',
' -h, --help output usage information',
' -v, --version output the version number',
' --verbose output',
'',
].join('\n'));
process.exit(0);
}
if (argv._.length === 0 && (argv.v || argv.version)) {
console.log(require('./package.json').version);
process.exit(0);
}
cli.run(argv._[0], argv)
.catch(console.error);

View File

@ -0,0 +1,29 @@
{
"name": "react-native-git-upgrade",
"version": "0.1.0",
"license": "BSD-3-Clause",
"description": "The React Native upgrade tool",
"main": "cli.js",
"bin": {
"react-native-git-upgrade": "index.js"
},
"engines": {
"node": ">=4"
},
"repository": {
"type": "git",
"url": "https://github.com/facebook/react-native.git"
},
"dependencies": {
"babel-core": "^6.18.0",
"babel-preset-es2015-node": "^6.1.1",
"babel-preset-stage-3": "^6.17.0",
"babel-register": "^6.18.0",
"npmlog": "^4.0.0",
"promise": "^7.1.1",
"rimraf": "^2.5.4",
"semver": "^5.0.3",
"shelljs": "^0.7.5",
"yeoman-environment": "1.5.3"
}
}