"use strict";

Object.defineProperty(exports, "__esModule", {
  value: true
});
exports.currentStatus = currentStatus;
exports.getManifestUrlWithFallbackAsync = getManifestUrlWithFallbackAsync;
exports.getSlugAsync = getSlugAsync;
exports.getLatestReleaseAsync = getLatestReleaseAsync;
exports.mergeAppDistributions = mergeAppDistributions;
exports.exportForAppHosting = exportForAppHosting;
exports.findReusableBuildAsync = findReusableBuildAsync;
exports.publishAsync = publishAsync;
exports.buildAsync = buildAsync;
exports.startReactNativeServerAsync = startReactNativeServerAsync;
exports.stopReactNativeServerAsync = stopReactNativeServerAsync;
exports.startExpoServerAsync = startExpoServerAsync;
exports.stopExpoServerAsync = stopExpoServerAsync;
exports.startTunnelsAsync = startTunnelsAsync;
exports.stopTunnelsAsync = stopTunnelsAsync;
exports.setOptionsAsync = setOptionsAsync;
exports.getUrlAsync = getUrlAsync;
exports.optimizeAsync = optimizeAsync;
exports.startAsync = startAsync;
exports.stopWebOnlyAsync = stopWebOnlyAsync;
exports.stopAsync = stopAsync;

function _axios() {
  const data = _interopRequireDefault(require("axios"));

  _axios = function () {
    return data;
  };

  return data;
}

function _chalk() {
  const data = _interopRequireDefault(require("chalk"));

  _chalk = function () {
    return data;
  };

  return data;
}

function _child_process() {
  const data = _interopRequireDefault(require("child_process"));

  _child_process = function () {
    return data;
  };

  return data;
}

function _crypto() {
  const data = _interopRequireDefault(require("crypto"));

  _crypto = function () {
    return data;
  };

  return data;
}

function _delayAsync() {
  const data = _interopRequireDefault(require("delay-async"));

  _delayAsync = function () {
    return data;
  };

  return data;
}

function _decache() {
  const data = _interopRequireDefault(require("decache"));

  _decache = function () {
    return data;
  };

  return data;
}

function _express() {
  const data = _interopRequireDefault(require("express"));

  _express = function () {
    return data;
  };

  return data;
}

function _freeportAsync() {
  const data = _interopRequireDefault(require("freeport-async"));

  _freeportAsync = function () {
    return data;
  };

  return data;
}

function _fsExtra() {
  const data = _interopRequireDefault(require("fs-extra"));

  _fsExtra = function () {
    return data;
  };

  return data;
}

function _hashids() {
  const data = _interopRequireDefault(require("hashids"));

  _hashids = function () {
    return data;
  };

  return data;
}

function _joi() {
  const data = _interopRequireDefault(require("joi"));

  _joi = function () {
    return data;
  };

  return data;
}

function _jsonFile() {
  const data = _interopRequireDefault(require("@expo/json-file"));

  _jsonFile = function () {
    return data;
  };

  return data;
}

function _util() {
  const data = require("util");

  _util = function () {
    return data;
  };

  return data;
}

function _chunk() {
  const data = _interopRequireDefault(require("lodash/chunk"));

  _chunk = function () {
    return data;
  };

  return data;
}

function _escapeRegExp() {
  const data = _interopRequireDefault(require("lodash/escapeRegExp"));

  _escapeRegExp = function () {
    return data;
  };

  return data;
}

function _get() {
  const data = _interopRequireDefault(require("lodash/get"));

  _get = function () {
    return data;
  };

  return data;
}

function _reduce() {
  const data = _interopRequireDefault(require("lodash/reduce"));

  _reduce = function () {
    return data;
  };

  return data;
}

function _set() {
  const data = _interopRequireDefault(require("lodash/set"));

  _set = function () {
    return data;
  };

  return data;
}

function _uniq() {
  const data = _interopRequireDefault(require("lodash/uniq"));

  _uniq = function () {
    return data;
  };

  return data;
}

function _minimatch() {
  const data = _interopRequireDefault(require("minimatch"));

  _minimatch = function () {
    return data;
  };

  return data;
}

function _ngrok() {
  const data = _interopRequireDefault(require("@expo/ngrok"));

  _ngrok = function () {
    return data;
  };

  return data;
}

function _os() {
  const data = _interopRequireDefault(require("os"));

  _os = function () {
    return data;
  };

  return data;
}

function _path() {
  const data = _interopRequireDefault(require("path"));

  _path = function () {
    return data;
  };

  return data;
}

function _semver() {
  const data = _interopRequireDefault(require("semver"));

  _semver = function () {
    return data;
  };

  return data;
}

function _split() {
  const data = _interopRequireDefault(require("split"));

  _split = function () {
    return data;
  };

  return data;
}

function _treeKill() {
  const data = _interopRequireDefault(require("tree-kill"));

  _treeKill = function () {
    return data;
  };

  return data;
}

function _md5hex() {
  const data = _interopRequireDefault(require("md5hex"));

  _md5hex = function () {
    return data;
  };

  return data;
}

function _prettyBytes() {
  const data = _interopRequireDefault(require("pretty-bytes"));

  _prettyBytes = function () {
    return data;
  };

  return data;
}

function _urlJoin() {
  const data = _interopRequireDefault(require("url-join"));

  _urlJoin = function () {
    return data;
  };

  return data;
}

function _uuid() {
  const data = _interopRequireDefault(require("uuid"));

  _uuid = function () {
    return data;
  };

  return data;
}

function _readLastLines() {
  const data = _interopRequireDefault(require("read-last-lines"));

  _readLastLines = function () {
    return data;
  };

  return data;
}

function ConfigUtils() {
  const data = _interopRequireWildcard(require("@expo/config"));

  ConfigUtils = function () {
    return data;
  };

  return data;
}

function Analytics() {
  const data = _interopRequireWildcard(require("./Analytics"));

  Analytics = function () {
    return data;
  };

  return data;
}

function Android() {
  const data = _interopRequireWildcard(require("./Android"));

  Android = function () {
    return data;
  };

  return data;
}

function _Api() {
  const data = _interopRequireDefault(require("./Api"));

  _Api = function () {
    return data;
  };

  return data;
}

function _ApiV() {
  const data = _interopRequireDefault(require("./ApiV2"));

  _ApiV = function () {
    return data;
  };

  return data;
}

function _AssetUtils() {
  const data = require("./AssetUtils");

  _AssetUtils = function () {
    return data;
  };

  return data;
}

function _Config() {
  const data = _interopRequireDefault(require("./Config"));

  _Config = function () {
    return data;
  };

  return data;
}

function Doctor() {
  const data = _interopRequireWildcard(require("./project/Doctor"));

  Doctor = function () {
    return data;
  };

  return data;
}

function DevSession() {
  const data = _interopRequireWildcard(require("./DevSession"));

  DevSession = function () {
    return data;
  };

  return data;
}

function _Logger() {
  const data = _interopRequireDefault(require("./Logger"));

  _Logger = function () {
    return data;
  };

  return data;
}

function ExponentTools() {
  const data = _interopRequireWildcard(require("./detach/ExponentTools"));

  ExponentTools = function () {
    return data;
  };

  return data;
}

function Exp() {
  const data = _interopRequireWildcard(require("./Exp"));

  Exp = function () {
    return data;
  };

  return data;
}

function ExpSchema() {
  const data = _interopRequireWildcard(require("./project/ExpSchema"));

  ExpSchema = function () {
    return data;
  };

  return data;
}

function _FormData() {
  const data = _interopRequireDefault(require("./tools/FormData"));

  _FormData = function () {
    return data;
  };

  return data;
}

function IosPlist() {
  const data = _interopRequireWildcard(require("./detach/IosPlist"));

  IosPlist = function () {
    return data;
  };

  return data;
}

function IosWorkspace() {
  const data = _interopRequireWildcard(require("./detach/IosWorkspace"));

  IosWorkspace = function () {
    return data;
  };

  return data;
}

function ProjectSettings() {
  const data = _interopRequireWildcard(require("./ProjectSettings"));

  ProjectSettings = function () {
    return data;
  };

  return data;
}

function ProjectUtils() {
  const data = _interopRequireWildcard(require("./project/ProjectUtils"));

  ProjectUtils = function () {
    return data;
  };

  return data;
}

function Sentry() {
  const data = _interopRequireWildcard(require("./Sentry"));

  Sentry = function () {
    return data;
  };

  return data;
}

function _StandaloneContext() {
  const data = _interopRequireDefault(require("./detach/StandaloneContext"));

  _StandaloneContext = function () {
    return data;
  };

  return data;
}

function ThirdParty() {
  const data = _interopRequireWildcard(require("./ThirdParty"));

  ThirdParty = function () {
    return data;
  };

  return data;
}

function UrlUtils() {
  const data = _interopRequireWildcard(require("./UrlUtils"));

  UrlUtils = function () {
    return data;
  };

  return data;
}

function _User() {
  const data = _interopRequireWildcard(require("./User"));

  _User = function () {
    return data;
  };

  return data;
}

function _UserSettings() {
  const data = _interopRequireDefault(require("./UserSettings"));

  _UserSettings = function () {
    return data;
  };

  return data;
}

function Versions() {
  const data = _interopRequireWildcard(require("./Versions"));

  Versions = function () {
    return data;
  };

  return data;
}

function Watchman() {
  const data = _interopRequireWildcard(require("./Watchman"));

  Watchman = function () {
    return data;
  };

  return data;
}

function _XDLError() {
  const data = _interopRequireDefault(require("./XDLError"));

  _XDLError = function () {
    return data;
  };

  return data;
}

function Webpack() {
  const data = _interopRequireWildcard(require("./Webpack"));

  Webpack = function () {
    return data;
  };

  return data;
}

function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } else { var newObj = {}; if (obj != null) { for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = Object.defineProperty && Object.getOwnPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : {}; if (desc.get || desc.set) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } } newObj.default = obj; return newObj; } }

function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; }

// @ts-ignore Doctor not yet converted to TypeScript
// @ts-ignore IosPlist not yet converted to TypeScript
// @ts-ignore IosWorkspace not yet converted to TypeScript
const EXPO_CDN = 'https://d1wp6m56sqw74a.cloudfront.net';
const MINIMUM_BUNDLE_SIZE = 500;
const TUNNEL_TIMEOUT = 10 * 1000;
const treekillAsync = (0, _util().promisify)(_treeKill().default);
const ngrokConnectAsync = (0, _util().promisify)(_ngrok().default.connect);
const ngrokKillAsync = (0, _util().promisify)(_ngrok().default.kill);
let _cachedSignedManifest = {
  manifestString: null,
  signedManifest: null
};

