"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