react-native-dom-expo
Version:
A patch library to make Expo work with react-native-dom
257 lines (216 loc) • 7.53 kB
JavaScript
// TODO this may include unnecessary code
/* eslint-disable global-require, prefer-destructuring, prefer-const */
import { Image, PixelRatio, Platform } from 'react-native';
import AssetRegistry from 'react-native/Libraries/Image/AssetRegistry';
import AssetSourceResolver from 'react-native/Libraries/Image/AssetSourceResolver';
import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource';
import uriparser from 'uri-parser';
import urljoin from 'url-join';
let Constants;
try {
Constants = require('expo-constants').default;
} catch (error) {
throw new Error('`expo-asset` requires `expo-constants` package to be installed and linked.');
}
const parser = new uriparser.Parser();
// Fast lookup check if asset map has any overrides in the manifest
const assetMapOverride = Constants.manifest && Constants.manifest.assetMapOverride;
// get base url of manifest (ie) https://site.com
const getManifestBaseUrl = (manifestUrl) => {
const urlComponents = parser.parse(manifestUrl);
// change to http(s) if scheme is exp/exps
if (urlComponents.protocol === 'exp') {
urlComponents.protocol = 'http';
} else if (urlComponents.protocol === 'exps') {
urlComponents.protocol = 'https';
}
// trim file extension and query params, if any (ie) https://site.com/index.exp?s=q -> https://site.com
urlComponents.relative = urlComponents.directory;
return parser.format(urlComponents);
};
// compute manifest base url if available
const manifestBaseUrl = Constants.experienceUrl
? getManifestBaseUrl(Constants.experienceUrl)
: null;
// resolve against manifestBaseUrl if uri is relative
const resolveIfRelative = (uri) => {
const uriComponents = parser.parse(uri);
if (uriComponents.protocol === '' && manifestBaseUrl) {
// uri is relative (ie) ./assets, remove ./ prefix if exists
return urljoin(manifestBaseUrl, uri.replace(/^\.?\//, ''));
}
return uri;
};
function isImageType(type) {
return /^(jpeg|jpg|gif|png|bmp)$/i.test(type);
}
function filenameFromUrl(url) {
const urlComponents = parser.parse(url);
return urlComponents.file;
}
function getExtension(url) {
const filename = filenameFromUrl(url);
const dotIndex = filename.lastIndexOf('.');
// Ignore leading dots for hidden files
return dotIndex > 0 ? filename.substring(dotIndex) : '';
}
// Return { uri, hash } for an asset's file, picking the correct scale, based on its React Native
// metadata. If the asset isn't an image just picks the first file.
const pickScale = (originalMeta) => {
// Override with the asset map in manifest if available
const meta = assetMapOverride
? { ...originalMeta, ...assetMapOverride[originalMeta.hash] }
: originalMeta;
// This logic is based on that in AssetSourceResolver.js, we just do it with our own tweaks for
// Expo
const scale = meta.scales.length > 1
? AssetSourceResolver.pickScale(meta.scales, PixelRatio.get()) : 1;
const index = meta.scales.findIndex(s => s === scale);
const hash = meta.fileHashes ? meta.fileHashes[index] || meta.fileHashes[0] : meta.hash;
const uri = meta.fileUris ? meta.fileUris[index] || meta.fileUris[0] : meta.uri;
const suffix = '/' + meta.name + (scale === 1 ? '' : '@' + scale + 'x')
+ '.' + meta.type + '?platform=' + Platform.OS + '&hash=' + meta.hash;
// Allow asset processors to directly provide the URL that will be loaded
if (uri) {
return {
uri: resolveIfRelative(uri),
hash,
};
}
// Check if the assetUrl was overriden in the manifest
const assetUrlOverride = Constants.manifest && Constants.manifest.assetUrlOverride;
if (assetUrlOverride) {
return {
uri: resolveIfRelative(urljoin(assetUrlOverride, hash)),
hash,
};
}
if (/^https?:/.test(meta.httpServerLocation)) {
// This is a full URL, so we avoid prepending bundle URL/cloudfront
// This usually means Asset is on a different server, and the URL is present in the bundle
return {
uri: meta.httpServerLocation + suffix,
hash,
};
}
if (Constants.manifest && Constants.manifest.developer) {
// Development server URI is pieced together
return {
uri: Constants.manifest.bundleUrl.match(/^https?:\/\/.*?\//)[0]
+ meta.httpServerLocation.replace(/^\/?/, '') + suffix,
hash,
};
}
// CDN URI is based directly on the hash
return {
uri: 'https://d1wp6m56sqw74a.cloudfront.net/~assets/' + hash,
hash,
};
};
const getImageInfoAsync = src => new Promise((resolve, reject) => {
Image.getSize(src, (width, height) => {
resolve({ width, height });
}, reject);
});
export default class Asset {
static byHash = {};
static byUri = {};
constructor({
name, type, hash, uri, width, height,
}) {
this.name = name;
this.type = type;
this.hash = hash;
this.uri = uri;
this.localUri = null;
if (typeof width === 'number') {
this.width = width;
}
if (typeof height === 'number') {
this.height = height;
}
this.downloading = false;
this.downloaded = !!this.localUri;
this.downloadCallbacks = [];
}
static loadAsync(moduleId) {
const moduleIds = typeof moduleId === 'number' ? [moduleId] : moduleId;
return Promise.all(moduleIds.map(m => Asset.fromModule(m).downloadAsync()));
}
static fromModule(virtualAssetModule) {
if (typeof virtualAssetModule === 'string') {
return Asset.fromURI(virtualAssetModule);
}
const meta = AssetRegistry.getAssetByID(virtualAssetModule);
return Asset.fromMetadata(meta);
}
static fromMetadata(meta) {
// The hash of the whole asset, not to confuse with the hash of a specific
// file returned from `pickScale`.
const metaHash = meta.hash;
if (Asset.byHash[metaHash]) {
return Asset.byHash[metaHash];
}
const { uri, hash } = pickScale(meta);
const asset = new Asset({
name: meta.name,
type: meta.type,
hash,
uri,
width: meta.width,
height: meta.height,
});
Asset.byHash[metaHash] = asset;
return asset;
}
static fromURI(uri) {
// Possibly Base64
if (Asset.byUri[uri]) {
return Asset.byUri[uri];
}
let type = '';
if (uri.indexOf(';base64') > -1) {
type = uri.split(';')[0].split('/')[1];
} else {
type = getExtension(uri);
}
const asset = new Asset({
type,
uri,
});
Asset.byUri[uri] = asset;
return asset;
}
async downloadAsync() {
if (this.downloaded) {
return;
}
if (this.downloading) {
await new Promise((resolve, reject) => this.downloadCallbacks.push({ resolve, reject }));
return;
}
this.downloading = true;
try {
if (isImageType(this.type)) {
const { width, height } = await getImageInfoAsync(this.uri);
this.width = width;
this.height = height;
}
this.name = filenameFromUrl(this.uri);
this.localUri = this.uri;
this.downloaded = true;
this.downloadCallbacks.forEach(({ resolve }) => resolve());
} catch (e) {
this.downloadCallbacks.forEach(({ reject }) => reject(e));
throw e;
} finally {
this.downloading = false;
this.downloadCallbacks = [];
}
}
}
// Override React Native's asset resolution for `Image` components
resolveAssetSource.setCustomSourceTransformer((resolver) => {
const asset = Asset.fromMetadata(resolver.asset);
return resolver.fromSource(asset.downloaded ? asset.localUri : asset.uri);
});