async function currentStatus(projectDir) {
  const {
    packagerPort,
    expoServerPort
  } = await ProjectSettings().readPackagerInfoAsync(projectDir);

  if (packagerPort && expoServerPort) {
    return 'running';
  } else if (packagerPort || expoServerPort) {
    return 'ill';
  } else {
    return 'exited';
  }
} // DECPRECATED: use UrlUtils.constructManifestUrlAsync


async function getManifestUrlWithFallbackAsync(projectRoot) {
  return {
    url: await UrlUtils().constructManifestUrlAsync(projectRoot),
    isUrlFallback: false
  };
}

async function _assertValidProjectRoot(projectRoot) {
  if (!projectRoot) {
    throw new (_XDLError().default)('NO_PROJECT_ROOT', 'No project root specified');
  }
}

async function _getFreePortAsync(rangeStart) {
  let port = await (0, _freeportAsync().default)(rangeStart);

  if (!port) {
    throw new (_XDLError().default)('NO_PORT_FOUND', 'No available port found');
  }

  return port;
}

async function _getForPlatformAsync(projectRoot, url, platform, {
  errorCode,
  minLength
}) {
  url = UrlUtils().getPlatformSpecificBundleUrl(url, platform);
  let fullUrl = `${url}&platform=${platform}`;
  let response;

  try {
    response = await _axios().default.get(fullUrl, {
      responseType: 'text',
      // Workaround for https://github.com/axios/axios/issues/907.
      // Without transformResponse, axios will parse the body as JSON regardless of the responseType/
      transformResponse: [data => data],
      proxy: false,
      validateStatus: status => status === 200,
      headers: {
        'Exponent-Platform': platform
      }
    });
  } catch (error) {
    if (error.response) {
      if (error.response.data) {
        let body;

        try {
          body = JSON.parse(error.response.data);
        } catch (e) {
          ProjectUtils().logError(projectRoot, 'expo', error.response.data);
        }

        if (body) {
          if (body.message) {
            ProjectUtils().logError(projectRoot, 'expo', body.message);
          } else {
            ProjectUtils().logError(projectRoot, 'expo', error.response.data);
          }
        }
      }

      throw new (_XDLError().default)(errorCode, `Packager URL ${fullUrl} returned unexpected code ${error.response.status}. ` + 'Please open your project in the Expo app and see if there are any errors. ' + 'Also scroll up and make sure there were no errors or warnings when opening your project.');
    } else {
      throw error;
    }
  }

  if (!response.data || minLength && response.data.length < minLength) {
    throw new (_XDLError().default)(errorCode, `Body is: ${response.data}`);
  }

  return response.data;
}

async function _resolveGoogleServicesFile(projectRoot, manifest) {
  if (manifest.android && manifest.android.googleServicesFile) {
    const contents = await _fsExtra().default.readFile(_path().default.resolve(projectRoot, manifest.android.googleServicesFile), 'utf8');
    manifest.android.googleServicesFile = contents;
  }

  if (manifest.ios && manifest.ios.googleServicesFile) {
    const contents = await _fsExtra().default.readFile(_path().default.resolve(projectRoot, manifest.ios.googleServicesFile), 'base64');
    manifest.ios.googleServicesFile = contents;
  }
}

async function _resolveManifestAssets(projectRoot, manifest, resolver, strict = false) {
  try {
    // Asset fields that the user has set
    const assetSchemas = (await ExpSchema().getAssetSchemasAsync(manifest.sdkVersion)).filter(assetSchema => (0, _get().default)(manifest, assetSchema.fieldPath)); // Get the URLs

    const urls = await Promise.all(assetSchemas.map(async assetSchema => {
      const pathOrURL = (0, _get().default)(manifest, assetSchema.fieldPath);

      if (pathOrURL.match(/^https?:\/\/(.*)$/)) {
        // It's a remote URL
        return pathOrURL;
      } else if (_fsExtra().default.existsSync(_path().default.resolve(projectRoot, pathOrURL))) {
        return await resolver(pathOrURL);
      } else {
        const err = new Error('Could not resolve local asset.');
        err.localAssetPath = pathOrURL;
        err.manifestField = assetSchema.fieldPath;
        throw err;
      }
    })); // Set the corresponding URL fields

    assetSchemas.forEach((assetSchema, index) => (0, _set().default)(manifest, assetSchema.fieldPath + 'Url', urls[index]));
  } catch (e) {
    let logMethod = ProjectUtils().logWarning;

    if (strict) {
      logMethod = ProjectUtils().logError;
    }

    if (e.localAssetPath) {
      logMethod(projectRoot, 'expo', `Unable to resolve asset "${e.localAssetPath}" from "${e.manifestField}" in your app/exp.json.`);
    } else {
      logMethod(projectRoot, 'expo', `Warning: Unable to resolve manifest assets. Icons might not work. ${e.message}.`);
    }

    if (strict) {
      throw new Error('Resolving assets failed.');
    }
  }
}

function _requireFromProject(modulePath, projectRoot, exp) {
  try {
    let fullPath = ConfigUtils().resolveModule(modulePath, projectRoot, exp); // Clear the require cache for this module so get a fresh version of it
    // without requiring the user to restart Expo CLI

    (0, _decache().default)(fullPath); // $FlowIssue: doesn't work with dynamic requires

    return require(fullPath);
  } catch (e) {
    return null;
  }
}

async function getSlugAsync(projectRoot, options = {}) {
  const {
    exp,
    pkg
  } = await ConfigUtils().readConfigJsonAsync(projectRoot);

  if (exp.slug) {
    return exp.slug;
  }

  if (pkg.name) {
    return pkg.name;
  }

  throw new (_XDLError().default)('INVALID_MANIFEST', `app.json in ${projectRoot} must contain the "slug" field`);
}

async function getLatestReleaseAsync(projectRoot, options) {
  // TODO(ville): move request from multipart/form-data to JSON once supported by the endpoint.
  let formData = new (_FormData().default)();
  formData.append('queryType', 'history');
  formData.append('slug', (await getSlugAsync(projectRoot)));
  formData.append('version', '2');
  formData.append('count', '1');
  formData.append('releaseChannel', options.releaseChannel);
  formData.append('platform', options.platform);
  const {
    queryResult
  } = await _Api().default.callMethodAsync('publishInfo', [], 'post', null, {
    formData
  });

  if (queryResult && queryResult.length > 0) {
    return queryResult[0];
  } else {
    return null;
  }
} // Takes multiple exported apps in sourceDirs and coalesces them to one app in outputDir


async function mergeAppDistributions(projectRoot, sourceDirs, outputDir) {
  const assetPathToWrite = _path().default.resolve(projectRoot, outputDir, 'assets');

  await _fsExtra().default.ensureDir(assetPathToWrite);

  const bundlesPathToWrite = _path().default.resolve(projectRoot, outputDir, 'bundles');

  await _fsExtra().default.ensureDir(bundlesPathToWrite); // merge files from bundles and assets

  const androidIndexes = [];
  const iosIndexes = [];

  for (let sourceDir of sourceDirs) {
    const promises = []; // copy over assets/bundles from other src dirs to the output dir

    if (sourceDir !== outputDir) {
      // copy file over to assetPath
      const sourceAssetDir = _path().default.resolve(projectRoot, sourceDir, 'assets');

      const outputAssetDir = _path().default.resolve(projectRoot, outputDir, 'assets');

      const assetPromise = _fsExtra().default.copy(sourceAssetDir, outputAssetDir);

      promises.push(assetPromise); // copy files over to bundlePath

      const sourceBundleDir = _path().default.resolve(projectRoot, sourceDir, 'bundles');

      const outputBundleDir = _path().default.resolve(projectRoot, outputDir, 'bundles');

      const bundlePromise = _fsExtra().default.copy(sourceBundleDir, outputBundleDir);

      promises.push(bundlePromise);
      await Promise.all(promises);
    } // put index.jsons into memory


    const putJsonInMemory = async (indexPath, accumulator) => {
      const index = await _jsonFile().default.readAsync(indexPath);

      if (!index.sdkVersion) {
        throw new (_XDLError().default)('INVALID_MANIFEST', `Invalid index.json, must specify an sdkVersion at ${indexPath}`);
      }

      if (Array.isArray(index)) {
        // index.json could also be an array
        accumulator.push(...index);
      } else {
        accumulator.push(index);
      }
    };

    const androidIndexPath = _path().default.resolve(projectRoot, sourceDir, 'android-index.json');

    await putJsonInMemory(androidIndexPath, androidIndexes);

    const iosIndexPath = _path().default.resolve(projectRoot, sourceDir, 'ios-index.json');

    await putJsonInMemory(iosIndexPath, iosIndexes);
  } // sort indexes by descending sdk value


  const getSortedIndex = indexes => {
    return indexes.sort((index1, index2) => {
      if (_semver().default.eq(index1.sdkVersion, index2.sdkVersion)) {
        _Logger().default.global.error(`Encountered multiple index.json with the same SDK version ${index1.sdkVersion}. This could result in undefined behavior.`);
      }

      return _semver().default.gte(index1.sdkVersion, index2.sdkVersion) ? -1 : 1;
    });
  };

  const sortedAndroidIndexes = getSortedIndex(androidIndexes);
  const sortedIosIndexes = getSortedIndex(iosIndexes); // Save the json arrays to disk

  await _writeArtifactSafelyAsync(projectRoot, null, _path().default.join(outputDir, 'android-index.json'), JSON.stringify(sortedAndroidIndexes));
  await _writeArtifactSafelyAsync(projectRoot, null, _path().default.join(outputDir, 'ios-index.json'), JSON.stringify(sortedIosIndexes));
}
/**
 * Apps exporting for self hosting will have the files created in the project directory the following way:
.
├── android-index.json
├── ios-index.json
├── assets
│   └── 1eccbc4c41d49fd81840aef3eaabe862
└── bundles
      ├── android-01ee6e3ab3e8c16a4d926c91808d5320.js
      └── ios-ee8206cc754d3f7aa9123b7f909d94ea.js
 */


