@expo/xdl
Version:
The Expo Development Library
478 lines (362 loc) • 13.7 kB
JavaScript
;
Object.defineProperty(exports, "__esModule", {
value: true
});
exports.resolveGoogleServicesFile = resolveGoogleServicesFile;
exports.resolveManifestAssets = resolveManifestAssets;
exports.publishAssetsAsync = publishAssetsAsync;
exports.exportAssetsAsync = exportAssetsAsync;
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 _ApiV() {
const data = _interopRequireDefault(require("./ApiV2"));
_ApiV = function () {
return data;
};
return data;
}
function _Logger() {
const data = _interopRequireDefault(require("./Logger"));
_Logger = function () {
return data;
};
return data;
}
function _User() {
const data = _interopRequireDefault(require("./User"));
_User = function () {
return data;
};
return data;
}
function ExpSchema() {
const data = _interopRequireWildcard(require("./project/ExpSchema"));
ExpSchema = function () {
return data;
};
return data;
}
function ProjectUtils() {
const data = _interopRequireWildcard(require("./project/ProjectUtils"));
ProjectUtils = function () {
return data;
};
return data;
}
function _getRequireWildcardCache() { if (typeof WeakMap !== "function") return null; var cache = new WeakMap(); _getRequireWildcardCache = function () { return cache; }; return cache; }
function _interopRequireWildcard(obj) { if (obj && obj.__esModule) { return obj; } if (obj === null || typeof obj !== "object" && typeof obj !== "function") { return { default: obj }; } var cache = _getRequireWildcardCache(); if (cache && cache.has(obj)) { return cache.get(obj); } var newObj = {}; var hasPropertyDescriptor = Object.defineProperty && Object.getOwnPropertyDescriptor; for (var key in obj) { if (Object.prototype.hasOwnProperty.call(obj, key)) { var desc = hasPropertyDescriptor ? Object.getOwnPropertyDescriptor(obj, key) : null; if (desc && (desc.get || desc.set)) { Object.defineProperty(newObj, key, desc); } else { newObj[key] = obj[key]; } } } newObj.default = obj; if (cache) { cache.set(obj, newObj); } return newObj; }
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 ? 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 ? 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 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 (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 = manifestField;
throw err;
}
})); // Set the corresponding URL fields
assetSchemas.forEach((manifestField, index) => (0, _set().default)(manifest, `${manifestField}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.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.
_Logger().default.global.info('\nProcessing 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 publishAssetsAsync(options) {
return exportAssetsAsync({ ...options,
hostedUrl: EXPO_CDN,
assetPath: '~assets'
});
}
async function exportAssetsAsync({
projectRoot,
exp,
hostedUrl,
assetPath,
outputDir,
bundles,
experimentalBundle
}) {
_Logger().default.global.info('Analyzing assets');
let assets;
if (experimentalBundle) {
(0, _assert().default)(outputDir, 'outputDir must be specified when exporting to EAS');
assets = (0, _uniqBy().default)([...bundles.android.assets, ...bundles.ios.assets], asset => asset.hash);
} else {
const assetCdnPath = (0, _urlJoin().default)(hostedUrl, assetPath);
assets = await collectAssets(projectRoot, exp, assetCdnPath, bundles);
}
_Logger().default.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 {
_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
};
}
/**
* 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 _User().default.ensureLoggedInAsync();
const api = _ApiV().default.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) {
ProjectUtils().logDebug(projectRoot, 'expo', `${action} ${pathName}`);
const relativePath = pathName.replace(projectRoot, '');
_Logger().default.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) {
_Logger().default.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 _User().default.ensureLoggedInAsync();
const api = _ApiV().default.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);
}
_Logger().default.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 [...bundles.ios.assets, ...bundles.android.assets, ...manifestAssets];
}
//# sourceMappingURL=__sourcemaps__/ProjectAssets.js.map