UNPKG

expo-asset

Version:

An Expo universal module to download assets and pass them into other APIs

178 lines (155 loc) 5.71 kB
import type { AssetDescriptor } from './Asset'; import type { AssetMetadata, AssetSource } from './AssetSources'; import * as AssetUris from './AssetUris'; import * as ImageAssets from './ImageAssets'; export class Asset { private static byHash: Record<string, Asset | undefined> = {}; private static byUri: Record<string, Asset | undefined> = {}; public name: string; public readonly type: string; public readonly hash: string | null = null; public readonly uri: string; public localUri: string | null = null; public width: number | null = null; public height: number | null = null; public downloaded: boolean = true; constructor({ name, type, hash = null, uri, width, height }: AssetDescriptor) { this.name = name; this.type = type; this.hash = hash; this.uri = uri; if (typeof width === 'number') { this.width = width; } if (typeof height === 'number') { this.height = height; } this.name ??= AssetUris.getFilename(uri); this.type ??= AssetUris.getFileExtension(uri); // Essentially run the contents of downloadAsync here. if (ImageAssets.isImageType(this.type)) { this.width = 0; this.height = 0; this.name = AssetUris.getFilename(this.uri); } else { this.name = AssetUris.getFilename(this.uri); } this.localUri = this.uri; } static loadAsync(moduleId: number | number[] | string | string[]): Promise<Asset[]> { const moduleIds = Array.isArray(moduleId) ? moduleId : [moduleId]; return Promise.all(moduleIds.map((moduleId) => Asset.fromModule(moduleId).downloadAsync())); } static fromModule( virtualAssetModule: number | string | { uri: string; width: number; height: number } ): Asset { if (typeof virtualAssetModule === 'string') { return Asset.fromURI(virtualAssetModule); } else if (typeof virtualAssetModule === 'number') { throw new Error( 'Cannot resolve numeric asset IDs on the server as they are non-deterministic identifiers.' ); } if ( typeof virtualAssetModule === 'object' && 'uri' in virtualAssetModule && typeof virtualAssetModule.uri === 'string' ) { const extension = AssetUris.getFileExtension(virtualAssetModule.uri); return new Asset({ name: '', type: extension.startsWith('.') ? extension.substring(1) : extension, hash: null, uri: virtualAssetModule.uri, width: virtualAssetModule.width, height: virtualAssetModule.height, }); } throw new Error('Unexpected asset module ID type: ' + typeof virtualAssetModule); } static fromMetadata(meta: AssetMetadata): Asset { const metaHash = meta.hash; if (Asset.byHash[metaHash]) { return Asset.byHash[metaHash]; } const { uri, hash } = selectAssetSource(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: string): Asset { if (Asset.byUri[uri]) { return Asset.byUri[uri]; } // Possibly a Base64-encoded URI let type = ''; if (uri.indexOf(';base64') > -1) { type = uri.split(';')[0].split('/')[1]; } else { const extension = AssetUris.getFileExtension(uri); type = extension.startsWith('.') ? extension.substring(1) : extension; } const asset = new Asset({ name: '', type, hash: null, uri, }); Asset.byUri[uri] = asset; return asset; } async downloadAsync(): Promise<this> { return this; } } function pickScale(scales: number[], deviceScale: number): number { for (let i = 0; i < scales.length; i++) { if (scales[i] >= deviceScale) { return scales[i]; } } return scales[scales.length - 1] || 1; } /** * Selects the best file for the given asset (ex: choosing the best scale for images) and returns * a { uri, hash } pair for the specific asset file. * * If the asset isn't an image with multiple scales, the first file is selected. */ function selectAssetSource(meta: AssetMetadata): AssetSource { // This logic is based on that of AssetSourceResolver, with additional support for file hashes and // explicitly provided URIs const scale = pickScale(meta.scales, 1); const index = meta.scales.findIndex((s) => s === scale); const hash = meta.fileHashes ? (meta.fileHashes[index] ?? meta.fileHashes[0]) : meta.hash; // Allow asset processors to directly provide the URL to load const uri = meta.fileUris ? (meta.fileUris[index] ?? meta.fileUris[0]) : meta.uri; if (uri) { return { uri, hash }; } const fileScale = scale === 1 ? '' : `@${scale}x`; const fileExtension = meta.type ? `.${encodeURIComponent(meta.type)}` : ''; const suffix = `/${encodeURIComponent(meta.name)}${fileScale}${fileExtension}`; const params = new URLSearchParams({ platform: process.env.EXPO_OS!, hash: meta.hash, }); // For assets with a specified absolute URL, we use the existing origin instead of prepending the // development server or production CDN URL origin if (/^https?:\/\//.test(meta.httpServerLocation)) { const uri = meta.httpServerLocation + suffix + '?' + params; return { uri, hash }; } // In correctly configured apps, we arrive here if the asset is locally available on disk due to // being managed by expo-updates, and `getLocalAssetUri(hash)` must return a local URI for this // hash. Since the asset is local, we don't have a remote URL and specify an invalid URL (an empty // string) as a placeholder. return { uri: '', hash }; }