async function exportForAppHosting(projectRoot, publicUrl, assetUrl, outputDir, options = {}) {
  await _validatePackagerReadyAsync(projectRoot); // build the bundles

  let packagerOpts = {
    dev: !!options.isDev,
    minify: true
  }; // make output dirs if not exists

  const assetPathToWrite = _path().default.resolve(projectRoot, _path().default.join(outputDir, 'assets'));

  await _fsExtra().default.ensureDir(assetPathToWrite);

  const bundlesPathToWrite = _path().default.resolve(projectRoot, _path().default.join(outputDir, 'bundles'));

  await _fsExtra().default.ensureDir(bundlesPathToWrite);
  const {
    iosBundle,
    androidBundle
  } = await _buildPublishBundlesAsync(projectRoot, packagerOpts);

  const iosBundleHash = _crypto().default.createHash('md5').update(iosBundle).digest('hex');

  const iosBundleUrl = `ios-${iosBundleHash}.js`;

  const iosJsPath = _path().default.join(outputDir, 'bundles', iosBundleUrl);

  const androidBundleHash = _crypto().default.createHash('md5').update(androidBundle).digest('hex');

  const androidBundleUrl = `android-${androidBundleHash}.js`;

  const androidJsPath = _path().default.join(outputDir, 'bundles', androidBundleUrl);

  await _writeArtifactSafelyAsync(projectRoot, null, iosJsPath, iosBundle);
  await _writeArtifactSafelyAsync(projectRoot, null, androidJsPath, androidBundle);

  _Logger().default.global.info('Finished saving JS Bundles.'); // save the assets
  // Get project config


  const publishOptions = options.publishOptions || {};
  const {
    exp,
    pkg
  } = await _getPublishExpConfigAsync(projectRoot, publishOptions);
  const {
    assets
  } = await _fetchAndSaveAssetsAsync(projectRoot, exp, publicUrl, outputDir);

  if (options.dumpAssetmap) {
    _Logger().default.global.info('Dumping asset map.');

    const assetmap = {};
    assets.forEach(asset => {
      assetmap[asset.hash] = asset;
    });
    await _writeArtifactSafelyAsync(projectRoot, null, _path().default.join(outputDir, 'assetmap.json'), JSON.stringify(assetmap));
  } // Delete keys that are normally deleted in the publish process


  delete exp.hooks; // Add assetUrl to manifest

  exp.assetUrlOverride = assetUrl;
  exp.publishedTime = new Date().toISOString();
  exp.commitTime = new Date().toISOString(); // generate revisionId and id the same way www does

  const hashIds = new (_hashids().default)(_uuid().default.v1(), 10);
  exp.revisionId = hashIds.encode(Date.now());

  if (options.isDev) {
    exp.developer = {
      tool: 'exp'
    };
  }

  if (!exp.slug) {
    throw new (_XDLError().default)('INVALID_MANIFEST', 'Must provide a slug field in the app.json manifest.');
  }

  let username = await _User().default.getCurrentUsernameAsync();

  if (!username) {
    username = _User().ANONYMOUS_USERNAME;
  }

  exp.id = `@${username}/${exp.slug}`; // save the android manifest

  exp.bundleUrl = (0, _urlJoin().default)(publicUrl, 'bundles', androidBundleUrl);
  exp.platform = 'android';
  await _writeArtifactSafelyAsync(projectRoot, null, _path().default.join(outputDir, 'android-index.json'), JSON.stringify({ ...exp,
    dependencies: Object.keys(pkg.dependencies)
  })); // save the ios manifest

  exp.bundleUrl = (0, _urlJoin().default)(publicUrl, 'bundles', iosBundleUrl);
  exp.platform = 'ios';
  await _writeArtifactSafelyAsync(projectRoot, null, _path().default.join(outputDir, 'ios-index.json'), JSON.stringify(exp)); // build source maps

  if (options.dumpSourcemap) {
    const {
      iosSourceMap,
      androidSourceMap
    } = await _buildSourceMapsAsync(projectRoot, exp); // write the sourcemap files

    const iosMapName = `ios-${iosBundleHash}.map`;

    const iosMapPath = _path().default.join(outputDir, 'bundles', iosMapName);

    await _writeArtifactSafelyAsync(projectRoot, null, iosMapPath, iosSourceMap);
    const androidMapName = `android-${androidBundleHash}.map`;

    const androidMapPath = _path().default.join(outputDir, 'bundles', androidMapName);

    await _writeArtifactSafelyAsync(projectRoot, null, androidMapPath, androidSourceMap); // Remove original mapping to incorrect sourcemap paths

    _Logger().default.global.info('Configuring sourcemaps');

    await truncateLastNLines(iosJsPath, 1);
    await truncateLastNLines(androidJsPath, 1); // Add correct mapping to sourcemap paths

    await _fsExtra().default.appendFile(iosJsPath, `\n//# sourceMappingURL=${iosMapName}`);
    await _fsExtra().default.appendFile(androidJsPath, `\n//# sourceMappingURL=${androidMapName}`); // Make a debug html so user can debug their bundles

    _Logger().default.global.info('Preparing additional debugging files');

    const debugHtml = `
    <script src="${(0, _urlJoin().default)('bundles', iosBundleUrl)}"></script>
    <script src="${(0, _urlJoin().default)('bundles', androidBundleUrl)}"></script>
    Open up this file in Chrome. In the Javascript developer console, navigate to the Source tab.
    You can see a red coloured folder containing the original source code from your bundle.
    `;
    await _writeArtifactSafelyAsync(projectRoot, null, _path().default.join(outputDir, 'debug.html'), debugHtml);
  }
} // truncate the last n lines in a file


async function truncateLastNLines(filePath, n) {
  const lines = await _readLastLines().default.read(filePath, n);
  const to_vanquish = lines.length;
  const {
    size
  } = await _fsExtra().default.stat(filePath);
  await _fsExtra().default.truncate(filePath, size - to_vanquish);
}

async function _saveAssetAsync(projectRoot, assets, outputDir) {
  // Collect paths by key, also effectively handles duplicates in the array
  const paths = {};
  assets.forEach(asset => {
    asset.files.forEach((path, index) => {
      paths[asset.fileHashes[index]] = path;
    });
  }); // save files one chunk at a time

  const keyChunks = (0, _chunk().default)(Object.keys(paths), 5);

  for (const keys of keyChunks) {
    const promises = [];

    for (const key of keys) {
      ProjectUtils().logDebug(projectRoot, 'expo', `uploading ${paths[key]}`);

      _Logger().default.global.info({
        quiet: true
      }, `Saving ${paths[key]}`);

      let assetPath = _path().default.resolve(outputDir, 'assets', key); // copy file over to assetPath


      const p = _fsExtra().default.copy(paths[key], assetPath);

      promises.push(p);
    }

    await Promise.all(promises);
  }

  _Logger().default.global.info('Files successfully saved.');
}

async function findReusableBuildAsync(releaseChannel, platform, sdkVersion, slug) {
  const user = await _User().default.getCurrentUserAsync();
  const buildReuseStatus = await _ApiV().default.clientForUser(user).postAsync('standalone-build/reuse', {
    releaseChannel,
    platform,
    sdkVersion,
    slug
  });
  return buildReuseStatus;
}

async function publishAsync(projectRoot, options = {}) {
  const user = await _User().default.ensureLoggedInAsync();
  await _validatePackagerReadyAsync(projectRoot);
  Analytics().logEvent('Publish', {
    projectRoot,
    developerTool: _Config().default.developerTool
  });
  const validationStatus = await Doctor().validateWithNetworkAsync(projectRoot);

  if (validationStatus === Doctor().ERROR || validationStatus === Doctor().FATAL) {
    throw new (_XDLError().default)('PUBLISH_VALIDATION_ERROR', "Couldn't publish because errors were found. (See logs above.) Please fix the errors and try again.");
  } // Get project config


  let {
    exp,
    pkg
  } = await _getPublishExpConfigAsync(projectRoot, options); // TODO: refactor this out to a function, throw error if length doesn't match

  let {
    hooks
  } = exp;
  delete exp.hooks;
  let validPostPublishHooks = [];

  if (hooks && hooks.postPublish) {
    hooks.postPublish.forEach(hook => {
      let {
        file
      } = hook;

      let fn = _requireFromProject(file, projectRoot, exp);

      if (typeof fn !== 'function') {
        _Logger().default.global.error(`Unable to load postPublishHook: '${file}'. The module does not export a function.`);
      } else {
        hook._fn = fn;
        validPostPublishHooks.push(hook);
      }
    });

    if (validPostPublishHooks.length !== hooks.postPublish.length) {
      _Logger().default.global.error();

      throw new (_XDLError().default)('HOOK_INITIALIZATION_ERROR', 'Please fix your postPublish hook configuration.');
    }
  }

  let {
    iosBundle,
    androidBundle
  } = await _buildPublishBundlesAsync(projectRoot);
  await _fetchAndUploadAssetsAsync(projectRoot, exp);
  let {
    iosSourceMap,
    androidSourceMap
  } = await _maybeBuildSourceMapsAsync(projectRoot, exp, {
    force: validPostPublishHooks.length > 0 || exp.android && exp.android.publishSourceMapPath || exp.ios && exp.ios.publishSourceMapPath
  });
  let response;

  try {
    response = await _uploadArtifactsAsync({
      pkg,
      exp,
      iosBundle,
      androidBundle,
      options
    });
  } catch (e) {
    if (e.serverError === 'SCHEMA_VALIDATION_ERROR') {
      throw new Error(`There was an error validating your project schema. Check for any warnings about the contents of your app/exp.json.`);
    }

    Sentry().captureException(e);
    throw e;
  }

  await _maybeWriteArtifactsToDiskAsync({
    exp,
    projectRoot,
    iosBundle,
    androidBundle,
    iosSourceMap,
    androidSourceMap
  });

  if (validPostPublishHooks.length || exp.ios && exp.ios.publishManifestPath || exp.android && exp.android.publishManifestPath) {
    let [androidManifest, iosManifest] = await Promise.all([ExponentTools().getManifestAsync(response.url, {
      'Exponent-SDK-Version': exp.sdkVersion,
      'Exponent-Platform': 'android',
      'Expo-Release-Channel': options.releaseChannel,
      Accept: 'application/expo+json,application/json'
    }), ExponentTools().getManifestAsync(response.url, {
      'Exponent-SDK-Version': exp.sdkVersion,
      'Exponent-Platform': 'ios',
      'Expo-Release-Channel': options.releaseChannel,
      Accept: 'application/expo+json,application/json'
    })]);
    const hookOptions = {
      url: response.url,
      exp,
      iosBundle,
      iosSourceMap,
      iosManifest,
      androidBundle,
      androidSourceMap,
      androidManifest,
      projectRoot,
      log: msg => {
        _Logger().default.global.info({
          quiet: true
        }, msg);
      }
    };

    for (let hook of validPostPublishHooks) {
      _Logger().default.global.info(`Running postPublish hook: ${hook.file}`);

      try {
        let result = hook._fn({
          config: hook.config,
          ...hookOptions
        }); // If it's a promise, wait for it to resolve


        if (result && result.then) {
          result = await result;
        }

        if (result) {
          _Logger().default.global.info({
            quiet: true
          }, result);
        }
      } catch (e) {
        _Logger().default.global.warn(`Warning: postPublish hook '${hook.file}' failed: ${e.stack}`);
      }
    }

    if (exp.ios && exp.ios.publishManifestPath) {
      await _writeArtifactSafelyAsync(projectRoot, 'ios.publishManifestPath', exp.ios.publishManifestPath, JSON.stringify(iosManifest));

      const context = _StandaloneContext().default.createUserContext(projectRoot, exp);

      const {
        supportingDirectory
      } = IosWorkspace().getPaths(context);
      await IosPlist().modifyAsync(supportingDirectory, 'EXShell', shellPlist => {
        shellPlist.releaseChannel = options.releaseChannel;
        return shellPlist;
      });
    }

    if (exp.android && exp.android.publishManifestPath) {
      await _writeArtifactSafelyAsync(projectRoot, 'android.publishManifestPath', exp.android.publishManifestPath, JSON.stringify(androidManifest));
    } // We need to add EmbeddedResponse instances on Android to tell the runtime
    // that the shell app manifest and bundle is packaged.


    if (exp.android && exp.android.publishManifestPath && exp.android.publishBundlePath) {
      let fullManifestUrl = response.url.replace('exp://', 'https://');

      let constantsPath = _path().default.join(projectRoot, 'android', 'app', 'src', 'main', 'java', 'host', 'exp', 'exponent', 'generated', 'AppConstants.java');

      await ExponentTools().deleteLinesInFileAsync(`START EMBEDDED RESPONSES`, `END EMBEDDED RESPONSES`, constantsPath);
      await ExponentTools().regexFileAsync('// ADD EMBEDDED RESPONSES HERE', `
        // ADD EMBEDDED RESPONSES HERE
        // START EMBEDDED RESPONSES
        embeddedResponses.add(new Constants.EmbeddedResponse("${fullManifestUrl}", "assets://shell-app-manifest.json", "application/json"));
        embeddedResponses.add(new Constants.EmbeddedResponse("${androidManifest.bundleUrl}", "assets://shell-app.bundle", "application/javascript"));
        // END EMBEDDED RESPONSES`, constantsPath);
      await ExponentTools().regexFileAsync(/RELEASE_CHANNEL = "[^"]*"/, `RELEASE_CHANNEL = "${options.releaseChannel}"`, constantsPath);
    }
  } // TODO: move to postPublish hook


  if (exp.isKernel) {
    await _handleKernelPublishedAsync({
      user,
      exp,
      projectRoot,
      url: response.url
    });
  }

  return { ...response,
    url: options.releaseChannel && options.releaseChannel !== 'default' ? `${response.url}?release-channel=${options.releaseChannel}` : response.url
  };
}

