UNPKG

13.5 kBJavaScriptView Raw
1"use strict";
2var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
3 function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
4 return new (P || (P = Promise))(function (resolve, reject) {
5 function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
6 function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
7 function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
8 step((generator = generator.apply(thisArg, _arguments || [])).next());
9 });
10};
11var __rest = (this && this.__rest) || function (s, e) {
12 var t = {};
13 for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
14 t[p] = s[p];
15 if (s != null && typeof Object.getOwnPropertySymbols === "function")
16 for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) {
17 if (e.indexOf(p[i]) < 0 && Object.prototype.propertyIsEnumerable.call(s, p[i]))
18 t[p[i]] = s[p[i]];
19 }
20 return t;
21};
22var __importDefault = (this && this.__importDefault) || function (mod) {
23 return (mod && mod.__esModule) ? mod : { "default": mod };
24};
25Object.defineProperty(exports, "__esModule", { value: true });
26const image_utils_1 = require("@expo/image-utils");
27const fs_extra_1 = __importDefault(require("fs-extra"));
28const mime_1 = __importDefault(require("mime"));
29const node_fetch_1 = __importDefault(require("node-fetch"));
30const path_1 = __importDefault(require("path"));
31const stream_1 = __importDefault(require("stream"));
32const tempy_1 = __importDefault(require("tempy"));
33const util_1 = __importDefault(require("util"));
34const chalk_1 = __importDefault(require("chalk"));
35const crypto_1 = __importDefault(require("crypto"));
36const Errors_1 = require("./Errors");
37const utils_1 = require("./utils");
38const Apple_1 = require("./validators/Apple");
39const ImageComposite_1 = require("./ImageComposite");
40const supportedMimeTypes = ['image/png', 'image/jpeg', 'image/webp'];
41function sanitizeIcon(iconSnippet) {
42 if (!iconSnippet.src) {
43 throw new Errors_1.IconError('Unknown icon source.');
44 }
45 const sizes = utils_1.toArray(iconSnippet.size || iconSnippet.sizes);
46 if (!sizes) {
47 throw new Errors_1.IconError('Unknown icon sizes.');
48 }
49 return {
50 src: iconSnippet.src,
51 resizeMode: iconSnippet.resizeMode,
52 sizes,
53 media: iconSnippet.media,
54 destination: iconSnippet.destination,
55 ios: iconSnippet.ios,
56 color: iconSnippet.color,
57 };
58}
59function getBufferWithMimeAsync({ src, resizeMode, color }, mimeType, { width, height }) {
60 return __awaiter(this, void 0, void 0, function* () {
61 let imagePath;
62 if (!supportedMimeTypes.includes(mimeType)) {
63 imagePath = src;
64 }
65 else {
66 const imageData = yield resize(src, mimeType, width, height, resizeMode, color);
67 if (imageData instanceof Buffer) {
68 return imageData;
69 }
70 else {
71 imagePath = imageData;
72 }
73 }
74 try {
75 return yield fs_extra_1.default.readFile(imagePath);
76 }
77 catch (err) {
78 throw new Errors_1.IconError(`It was not possible to read '${src}'.`);
79 }
80 });
81}
82function downloadImage(url) {
83 return __awaiter(this, void 0, void 0, function* () {
84 const outputPath = tempy_1.default.directory();
85 const localPath = path_1.default.join(outputPath, path_1.default.basename(stripQueryParams(url)));
86 const response = yield node_fetch_1.default(url);
87 if (!response.ok) {
88 throw new Errors_1.IconError(`It was not possible to download splash screen from '${url}'`);
89 }
90 // Download to local file
91 const streamPipeline = util_1.default.promisify(stream_1.default.pipeline);
92 yield streamPipeline(response.body, fs_extra_1.default.createWriteStream(localPath));
93 return localPath;
94 });
95}
96function ensureCacheDirectory(projectRoot, cacheKey) {
97 return __awaiter(this, void 0, void 0, function* () {
98 const cacheFolder = path_1.default.join(projectRoot, '.expo/web/cache/production/images', cacheKey);
99 yield fs_extra_1.default.ensureDir(cacheFolder);
100 return cacheFolder;
101 });
102}
103function getImageFromCacheAsync(fileName, cacheKey) {
104 return __awaiter(this, void 0, void 0, function* () {
105 try {
106 return yield fs_extra_1.default.readFile(path_1.default.resolve(cacheKeys[cacheKey], fileName));
107 }
108 catch (_) {
109 return null;
110 }
111 });
112}
113function cacheImageAsync(fileName, buffer, cacheKey) {
114 return __awaiter(this, void 0, void 0, function* () {
115 try {
116 yield fs_extra_1.default.writeFile(path_1.default.resolve(cacheKeys[cacheKey], fileName), buffer);
117 }
118 catch ({ message }) {
119 console.warn(`error caching image: "${fileName}". ${message}`);
120 }
121 });
122}
123let hasWarned = false;
124function processImageAsync(size, icon, publicPath, cacheKey) {
125 return __awaiter(this, void 0, void 0, function* () {
126 const { width, height } = utils_1.toSize(size);
127 if (width <= 0 || height <= 0) {
128 throw Error(`Failed to process image with invalid size: { width: ${width}, height: ${height}}`);
129 }
130 const mimeType = mime_1.default.getType(icon.src);
131 if (!mimeType) {
132 throw new Error(`Invalid mimeType for image with source: ${icon.src}`);
133 }
134 const dimensions = `${width}x${height}`;
135 const fileName = `icon_${dimensions}.${mime_1.default.getExtension(mimeType)}`;
136 let imageBuffer = yield getImageFromCacheAsync(fileName, cacheKey);
137 if (!imageBuffer) {
138 // Putting the warning here will prevent the warning from showing if all images were reused from the cache
139 if (!hasWarned && !(yield image_utils_1.isAvailableAsync())) {
140 hasWarned = true;
141 // TODO: Bacon: Fallback to nodejs image resizing as native doesn't work in the host environment.
142 console.log('ff', cacheKey, fileName, dimensions);
143 console.log();
144 console.log(chalk_1.default.bgYellow.black(`PWA Images: Using node to generate images. This is much slower than using native packages.`));
145 console.log(chalk_1.default.yellow(`- Optionally you can stop the process and try again after successfully running \`npm install -g sharp-cli\`.\n- If you are using \`expo-cli\` to build your project then you could use the \`--no-pwa\` flag to skip the PWA asset generation step entirely.`));
146 }
147 imageBuffer = yield getBufferWithMimeAsync(icon, mimeType, { width, height });
148 yield cacheImageAsync(fileName, imageBuffer, cacheKey);
149 }
150 const iconOutputDir = icon.destination ? utils_1.joinURI(icon.destination, fileName) : fileName;
151 const iconPublicUrl = utils_1.joinURI(publicPath, iconOutputDir);
152 return {
153 manifestIcon: {
154 src: iconPublicUrl,
155 sizes: dimensions,
156 type: mimeType,
157 },
158 webpackAsset: {
159 output: iconOutputDir,
160 url: iconPublicUrl,
161 source: imageBuffer,
162 size: imageBuffer.length,
163 ios: icon.ios
164 ? { valid: icon.ios, media: icon.media, size: dimensions, href: iconPublicUrl }
165 : false,
166 resizeMode: icon.resizeMode,
167 color: icon.color,
168 },
169 };
170 });
171}
172function ensureValidMimeType(mimeType) {
173 if (['input', 'jpeg', 'jpg', 'png', 'raw', 'tiff', 'webp'].includes(mimeType)) {
174 return mimeType;
175 }
176 return 'png';
177}
178function resize(inputPath, mimeType, width, height, fit = 'contain', background) {
179 return __awaiter(this, void 0, void 0, function* () {
180 if (!(yield image_utils_1.isAvailableAsync())) {
181 return yield ImageComposite_1.resize(inputPath, mimeType, width, height, fit, background);
182 }
183 const format = ensureValidMimeType(mimeType.split('/')[1]);
184 const outputPath = tempy_1.default.directory();
185 try {
186 yield image_utils_1.sharpAsync({
187 input: inputPath,
188 output: outputPath,
189 format,
190 }, [
191 {
192 operation: 'flatten',
193 background,
194 },
195 {
196 operation: 'resize',
197 width,
198 height,
199 fit,
200 background,
201 },
202 ]);
203 return path_1.default.join(outputPath, path_1.default.basename(inputPath));
204 }
205 catch ({ message }) {
206 throw new Errors_1.IconError(`It was not possible to generate splash screen '${inputPath}'. ${message}`);
207 }
208 });
209}
210function retrieveIcons(manifest) {
211 // Remove these items so they aren't written to disk.
212 const { startupImages, icons } = manifest, config = __rest(manifest, ["startupImages", "icons"]);
213 const parsedStartupImages = utils_1.toArray(startupImages);
214 let parsedIcons = utils_1.toArray(icons);
215 if (parsedStartupImages.length) {
216 // TODO: Bacon: use all of the startup images
217 const startupImage = parsedStartupImages[0];
218 parsedIcons = [...parsedIcons, ...Apple_1.fromStartupImage(startupImage)];
219 }
220 const response = parsedIcons.map(icon => sanitizeIcon(icon));
221 return [response, config];
222}
223exports.retrieveIcons = retrieveIcons;
224// Calculate SHA256 Checksum value of a file based on its contents
225function calculateHash(filePath) {
226 const contents = filePath.startsWith('http') ? filePath : fs_extra_1.default.readFileSync(filePath);
227 return crypto_1.default
228 .createHash('sha256')
229 .update(contents)
230 .digest('hex');
231}
232// Create a hash key for caching the images between builds
233function createCacheKey(icon) {
234 const hash = calculateHash(icon.src);
235 return [hash, icon.resizeMode, icon.color].filter(Boolean).join('-');
236}
237const cacheKeys = {};
238const cacheDownloadedKeys = {};
239function stripQueryParams(url) {
240 return url.split('?')[0].split('#')[0];
241}
242function downloadOrUseCachedImage(url) {
243 return __awaiter(this, void 0, void 0, function* () {
244 if (url in cacheDownloadedKeys) {
245 return cacheDownloadedKeys[url];
246 }
247 if (url.startsWith('http')) {
248 cacheDownloadedKeys[url] = yield downloadImage(url);
249 }
250 else {
251 cacheDownloadedKeys[url] = url;
252 }
253 return cacheDownloadedKeys[url];
254 });
255}
256function parseIconsAsync(projectRoot, inputIcons, publicPath) {
257 return __awaiter(this, void 0, void 0, function* () {
258 if (!inputIcons.length) {
259 return {};
260 }
261 const icons = [];
262 const assets = [];
263 let promises = [];
264 for (const icon of inputIcons) {
265 const cacheKey = createCacheKey(icon);
266 icon.src = yield downloadOrUseCachedImage(icon.src);
267 if (!(cacheKey in cacheKeys)) {
268 cacheKeys[cacheKey] = yield ensureCacheDirectory(projectRoot, cacheKey);
269 }
270 const { sizes } = icon;
271 promises = [
272 ...promises,
273 ...sizes.map((size) => __awaiter(this, void 0, void 0, function* () {
274 const { manifestIcon, webpackAsset } = yield processImageAsync(size, icon, publicPath, cacheKey);
275 icons.push(manifestIcon);
276 assets.push(webpackAsset);
277 })),
278 ];
279 }
280 yield Promise.all(promises);
281 yield clearUnusedCachesAsync(projectRoot);
282 return {
283 icons: sortByAttribute(icons, 'sizes'),
284 // startupImages: icons.filter(({ isStartupImage }) => isStartupImage),
285 assets: sortByAttribute(assets, 'output'),
286 };
287 });
288}
289exports.parseIconsAsync = parseIconsAsync;
290function sortByAttribute(arr, key) {
291 return arr.filter(Boolean).sort((valueA, valueB) => {
292 if (valueA[key] < valueB[key])
293 return -1;
294 else if (valueA[key] > valueB[key])
295 return 1;
296 return 0;
297 });
298}
299function clearUnusedCachesAsync(projectRoot) {
300 return __awaiter(this, void 0, void 0, function* () {
301 // Clean up any old caches
302 const cacheFolder = path_1.default.join(projectRoot, '.expo/web/cache/production/images');
303 const currentCaches = fs_extra_1.default.readdirSync(cacheFolder);
304 if (!Array.isArray(currentCaches)) {
305 console.warn('Failed to read the icon cache');
306 return;
307 }
308 const deleteCachePromises = [];
309 for (const cache of currentCaches) {
310 // skip hidden folders
311 if (cache.startsWith('.')) {
312 continue;
313 }
314 // delete
315 if (!(cache in cacheKeys)) {
316 deleteCachePromises.push(fs_extra_1.default.remove(path_1.default.join(cacheFolder, cache)));
317 }
318 }
319 yield Promise.all(deleteCachePromises);
320 });
321}
322//# sourceMappingURL=icons.js.map
\No newline at end of file