diff --git a/Examples/create-app.js b/Examples/create-app.js new file mode 100644 index 0000000..3b7be80 --- /dev/null +++ b/Examples/create-app.js @@ -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 + 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. - CodePushDemoAppTest + 2. - react-native@latest + 3. - 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 to ignore)") + .sendline(androidStagingDeploymentKey) + .wait("What is your CodePush deployment key for iOS (hit 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); + } +} \ No newline at end of file diff --git a/Examples/generate-app.sh b/Examples/generate-app.sh deleted file mode 100755 index cbc4ca8..0000000 --- a/Examples/generate-app.sh +++ /dev/null @@ -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 diff --git a/Examples/nexpect.js b/Examples/nexpect.js new file mode 100644 index 0000000..eb86222 --- /dev/null +++ b/Examples/nexpect.js @@ -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 +};