async function _uploadArtifactsAsync({
  exp,
  iosBundle,
  androidBundle,
  options,
  pkg
}) {
  _Logger().default.global.info('Uploading JavaScript bundles');

  let formData = new (_FormData().default)();
  formData.append('expJson', JSON.stringify(exp));
  formData.append('packageJson', JSON.stringify(pkg));
  formData.append('iosBundle', iosBundle, 'iosBundle');
  formData.append('androidBundle', androidBundle, 'androidBundle');
  formData.append('options', JSON.stringify(options));
  let response = await _Api().default.callMethodAsync('publish', null, 'put', null, {
    formData
  });
  return response;
}

async function _validatePackagerReadyAsync(projectRoot) {
  _assertValidProjectRoot(projectRoot); // Ensure the packager is started


  let packagerInfo = await ProjectSettings().readPackagerInfoAsync(projectRoot);

  if (!packagerInfo.packagerPort) {
    ProjectUtils().logWarning(projectRoot, 'expo', 'Metro Bundler is not running. Trying to restart it...');
    await startReactNativeServerAsync(projectRoot, {
      reset: true
    });
  }
}

async function _getPublishExpConfigAsync(projectRoot, options) {
  let schema = _joi().default.object().keys({
    releaseChannel: _joi().default.string()
  }); // Validate schema


  const {
    error
  } = _joi().default.validate(options, schema);

  if (error) {
    throw new (_XDLError().default)('INVALID_OPTIONS', error.toString());
  }

  options.releaseChannel = options.releaseChannel || 'default'; // joi default not enforcing this :/
  // Verify that exp/app.json and package.json exist

  let {
    exp,
    pkg
  } = await ProjectUtils().readConfigJsonAsync(projectRoot);

  if (!exp || !pkg) {
    const configName = await ConfigUtils().configFilenameAsync(projectRoot);
    throw new (_XDLError().default)('NO_PACKAGE_JSON', `Couldn't read ${configName} file in project at ${projectRoot}`);
  } // Support version and name being specified in package.json for legacy
  // support pre: exp.json


  if (!exp.version && pkg.version) {
    exp.version = pkg.version;
  }

  if (!exp.slug && pkg.name) {
    exp.slug = pkg.name;
  }

  if (exp.android && exp.android.config) {
    delete exp.android.config;
  }

  if (exp.ios && exp.ios.config) {
    delete exp.ios.config;
  }

  const sdkVersion = exp.sdkVersion;

  if (!sdkVersion) {
    throw new (_XDLError().default)('INVALID_OPTIONS', `Cannot publish with sdkVersion '${exp.sdkVersion}'.`);
  } // Only allow projects to be published with UNVERSIONED if a correct token is set in env


  if (sdkVersion === 'UNVERSIONED' && !process.env['EXPO_SKIP_MANIFEST_VALIDATION_TOKEN']) {
    throw new (_XDLError().default)('INVALID_OPTIONS', 'Cannot publish with sdkVersion UNVERSIONED.');
  }

  exp.locales = await ExponentTools().getResolvedLocalesAsync(exp);
  return {
    exp: { ...exp,
      sdkVersion
    },
    pkg
  };
} // Fetch iOS and Android bundles for publishing


async function _buildPublishBundlesAsync(projectRoot, opts) {
  let entryPoint = await Exp().determineEntryPointAsync(projectRoot);
  let publishUrl = await UrlUtils().constructPublishUrlAsync(projectRoot, entryPoint, undefined, opts);

  _Logger().default.global.info('Building iOS bundle');

  let iosBundle = await _getForPlatformAsync(projectRoot, publishUrl, 'ios', {
    errorCode: 'INVALID_BUNDLE',
    minLength: MINIMUM_BUNDLE_SIZE
  });

  _Logger().default.global.info('Building Android bundle');

  let androidBundle = await _getForPlatformAsync(projectRoot, publishUrl, 'android', {
    errorCode: 'INVALID_BUNDLE',
    minLength: MINIMUM_BUNDLE_SIZE
  });
  return {
    iosBundle,
    androidBundle
  };
}

async function _maybeBuildSourceMapsAsync(projectRoot, exp, options = {
  force: false
}) {
  if (options.force) {
    return _buildSourceMapsAsync(projectRoot, exp);
  } else {
    return {
      iosSourceMap: null,
      androidSourceMap: null
    };
  }
} // note(brentvatne): currently we build source map anytime there is a
// postPublish hook -- we may have an option in the future to manually
// enable sourcemap building, but for now it's very fast, most apps in
// production should use sourcemaps for error reporting, and in the worst
// case, adding a few seconds to a postPublish hook isn't too annoying


async function _buildSourceMapsAsync(projectRoot, exp) {
  let entryPoint = await Exp().determineEntryPointAsync(projectRoot);
  let sourceMapUrl = await UrlUtils().constructSourceMapUrlAsync(projectRoot, entryPoint);

  _Logger().default.global.info('Building sourcemaps');

  let iosSourceMap = await _getForPlatformAsync(projectRoot, sourceMapUrl, 'ios', {
    errorCode: 'INVALID_BUNDLE',
    minLength: MINIMUM_BUNDLE_SIZE
  });
  let androidSourceMap = await _getForPlatformAsync(projectRoot, sourceMapUrl, 'android', {
    errorCode: 'INVALID_BUNDLE',
    minLength: MINIMUM_BUNDLE_SIZE
  });
  return {
    iosSourceMap,
    androidSourceMap
  };
}
/**
 * Collects all the assets declared in the android app, ios app and manifest
 *
 * @param {string} hostedAssetPrefix
 *    The path where assets are hosted (ie) http://xxx.cloudfront.com/assets/
 *
 * @modifies {exp} Replaces relative asset paths in the manifest with hosted URLS
 *
 */


async function _collectAssets(projectRoot, exp, hostedAssetPrefix) {
  let entryPoint = await Exp().determineEntryPointAsync(projectRoot);
  let assetsUrl = await UrlUtils().constructAssetsUrlAsync(projectRoot, entryPoint);
  let iosAssetsJson = await _getForPlatformAsync(projectRoot, assetsUrl, 'ios', {
    errorCode: 'INVALID_ASSETS'
  });
  let androidAssetsJson = await _getForPlatformAsync(projectRoot, assetsUrl, 'android', {
    errorCode: 'INVALID_ASSETS'
  }); // Resolve manifest assets to their hosted URL and add them to the list of assets to
  // be uploaded. Modifies exp.

  const manifestAssets = [];
  await _resolveManifestAssets(projectRoot, exp, async assetPath => {
    const absolutePath = _path().default.resolve(projectRoot, assetPath);

    const contents = await _fsExtra().default.readFile(absolutePath);
    const hash = (0, _md5hex().default)(contents);
    manifestAssets.push({
      files: [absolutePath],
      fileHashes: [hash],
      hash
    });
    return (0, _urlJoin().default)(hostedAssetPrefix, hash);
  }, true); // Upload asset files

  const iosAssets = JSON.parse(iosAssetsJson);
  const androidAssets = JSON.parse(androidAssetsJson);
  return iosAssets.concat(androidAssets).concat(manifestAssets);
}
/**
 * Configures exp, preparing it for asset export
 *
 * @modifies {exp}
 *
 */


