spinjs
Version:
<p align="center"><a href="#"><img width="150" src="https://rawgit.com/sysgears/spin.js/master/logo.svg"></a></p>
808 lines (720 loc) • 31.3 kB
text/typescript
import * as http from 'http';
import * as path from 'path';
import * as fs from 'fs';
import * as minilog from 'minilog';
import * as crypto from 'crypto';
import * as mkdirp from 'mkdirp';
import { spawn } from 'child_process';
import * as _ from 'lodash';
import * as ip from 'ip';
import * as url from 'url';
import * as containerized from 'containerized';
import { fromStringWithSourceMap, SourceListMap } from 'source-list-map';
import { RawSource } from 'webpack-sources';
import requireModule from './requireModule';
import liveReloadMiddleware from './plugins/react-native/liveReloadMiddleware';
const VirtualModules = requireModule('webpack-virtual-modules');
const expoPorts = {};
minilog.enable();
process.on('uncaughtException', (ex) => {
console.error(ex);
});
process.on('unhandledRejection', reason => {
console.error(reason);
});
const __WINDOWS__ = /^win/.test(process.platform);
let server;
let startBackend = false;
let backendFirstStart = true;
process.on('exit', () => {
if (server) {
server.kill('SIGTERM');
}
});
function runServer(serverPath, logger) {
if (!fs.existsSync(serverPath)) {
throw new Error(`Backend doesn't exist at ${serverPath}, exiting`);
}
if (startBackend) {
startBackend = false;
logger('Starting backend');
server = spawn('node', [serverPath], {stdio: [0, 1, 2]});
server.on('exit', code => {
if (code === 250) {
// App requested full reload
startBackend = true;
}
logger('Backend has been stopped');
server = undefined;
runServer(serverPath, logger);
});
}
}
function webpackReporter(watch, outputPath, log, err?, stats?) {
if (err) {
log(err.stack);
throw new Error('Build error');
}
if (stats) {
log(stats.toString({
hash: false,
version: false,
timings: true,
assets: false,
chunks: false,
modules: false,
reasons: false,
children: false,
source: true,
errors: true,
errorDetails: true,
warnings: true,
publicPath: false,
colors: true,
}));
if (!watch) {
mkdirp.sync(outputPath);
fs.writeFileSync(path.join(outputPath, 'stats.json'), JSON.stringify(stats.toJson()));
}
}
}
let frontendVirtualModules = new VirtualModules({ 'node_modules/backend_reload.js': '' });
class MobileAssetsPlugin {
vendorAssets: any;
constructor(vendorAssets?) {
this.vendorAssets = vendorAssets || [];
}
apply(compiler) {
const self = this;
compiler.plugin('after-compile', (compilation, callback) => {
compilation.chunks.forEach(chunk => {
chunk.files.forEach(file => {
if (file.endsWith('.bundle')) {
let assets = self.vendorAssets;
compilation.modules.forEach(function (module) {
if (module._asset) {
assets.push(module._asset);
}
});
compilation.assets[file.replace('.bundle', '') + '.assets'] = new RawSource(JSON.stringify(assets));
}
});
});
callback();
});
}
}
function startClientWebpack(hasBackend, watch, builder, options) {
const webpack = requireModule('webpack');
const config = builder.config;
config.plugins.push(frontendVirtualModules);
const logger = minilog(`webpack-for-${config.name}`);
try {
const reporter = (...args) => webpackReporter(watch, config.output.path, logger, ...args);
if (watch) {
startWebpackDevServer(hasBackend, builder, options, reporter, logger);
} else {
if (builder.stack.platform !== 'web') {
config.plugins.push(new MobileAssetsPlugin());
}
const compiler = webpack(config);
compiler.run(reporter);
}
} catch (err) {
logger(err.message, err.stack);
}
}
let backendReloadCount = 0;
function increaseBackendReloadCount() {
backendReloadCount++;
frontendVirtualModules.writeModule('node_modules/backend_reload.js',
`var count = ${backendReloadCount};\n`);
}
function startServerWebpack(watch, builder, options) {
const config = builder.config;
const logger = minilog(`webpack-for-${config.name}`);
try {
const webpack = requireModule('webpack');
const reporter = (...args) => webpackReporter(watch, config.output.path, logger, ...args);
const compiler = webpack(config);
if (watch) {
compiler.plugin('compilation', compilation => {
compilation.plugin('after-optimize-assets', assets => {
// Patch webpack-generated original source files path, by stripping hash after filename
const mapKey = _.findKey(assets, (v, k) => k.endsWith('.map'));
if (mapKey) {
let srcMap = JSON.parse(assets[mapKey]._value);
for (let idx in srcMap.sources) {
srcMap.sources[idx] = srcMap.sources[idx].split(';')[0];
}
assets[mapKey]._value = JSON.stringify(srcMap);
}
});
});
compiler.watch({}, reporter);
compiler.plugin('done', stats => {
if (!stats.compilation.errors.length) {
const {output} = config;
startBackend = true;
if (server) {
if (!__WINDOWS__) {
server.kill('SIGUSR2');
}
if (options.frontendRefreshOnBackendChange) {
for (let module of stats.compilation.modules) {
if (module.built && module.resource &&
module.resource.indexOf(path.resolve('./src/server')) === 0) {
// Force front-end refresh on back-end change
logger.debug('Force front-end current page refresh, due to change in backend at:', module.resource);
increaseBackendReloadCount();
break;
}
}
}
} else {
runServer(path.join(output.path, 'index.js'), logger);
}
}
});
} else {
compiler.run(reporter);
}
} catch (err) {
logger(err.message, err.stack);
}
}
function openFrontend(builder, logger) {
const openurl = requireModule('openurl');
try {
if (builder.stack.hasAny('web')) {
const lanUrl = `http://${ip.address()}:${builder.config.devServer.port}`;
const url = `http://localhost:${builder.config.devServer.port}`;
if (containerized() || builder.openBrowser === false) {
logger.info(`App is running at, Local: ${url} LAN: ${lanUrl}`);
} else {
openurl.open(url);
}
} else if (builder.stack.hasAny('react-native')) {
startExpoProject(builder.config, builder.stack.platform);
}
} catch (e) {
console.error(e.stack);
}
}
function debugMiddleware(req, res, next) {
if (['/debug', '/debug/bundles'].indexOf(req.path) >= 0) {
res.writeHead(200, {'Content-Type': 'text/html'});
res.end('<!doctype html><div><a href="/debug/bundles">Cached Bundles</a></div>');
} else {
next();
}
}
function startWebpackDevServer(hasBackend, builder, options, reporter, logger) {
const webpack = requireModule('webpack');
const connect = requireModule('connect');
const compression = requireModule('compression');
const mime = requireModule('mime');
const webpackDevMiddleware = requireModule('webpack-dev-middleware');
const webpackHotMiddleware = requireModule('webpack-hot-middleware');
const httpProxyMiddleware = requireModule('http-proxy-middleware');
const waitOn = requireModule('wait-on');
const config = builder.config;
const platform = builder.stack.platform;
const configOutputPath = config.output.path;
config.output.path = '/';
let vendorHashesJson, vendorSourceListMap, vendorSource, vendorMap;
if (options.webpackDll && builder.child) {
const name = `vendor_${platform}`;
const jsonPath = path.join(options.dllBuildDir, `${name}_dll.json`);
config.plugins.push(new webpack.DllReferencePlugin({
context: process.cwd(),
manifest: requireModule('./' + jsonPath),
}));
vendorHashesJson = JSON.parse(fs.readFileSync(path.join(options.dllBuildDir, `${name}_dll_hashes.json`)).toString());
vendorSource = new RawSource(fs.readFileSync(path.join(options.dllBuildDir, vendorHashesJson.name)).toString() + '\n');
vendorMap = new RawSource(fs.readFileSync(path.join(options.dllBuildDir, vendorHashesJson.name + '.map')).toString());
if (platform !== 'web') {
const vendorAssets = JSON.parse(fs.readFileSync(path.join(options.dllBuildDir, vendorHashesJson.name + '.assets')).toString());
config.plugins.push(new MobileAssetsPlugin(vendorAssets));
}
vendorSourceListMap = fromStringWithSourceMap(
vendorSource.source(),
JSON.parse(vendorMap.source()),
);
}
let compiler = webpack(config);
compiler.plugin('after-emit', (compilation, callback) => {
if (backendFirstStart) {
if (hasBackend) {
logger.debug('Webpack dev server is waiting for backend to start...');
const { host } = url.parse(options.backendUrl.replace('{ip}', ip.address()));
waitOn({resources: [`tcp:${host}`]}, err => {
if (err) {
logger.error(err);
callback();
} else {
logger.debug('Backend has been started, resuming webpack dev server...');
backendFirstStart = false;
callback();
}
});
} else {
callback();
}
} else {
callback();
}
});
if (options.webpackDll && builder.child && platform !== 'web') {
compiler.plugin('after-compile', (compilation, callback) => {
compilation.chunks.forEach(chunk => {
chunk.files.forEach(file => {
if (file.endsWith('.bundle')) {
let sourceListMap = new SourceListMap();
sourceListMap.add(vendorSourceListMap);
sourceListMap.add(fromStringWithSourceMap(compilation.assets[file].source(),
JSON.parse(compilation.assets[file + '.map'].source())));
let sourceAndMap = sourceListMap.toStringWithSourceMap({file});
compilation.assets[file] = new RawSource(sourceAndMap.source);
compilation.assets[file + '.map'] = new RawSource(JSON.stringify(sourceAndMap.map));
}
});
});
callback();
});
}
if (options.webpackDll && builder.child && platform === 'web' && !options.ssr) {
compiler.plugin('after-compile', (compilation, callback) => {
compilation.assets[vendorHashesJson.name] = vendorSource;
compilation.assets[vendorHashesJson.name + '.map'] = vendorMap;
callback();
});
compiler.plugin('compilation', function (compilation) {
compilation.plugin('html-webpack-plugin-before-html-processing', function (htmlPluginData, callback) {
htmlPluginData.assets.js.unshift('/' + vendorHashesJson.name);
callback(null, htmlPluginData);
});
});
}
let frontendFirstStart = true;
compiler.plugin('done', stats => {
const dir = configOutputPath;
mkdirp.sync(dir);
if (stats.compilation.assets['assets.json']) {
const assetsMap = JSON.parse(stats.compilation.assets['assets.json'].source());
_.each(stats.toJson().assetsByChunkName, (assets, bundle) => {
const bundleJs = assets.constructor === Array ? assets[0] : assets;
assetsMap[`${bundle}.js`] = bundleJs;
if (assets.length > 1) {
assetsMap[`${bundle}.js.map`] = `${bundleJs}.map`;
}
});
if (options.webpackDll) {
assetsMap['vendor.js'] = vendorHashesJson.name;
}
fs.writeFileSync(path.join(dir, 'assets.json'), JSON.stringify(assetsMap));
}
if (frontendFirstStart) {
frontendFirstStart = false;
openFrontend(builder, logger);
}
});
const app = connect();
const serverInstance: any = http.createServer(app);
let webSocketProxy, messageSocket;
let wsProxy, ms, inspectorProxy;
if (platform !== 'web') {
mime.define({'application/javascript': ['bundle']});
mime.define({'application/json': ['assets']});
messageSocket = requireModule('react-native/local-cli/server/util/messageSocket.js');
webSocketProxy = requireModule('react-native/local-cli/server/util/webSocketProxy.js');
try {
const InspectorProxy = requireModule('react-native/local-cli/server/util/inspectorProxy.js');
inspectorProxy = new InspectorProxy();
} catch (ignored) {}
const copyToClipBoardMiddleware = requireModule('react-native/local-cli/server/middleware/copyToClipBoardMiddleware');
let cpuProfilerMiddleware;
try {
cpuProfilerMiddleware = requireModule('react-native/local-cli/server/middleware/cpuProfilerMiddleware');
} catch (ignored) {}
const getDevToolsMiddleware = requireModule('react-native/local-cli/server/middleware/getDevToolsMiddleware');
let heapCaptureMiddleware;
try {
heapCaptureMiddleware = requireModule('react-native/local-cli/server/middleware/heapCaptureMiddleware.js');
} catch (ignored) {}
const indexPageMiddleware = requireModule('react-native/local-cli/server/middleware/indexPage');
const loadRawBodyMiddleware = requireModule('react-native/local-cli/server/middleware/loadRawBodyMiddleware');
const openStackFrameInEditorMiddleware = requireModule('react-native/local-cli/server/middleware/openStackFrameInEditorMiddleware');
const statusPageMiddleware = requireModule('react-native/local-cli/server/middleware/statusPageMiddleware.js');
const systraceProfileMiddleware = requireModule('react-native/local-cli/server/middleware/systraceProfileMiddleware.js');
const unless = requireModule('react-native/local-cli/server/middleware/unless');
const symbolicateMiddleware = requireModule('haul/src/server/middleware/symbolicateMiddleware');
const args = {
port: config.
devServer.port, projectRoots: [path.resolve('.')],
};
app
.use(loadRawBodyMiddleware)
.use(function (req, res, next) {
req.path = req.url.split('?')[0];
// console.log('req:', req.path);
next();
})
.use(compression())
.use(getDevToolsMiddleware(args, () => wsProxy && wsProxy.isChromeConnected()))
.use(getDevToolsMiddleware(args, () => ms && ms.isChromeConnected()))
.use(liveReloadMiddleware(compiler))
.use(symbolicateMiddleware(compiler))
.use(openStackFrameInEditorMiddleware(args))
.use(copyToClipBoardMiddleware)
.use(statusPageMiddleware)
.use(systraceProfileMiddleware)
.use(indexPageMiddleware)
.use(debugMiddleware)
.use(function (req, res, next) {
const platformPrefix = `/assets/${platform}/`;
if (req.path.indexOf(platformPrefix) === 0) {
const origPath = path.join(path.resolve('.'), req.path.substring(platformPrefix.length));
const extension = path.extname(origPath);
const basePath = path.join(path.dirname(origPath), path.basename(origPath, extension));
const files = [`.${platform}`, '.native', ''].map(suffix => basePath + suffix + extension);
let assetExists = false;
for (const filePath of files) {
if (fs.existsSync(filePath)) {
assetExists = true;
res.writeHead(200, {'Content-Type': mime.lookup(filePath)});
fs.createReadStream(filePath)
.pipe(res);
}
}
if (!assetExists) {
logger.warn('Asset not found:', origPath);
res.writeHead(404, {'Content-Type': 'plain'});
res.end('Asset: ' + origPath + ' not found. Tried: ' + JSON.stringify(files));
}
} else {
next();
}
});
if (heapCaptureMiddleware) {
app.use(heapCaptureMiddleware);
}
if (cpuProfilerMiddleware) {
app.use(cpuProfilerMiddleware);
}
if (inspectorProxy) {
app.use(unless('/inspector', inspectorProxy.processRequest.bind(inspectorProxy)));
}
}
const devMiddleware = webpackDevMiddleware(compiler, _.merge({}, config.devServer, {
reporter({state, stats}) {
if (state) {
logger('bundle is now VALID.');
} else {
logger('bundle is now INVALID.');
}
reporter(null, stats);
},
}));
app.use(function(req, res, next) {
if (platform !== 'web') {
// Workaround for Expo Client bug in parsing Content-Type header with charset
const origSetHeader = res.setHeader;
res.setHeader = function (key, value) {
let val = value;
if (key === 'Content-Type' && value.indexOf('application/javascript') >= 0) {
val = value.split(';')[0];
}
origSetHeader.call(res, key, val);
};
}
return devMiddleware(req, res, next);
})
.use(webpackHotMiddleware(compiler, {log: false}));
if (config.devServer.proxy) {
Object.keys(config.devServer.proxy).forEach(key => {
app.use(httpProxyMiddleware(key, config.devServer.proxy[key]));
});
}
logger(`Webpack ${config.name} dev server listening on http://localhost:${config.devServer.port}`);
serverInstance.listen(config.devServer.port, function () {
if (platform !== 'web') {
wsProxy = webSocketProxy.attachToServer(serverInstance, '/debugger-proxy');
ms = messageSocket.attachToServer(serverInstance, '/message');
webSocketProxy.attachToServer(serverInstance, '/devtools');
if (inspectorProxy) {
inspectorProxy.attachToServer(serverInstance, '/inspector');
}
}
});
serverInstance.timeout = 0;
serverInstance.keepAliveTimeout = 0;
}
function isDllValid(platform, config, options): boolean {
const name = `vendor_${platform}`;
try {
const hashesPath = path.join(options.dllBuildDir, `${name}_dll_hashes.json`);
if (!fs.existsSync(hashesPath)) {
return false;
}
let meta = JSON.parse(fs.readFileSync(hashesPath).toString());
if (!fs.existsSync(path.join(options.dllBuildDir, meta.name))) {
return false;
}
if (!_.isEqual(meta.modules, config.entry.vendor)) {
return false;
}
let json = JSON.parse(fs.readFileSync(path.join(options.dllBuildDir, `${name}_dll.json`)).toString());
for (let filename of Object.keys(json.content)) {
if (filename.indexOf(' ') < 0) {
if (!fs.existsSync(filename)) {
console.warn(`${name} DLL need to be regenerated, file: ${filename} is missing.`);
return false;
}
const hash = crypto.createHash('md5').update(fs.readFileSync(filename)).digest('hex');
if (meta.hashes[filename] !== hash) {
console.warn(`Hash for ${name} DLL file ${filename} has changed, need to rebuild it`);
return false;
}
}
}
return true;
} catch (e) {
console.warn(`Error checking vendor bundle ${name}, regenerating it...`, e);
return false;
}
}
function buildDll(platform, config, options) {
const webpack = requireModule('webpack');
return new Promise(done => {
const name = `vendor_${platform}`;
const logger = minilog(`webpack-for-${config.name}`);
const reporter = (...args) => webpackReporter(true, config.output.path, logger, ...args);
if (!isDllValid(platform, config, options)) {
console.log(`Generating ${name} DLL bundle with modules:\n${JSON.stringify(config.entry.vendor)}`);
mkdirp.sync(options.dllBuildDir);
const compiler = webpack(config);
compiler.plugin('done', stats => {
try {
let json = JSON.parse(fs.readFileSync(path.join(options.dllBuildDir, `${name}_dll.json`)).toString());
const vendorKey = _.findKey(stats.compilation.assets,
(v, key) => key.startsWith('vendor') && key.endsWith('_dll.js'));
let assets = [];
stats.compilation.modules.forEach(function (module) {
if (module._asset) {
assets.push(module._asset);
}
});
fs.writeFileSync(path.join(options.dllBuildDir, `${vendorKey}.assets`), JSON.stringify(assets));
const meta = {name: vendorKey, hashes: {}, modules: config.entry.vendor};
for (let filename of Object.keys(json.content)) {
if (filename.indexOf(' ') < 0) {
meta.hashes[filename] = crypto.createHash('md5').update(fs.readFileSync(filename)).digest('hex');
fs.writeFileSync(path.join(options.dllBuildDir, `${name}_dll_hashes.json`), JSON.stringify(meta));
}
}
} catch (e) {
logger.error(e.stack);
process.exit(1);
}
done();
});
compiler.run(reporter);
} else {
done();
}
});
}
function setupExpoDir(dir, platform) {
const reactNativeDir = path.join(dir, 'node_modules', 'react-native');
mkdirp.sync(path.join(reactNativeDir, 'local-cli'));
fs.writeFileSync(path.join(reactNativeDir, 'package.json'),
fs.readFileSync('node_modules/react-native/package.json'));
fs.writeFileSync(path.join(reactNativeDir, 'local-cli/cli.js'), '');
const pkg = JSON.parse(fs.readFileSync('package.json').toString());
const origDeps = pkg.dependencies;
pkg.dependencies = {'react-native': origDeps['react-native']};
if (platform !== 'all') {
pkg.name = pkg.name + '-' + platform;
}
pkg.main = `index.mobile`;
fs.writeFileSync(path.join(dir, 'package.json'), JSON.stringify(pkg));
const appJson = JSON.parse(fs.readFileSync('app.json').toString());
if (appJson.expo.icon) {
appJson.expo.icon = path.join(path.resolve('.'), appJson.expo.icon);
}
fs.writeFileSync(path.join(dir, 'app.json'), JSON.stringify(appJson));
if (platform !== 'all') {
fs.writeFileSync(path.join(dir, '.exprc'), JSON.stringify({manifestPort: expoPorts[platform]}));
}
}
async function startExpoServer(projectRoot, packagerPort) {
const { Config, Project, ProjectSettings } = requireModule('xdl');
Config.validation.reactNativeVersionWarnings = false;
Config.developerTool = 'crna';
Config.offline = true;
await Project.startExpoServerAsync(projectRoot);
await ProjectSettings.setPackagerInfoAsync(projectRoot, {
packagerPort,
});
}
async function startExpoProject(config, platform) {
const { UrlUtils, Android, Simulator } = requireModule('xdl');
const qr = requireModule('qrcode-terminal');
try {
const projectRoot = path.join(path.resolve('.'), '.expo', platform);
setupExpoDir(projectRoot, platform);
await startExpoServer(projectRoot, config.devServer.port);
const address = await UrlUtils.constructManifestUrlAsync(projectRoot);
const localAddress = await UrlUtils.constructManifestUrlAsync(projectRoot, {
hostType: 'localhost',
});
console.log(`Expo address for ${platform}, Local: ${localAddress}, LAN: ${address}`);
console.log('To open this app on your phone scan this QR code in Expo Client (if it doesn\'t get started automatically)');
qr.generate(address, code => {
console.log(code);
});
if (!containerized()) {
if (platform === 'android') {
const {success, error} = await Android.openProjectAsync(projectRoot);
if (!success) {
console.error(error.message);
}
} else if (platform === 'ios') {
const {success, msg} = await Simulator.openUrlInSimulatorSafeAsync(localAddress);
if (!success) {
console.error('Failed to start Simulator: ', msg);
}
}
}
} catch (e) {
console.error(e.stack);
}
}
function startWebpack(platforms, watch, builder, options) {
if (builder.stack.platform === 'server') {
startServerWebpack(watch, builder, options);
} else {
startClientWebpack(!!platforms.server, watch, builder, options);
}
}
async function allocateExpoPorts(expoPlatforms) {
let startPort = 19000;
const freeportAsync = requireModule('freeport-async');
for (const platform of expoPlatforms) {
const expoPort = await freeportAsync(startPort);
expoPorts[platform] = expoPort;
startPort = expoPort + 1;
}
}
async function startExpoProdServer(options) {
const connect = requireModule('connect');
const mime = requireModule('mime');
const compression = requireModule('compression');
console.log(`Starting Expo prod server`);
const packagerPort = 3030;
const projectRoot = path.join(path.resolve('.'), '.expo', 'all');
startExpoServer(projectRoot, packagerPort);
const app = connect();
app
.use(function (req, res, next) {
req.path = req.url.split('?')[0];
console.log('req:', req.url);
next();
})
.use(compression())
.use(debugMiddleware)
.use(function (req, res, next) {
let platform = url.parse(req.url, true).query.platform;
if (platform) {
const filePath = path.join(options.frontendBuildDir, platform, req.path);
if (fs.existsSync(filePath)) {
res.writeHead(200, {'Content-Type': mime.lookup(filePath)});
fs.createReadStream(filePath)
.pipe(res);
} else {
res.writeHead(404, {'Content-Type': 'application/json'});
res.end(`{'message': 'File not found: ${filePath}'}`);
}
} else {
next();
}
});
const serverInstance: any = http.createServer(app);
console.log(`Production mobile packager listening on http://localhost:${packagerPort}`);
serverInstance.listen(packagerPort);
serverInstance.timeout = 0;
serverInstance.keepAliveTimeout = 0;
}
async function startExp(options) {
const projectRoot = path.join(process.cwd(), '.expo', 'all');
setupExpoDir(projectRoot, 'all');
if (['ba', 'bi', 'build:android', 'build:ios'].indexOf(process.argv[3]) >= 0) {
await startExpoProdServer(options);
}
const exp = spawn(path.join(process.cwd(), 'node_modules/.bin/exp'), process.argv.splice(3), {
cwd: projectRoot,
stdio: [0, 1, 2],
});
exp.on('exit', code => {
process.exit(code);
});
}
const execute = (cmd, argv, builders: Object, options) => {
if (argv.verbose) {
const logger = minilog(`spin`);
for (let name in builders) {
const builder = builders[name];
logger.log(`${name} = `, require('util').inspect(builder.config, false, null));
}
}
if (cmd === 'exp') {
startExp(options);
} else if (cmd === 'test') {
const mochaWebpack = spawn(path.join(process.cwd(), 'node_modules/.bin/mocha-webpack'),
[
'--include',
'babel-polyfill',
'--webpack-config',
'node_modules/spinjs/webpack.config.js',
].concat(process.argv.slice(process.argv.indexOf('test') + 1)), {
stdio: [0, 1, 2],
});
mochaWebpack.on('close', code => {
process.exit(code);
});
} else {
let prepareExpoPromise;
const expoPlatforms = [];
const watch = cmd === 'watch';
const platforms = {};
for (let name in builders) {
const builder = builders[name];
const stack = builder.stack;
platforms[stack.platform] = true;
if (stack.hasAny('react-native') && stack.hasAny('ios')) {
expoPlatforms.push('ios');
} else if (stack.hasAny('react-native') && stack.hasAny('android')) {
expoPlatforms.push('android');
}
}
if (watch && expoPlatforms.length > 0) {
prepareExpoPromise = allocateExpoPorts(expoPlatforms);
} else {
prepareExpoPromise = Promise.resolve();
}
prepareExpoPromise.then(() => {
for (let name in builders) {
const builder = builders[name];
const stack = builder.stack;
if (stack.hasAny(['dll', 'test']))
continue;
const prepareDllPromise: PromiseLike<any> = (cmd === 'watch' && options.webpackDll && builder.child) ?
buildDll(stack.platform, builder.child.config, options) : Promise.resolve();
prepareDllPromise.then(() =>
startWebpack(platforms, watch, builder, options));
}
});
}
};
export default execute;