UNPKG

5.27 kBJavaScriptView Raw
1'use strict';
2
3import { NativeModules, PixelRatio, Platform } from 'react-native';
4import AssetRegistry from 'react-native/Libraries/Image/AssetRegistry';
5import AssetSourceResolver from 'react-native/Libraries/Image/AssetSourceResolver';
6import resolveAssetSource from 'react-native/Libraries/Image/resolveAssetSource';
7
8import Constants from './Constants';
9
10const FS = NativeModules.ExponentFileSystem;
11
12// Fast lookup check if assets are available in the local bundle.
13const bundledAssets = new Set(FS.bundledAssets || []);
14
15// Return { uri, hash } for an asset's file, picking the correct scale, based on its React Native
16// metadata. If the asset isn't an image just picks the first file.
17const pickScale = meta => {
18 // This logic is based on that in AssetSourceResolver.js, we just do it with our own tweaks for
19 // Expo
20
21 const scale =
22 meta.scales.length > 1 ? AssetSourceResolver.pickScale(meta.scales, PixelRatio.get()) : 1;
23 const index = meta.scales.findIndex(s => s === scale);
24 const hash = meta.fileHashes[index] || meta.fileHashes[0];
25
26 const suffix =
27 '/' +
28 meta.name +
29 (scale === 1 ? '' : '@' + scale + 'x') +
30 '.' +
31 meta.type +
32 '?platform=' +
33 Platform.OS +
34 '&hash=' +
35 meta.hash;
36
37 // Allow asset processors to directly provide the URL that will be loaded
38 if (meta.uri) {
39 return {
40 uri: meta.uri,
41 hash,
42 };
43 }
44
45 if (/^https?:/.test(meta.httpServerLocation)) {
46 // This is a full URL, so we avoid prepending bundle URL/cloudfront
47 // This usually means Asset is on a different server, and the URL is present in the bundle
48 return {
49 uri: meta.httpServerLocation + suffix,
50 hash,
51 };
52 }
53
54 if (Constants.manifest.xde) {
55 // Development server URI is pieced together
56 return {
57 uri:
58 Constants.manifest.bundleUrl.match(/^https?:\/\/.*?\//)[0] +
59 meta.httpServerLocation.replace(/^\/?/, '') +
60 suffix,
61 hash,
62 };
63 }
64
65 // CDN URI is based directly on the hash
66 return {
67 uri: 'https://d1wp6m56sqw74a.cloudfront.net/~assets/' + hash,
68 hash,
69 };
70};
71
72// Returns the uri of an asset from its hash and type or null if the asset is
73// not included in the app bundle.
74const getUriInBundle = (hash, type) => {
75 const assetName = 'asset_' + hash + (type ? '.' + type : '');
76 if (Constants.appOwnership !== 'standalone' || !bundledAssets.has(assetName)) {
77 return null;
78 }
79 return `${FS.bundleDirectory}${assetName}`;
80};
81
82export default class Asset {
83 static byHash = {};
84
85 constructor({ name, type, hash, uri, width, height }) {
86 this.name = name;
87 this.type = type;
88 this.hash = hash;
89 this.uri = uri;
90 this.localUri = getUriInBundle(hash, type);
91 if (typeof width === 'number') {
92 this.width = width;
93 }
94 if (typeof height === 'number') {
95 this.height = height;
96 }
97
98 this.downloading = false;
99 this.downloaded = !!this.localUri;
100 this.downloadCallbacks = [];
101 }
102
103 static loadAsync(moduleId) {
104 let moduleIds = typeof moduleId === 'number' ? [moduleId] : moduleId;
105 return Promise.all(moduleIds.map(m => Asset.fromModule(m).downloadAsync()));
106 }
107
108 static fromModule(moduleId) {
109 const meta = AssetRegistry.getAssetByID(moduleId);
110 return Asset.fromMetadata(meta);
111 }
112
113 static fromMetadata(meta) {
114 // The hash of the whole asset, not to confuse with the hash of a specific
115 // file returned from `pickScale`.
116 const metaHash = meta.hash;
117 if (Asset.byHash[metaHash]) {
118 return Asset.byHash[metaHash];
119 }
120
121 const { uri, hash } = pickScale(meta);
122
123 const asset = new Asset({
124 name: meta.name,
125 type: meta.type,
126 hash,
127 uri,
128 width: meta.width,
129 height: meta.height,
130 });
131 Asset.byHash[metaHash] = asset;
132 return asset;
133 }
134
135 async downloadAsync() {
136 if (this.downloaded) {
137 return;
138 }
139 if (this.downloading) {
140 await new Promise((resolve, reject) => this.downloadCallbacks.push({ resolve, reject }));
141 return;
142 }
143 this.downloading = true;
144 try {
145 const localUri = `${FS.cacheDirectory}ExponentAsset-${this.hash}.${this.type}`;
146 let exists, md5;
147 ({ exists, md5 } = await FS.getInfoAsync(localUri, {
148 cache: true,
149 md5: true,
150 }));
151 if (!exists || md5 !== this.hash) {
152 ({ md5 } = await FS.downloadAsync(this.uri, localUri, {
153 cache: true,
154 md5: true,
155 }));
156 if (md5 !== this.hash) {
157 throw new Error(
158 `Downloaded file for asset '${this.name}.${this.type}' ` +
159 `Located at ${this.uri} ` +
160 `failed MD5 integrity check`
161 );
162 }
163 }
164
165 this.localUri = localUri;
166 this.downloaded = true;
167 this.downloadCallbacks.forEach(({ resolve }) => resolve());
168 } catch (e) {
169 this.downloadCallbacks.forEach(({ reject }) => reject(e));
170 throw e;
171 } finally {
172 this.downloading = false;
173 this.downloadCallbacks = [];
174 }
175 }
176}
177
178// Override React Native's asset resolution for `Image` components
179resolveAssetSource.setCustomSourceTransformer(resolver => {
180 const asset = Asset.fromMetadata(resolver.asset);
181 return resolver.fromSource(asset.downloaded ? asset.localUri : asset.uri);
182});