async function _configureExpForAssets(projectRoot, exp, assets) {
  // Add google services file if it exists
  await _resolveGoogleServicesFile(projectRoot, exp); // Convert asset patterns to a list of asset strings that match them.
  // Assets strings are formatted as `asset_<hash>.<type>` and represent
  // the name that the file will have in the app bundle. The `asset_` prefix is
  // needed because android doesn't support assets that start with numbers.

  if (exp.assetBundlePatterns) {
    const fullPatterns = exp.assetBundlePatterns.map(p => _path().default.join(projectRoot, p));

    _Logger().default.global.info('Processing asset bundle patterns:');

    fullPatterns.forEach(p => _Logger().default.global.info('- ' + p)); // The assets returned by the RN packager has duplicates so make sure we
    // only bundle each once.

    const bundledAssets = new Set();

    for (const asset of assets) {
      const file = asset.files && asset.files[0];
      const shouldBundle = '__packager_asset' in asset && asset.__packager_asset && file && fullPatterns.some(p => (0, _minimatch().default)(file, p));
      ProjectUtils().logDebug(projectRoot, 'expo', `${shouldBundle ? 'Include' : 'Exclude'} asset ${file}`);

      if (shouldBundle) {
        asset.fileHashes.forEach(hash => bundledAssets.add('asset_' + hash + ('type' in asset && asset.type ? '.' + asset.type : '')));
      }
    }

    exp.bundledAssets = [...bundledAssets];
    delete exp.assetBundlePatterns;
  }

  return exp;
}

async function _fetchAndUploadAssetsAsync(projectRoot, exp) {
  _Logger().default.global.info('Analyzing assets');

  const assetCdnPath = (0, _urlJoin().default)(EXPO_CDN, '~assets');
  const assets = await _collectAssets(projectRoot, exp, assetCdnPath);

  _Logger().default.global.info('Uploading assets');

  if (assets.length > 0 && assets[0].fileHashes) {
    await uploadAssetsAsync(projectRoot, assets);
  } else {
    _Logger().default.global.info({
      quiet: true
    }, 'No assets to upload, skipped.');
  } // Updates the manifest to reflect additional asset bundling + configs


  await _configureExpForAssets(projectRoot, exp, assets);
  return exp;
}

async function _fetchAndSaveAssetsAsync(projectRoot, exp, hostedUrl, outputDir) {
  _Logger().default.global.info('Analyzing assets');

  const assetCdnPath = (0, _urlJoin().default)(hostedUrl, 'assets');
  const assets = await _collectAssets(projectRoot, exp, assetCdnPath);

  _Logger().default.global.info('Saving assets');

  if (assets.length > 0 && assets[0].fileHashes) {
    await _saveAssetAsync(projectRoot, assets, outputDir);
  } else {
    _Logger().default.global.info({
      quiet: true
    }, 'No assets to upload, skipped.');
  } // Updates the manifest to reflect additional asset bundling + configs


  await _configureExpForAssets(projectRoot, exp, assets);
  return {
    exp,
    assets
  };
}

async function _writeArtifactSafelyAsync(projectRoot, keyName, artifactPath, artifact) {
  const pathToWrite = _path().default.resolve(projectRoot, artifactPath);

  if (!_fsExtra().default.existsSync(_path().default.dirname(pathToWrite))) {
    const errorMsg = keyName ? `app.json specifies: ${pathToWrite}, but that directory does not exist.` : `app.json specifies ${keyName}: ${pathToWrite}, but that directory does not exist.`;

    _Logger().default.global.warn(errorMsg);
  } else {
    await _fsExtra().default.writeFile(pathToWrite, artifact);
  }
}

async function _maybeWriteArtifactsToDiskAsync({
  exp,
  projectRoot,
  iosBundle,
  androidBundle,
  iosSourceMap,
  androidSourceMap
}) {
  if (exp.android && exp.android.publishBundlePath) {
    await _writeArtifactSafelyAsync(projectRoot, 'android.publishBundlePath', exp.android.publishBundlePath, androidBundle);
  }

  if (exp.ios && exp.ios.publishBundlePath) {
    await _writeArtifactSafelyAsync(projectRoot, 'ios.publishBundlePath', exp.ios.publishBundlePath, iosBundle);
  }

  if (exp.android && exp.android.publishSourceMapPath && androidSourceMap) {
    await _writeArtifactSafelyAsync(projectRoot, 'android.publishSourceMapPath', exp.android.publishSourceMapPath, androidSourceMap);
  }

  if (exp.ios && exp.ios.publishSourceMapPath && iosSourceMap) {
    await _writeArtifactSafelyAsync(projectRoot, 'ios.publishSourceMapPath', exp.ios.publishSourceMapPath, iosSourceMap);
  }
}

async function _handleKernelPublishedAsync({
  projectRoot,
  user,
  exp,
  url
}) {
  let kernelBundleUrl = `${_Config().default.api.scheme}://${_Config().default.api.host}`;

  if (_Config().default.api.port) {
    kernelBundleUrl = `${kernelBundleUrl}:${_Config().default.api.port}`;
  }

  kernelBundleUrl = `${kernelBundleUrl}/@${user.username}/${exp.slug}/bundle`;

  if (exp.kernel.androidManifestPath) {
    let manifest = await ExponentTools().getManifestAsync(url, {
      'Exponent-SDK-Version': exp.sdkVersion,
      'Exponent-Platform': 'android',
      Accept: 'application/expo+json,application/json'
    });
    manifest.bundleUrl = kernelBundleUrl;
    manifest.sdkVersion = 'UNVERSIONED';
    await _fsExtra().default.writeFile(_path().default.resolve(projectRoot, exp.kernel.androidManifestPath), JSON.stringify(manifest));
  }

  if (exp.kernel.iosManifestPath) {
    let manifest = await ExponentTools().getManifestAsync(url, {
      'Exponent-SDK-Version': exp.sdkVersion,
      'Exponent-Platform': 'ios',
      Accept: 'application/expo+json,application/json'
    });
    manifest.bundleUrl = kernelBundleUrl;
    manifest.sdkVersion = 'UNVERSIONED';
    await _fsExtra().default.writeFile(_path().default.resolve(projectRoot, exp.kernel.iosManifestPath), JSON.stringify(manifest));
  }
} // TODO(jesse): Add analytics for upload


async function uploadAssetsAsync(projectRoot, assets) {
  // Collect paths by key, also effectively handles duplicates in the array
  const paths = {};
  assets.forEach(asset => {
    asset.files.forEach((path, index) => {
      paths[asset.fileHashes[index]] = path;
    });
  }); // Collect list of assets missing on host

  const metas = (await _Api().default.callMethodAsync('assetsMetadata', [], 'post', {
    keys: Object.keys(paths)
  })).metadata;
  const missing = Object.keys(paths).filter(key => !metas[key].exists);

  if (missing.length === 0) {
    _Logger().default.global.info({
      quiet: true
    }, `No assets changed, skipped.`);
  } // Upload them!


  await Promise.all((0, _chunk().default)(missing, 5).map(async keys => {
    let formData = new (_FormData().default)();

    for (const key of keys) {
      ProjectUtils().logDebug(projectRoot, 'expo', `uploading ${paths[key]}`);
      let relativePath = paths[key].replace(projectRoot, '');

      _Logger().default.global.info({
        quiet: true
      }, `Uploading ${relativePath}`);

      formData.append(key, _fsExtra().default.createReadStream(paths[key]), paths[key]);
    }

    await _Api().default.callMethodAsync('uploadAssets', [], 'put', null, {
      formData
    });
  }));
}

async function getConfigAsync(projectRoot, options = {}) {
  if (!options.publicUrl) {
    // get the manifest from the project directory
    const {
      exp,
      pkg
    } = await ProjectUtils().readConfigJsonAsync(projectRoot);
    const configName = await ConfigUtils().configFilenameAsync(projectRoot);
    return {
      exp,
      pkg,
      configName: await ConfigUtils().configFilenameAsync(projectRoot),
      configPrefix: configName === 'app.json' ? 'expo.' : ''
    };
  } else {
    // get the externally hosted manifest
    return {
      exp: await ThirdParty().getManifest(options.publicUrl, options),
      configName: options.publicUrl,
      configPrefix: '',
      pkg: {}
    };
  }
} // TODO(ville): add the full type


async function buildAsync(projectRoot, options = {}) {
  await _User().default.ensureLoggedInAsync();

  _assertValidProjectRoot(projectRoot);

  Analytics().logEvent('Build Shell App', {
    projectRoot,
    developerTool: _Config().default.developerTool,
    platform: options.platform
  });

  const schema = _joi().default.object().keys({
    current: _joi().default.boolean(),
    mode: _joi().default.string(),
    platform: _joi().default.any().valid('ios', 'android', 'all'),
    expIds: _joi().default.array(),
    type: _joi().default.any().valid('archive', 'simulator', 'client', 'app-bundle', 'apk'),
    releaseChannel: _joi().default.string().regex(/[a-z\d][a-z\d._-]*/),
    bundleIdentifier: _joi().default.string().regex(/^[a-zA-Z][a-zA-Z0-9\-.]+$/),
    publicUrl: _joi().default.string(),
    sdkVersion: _joi().default.strict()
  });

  const {
    error
  } = _joi().default.validate(options, schema);

  if (error) {
    throw new (_XDLError().default)('INVALID_OPTIONS', error.toString());
  }

  const {
    exp,
    pkg,
    configName,
    configPrefix
  } = await getConfigAsync(projectRoot, options);

  if (!exp || !pkg) {
    throw new (_XDLError().default)('NO_PACKAGE_JSON', `Couldn't read ${configName} file in project at ${projectRoot}`);
  } // Support version and name being specified in package.json for legacy
  // support pre: exp.json


  if (!exp.version && 'version' in pkg && pkg.version) {
    exp.version = pkg.version;
  }

  if (!exp.slug && 'name' in pkg && pkg.name) {
    exp.slug = pkg.name;
  }

  if (options.mode !== 'status' && (options.platform === 'ios' || options.platform === 'all')) {
    if (!exp.ios || !exp.ios.bundleIdentifier) {
      throw new (_XDLError().default)('INVALID_MANIFEST', `Must specify a bundle identifier in order to build this experience for iOS. ` + `Please specify one in ${configName} at "${configPrefix}ios.bundleIdentifier"`);
    }
  }

  if (options.mode !== 'status' && (options.platform === 'android' || options.platform === 'all')) {
    if (!exp.android || !exp.android.package) {
      throw new (_XDLError().default)('INVALID_MANIFEST', `Must specify a java package in order to build this experience for Android. ` + `Please specify one in ${configName} at "${configPrefix}android.package"`);
    }
  }

  return await _Api().default.callMethodAsync('build', [], 'put', {
    manifest: exp,
    options
  });
}

