"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