forked from facebook/react-native
-
Notifications
You must be signed in to change notification settings - Fork 0
/
Copy pathcliEntry.js
355 lines (305 loc) · 11.5 KB
/
cliEntry.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
/**
* 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 assert = require('assert');
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 yarn = require('./yarn');
const {
checkDeclaredVersion,
checkMatchingVersions,
checkReactPeerDependency,
checkGitAvailable,
} = require('./checks');
log.heading = 'git-upgrade';
/**
* Promisify the callback-based shelljs function exec
* @param logOutput If true, log the stdout of the command.
* @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 parseJsonFile(path, useYarn) {
const installHint = useYarn ?
'Make sure you ran "yarn" and that you are inside a React Native project.' :
'Make sure you ran "npm install" and that you are inside a React Native project.';
let fileContents;
try {
fileContents = fs.readFileSync(path, 'utf8');
} catch (err) {
throw new Error('Cannot find "' + path + '". ' + installHint);
}
try {
return JSON.parse(fileContents);
} catch (err) {
throw new Error('Cannot parse "' + path + '": ' + err.message);
}
}
function readPackageFiles(useYarn) {
const reactNativeNodeModulesPakPath = path.resolve(
process.cwd(), 'node_modules', 'react-native', 'package.json'
);
const reactNodeModulesPakPath = path.resolve(
process.cwd(), 'node_modules', 'react', 'package.json'
);
const pakPath = path.resolve(
process.cwd(), 'package.json'
);
return {
reactNativeNodeModulesPak: parseJsonFile(reactNativeNodeModulesPakPath),
reactNodeModulesPak: parseJsonFile(reactNodeModulesPakPath),
pak: parseJsonFile(pakPath)
}
}
function parseInformationJsonOutput(jsonOutput, requestedVersion) {
try {
const output = JSON.parse(jsonOutput);
const newVersion = output.version;
const peerDependencies = output.peerDependencies;
const newReactVersionRange = peerDependencies.react;
assert(semver.valid(newVersion));
return {newVersion, newReactVersionRange}
} catch (err) {
throw new Error(
'The specified version of React Native ' + requestedVersion + ' 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".'
);
}
}
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 can make Git use a different directory for its ".git" folder.
* 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[require.resolve(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));
}
/**
* If there's a newer version of react-native-git-upgrade in npm, suggest to the user to upgrade.
*/
async function checkForUpdates() {
try {
log.info('Check for updates');
const lastGitUpgradeVersion = await exec('npm view react-native-git-upgrade@latest version');
const current = require('./package').version;
const latest = semver.clean(lastGitUpgradeVersion);
if (semver.gt(latest, current)) {
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"'
);
}
} catch (err) {
log.warn('Check for latest version failed', err.message);
}
}
/**
* If true, use yarn instead of the npm client to upgrade the project.
*/
function shouldUseYarn(cliArgs, projectDir) {
if (cliArgs && cliArgs.npm) {
return false;
}
const yarnVersion = yarn.getYarnVersionIfAvailable();
if (yarnVersion && yarn.isProjectUsingYarn(projectDir)) {
log.info('Using yarn ' + yarnVersion);
return true;
}
return false;
}
/**
* @param requestedVersion The version argument, e.g. 'react-native-git-upgrade 0.38'.
* `undefined` if no argument passed.
* @param cliArgs Additional arguments parsed using minimist.
*/
async function run(requestedVersion, cliArgs) {
const tmpDir = path.resolve(os.tmpdir(), 'react-native-git-upgrade');
const generatorDir = path.resolve(process.cwd(), 'node_modules', 'react-native', 'local-cli', 'generator');
let projectBackupCreated = false;
try {
await checkForUpdates();
const useYarn = shouldUseYarn(cliArgs, path.resolve(process.cwd()));
log.info('Read package.json files');
const {reactNativeNodeModulesPak, reactNodeModulesPak, pak} = readPackageFiles(useYarn);
const appName = pak.name;
const currentVersion = reactNativeNodeModulesPak.version;
const currentReactVersion = reactNodeModulesPak.version;
const declaredVersion = pak.dependencies['react-native'];
const declaredReactVersion = pak.dependencies.react;
const verbose = cliArgs.verbose;
log.info('Check declared version');
checkDeclaredVersion(declaredVersion);
log.info('Check matching versions');
checkMatchingVersions(currentVersion, declaredVersion, useYarn);
log.info('Check React peer dependency');
checkReactPeerDependency(currentVersion, declaredReactVersion);
log.info('Check that Git is installed');
checkGitAvailable();
log.info('Get information from NPM registry');
const viewCommand = 'npm view react-native@' + (requestedVersion || 'latest') + ' --json';
const jsonOutput = await exec(viewCommand, verbose);
const {newVersion, newReactVersionRange} = parseInformationJsonOutput(jsonOutput, requestedVersion);
// Print which versions we're upgrading to
log.info('Upgrading to React Native ' + newVersion + (newReactVersionRange ? ', React ' + newReactVersionRange : ''));
log.info('Setup temporary working directory');
await setupWorkingDir(tmpDir);
log.info('Configure Git environment');
configureGitEnv(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 current project sources');
await exec('git commit -m "Project snapshot"', verbose);
log.info ('Create a tag before updating sources');
await exec('git tag project-snapshot', verbose);
projectBackupCreated = true;
log.info('Generate old version template');
await generateTemplates(generatorDir, 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');
let installCommand;
if (useYarn) {
installCommand = 'yarn add';
} else {
installCommand = 'npm install --save --color=always';
}
installCommand += ' react-native@' + newVersion;
if (newReactVersionRange && !semver.satisfies(currentReactVersion, newReactVersionRange)) {
// Install React as well to avoid unmet peer dependency
installCommand += ' react@' + newReactVersionRange;
}
await exec(installCommand, verbose);
log.info('Generate new version template');
await generateTemplates(generatorDir, 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 --binary --no-color HEAD~1 HEAD', verbose);
log.info('Save the patch in temp directory');
const patchPath = path.resolve(tmpDir, `upgrade_${currentVersion}_${newVersion}.patch`);
fs.writeFileSync(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 ${patchPath}`, true);
} catch (err) {
log.warn(
'The upgrade process succeeded but there might be conflicts to be resolved. ' +
'See above for the list of files that have merge conflicts.');
} finally {
log.info('Upgrade done');
if (cliArgs.verbose) {
log.info(`Temporary working directory: ${tmpDir}`);
}
}
} catch (err) {
log.error('An error occurred during upgrade:');
log.error(err.stack);
if (projectBackupCreated) {
log.error('Restore initial sources');
await exec('git checkout project-snapshot', true);
}
}
}
module.exports = {
run: run,
};