UNPKG

appetizer-bundle

Version:

Create/Bundle React-Native apps to on appetize.io

300 lines (251 loc) 7.87 kB
'use strict'; const debug = require('diagnostics')('appetizer-bundle'); const { spawn } = require('child_process'); const path = require('path'); const fs = require('fs'); /** * Bundle the react-native application for Appetize.io uploading. * * @constructor * @param {String} name Name of the application. * @param {String} dir Directory of project * @param {Object} opts Options. * @public */ class Bundle { constructor(name, dir, opts = {}) { this.name = name; this.dir = dir || process.cwd(); this.project = path.join(this.dir, 'ios'); this.release = path.join(this.project, 'build', 'Build', 'Products', 'Debug-iphonesimulator'); // // Don't actually write to disk. // this.dryrun = opts.dryrun; } /** * Generate the react-native offline bundle * * @param {String} dir Location where the offline bundle is placed; * @param {Function} next Completion callback. * @public */ offline(dir, next) { debug('creating an offline bundle in %s', dir); let entry = 'index.js'; if (fs.existsSync(path.join(this.dir, 'index.ios.js'))) { entry = 'index.ios.js'; } else if (fs.existsSync(path.join(this.dir, 'index.native.js'))) { entry = 'index.native.js'; } this.spawns('react-native', [ 'bundle', '--platform', 'ios', '--dev', 'false', '--entry-file', entry, '--bundle-output', dir + '/main.jsbundle', '--assets-dest', dir ], { cwd: this.dir }, next); } /** * Rewrite the `AppDelegate.m` so it points to the offline bundle. * * @param {Function} next Completion callback. * @public */ rewrite(next) { const file = path.join(this.project, this.name, 'AppDelegate.m'); fs.readFile(file, 'utf-8', (err, data) => { if (err) return next(err); /** * Restore the AppDelegate to it's original state. * * @param {Error} failure Error that needs to be passed in to the callback. * @param {Function} fn Completion callback once restored. * @private */ function restore(failure, fn) { if (failure) { debug(failure); debug('something went wrong, rolling back AppDelegate changes'); } fs.writeFile(file, data, 'utf-8', (writeErr) => { fn(failure || writeErr); }); } // // Update the code location to the new bundle location. // const changes = data.split('\n').map((line) => { // // React-Native is always actively iterated upon, that means that the // code structure of this file is also iterated upon. We want to support // as many versions as possible so we need to drill down further here to // ensure that we return the correct new bundle location. // if (!!~line.indexOf('jsBundleURLForBundleRoot:@"index.')) { return ' jsCodeLocation = [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];'; } else if (!!~line.indexOf('jsBundleURLForBundleRoot:@"index"')) { return ' return [[NSBundle mainBundle] URLForResource:@"main" withExtension:@"jsbundle"];' } return line; }).join('\n'); debug('updated the AppDelegate to ', changes); fs.writeFile(file, changes, 'UTF-8', (writeErr) => { if (writeErr) return next(writeErr, restore); next(writeErr, restore); }); }); } /** * Generate a zipfile from the resulting application build. * * @param {Function} next Completion callback. * @public */ zip(next) { const release = this.release; const input = path.join(release, this.name + '.app'); const output = path.join(release, this.name + '.zip'); this.spawns('zip', [ '-r', output, input ], { cwd: this.dir }, (err) => { next(err, output); }); } /** * Finds the xcodeproject files. * * @param {String} dir Location of the directory. * @param {Function} next Completion callback. * @public */ xcodeproj(dir, next) { const name = this.name; fs.readdir(dir, function readdir(err, files) { if (err) return next(err); /** * Search the resolved files for files with a given extension. * * @param {String} target Extension we're looking for * @returns {String|Boolean} Name of the file, or false. * @private */ function search(target) { for (let i = 0; i < files.length; i++) { const file = files[i]; const ext = path.extname(file); if (ext === target) return file; } return false; } const workspace = search('.xcworkspace'); const project = search('.xcodeproj'); let result; if (workspace) { result = { file: workspace, name, workspace: true }; } else if (project) { result = { file: project, name, workspace: false }; } if (result) return next(err, result); next(new Error('Unable to locate xcode project files.')); }); } /** * Build the xcode project so we can create the iphone simulator app. * * @param {Function} next Completion callback. * @public */ xcodebuild(next) { const dir = path.join(this.dir, 'ios'); this.xcodeproj(dir, (err, project) => { if (err) return next(err); this.spawns('xcodebuild', [ project.workspace ? '-workspace' : '-project', project.file, '-sdk', 'iphonesimulator', '-configuration', 'Debug', '-scheme', project.name, '-derivedDataPath', 'build' ], { cwd: dir, // // Prevent the build process from launching the packager as we're using // a pre-bundled/packaged build // // https://github.com/facebook/react-native/blob/242d29d37486054ee31304db85660fca43027a1c/local-cli/runIOS/runIOS.js#L200-L208 // env: Object.assign({}, process.env, { RCT_NO_LAUNCH_PACKAGER: true }) }, next); }); } /** * Small wrapper around spawn to deal with all the stream callback tracking so * we can just have a single callback function. * * @param {String} cmd The cmd we want to execute. * @param {Array} flags flags for the cmd. * @param {Object} opts Options. * @param {Function} fn Completion callback. * @private */ spawns(cmd, flags, opts, fn) { debug('spawning command', cmd, flags); const stream = spawn(cmd, flags, opts); const errs = []; const data = []; stream.stderr.on('data', (line) => { line = line.toString(); debug('[%s] received error', cmd, line); errs.push(line); }); stream.stdout.on('data', (line) => { line = line.toString(); debug('[%s] received data', cmd, line); data.push(line); }); stream.once('close', (code) => { debug('[%s] closed with code: ', cmd, code); if (+code !== 0) return fn(new Error(errs.join('\n') || 'Recieved a non 0 exit code')); fn(undefined, data.join('\n')); }); } /** * Run the various of build steps. * * @param {Function} fn Completion callback. * @public */ run(fn) { const dir = path.join(this.release, this.name +'.app'); this.rewrite((errWrite, restore) => { if (errWrite) return restore(errWrite, fn); this.xcodebuild((errBuild) => { if (errBuild) return restore(errBuild, fn); this.offline(dir, (errOffline) => { if (errOffline) return restore(errOffline, fn); this.zip((errZip, dir) => { restore(errZip, (restoreErr) => { fn(restoreErr, dir); }); }); }); }); }); } } // // Expose the module. // module.exports = Bundle;