implement script to generate CodePushified React Native apps (#958)

* implement fully automatic script to generate CodePushified React Native apps

* fix compatibility issues, remove generate-app.sh

* update docs, improve react-native version detection
This commit is contained in:
Sergey Akhalkov
2017-08-09 14:07:15 +03:00
committed by GitHub
parent 416af9b738
commit 6f6f145cbd
3 changed files with 571 additions and 63 deletions

178
Examples/create-app.js Normal file
View File

@@ -0,0 +1,178 @@
/*
The script serves to generate CodePushified React Native app to reproduce issues or for testing purposes.
Requirements:
1. npm i -g react-native-cli
2. npm i -g code-push-cli
3. code-push register
Usage: node create-app.js <appName> <reactNativeVersion> <reactNativeCodePushVersion>
1. node create-app.js
2. node create-app.js myapp
3. node create-app.js myapp react-native@0.47.1 react-native-code-push@5.0.0-beta
4. node create-app.js myapp react-native@latest Microsoft/react-native-code-push
Parameters:
1. <appName> - CodePushDemoAppTest
2. <reactNativeVersion> - react-native@latest
3. <reactNativeCodePushVersion> - react-native-code-push@latest
*/
let fs = require('fs');
let path = require('path');
let nexpect = require('./nexpect');
let child_proces = require('child_process');
let execSync = child_proces.execSync;
let args = process.argv.slice(2);
let appName = args[0] || 'CodePushDemoAppTest';
if (fs.existsSync(appName)) {
console.error(`Folder with name "${appName}" already exists! Please delete`);
process.exit();
}
let appNameAndroid = `${appName}-android`;
let appNameIOS = `${appName}-ios`;
let reactNativeVersion = args[1] || `react-native@${execSync('npm view react-native version')}`.trim();
let reactNativeCodePushVersion = args[2] || `react-native-code-push@${execSync('npm view react-native-code-push version')}`.trim();
console.log(`App name: ${appName}`);
console.log(`React Native version: ${reactNativeVersion}`);
console.log(`React Native Module for CodePush version: ${reactNativeCodePushVersion} \n`);
let androidStagingDeploymentKey = null;
let iosStagingDeploymentKey = null;
//GENERATE START
createCodePushApp(appNameAndroid, 'android');
createCodePushApp(appNameIOS, 'ios');
generatePlainReactNativeApp(appName, reactNativeVersion);
process.chdir(appName);
installCodePush(reactNativeCodePushVersion);
linkCodePush(androidStagingDeploymentKey, iosStagingDeploymentKey);
//GENERATE END
function createCodePushApp(name, platform) {
try {
console.log(`Creating CodePush app "${name}" to release updates for ${platform}...`);
execSync(`code-push app add ${name} ${platform} react-native`);
console.log(`App "${name}" has been created \n`);
} catch (e) {
console.log(`App "${name}" already exists \n`);
}
let deploymentKeys = JSON.parse(execSync(`code-push deployment ls ${name} -k --format json`));
let stagingDeploymentKey = deploymentKeys[1].key;
console.log(`Deployment key for ${platform}: ${stagingDeploymentKey}`);
console.log(`Use "code-push release-react ${name} ${platform}" command to release updates for ${platform} \n`);
switch (platform) {
case 'android':
androidStagingDeploymentKey = stagingDeploymentKey;
break;
case 'ios':
iosStagingDeploymentKey = stagingDeploymentKey;
break;
}
}
function generatePlainReactNativeApp(appName, reactNativeVersion) {
console.log(`Installing React Native...`);
execSync(`react-native init ${appName} --version ${reactNativeVersion}`);
console.log(`React Native has been installed \n`);
}
function installCodePush(reactNativeCodePushVersion) {
console.log(`Installing React Native Module for CodePush...`);
execSync(`npm i --save ${reactNativeCodePushVersion}`);
console.log(`React Native Module for CodePush has been installed \n`);
}
function linkCodePush(androidStagingDeploymentKey, iosStagingDeploymentKey) {
console.log(`Linking React Native Module for CodePush...`);
nexpect.spawn(`react-native link react-native-code-push`)
.wait("What is your CodePush deployment key for Android (hit <ENTER> to ignore)")
.sendline(androidStagingDeploymentKey)
.wait("What is your CodePush deployment key for iOS (hit <ENTER> to ignore)")
.sendline(iosStagingDeploymentKey)
.run(function (err) {
if (!err) {
console.log(`React Native Module for CodePush has been linked \n`);
setupAssets();
}
else {
console.log(err);
}
});
}
function setupAssets() {
fs.unlinkSync('./index.ios.js');
fs.unlinkSync('./index.android.js');
fs.writeFileSync('demo.js', fs.readFileSync('../CodePushDemoApp/demo.js'));
fs.writeFileSync('index.ios.js', fs.readFileSync('../CodePushDemoApp/index.ios.js'));
fs.writeFileSync('index.android.js', fs.readFileSync('../CodePushDemoApp/index.android.js'));
copyRecursiveSync('../CodePushDemoApp/images', './images');
fs.readFile('demo.js', 'utf8', function (err, data) {
if (err) {
return console.error(err);
}
var result = data.replace(/CodePushDemoApp/g, appName);
fs.writeFile('demo.js', result, 'utf8', function (err) {
if (err) return console.error(err);
if (!/^win/.test(process.platform)) {
optimizeToTestInDebugMode();
process.chdir('../');
grantAccess(appName);
}
console.log(`\nReact Native app "${appName}" has been generated and CodePushified!`);
process.exit();
});
});
}
function optimizeToTestInDebugMode() {
let rnXcodeShLocationFolder = 'scripts';
try {
let rnVersions = JSON.parse(execSync(`npm view react-native versions --json`));
let currentRNversion = JSON.parse(fs.readFileSync('./package.json'))['dependencies']['react-native'];
if (rnVersions.indexOf(currentRNversion) > -1 &&
rnVersions.indexOf(currentRNversion) < rnVersions.indexOf("0.46.0-rc.0")) {
rnXcodeShLocationFolder = 'packager';
}
} catch(e) {}
execSync(`perl -i -p0e 's/#ifdef DEBUG.*?#endif/jsCodeLocation = [CodePush bundleURL];/s' ios/${appName}/AppDelegate.m`);
execSync(`sed -ie '17,20d' node_modules/react-native/${rnXcodeShLocationFolder}/react-native-xcode.sh`);
execSync(`sed -ie 's/targetName.toLowerCase().contains("release")$/true/' node_modules/react-native/react.gradle`);
}
function grantAccess(folderPath) {
execSync('chown -R `whoami` ' + folderPath);
execSync('chmod -R 755 ' + folderPath);
}
function copyRecursiveSync(src, dest) {
var exists = fs.existsSync(src);
var stats = exists && fs.statSync(src);
var isDirectory = exists && stats.isDirectory();
if (exists && isDirectory) {
fs.mkdirSync(dest);
fs.readdirSync(src).forEach(function (childItemName) {
copyRecursiveSync(path.join(src, childItemName),
path.join(dest, childItemName));
});
} else {
fs.linkSync(src, dest);
}
}

View File

@@ -1,63 +0,0 @@
#!/bin/bash
# Copyright (c) 2015-present, Microsoft 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.
echo 'CodePush + RN sample app generation script';
echo
rm -rf testapp_rn
echo '************************ Configuration ***********************************';
#################### Configure versions #################################################
read -p "Enter React Native version (default: latest):" react_native_version
read -p "Enter CodePush version (default: latest): " react_native_code_push_version
echo
if [ ! $react_native_version]; then
react_native_version=`npm view react-native version`
fi
echo 'React Native version: ' + $react_native_version
if [ ! $react_native_code_push_version ]; then
react_native_code_push_version=`npm view react-native-code-push version`
fi
echo 'React Native Code Push version: ' + $react_native_code_push_version
echo
#################### Create app #########################################################
echo '********************* Creating app ***************************************';
current_dir=`pwd`;
echo 'Current directory: ' + $current_dir;
echo 'Create testapp_rn app';
rninit init testapp_rn --source react-native@$react_native_version
cd testapp_rn
echo 'Install React Native Code Push Version $react_native_code_push_version'
npm install --save react-native-code-push@$react_native_code_push_version
echo 'react native link to react native code push'
react-native link react-native-code-push
rm index.android.js
rm index.ios.js
cp ../CodePushDemoApp/*js .
mkdir images
cp ../CodePushDemoApp/images/* images
# Make changes required to test CodePush in debug mode (see OneNote)
sed -ie '162s/AppRegistry.registerComponent("CodePushDemoApp", () => CodePushDemoApp);/AppRegistry.registerComponent("testapp_rn", () => CodePushDemoApp);/' demo.js
perl -i -p0e 's/#ifdef DEBUG.*?#endif/jsCodeLocation = [CodePush bundleURL];/s' ios/testapp_rn/AppDelegate.m
sed -ie '17,20d' node_modules/react-native/packager/react-native-xcode.sh
sed -ie '90s/targetName.toLowerCase().contains("release")/true/' node_modules/react-native/react.gradle

393
Examples/nexpect.js Normal file
View File

@@ -0,0 +1,393 @@
/*
* nexpect.js: Top-level include for the `nexpect` module.
*
* (C) 2011, Elijah Insua, Marak Squires, Charlie Robbins.
*
*/
var spawn = require('child_process').spawn;
var util = require('util');
var AssertionError = require('assert').AssertionError;
function chain (context) {
return {
expect: function (expectation) {
var _expect = function _expect (data) {
return testExpectation(data, expectation);
};
_expect.shift = true;
_expect.expectation = expectation;
_expect.description = '[expect] ' + expectation;
_expect.requiresInput = true;
context.queue.push(_expect);
return chain(context);
},
wait: function (expectation, callback) {
var _wait = function _wait (data) {
var val = testExpectation(data, expectation);
if (val === true && typeof callback === 'function') {
callback(data);
}
return val;
};
_wait.shift = false;
_wait.expectation = expectation;
_wait.description = '[wait] ' + expectation;
_wait.requiresInput = true;
context.queue.push(_wait);
return chain(context);
},
sendline: function (line) {
var _sendline = function _sendline () {
context.process.stdin.write(line + '\n');
if (context.verbose) {
process.stdout.write(line + '\n');
}
};
_sendline.shift = true;
_sendline.description = '[sendline] ' + line;
_sendline.requiresInput = false;
context.queue.push(_sendline);
return chain(context);
},
sendEof: function() {
var _sendEof = function _sendEof () {
context.process.stdin.destroy();
};
_sendEof.shift = true;
_sendEof.description = '[sendEof]';
_sendEof.requiresInput = false;
context.queue.push(_sendEof);
return chain(context);
},
run: function (callback) {
var errState = null,
responded = false,
stdout = [],
options;
//
// **onError**
//
// Helper function to respond to the callback with a
// specified error. Kills the child process if necessary.
//
function onError (err, kill) {
if (errState || responded) {
return;
}
errState = err;
responded = true;
if (kill) {
try { context.process.kill(); }
catch (ex) { }
}
callback(err);
}
//
// **validateFnType**
//
// Helper function to validate the `currentFn` in the
// `context.queue` for the target chain.
//
function validateFnType (currentFn) {
if (typeof currentFn !== 'function') {
//
// If the `currentFn` is not a function, short-circuit with an error.
//
onError(new Error('Cannot process non-function on nexpect stack.'), true);
return false;
}
else if (['_expect', '_sendline', '_wait', '_sendEof'].indexOf(currentFn.name) === -1) {
//
// If the `currentFn` is a function, but not those set by `.sendline()` or
// `.expect()` then short-circuit with an error.
//
onError(new Error('Unexpected context function name: ' + currentFn.name), true);
return false;
}
return true;
}
//
// **evalContext**
//
// Core evaluation logic that evaluates the next function in
// `context.queue` against the specified `data` where the last
// function run had `name`.
//
function evalContext (data, name) {
var currentFn = context.queue[0];
if (!currentFn || (name === '_expect' && currentFn.name === '_expect')) {
//
// If there is nothing left on the context or we are trying to
// evaluate two consecutive `_expect` functions, return.
//
return;
}
if (currentFn.shift) {
context.queue.shift();
}
if (!validateFnType(currentFn)) {
return;
}
if (currentFn.name === '_expect') {
//
// If this is an `_expect` function, then evaluate it and attempt
// to evaluate the next function (in case it is a `_sendline` function).
//
return currentFn(data) === true ?
evalContext(data, '_expect') :
onError(createExpectationError(currentFn.expectation, data), true);
}
else if (currentFn.name === '_wait') {
//
// If this is a `_wait` function, then evaluate it and if it returns true,
// then evaluate the function (in case it is a `_sendline` function).
//
if (currentFn(data) === true) {
context.queue.shift();
evalContext(data, '_expect');
}
}
else {
//
// If the `currentFn` is any other function then evaluate it
//
currentFn();
// Evaluate the next function if it does not need input
var nextFn = context.queue[0];
if (nextFn && !nextFn.requiresInput)
evalContext(data);
}
}
//
// **onLine**
//
// Preprocesses the `data` from `context.process` on the
// specified `context.stream` and then evaluates the processed lines:
//
// 1. Stripping ANSI colors (if necessary)
// 2. Removing case sensitivity (if necessary)
// 3. Splitting `data` into multiple lines.
//
function onLine (data) {
data = data.toString();
if (context.stripColors) {
data = data.replace(/\u001b\[\d{0,2}m/g, '');
}
if (context.ignoreCase) {
data = data.toLowerCase();
}
var lines = data.split('\n').filter(function (line) { return line.length > 0; });
stdout = stdout.concat(lines);
while (lines.length > 0) {
evalContext(lines.shift(), null);
}
}
//
// **flushQueue**
//
// Helper function which flushes any remaining functions from
// `context.queue` and responds to the `callback` accordingly.
//
function flushQueue () {
var remainingQueue = context.queue.slice(),
currentFn = context.queue.shift(),
lastLine = stdout[stdout.length - 1];
if (!lastLine) {
onError(createUnexpectedEndError(
'No data from child with non-empty queue.', remainingQueue));
return false;
}
else if (context.queue.length > 0) {
onError(createUnexpectedEndError(
'Non-empty queue on spawn exit.', remainingQueue));
return false;
}
else if (!validateFnType(currentFn)) {
// onError was called
return false;
}
else if (currentFn.name === '_sendline') {
onError(new Error('Cannot call sendline after the process has exited'));
return false;
}
else if (currentFn.name === '_wait' || currentFn.name === '_expect') {
if (currentFn(lastLine) !== true) {
onError(createExpectationError(currentFn.expectation, lastLine));
return false;
}
}
return true;
}
//
// **onData**
//
// Helper function for writing any data from a stream
// to `process.stdout`.
//
function onData (data) {
process.stdout.write(data);
}
options = {
cwd: context.cwd,
env: context.env
};
//
// Spawn the child process and begin processing the target
// stream for this chain.
//
if (!/^win/.test(process.platform)) {
context.process = spawn(context.command, context.params, options);
} else {
context.process = spawn('cmd', ['/c', `${context.command}`].concat(context.params), options);
}
if (context.verbose) {
context.process.stdout.on('data', onData);
context.process.stderr.on('data', onData);
}
if (context.stream === 'all') {
context.process.stdout.on('data', onLine);
context.process.stderr.on('data', onLine);
} else {
context.process[context.stream].on('data', onLine);
}
context.process.on('error', onError);
//
// When the process exits, check the output `code` and `signal`,
// flush `context.queue` (if necessary) and respond to the callback
// appropriately.
//
context.process.on('close', function (code, signal) {
if (code === 127) {
// XXX(sam) Not how node works (anymore?), 127 is what /bin/sh returns,
// but it appears node does not, or not in all conditions, blithely
// return 127 to user, it emits an 'error' from the child_process.
//
// If the response code is `127` then `context.command` was not found.
//
return onError(new Error('Command not found: ' + context.command));
}
else if (context.queue.length && !flushQueue()) {
// if flushQueue returned false, onError was called
return;
}
callback(null, stdout, signal || code);
});
return context.process;
}
};
}
function testExpectation(data, expectation) {
if (util.isRegExp(expectation)) {
return expectation.test(data);
} else {
return data.indexOf(expectation) > -1;
}
}
function createUnexpectedEndError(message, remainingQueue) {
var desc = remainingQueue.map(function(it) { return it.description; });
var msg = message + '\n' + desc.join('\n');
return new AssertionError({
message: msg,
expected: [],
actual: desc
});
}
function createExpectationError(expected, actual) {
var expectation;
if (util.isRegExp(expected))
expectation = 'to match ' + expected;
else
expectation = 'to contain ' + JSON.stringify(expected);
var err = new AssertionError({
message: util.format('expected %j %s', actual, expectation),
actual: actual,
expected: expected
});
return err;
}
function nspawn (command, params, options) {
if (arguments.length === 2) {
if (Array.isArray(arguments[1])) {
options = {};
}
else {
options = arguments[1];
params = null;
}
}
if (Array.isArray(command)) {
params = command;
command = params.shift();
}
else if (typeof command === 'string') {
command = command.split(' ');
params = params || command.slice(1);
command = command[0];
}
options = options || {};
context = {
command: command,
cwd: options.cwd || undefined,
env: options.env || undefined,
ignoreCase: options.ignoreCase,
params: params,
queue: [],
stream: options.stream || 'stdout',
stripColors: options.stripColors,
verbose: options.verbose
};
return chain(context);
}
//
// Export the core `nspawn` function as well as `nexpect.nspawn` for
// backwards compatibility.
//
module.exports.spawn = nspawn;
module.exports.nspawn = {
spawn: nspawn
};