"use strict"; Object.defineProperty(exports, "__esModule", { value: true }); exports.exportAssetsAsync = exportAssetsAsync; exports.publishAssetsAsync = publishAssetsAsync; exports.resolveGoogleServicesFile = resolveGoogleServicesFile; exports.resolveManifestAssets = resolveManifestAssets; function _assert() { const data = _interopRequireDefault(require("assert")); _assert = function () { return data; }; return data; } function _formData() { const data = _interopRequireDefault(require("form-data")); _formData = function () { return data; }; return data; } function _fsExtra() { const data = _interopRequireDefault(require("fs-extra")); _fsExtra = function () { return data; }; return data; } function _chunk() { const data = _interopRequireDefault(require("lodash/chunk")); _chunk = function () { return data; }; return data; } function _get() { const data = _interopRequireDefault(require("lodash/get")); _get = function () { return data; }; return data; } function _set() { const data = _interopRequireDefault(require("lodash/set")); _set = function () { return data; }; return data; } function _uniqBy() { const data = _interopRequireDefault(require("lodash/uniqBy")); _uniqBy = function () { return data; }; return data; } function _md5hex() { const data = _interopRequireDefault(require("md5hex")); _md5hex = function () { return data; }; return data; } function _minimatch() { const data = _interopRequireDefault(require("minimatch")); _minimatch = function () { return data; }; return data; } function _path() { const data = _interopRequireDefault(require("path")); _path = function () { return data; }; return data; } function _urlJoin() { const data = _interopRequireDefault(require("url-join")); _urlJoin = function () { return data; }; return data; } function _internal() { const data = require("./internal"); _internal = function () { return data; }; return data; } function _interopRequireDefault(obj) { return obj && obj.__esModule ? obj : { default: obj }; } const EXPO_CDN = 'https://classic-assets.eascdn.net'; async function resolveGoogleServicesFile(projectRoot, manifest) { var _manifest$android, _manifest$ios; if ((_manifest$android = manifest.android) !== null && _manifest$android !== void 0 && _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) !== null && _manifest$ios !== void 0 && _manifest$ios.googleServicesFile) { const contents = await _fsExtra().default.readFile(_path().default.resolve(projectRoot, manifest.ios.googleServicesFile), 'base64'); manifest.ios.googleServicesFile = contents; } } /** * Get all fields in the manifest that match assets, then filter the ones that aren't set. * * @param manifest * @returns Asset fields that the user has set like ["icon", "splash.image", ...] */ async function getAssetFieldPathsForManifestAsync(manifest) { // String array like ["icon", "notification.icon", "loading.icon", "loading.backgroundImage", "ios.icon", ...] const sdkAssetFieldPaths = await _internal().ExpSchema.getAssetSchemasAsync(manifest.sdkVersion); return sdkAssetFieldPaths.filter(assetSchema => (0, _get().default)(manifest, assetSchema)); } async function resolveManifestAssets({ projectRoot, manifest, resolver, strict = false }) { try { // Asset fields that the user has set like ["icon", "splash.image"] const assetSchemas = await getAssetFieldPathsForManifestAsync(manifest); // Get the URLs const urls = await Promise.all(assetSchemas.map(async manifestField => { const pathOrURL = (0, _get().default)(manifest, manifestField); if (/^https?:\/\//.test(pathOrURL)) { // 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 = manifestField; throw err; } })); // Set the corresponding URL fields assetSchemas.forEach((manifestField, index) => (0, _set().default)(manifest, `${manifestField}Url`, urls[index])); } catch (e) { let logMethod = _internal().ProjectUtils.logWarning; if (strict) { logMethod = _internal().ProjectUtils.logError; } if (e.localAssetPath) { logMethod(projectRoot, 'expo', `Unable to resolve asset "${e.localAssetPath}" from "${e.manifestField}" in your app.json or app.config.js`); } else { logMethod(projectRoot, 'expo', `Warning: Unable to resolve manifest assets. Icons might not work. ${e.message}.`); } if (strict) { throw new Error('Resolving assets failed.'); } } } /** * 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_.` 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)); // Only log the patterns in debug mode, if they aren't already defined in the app.json, then all files will be targeted. _internal().Logger.global.info('\nProcessing asset bundle patterns:'); fullPatterns.forEach(p => _internal().Logger.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)); _internal().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 publishAssetsAsync(options) { return exportAssetsAsync({ ...options, hostedUrl: EXPO_CDN, assetPath: '~assets' }); } async function exportAssetsAsync({ projectRoot, exp, hostedUrl, assetPath, outputDir, bundles, experimentalBundle }) { _internal().Logger.global.info('Analyzing assets'); let assets; if (experimentalBundle) { (0, _assert().default)(outputDir, 'outputDir must be specified when exporting to EAS'); assets = (0, _uniqBy().default)(Object.values(bundles).flatMap(bundle => bundle.assets), asset => asset.hash); } else { const assetCdnPath = (0, _urlJoin().default)(hostedUrl, assetPath); assets = await collectAssets(projectRoot, exp, assetCdnPath, bundles); } _internal().Logger.global.info('Saving assets'); if (assets.length > 0 && assets[0].fileHashes) { if (outputDir) { await saveAssetsAsync(projectRoot, assets, outputDir); } else { // No output directory defined, use remote url. await uploadAssetsAsync(projectRoot, assets); } } else { _internal().Logger.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 }; } /** * Collect list of assets missing on host * * @param paths asset paths found locally that need to be uploaded. */ async function fetchMissingAssetsAsync(paths) { const user = await _internal().UserManager.ensureLoggedInAsync(); const api = _internal().ApiV2.clientForUser(user); const result = await api.postAsync('assets/metadata', { keys: paths }); const metas = result.metadata; const missing = paths.filter(key => !metas[key].exists); return missing; } function logAssetTask(projectRoot, action, pathName) { _internal().ProjectUtils.logDebug(projectRoot, 'expo', `${action} ${pathName}`); const relativePath = pathName.replace(projectRoot, ''); _internal().Logger.global.info({ quiet: true }, `${action} ${relativePath}`); } // TODO(jesse): Add analytics for upload async function uploadAssetsAsync(projectRoot, assets) { // Collect paths by key, also effectively handles duplicates in the array const paths = collectAssetPaths(assets); const missing = await fetchMissingAssetsAsync(Object.keys(paths)); if (missing.length === 0) { _internal().Logger.global.info({ quiet: true }, `No assets changed, skipped.`); return; } const keyChunks = (0, _chunk().default)(missing, 5); // Upload them in chunks of 5 to prevent network and system issues. for (const keys of keyChunks) { const formData = new (_formData().default)(); for (const key of keys) { const pathName = paths[key]; logAssetTask(projectRoot, 'uploading', pathName); formData.append(key, _fsExtra().default.createReadStream(pathName), pathName); } // TODO: Document what's going on const user = await _internal().UserManager.ensureLoggedInAsync(); const api = _internal().ApiV2.clientForUser(user); await api.uploadFormDataAsync('assets/upload', formData); } } function collectAssetPaths(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; }); }); return paths; } async function saveAssetsAsync(projectRoot, assets, outputDir) { // Collect paths by key, also effectively handles duplicates in the array const paths = collectAssetPaths(assets); // 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) { const pathName = paths[key]; logAssetTask(projectRoot, 'saving', pathName); const assetPath = _path().default.resolve(outputDir, 'assets', key); // copy file over to assetPath promises.push(_fsExtra().default.copy(pathName, assetPath)); } await Promise.all(promises); } _internal().Logger.global.info('Files successfully saved.'); } /** * 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, bundles) { // 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, manifest: exp, async resolver(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); }, strict: true }); return [...Object.values(bundles).flatMap(bundle => bundle.assets), ...manifestAssets]; } //# sourceMappingURL=ProjectAssets.js.map