async function _waitForRunningAsync(projectRoot, url, retries = 300) {
  try {
    let response = await _axios().default.get(url, {
      responseType: 'text',
      proxy: false
    });

    if (/packager-status:running/.test(response.data)) {
      return true;
    } else if (retries === 0) {
      ProjectUtils().logError(projectRoot, 'expo', `Could not get status from Metro bundler. Server response: ${response.data}`);
    }
  } catch (e) {
    if (retries === 0) {
      ProjectUtils().logError(projectRoot, 'expo', `Could not get status from Metro bundler. ${e.message}`);
    }
  }

  if (retries <= 0) {
    throw new Error('Connecting to Metro bundler failed.');
  } else {
    await (0, _delayAsync().default)(100);
    return _waitForRunningAsync(projectRoot, url, retries - 1);
  }
}

function _logPackagerOutput(projectRoot, level, data) {
  let output = data.toString();

  if (!output) {
    return;
  } // Temporarily hide warnings about duplicate providesModule declarations
  // under react-native


  if (_isIgnorableDuplicateModuleWarning(projectRoot, level, output)) {
    ProjectUtils().logDebug(projectRoot, 'expo', `Suppressing @providesModule warning: ${output}`, 'project-suppress-providesmodule-warning');
    return;
  }

  if (/^Scanning folders for symlinks in /.test(output)) {
    ProjectUtils().logDebug(projectRoot, 'metro', output);
    return;
  }

  if (level === 'info') {
    ProjectUtils().logInfo(projectRoot, 'metro', output);
  } else {
    ProjectUtils().logError(projectRoot, 'metro', output);
  }
}

function _isIgnorableDuplicateModuleWarning(projectRoot, level, output) {
  if (level !== 'error' || !output.startsWith('jest-haste-map: @providesModule naming collision:')) {
    return false;
  }

  let reactNativeNodeModulesPath = _path().default.join(projectRoot, 'node_modules', 'react-native', 'node_modules');

  let reactNativeNodeModulesPattern = (0, _escapeRegExp().default)(reactNativeNodeModulesPath);
  let reactNativeNodeModulesCollisionRegex = new RegExp(`Paths: ${reactNativeNodeModulesPattern}.+ collides with ${reactNativeNodeModulesPattern}.+`);
  return reactNativeNodeModulesCollisionRegex.test(output);
}

function _isIgnorableBugReportingExtraData(body) {
  return body.length === 2 && body[0] === 'BugReporting extraData:';
}

function _isAppRegistryStartupMessage(body) {
  return body.length === 1 && /^Running application "main" with appParams:/.test(body[0]);
}

function _handleDeviceLogs(projectRoot, deviceId, deviceName, logs) {
  for (let i = 0; i < logs.length; i++) {
    let log = logs[i];
    let body = typeof log.body === 'string' ? [log.body] : log.body;
    let {
      level
    } = log;

    if (_isIgnorableBugReportingExtraData(body)) {
      level = _Logger().default.DEBUG;
    }

    if (_isAppRegistryStartupMessage(body)) {
      body = [`Running application on ${deviceName}.`];
    }

    let string = body.map(obj => {
      if (typeof obj === 'undefined') {
        return 'undefined';
      }

      if (obj === 'null') {
        return 'null';
      }

      if (typeof obj === 'string' || typeof obj === 'number' || typeof obj === 'boolean') {
        return obj;
      }

      try {
        return JSON.stringify(obj);
      } catch (e) {
        return obj.toString();
      }
    }).join(' ');
    ProjectUtils().logWithLevel(projectRoot, level, {
      tag: 'device',
      deviceId,
      deviceName,
      groupDepth: log.groupDepth,
      shouldHide: log.shouldHide,
      includesStack: log.includesStack
    }, string);
  }
}

async function startReactNativeServerAsync(projectRoot, options = {}, verbose = true) {
  _assertValidProjectRoot(projectRoot);

  await stopReactNativeServerAsync(projectRoot);
  await Watchman().addToPathAsync(); // Attempt to fix watchman if it's hanging

  await Watchman().unblockAndGetVersionAsync(projectRoot);
  let {
    exp
  } = await ConfigUtils().readConfigJsonAsync(projectRoot);
  let packagerPort = await _getFreePortAsync(19001); // Create packager options

  let packagerOpts = {
    port: packagerPort,
    customLogReporterPath: ConfigUtils().resolveModule('expo/tools/LogReporter', projectRoot, exp),
    assetExts: ['ttf'],
    sourceExts: ['expo.js', 'expo.ts', 'expo.tsx', 'expo.json', 'js', 'json', 'ts', 'tsx'],
    nonPersistent: !!options.nonPersistent
  };

  if (Versions().gteSdkVersion(exp, '33.0.0')) {
    packagerOpts.assetPlugins = ConfigUtils().resolveModule('expo/tools/hashAssetFiles', projectRoot, exp);
  }

  if (options.maxWorkers) {
    packagerOpts['max-workers'] = options.maxWorkers;
  }

  if (!Versions().gteSdkVersion(exp, '16.0.0')) {
    delete packagerOpts.customLogReporterPath;
  }

  const userPackagerOpts = exp.packagerOpts;

  if (userPackagerOpts) {
    // The RN CLI expects rn-cli.config.js's path to be absolute. We use the
    // project root to resolve relative paths since that was the original
    // behavior of the RN CLI.
    if (userPackagerOpts.config) {
      userPackagerOpts.config = _path().default.resolve(projectRoot, userPackagerOpts.config);
    }

    packagerOpts = { ...packagerOpts,
      ...userPackagerOpts,
      ...(userPackagerOpts.assetExts ? {
        assetExts: (0, _uniq().default)([...packagerOpts.assetExts, ...userPackagerOpts.assetExts])
      } : {})
    };

    if (userPackagerOpts.port !== undefined && userPackagerOpts.port !== null) {
      packagerPort = userPackagerOpts.port;
    }
  }

  let cliOpts = (0, _reduce().default)(packagerOpts, (opts, val, key) => {
    // If the packager opt value is boolean, don't set
    // --[opt] [value], just set '--opt'
    if (val && typeof val === 'boolean') {
      opts.push(`--${key}`);
    } else if (val) {
      opts.push(`--${key}`, val);
    }

    return opts;
  }, ['start']);

  if (options.reset) {
    cliOpts.push('--reset-cache');
  } // Get custom CLI path from project package.json, but fall back to node_module path


  let defaultCliPath = ConfigUtils().resolveModule('react-native/local-cli/cli.js', projectRoot, exp);
  const cliPath = exp.rnCliPath || defaultCliPath;
  let nodePath; // When using a custom path for the RN CLI, we want it to use the project
  // root to look up config files and Node modules

  if (exp.rnCliPath) {
    nodePath = _nodePathForProjectRoot(projectRoot);
  } else {
    nodePath = null;
  } // Run the copy of Node that's embedded in Electron by setting the
  // ELECTRON_RUN_AS_NODE environment variable
  // Note: the CLI script sets up graceful-fs and sets ulimit to 4096 in the
  // child process


  let packagerProcess = _child_process().default.fork(cliPath, cliOpts, {
    cwd: projectRoot,
    env: { ...process.env,
      REACT_NATIVE_APP_ROOT: projectRoot,
      ELECTRON_RUN_AS_NODE: '1',
      ...(nodePath ? {
        NODE_PATH: nodePath
      } : {})
    },
    silent: true
  });

  await ProjectSettings().setPackagerInfoAsync(projectRoot, {
    packagerPort,
    packagerPid: packagerProcess.pid
  }); // TODO: do we need this? don't know if it's ever called

  process.on('exit', () => {
    (0, _treeKill().default)(packagerProcess.pid);
  });

  if (!packagerProcess.stdout) {
    throw new Error('Expected spawned process to have a stdout stream, but none was found.');
  }

  if (!packagerProcess.stderr) {
    throw new Error('Expected spawned process to have a stderr stream, but none was found.');
  }

  packagerProcess.stdout.setEncoding('utf8');
  packagerProcess.stderr.setEncoding('utf8');
  packagerProcess.stdout.pipe((0, _split().default)()).on('data', data => {
    if (verbose) {
      _logPackagerOutput(projectRoot, 'info', data);
    }
  });
  packagerProcess.stderr.on('data', data => {
    if (verbose) {
      _logPackagerOutput(projectRoot, 'error', data);
    }
  });
  let exitPromise = new Promise((resolve, reject) => {
    packagerProcess.once('exit', async code => {
      ProjectUtils().logDebug(projectRoot, 'expo', `Metro Bundler process exited with code ${code}`);

      if (code) {
        reject(new Error(`Metro Bundler process exited with code ${code}`));
      } else {
        resolve();
      }

      try {
        await ProjectSettings().setPackagerInfoAsync(projectRoot, {
          packagerPort: null,
          packagerPid: null
        });
      } catch (e) {}
    });
  });
  let packagerUrl = await UrlUtils().constructBundleUrlAsync(projectRoot, {
    urlType: 'http',
    hostType: 'localhost'
  });
  await Promise.race([_waitForRunningAsync(projectRoot, `${packagerUrl}/status`), exitPromise]);
} // Simulate the node_modules resolution
// If you project dir is /Jesse/Expo/Universe/BubbleBounce, returns
// "/Jesse/node_modules:/Jesse/Expo/node_modules:/Jesse/Expo/Universe/node_modules:/Jesse/Expo/Universe/BubbleBounce/node_modules"


function _nodePathForProjectRoot(projectRoot) {
  let paths = [];

  let directory = _path().default.resolve(projectRoot);

  while (true) {
    paths.push(_path().default.join(directory, 'node_modules'));

    let parentDirectory = _path().default.dirname(directory);

    if (directory === parentDirectory) {
      break;
    }

    directory = parentDirectory;
  }

  return paths.join(_path().default.delimiter);
}

async function stopReactNativeServerAsync(projectRoot) {
  _assertValidProjectRoot(projectRoot);

  let packagerInfo = await ProjectSettings().readPackagerInfoAsync(projectRoot);

  if (!packagerInfo.packagerPort || !packagerInfo.packagerPid) {
    ProjectUtils().logDebug(projectRoot, 'expo', `No packager found for project at ${projectRoot}.`);
    return;
  }

  ProjectUtils().logDebug(projectRoot, 'expo', `Killing packager process tree: ${packagerInfo.packagerPid}`);

  try {
    await treekillAsync(packagerInfo.packagerPid, 'SIGKILL');
  } catch (e) {
    ProjectUtils().logDebug(projectRoot, 'expo', `Error stopping packager process: ${e.toString()}`);
  }

  await ProjectSettings().setPackagerInfoAsync(projectRoot, {
    packagerPort: null,
    packagerPid: null
  });
}

