You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

358 lines
12 KiB

"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_<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));
// 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