react-native-svg-asset-plugin
Version:
Asset plugin for importing SVG images in React Native
218 lines (217 loc) • 7.68 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
const node_path_1 = __importDefault(require("node:path"));
const fs_extra_1 = __importDefault(require("fs-extra"));
const cache = __importStar(require("./cache"));
const config = __importStar(require("./config"));
const sharp = __importStar(require("./sharp"));
const fsUtils = __importStar(require("./utils/fs"));
const funcUtils = __importStar(require("./utils/func"));
const asyncConfig = config.load();
/**
* Main function called by the metro bundler when processing an asset. This
* plugin renders SVG assets into PNG files that can be used natively by
* React Native apps.
*
* Note that metro caches the results of this function, so that multiple uses
* of the same asset will not cause multiple calls to this function. This also
* means that return values for old versions of a source file might be cached,
* leading to metro expecting previously rendered PNG files to exist for
* example when undoing changes to a source SVG file.
*
* @param assetData Data on the required asset provided by the metro bundler.
* See below for examples.
* @returns Modified asset data that points to the rendered PNG files.
*/
async function reactNativeSvgAssetPlugin(
/**
* @example // Metro and React Native
* {
* __packager_asset: true,
* fileSystemLocation: '/src/app/images',
* httpServerLocation: '/assets/images',
* width: 100,
* height: 100,
* scales: [ 1 ],
* files: [ '/src/app/images/icon.svg' ],
* hash: '2f361af1b5dee4c16c567c61d8623df0',
* name: 'icon',
* type: 'svg'
* }
*
* @example // Expo dev build
* {
* __packager_asset: true,
* fileSystemLocation: '/src/app/images',
* httpServerLocation: '/assets/?unstable_path=./images',
* width: 200,
* height: 200,
* scales: [ 1 ],
* files: [ '/src/app/images/icon.svg' ],
* hash: '2f361af1b5dee4c16c567c61d8623df0',
* name: 'icon',
* type: 'svg'
* }
*
* @example // Expo production build
* {
* __packager_asset: true,
* fileSystemLocation: '/src/app/images',
* httpServerLocation: '/assets?export_path=/assets/images',
* width: 200,
* height: 200,
* scales: [ 1 ],
* files: [ '/src/app/images/icon.svg' ],
* hash: '2f361af1b5dee4c16c567c61d8623df0',
* name: 'icon',
* type: 'svg'
* }
*/
assetData) {
const filePath = assetData.files[0] || "";
if (await shouldConvertFile(assetData, filePath)) {
return convertSvg(assetData);
}
else {
return assetData;
}
}
async function shouldConvertFile(assetData, filePath) {
if (assetData.type !== "svg") {
return false;
}
const ignoreRegex = (await asyncConfig).ignoreRegex;
if (ignoreRegex?.test(filePath)) {
return false;
}
return true;
}
async function convertSvg(assetData) {
if (assetData.scales.length !== assetData.files.length) {
throw new Error("Passed scales doesn't match passed files.");
}
else if (assetData.files[0] === undefined) {
throw new Error("No files passed.");
}
else if (assetData.files.length > 1) {
throw new Error("Multiple SVG scales not supported.");
}
else if (assetData.scales[0] !== 1) {
throw new Error("Scaled SVGs not supported.");
}
const inputFilePath = assetData.files[0];
const inputFileScale = assetData.scales[0];
const config = await asyncConfig;
// Get the output directory where generated assets should be placed
const outputDirectory = await cache.getCacheStoragePath(config, assetData);
const outputName = `${assetData.name}-${assetData.hash}`;
const imageLoader = createimageLoader(inputFilePath);
const outputImages = await Promise.all(config.scales.map((imageScale) => ensurePngUpToDate(imageLoader, imageScale / inputFileScale, node_path_1.default.join(outputDirectory, `${outputName}${getScaleSuffix(imageScale)}.png`), config.output)));
return {
...assetData,
fileSystemLocation: outputDirectory,
httpServerLocation: `${assetData.httpServerLocation}/${config.cacheDir}`,
files: outputImages.map((outputImage) => outputImage.filePath),
scales: outputImages.map((outputImage) => outputImage.scale),
name: outputName,
type: "png",
};
}
/**
* Creates an image loader for input file.
* This provides lazy cached loading of image data.
*/
function createimageLoader(inputFilePath) {
return funcUtils.memo(async () => {
const [fileBuffer, loadedSharp] = await Promise.all([
fs_extra_1.default.readFile(inputFilePath),
sharp.load(),
]);
const metadata = await loadedSharp(fileBuffer).metadata();
return {
buffer: fileBuffer,
metadata: metadata,
};
});
}
/**
* Ensures that the resultign PNG file exists on the fileystem.
*
* In case the file does not exist yet, or it is older than the
* current configuration, it will be generated.
*
* Otherwise the existing file will be left in place, and its
* last modified time will be updated.
*/
async function ensurePngUpToDate(imageLoader, scale, outputFilePath, outputOptions) {
if (await cache.isFileOutdated(outputFilePath, await asyncConfig)) {
const inputFile = await imageLoader();
await generatePng(inputFile, scale, outputFilePath, outputOptions);
}
else {
await fsUtils.updateLastModifiedTime(outputFilePath);
}
return {
filePath: outputFilePath,
scale: scale,
};
}
/**
* Generates a PNG file from a loaded SVG file.
*/
async function generatePng(inputFile, scale, outputFilePath, outputOptions) {
if (inputFile.metadata.density === undefined) {
throw new Error("Input image missing density information");
}
const density = inputFile.metadata.density;
const loadedSharp = await sharp.load();
await loadedSharp(inputFile.buffer, {
density: density * scale,
})
.png(outputOptions)
.toFile(outputFilePath);
}
function getScaleSuffix(scale) {
switch (scale) {
case 1:
return "";
default:
return `@${scale}x`;
}
}
module.exports = reactNativeSvgAssetPlugin;