let blacklistedEnvironmentVariables = new Set(['EXPO_APPLE_PASSWORD', 'EXPO_ANDROID_KEY_PASSWORD', 'EXPO_ANDROID_KEYSTORE_PASSWORD', 'EXPO_IOS_DIST_P12_PASSWORD', 'EXPO_IOS_PUSH_P12_PASSWORD', 'EXPO_CLI_PASSWORD']);

function shouldExposeEnvironmentVariableInManifest(key) {
  if (blacklistedEnvironmentVariables.has(key.toUpperCase())) {
    return false;
  }

  return key.startsWith('REACT_NATIVE_') || key.startsWith('EXPO_');
}

async function startExpoServerAsync(projectRoot) {
  _assertValidProjectRoot(projectRoot);

  await stopExpoServerAsync(projectRoot);
  let app = (0, _express().default)();
  app.use(_express().default.json({
    limit: '10mb'
  }));
  app.use(_express().default.urlencoded({
    limit: '10mb',
    extended: true
  }));

  if ((await Doctor().validateWithNetworkAsync(projectRoot)) === Doctor().FATAL) {
    throw new Error(`Couldn't start project. Please fix the errors and restart the project.`);
  } // Serve the manifest.


  const manifestHandler = async (req, res) => {
    try {
      // We intentionally don't `await`. We want to continue trying even
      // if there is a potential error in the package.json and don't want to slow
      // down the request
      Doctor().validateWithNetworkAsync(projectRoot);
      let {
        exp: manifest
      } = await ConfigUtils().readConfigJsonAsync(projectRoot);

      if (!manifest) {
        const configName = await ConfigUtils().configFilenameAsync(projectRoot);
        throw new Error(`No ${configName} file found`);
      } // Get packager opts and then copy into bundleUrlPackagerOpts


      let packagerOpts = await ProjectSettings().getPackagerOptsAsync(projectRoot);
      let bundleUrlPackagerOpts = JSON.parse(JSON.stringify(packagerOpts));
      bundleUrlPackagerOpts.urlType = 'http';

      if (bundleUrlPackagerOpts.hostType === 'redirect') {
        bundleUrlPackagerOpts.hostType = 'tunnel';
      }

      manifest.xde = true; // deprecated

      manifest.developer = {
        tool: _Config().default.developerTool,
        projectRoot
      };
      manifest.packagerOpts = packagerOpts;
      manifest.env = {};

      for (let key of Object.keys(process.env)) {
        if (shouldExposeEnvironmentVariableInManifest(key)) {
          manifest.env[key] = process.env[key];
        }
      }

      let entryPoint = await Exp().determineEntryPointAsync(projectRoot);
      let platform = (req.headers['exponent-platform'] || 'ios').toString();
      entryPoint = UrlUtils().getPlatformSpecificBundleUrl(entryPoint, platform);
      let mainModuleName = UrlUtils().guessMainModulePath(entryPoint);
      let queryParams = await UrlUtils().constructBundleQueryParamsAsync(projectRoot, packagerOpts);
      let path = `/${encodeURI(mainModuleName)}.bundle?platform=${encodeURIComponent(platform)}&${queryParams}`;
      manifest.bundleUrl = (await UrlUtils().constructBundleUrlAsync(projectRoot, bundleUrlPackagerOpts, req.hostname)) + path;
      manifest.debuggerHost = await UrlUtils().constructDebuggerHostAsync(projectRoot, req.hostname);
      manifest.mainModuleName = mainModuleName;
      manifest.logUrl = await UrlUtils().constructLogUrlAsync(projectRoot, req.hostname);
      manifest.hostUri = await UrlUtils().constructHostUriAsync(projectRoot, req.hostname);
      await _resolveManifestAssets(projectRoot, manifest, async path => manifest.bundleUrl.match(/^https?:\/\/.*?\//)[0] + 'assets/' + path); // the server normally inserts this but if we're offline we'll do it here

      await _resolveGoogleServicesFile(projectRoot, manifest);
      const hostUUID = await _UserSettings().default.anonymousIdentifier();
      let currentSession = await _User().default.getSessionAsync();

      if (!currentSession || _Config().default.offline) {
        manifest.id = `@${_User().ANONYMOUS_USERNAME}/${manifest.slug}-${hostUUID}`;
      }

      let manifestString = JSON.stringify(manifest);

      if (req.headers['exponent-accept-signature']) {
        if (_cachedSignedManifest.manifestString === manifestString) {
          manifestString = _cachedSignedManifest.signedManifest;
        } else {
          if (!currentSession || _Config().default.offline) {
            const unsignedManifest = {
              manifestString,
              signature: 'UNSIGNED'
            };
            _cachedSignedManifest.manifestString = manifestString;
            manifestString = JSON.stringify(unsignedManifest);
            _cachedSignedManifest.signedManifest = manifestString;
          } else {
            let publishInfo = await Exp().getPublishInfoAsync(projectRoot);
            let signedManifest = await _Api().default.callMethodAsync('signManifest', [publishInfo.args], 'post', manifest);
            _cachedSignedManifest.manifestString = manifestString;
            _cachedSignedManifest.signedManifest = signedManifest.response;
            manifestString = signedManifest.response;
          }
        }
      }

      const hostInfo = {
        host: hostUUID,
        server: 'xdl',
        serverVersion: require('../package.json').version,
        serverDriver: _Config().default.developerTool,
        serverOS: _os().default.platform(),
        serverOSVersion: _os().default.release()
      };
      res.append('Exponent-Server', JSON.stringify(hostInfo));
      res.send(manifestString);
      Analytics().logEvent('Serve Manifest', {
        projectRoot,
        developerTool: _Config().default.developerTool
      });
    } catch (e) {
      ProjectUtils().logError(projectRoot, 'expo', e.stack); // 5xx = Server Error HTTP code

      res.status(520).send({
        error: e.toString()
      });
    }
  };

  app.get('/', manifestHandler);
  app.get('/manifest', manifestHandler);
  app.get('/index.exp', manifestHandler);
  app.post('/logs', async (req, res) => {
    try {
      let deviceId = req.get('Device-Id');
      let deviceName = req.get('Device-Name');

      if (deviceId && deviceName && req.body) {
        _handleDeviceLogs(projectRoot, deviceId, deviceName, req.body);
      }
    } catch (e) {
      ProjectUtils().logError(projectRoot, 'expo', `Error getting device logs: ${e} ${e.stack}`);
    }

    res.send('Success');
  });
  app.post('/shutdown', async (req, res) => {
    server.close();
    res.send('Success');
  });
  let expRc = await ProjectUtils().readExpRcAsync(projectRoot);
  let expoServerPort = expRc.manifestPort ? expRc.manifestPort : await _getFreePortAsync(19000);
  await ProjectSettings().setPackagerInfoAsync(projectRoot, {
    expoServerPort
  });
  let server = app.listen(expoServerPort, () => {
    const info = server.address();
    const host = info.address;
    const port = info.port;
    ProjectUtils().logDebug(projectRoot, 'expo', `Local server listening at http://${host}:${port}`);
  });
  await Exp().saveRecentExpRootAsync(projectRoot);
}

async function stopExpoServerAsync(projectRoot) {
  _assertValidProjectRoot(projectRoot);

  let packagerInfo = await ProjectSettings().readPackagerInfoAsync(projectRoot);

  if (packagerInfo && packagerInfo.expoServerPort) {
    try {
      await _axios().default.post(`http://127.0.0.1:${packagerInfo.expoServerPort}/shutdown`);
    } catch (e) {}
  }

  await ProjectSettings().setPackagerInfoAsync(projectRoot, {
    expoServerPort: null
  });
}

async function _connectToNgrokAsync(projectRoot, args, hostnameAsync, ngrokPid, attempts = 0) {
  try {
    const configPath = _path().default.join(_UserSettings().default.dotExpoHomeDirectory(), 'ngrok.yml');

    const hostname = await hostnameAsync();
    const url = await ngrokConnectAsync({
      hostname,
      configPath,
      ...args
    });
    return url;
  } catch (e) {
    // Attempt to connect 3 times
    if (attempts >= 2) {
      if (e.message) {
        throw new (_XDLError().default)('NGROK_ERROR', e.toString());
      } else {
        throw new (_XDLError().default)('NGROK_ERROR', JSON.stringify(e));
      }
    }

    if (!attempts) {
      attempts = 0;
    } // Attempt to fix the issue


    if (e.error_code && e.error_code === 103) {
      if (attempts === 0) {
        // Failed to start tunnel. Might be because url already bound to another session.
        if (ngrokPid) {
          try {
            process.kill(ngrokPid, 'SIGKILL');
          } catch (e) {
            ProjectUtils().logDebug(projectRoot, 'expo', `Couldn't kill ngrok with PID ${ngrokPid}`);
          }
        } else {
          await ngrokKillAsync();
        }
      } else {
        // Change randomness to avoid conflict if killing ngrok didn't help
        await Exp().resetProjectRandomnessAsync(projectRoot);
      }
    } // Wait 100ms and then try again


    await (0, _delayAsync().default)(100);
    return _connectToNgrokAsync(projectRoot, args, hostnameAsync, null, attempts + 1);
  }
}

async function startTunnelsAsync(projectRoot) {
  const username = (await _User().default.getCurrentUsernameAsync()) || _User().ANONYMOUS_USERNAME;

  _assertValidProjectRoot(projectRoot);

  const packagerInfo = await ProjectSettings().readPackagerInfoAsync(projectRoot);

  if (!packagerInfo.packagerPort) {
    throw new (_XDLError().default)('NO_PACKAGER_PORT', `No packager found for project at ${projectRoot}.`);
  }

  if (!packagerInfo.expoServerPort) {
    throw new (_XDLError().default)('NO_EXPO_SERVER_PORT', `No Expo server found for project at ${projectRoot}.`);
  }

  const expoServerPort = packagerInfo.expoServerPort;
  await stopTunnelsAsync(projectRoot);

  if (await Android().startAdbReverseAsync(projectRoot)) {
    ProjectUtils().logInfo(projectRoot, 'expo', 'Successfully ran `adb reverse`. Localhost URLs should work on the connected Android device.');
  }

  let packageShortName = _path().default.parse(projectRoot).base;

  let expRc = await ConfigUtils().readExpRcAsync(projectRoot);
  let startedTunnelsSuccessfully = false; // Some issues with ngrok cause it to hang indefinitely. After
  // TUNNEL_TIMEOUTms we just throw an error.

  await Promise.race([(async () => {
    await (0, _delayAsync().default)(TUNNEL_TIMEOUT);

    if (!startedTunnelsSuccessfully) {
      throw new Error('Starting tunnels timed out');
    }
  })(), (async () => {
    let expoServerNgrokUrl = await _connectToNgrokAsync(projectRoot, {
      authtoken: _Config().default.ngrok.authToken,
      port: expoServerPort,
      proto: 'http'
    }, async () => {
      let randomness = expRc.manifestTunnelRandomness ? expRc.manifestTunnelRandomness : await Exp().getProjectRandomnessAsync(projectRoot);
      return [randomness, UrlUtils().domainify(username), UrlUtils().domainify(packageShortName), _Config().default.ngrok.domain].join('.');
    }, packagerInfo.ngrokPid);
    let packagerNgrokUrl = await _connectToNgrokAsync(projectRoot, {
      authtoken: _Config().default.ngrok.authToken,
      port: packagerInfo.packagerPort,
      proto: 'http'
    }, async () => {
      let randomness = expRc.manifestTunnelRandomness ? expRc.manifestTunnelRandomness : await Exp().getProjectRandomnessAsync(projectRoot);
      return ['packager', randomness, UrlUtils().domainify(username), UrlUtils().domainify(packageShortName), _Config().default.ngrok.domain].join('.');
    }, packagerInfo.ngrokPid);
    await ProjectSettings().setPackagerInfoAsync(projectRoot, {
      expoServerNgrokUrl,
      packagerNgrokUrl,
      ngrokPid: _ngrok().default.process().pid
    });
    startedTunnelsSuccessfully = true;
    ProjectUtils().logWithLevel(projectRoot, 'info', {
      tag: 'expo',
      _expoEventType: 'TUNNEL_READY'
    }, 'Tunnel ready.');

    _ngrok().default.addListener('statuschange', status => {
      if (status === 'reconnecting') {
        ProjectUtils().logError(projectRoot, 'expo', 'We noticed your tunnel is having issues. ' + 'This may be due to intermittent problems with our tunnel provider. ' + 'If you have trouble connecting to your app, try to Restart the project, ' + 'or switch Host to LAN.');
      } else if (status === 'online') {
        ProjectUtils().logInfo(projectRoot, 'expo', 'Tunnel connected.');
      }
    });
  })()]);
}

async function stopTunnelsAsync(projectRoot) {
  _assertValidProjectRoot(projectRoot); // This will kill all ngrok tunnels in the process.
  // We'll need to change this if we ever support more than one project
  // open at a time in XDE.


  let packagerInfo = await ProjectSettings().readPackagerInfoAsync(projectRoot);

  let ngrokProcess = _ngrok().default.process();

  let ngrokProcessPid = ngrokProcess ? ngrokProcess.pid : null;

  _ngrok().default.removeAllListeners('statuschange');

  if (packagerInfo.ngrokPid && packagerInfo.ngrokPid !== ngrokProcessPid) {
    // Ngrok is running in some other process. Kill at the os level.
    try {
      process.kill(packagerInfo.ngrokPid);
    } catch (e) {
      ProjectUtils().logDebug(projectRoot, 'expo', `Couldn't kill ngrok with PID ${packagerInfo.ngrokPid}`);
    }
  } else {
    // Ngrok is running from the current process. Kill using ngrok api.
    await ngrokKillAsync();
  }

  await ProjectSettings().setPackagerInfoAsync(projectRoot, {
    expoServerNgrokUrl: null,
    packagerNgrokUrl: null,
    ngrokPid: null
  });
  await Android().stopAdbReverseAsync(projectRoot);
}

async function setOptionsAsync(projectRoot, options) {
  _assertValidProjectRoot(projectRoot); // Check to make sure all options are valid


  let schema = _joi().default.object().keys({
    packagerPort: _joi().default.number().integer()
  });

  const {
    error
  } = _joi().default.validate(options, schema);

  if (error) {
    throw new (_XDLError().default)('INVALID_OPTIONS', error.toString());
  }

  await ProjectSettings().setPackagerInfoAsync(projectRoot, options);
} // DEPRECATED(2019-08-21): use UrlUtils.constructManifestUrlAsync


async function getUrlAsync(projectRoot, options = {}) {
  _assertValidProjectRoot(projectRoot);

  return await UrlUtils().constructManifestUrlAsync(projectRoot, options);
}

async function optimizeAsync(projectRoot = './', options) {
  _Logger().default.global.info(_chalk().default.green('Optimizing assets...'));

  const {
    assetJson,
    assetInfo
  } = await (0, _AssetUtils().readAssetJsonAsync)(projectRoot); // Keep track of which hash values in assets.json are no longer in use

  const outdated = new Set();

  for (const fileHash in assetInfo) outdated.add(fileHash);

  let totalSaved = 0;
  const {
    allFiles,
    selectedFiles
  } = await (0, _AssetUtils().getAssetFilesAsync)(projectRoot, options);
  const hashes = {}; // Remove assets that have been deleted/modified from assets.json

  allFiles.forEach(filePath => {
    const hash = (0, _AssetUtils().calculateHash)(filePath);

    if (assetInfo[hash]) {
      outdated.delete(hash);
    }

    hashes[filePath] = hash;
  });
  outdated.forEach(outdatedHash => {
    delete assetInfo[outdatedHash];
  });
  const {
    quality,
    include,
    exclude,
    save
  } = options;
  const images = include || exclude ? selectedFiles : allFiles;

  for (const image of images) {
    const hash = hashes[image];

    if (assetInfo[hash]) {
      continue;
    }

    const {
      size: prevSize
    } = _fsExtra().default.statSync(image);

    const newName = (0, _AssetUtils().createNewFilename)(image);
    const optimizedImage = await (0, _AssetUtils().optimizeImageAsync)(image, quality);

    const {
      size: newSize
    } = _fsExtra().default.statSync(optimizedImage);

    const amountSaved = prevSize - newSize;

    if (amountSaved > 0) {
      await _fsExtra().default.move(image, newName);
      await _fsExtra().default.move(optimizedImage, image);
    } else {
      assetInfo[hash] = true;

      _Logger().default.global.info(_chalk().default.gray(amountSaved === 0 ? `Compressed version of ${image} same size as original. Using original instead.` : `Compressed version of ${image} was larger than original. Using original instead.`));

      continue;
    } // Recalculate hash since the image has changed


    const newHash = (0, _AssetUtils().calculateHash)(image);
    assetInfo[newHash] = true;

    if (save) {
      if (hash === newHash) {
        _Logger().default.global.info(_chalk().default.gray(`Compressed asset ${image} is identical to the original. Using original instead.`));

        _fsExtra().default.unlinkSync(newName);
      } else {
        _Logger().default.global.info(_chalk().default.gray(`Saving original asset to ${newName}`)); // Save the old hash to prevent reoptimizing


        assetInfo[hash] = true;
      }
    } else {
      // Delete the renamed original asset
      _fsExtra().default.unlinkSync(newName);
    }

    if (amountSaved) {
      totalSaved += amountSaved;

      _Logger().default.global.info(`Saved ${(0, _prettyBytes().default)(amountSaved)}`);
    } else {
      _Logger().default.global.info(_chalk().default.gray(`Nothing to compress.`));
    }
  }

  if (totalSaved === 0) {
    _Logger().default.global.info('No assets optimized. Everything is fully compressed!');
  } else {
    _Logger().default.global.info(`Finished compressing assets. ${_chalk().default.green((0, _prettyBytes().default)(totalSaved))} saved.`);
  }

  assetJson.writeAsync(assetInfo);
}

async function startAsync(projectRoot, options = {}, verbose = true) {
  _assertValidProjectRoot(projectRoot);

  Analytics().logEvent('Start Project', {
    projectRoot,
    developerTool: _Config().default.developerTool
  });
  let {
    exp
  } = await ConfigUtils().readConfigJsonAsync(projectRoot, options.webOnly);

  if (options.webOnly) {
    await Webpack().restartAsync(projectRoot, options);
    DevSession().startSession(projectRoot, exp, 'web');
    return exp;
  } else {
    await startExpoServerAsync(projectRoot);
    await startReactNativeServerAsync(projectRoot, options, verbose);
    DevSession().startSession(projectRoot, exp, 'native');
  }

  if (!_Config().default.offline) {
    try {
      await startTunnelsAsync(projectRoot);
    } catch (e) {
      ProjectUtils().logDebug(projectRoot, 'expo', `Error starting tunnel ${e.message}`);
    }
  }

  return exp;
}

async function _stopInternalAsync(projectRoot) {
  DevSession().stopSession();
  await Webpack().stopAsync(projectRoot);
  ProjectUtils().logInfo(projectRoot, 'expo', '\u203A Closing Expo server');
  await stopExpoServerAsync(projectRoot);
  ProjectUtils().logInfo(projectRoot, 'expo', '\u203A Stopping Metro bundler');
  await stopReactNativeServerAsync(projectRoot);

  if (!_Config().default.offline) {
    try {
      await stopTunnelsAsync(projectRoot);
    } catch (e) {
      ProjectUtils().logDebug(projectRoot, 'expo', `Error stopping ngrok ${e.message}`);
    }
  }
}

async function stopWebOnlyAsync(projectDir) {
  await Webpack().stopAsync(projectDir);
  await DevSession().stopSession();
}

async function stopAsync(projectDir) {
  const result = await Promise.race([_stopInternalAsync(projectDir), new Promise((resolve, reject) => setTimeout(resolve, 2000, 'stopFailed'))]);

  if (result === 'stopFailed') {
    // find RN packager and ngrok pids, attempt to kill them manually
    const {
      packagerPid,
      ngrokPid
    } = await ProjectSettings().readPackagerInfoAsync(projectDir);

    if (packagerPid) {
      try {
        process.kill(packagerPid);
      } catch (e) {}
    }

    if (ngrokPid) {
      try {
        process.kill(ngrokPid);
      } catch (e) {}
    }

    await ProjectSettings().setPackagerInfoAsync(projectDir, {
      expoServerPort: null,
      packagerPort: null,
      packagerPid: null,
      expoServerNgrokUrl: null,
      packagerNgrokUrl: null,
      ngrokPid: null,
      webpackServerPort: null
    });
  }
}
//# sourceMappingURL=__sourcemaps__/Project.js.map