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,
|
|
};
|