mirror of
https://github.com/status-im/react-native.git
synced 2025-01-30 03:05:44 +00:00
7d89b773a7
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
284 lines
8.8 KiB
JavaScript
284 lines
8.8 KiB
JavaScript
/**
|
|
* 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,
|
|
};
|