@expo/webpack-pwa-manifest-plugin
Version:
Generates a progressive web app (PWA) manifest.json from a React Native app.json
289 lines • 12.3 kB
JavaScript
var __awaiter = (this && this.__awaiter) || function (thisArg, _arguments, P, generator) {
return new (P || (P = Promise))(function (resolve, reject) {
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
function step(result) { result.done ? resolve(result.value) : new P(function (resolve) { resolve(result.value); }).then(fulfilled, rejected); }
step((generator = generator.apply(thisArg, _arguments || [])).next());
});
};
var __rest = (this && this.__rest) || function (s, e) {
var t = {};
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p) && e.indexOf(p) < 0)
t[p] = s[p];
if (s != null && typeof Object.getOwnPropertySymbols === "function")
for (var i = 0, p = Object.getOwnPropertySymbols(s); i < p.length; i++) if (e.indexOf(p[i]) < 0)
t[p[i]] = s[p[i]];
return t;
};
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const image_utils_1 = require("@expo/image-utils");
const fs_extra_1 = __importDefault(require("fs-extra"));
const mime_1 = __importDefault(require("mime"));
const node_fetch_1 = __importDefault(require("node-fetch"));
const path_1 = __importDefault(require("path"));
const stream_1 = __importDefault(require("stream"));
const tempy_1 = __importDefault(require("tempy"));
const util_1 = __importDefault(require("util"));
const chalk_1 = __importDefault(require("chalk"));
const Errors_1 = require("./Errors");
const utils_1 = require("./utils");
const Apple_1 = require("./validators/Apple");
const supportedMimeTypes = ['image/png', 'image/jpeg', 'image/webp'];
function sanitizeIcon(iconSnippet) {
if (!iconSnippet.src) {
throw new Errors_1.IconError('Unknown icon source.');
}
const sizes = utils_1.toArray(iconSnippet.size || iconSnippet.sizes);
if (!sizes) {
throw new Errors_1.IconError('Unknown icon sizes.');
}
return {
src: iconSnippet.src,
resizeMode: iconSnippet.resizeMode,
sizes,
media: iconSnippet.media,
destination: iconSnippet.destination,
ios: iconSnippet.ios,
color: iconSnippet.color,
};
}
function getBufferWithMimeAsync({ src, resizeMode, color }, mimeType, { width, height }) {
return __awaiter(this, void 0, void 0, function* () {
let imagePath;
if (!supportedMimeTypes.includes(mimeType)) {
imagePath = src;
}
else {
let localSrc = src;
// In case the icon is a remote URL we need to download it first
if (src.startsWith('http')) {
localSrc = yield downloadImage(src);
}
imagePath = yield resize(localSrc, mimeType, width, height, resizeMode, color);
}
try {
return yield fs_extra_1.default.readFile(imagePath);
}
catch (err) {
throw new Errors_1.IconError(`It was not possible to read '${src}'.`);
}
});
}
function downloadImage(url) {
return __awaiter(this, void 0, void 0, function* () {
const outputPath = tempy_1.default.directory();
const localPath = path_1.default.join(outputPath, path_1.default.basename(url));
const response = yield node_fetch_1.default(url);
if (!response.ok) {
throw new Errors_1.IconError(`It was not possible to download splash screen from '${url}'`);
}
// Download to local file
const streamPipeline = util_1.default.promisify(stream_1.default.pipeline);
yield streamPipeline(response.body, fs_extra_1.default.createWriteStream(localPath));
return localPath;
});
}
function ensureCacheDirectory(projectRoot, cacheKey) {
return __awaiter(this, void 0, void 0, function* () {
const cacheFolder = path_1.default.join(projectRoot, '.expo/web/cache/production/images', cacheKey);
yield fs_extra_1.default.ensureDir(cacheFolder);
return cacheFolder;
});
}
function getImageFromCacheAsync(fileName, cacheKey) {
return __awaiter(this, void 0, void 0, function* () {
try {
return yield fs_extra_1.default.readFile(path_1.default.resolve(cacheKeys[cacheKey], fileName));
}
catch (_) {
return null;
}
});
}
function cacheImageAsync(fileName, buffer, cacheKey) {
return __awaiter(this, void 0, void 0, function* () {
try {
yield fs_extra_1.default.writeFile(path_1.default.resolve(cacheKeys[cacheKey], fileName), buffer);
}
catch ({ message }) {
console.warn(`error caching image: "${fileName}". ${message}`);
return;
}
});
}
function processImageAsync(size, icon, publicPath, cacheKey) {
return __awaiter(this, void 0, void 0, function* () {
const { width, height } = utils_1.toSize(size);
if (width <= 0 || height <= 0) {
throw Error(`Failed to process image with invalid size: { width: ${width}, height: ${height}}`);
}
const mimeType = mime_1.default.getType(icon.src);
if (!mimeType) {
throw new Error(`Invalid mimeType for image with source: ${icon.src}`);
}
const dimensions = `${width}x${height}`;
const fileName = `icon_${dimensions}.${mime_1.default.getExtension(mimeType)}`;
let imageBuffer = yield getImageFromCacheAsync(fileName, cacheKey);
if (!imageBuffer) {
imageBuffer = yield getBufferWithMimeAsync(icon, mimeType, { width, height });
yield cacheImageAsync(fileName, imageBuffer, cacheKey);
}
const iconOutputDir = icon.destination ? utils_1.joinURI(icon.destination, fileName) : fileName;
const iconPublicUrl = utils_1.joinURI(publicPath, iconOutputDir);
return {
manifestIcon: {
src: iconPublicUrl,
sizes: dimensions,
type: mimeType,
},
webpackAsset: {
output: iconOutputDir,
url: iconPublicUrl,
source: imageBuffer,
size: imageBuffer.length,
ios: icon.ios
? { valid: icon.ios, media: icon.media, size: dimensions, href: iconPublicUrl }
: false,
resizeMode: icon.resizeMode,
color: icon.color,
},
};
});
}
function ensureValidMimeType(mimeType) {
if (['input', 'jpeg', 'jpg', 'png', 'raw', 'tiff', 'webp'].includes(mimeType)) {
return mimeType;
}
return 'png';
}
function resize(inputPath, mimeType, width, height, fit = 'contain', background) {
return __awaiter(this, void 0, void 0, function* () {
const format = ensureValidMimeType(mimeType.split('/')[1]);
try {
const outputPath = tempy_1.default.directory();
yield image_utils_1.sharpAsync({
input: inputPath,
output: outputPath,
format,
}, [
{
operation: 'flatten',
background,
},
{
operation: 'resize',
width,
height,
fit,
background,
},
]);
return path_1.default.join(outputPath, path_1.default.basename(inputPath));
}
catch ({ message }) {
throw new Errors_1.IconError(`It was not possible to generate splash screen '${inputPath}'. ${message}`);
}
});
}
function retrieveIcons(manifest) {
// Remove these items so they aren't written to disk.
const { startupImages, icons } = manifest, config = __rest(manifest, ["startupImages", "icons"]);
const parsedStartupImages = utils_1.toArray(startupImages);
let parsedIcons = utils_1.toArray(icons);
if (parsedStartupImages.length) {
// TODO: Bacon: use all of the startup images
const startupImage = parsedStartupImages[0];
parsedIcons = [...parsedIcons, ...Apple_1.fromStartupImage(startupImage)];
}
const response = parsedIcons.map(icon => sanitizeIcon(icon));
return [response, config];
}
exports.retrieveIcons = retrieveIcons;
const crypto_1 = __importDefault(require("crypto"));
// Calculate SHA256 Checksum value of a file based on its contents
function calculateHash(filePath) {
const contents = fs_extra_1.default.readFileSync(filePath);
return crypto_1.default
.createHash('sha256')
.update(contents)
.digest('hex');
}
// Create a hash key for caching the images between builds
function createCacheKey(icon) {
const hash = calculateHash(icon.src);
return [hash, icon.resizeMode, icon.color].filter(Boolean).join('-');
}
const cacheKeys = {};
function parseIconsAsync(projectRoot, inputIcons, publicPath) {
return __awaiter(this, void 0, void 0, function* () {
if (!inputIcons.length) {
return {};
}
if (!(yield image_utils_1.isAvailableAsync())) {
// TODO: Bacon: Fallback to nodejs image resizing as native doesn't work in the host environment.
console.log();
console.log(chalk_1.default.bgRed.black(`PWA Error: It was not possible to generate the images for your progressive web app (splash screens and icons) because the host computer does not have \`sharp\` installed, and \`image-utils\` was unable to install it automatically.`));
console.log(chalk_1.default.red(`- You may stop the process and try again after successfully running \`npm install -g sharp\`.\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.`));
return {};
}
const icons = [];
const assets = [];
let promises = [];
for (const icon of inputIcons) {
const cacheKey = createCacheKey(icon);
if (!(cacheKey in cacheKeys)) {
cacheKeys[cacheKey] = yield ensureCacheDirectory(projectRoot, cacheKey);
}
const { sizes } = icon;
promises = [
...promises,
...sizes.map((size) => __awaiter(this, void 0, void 0, function* () {
const { manifestIcon, webpackAsset } = yield processImageAsync(size, icon, publicPath, cacheKey);
icons.push(manifestIcon);
assets.push(webpackAsset);
})),
];
}
yield Promise.all(promises);
yield clearUnusedCachesAsync(projectRoot);
return {
icons: sortByAttribute(icons, 'sizes'),
// startupImages: icons.filter(({ isStartupImage }) => isStartupImage),
assets: sortByAttribute(assets, 'output'),
};
});
}
exports.parseIconsAsync = parseIconsAsync;
function sortByAttribute(arr, key) {
return arr.filter(Boolean).sort((valueA, valueB) => {
if (valueA[key] < valueB[key])
return -1;
else if (valueA[key] > valueB[key])
return 1;
return 0;
});
}
function clearUnusedCachesAsync(projectRoot) {
return __awaiter(this, void 0, void 0, function* () {
// Clean up any old caches
const cacheFolder = path_1.default.join(projectRoot, '.expo/web/cache/production/images');
const currentCaches = yield fs_extra_1.default.readdir(cacheFolder);
const deleteCachePromises = [];
for (const cache of currentCaches) {
// skip hidden folders
if (cache.startsWith('.')) {
continue;
}
// delete
if (!(cache in cacheKeys)) {
deleteCachePromises.push(fs_extra_1.default.remove(path_1.default.join(cacheFolder, cache)));
}
}
yield Promise.all(deleteCachePromises);
});
}
//# sourceMappingURL=icons.js.map
;