@poppanator/sveltekit-svg
Version:
Import SVG files as Svelte components
202 lines (201 loc) • 8.32 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
const pluginutils_1 = require("@rollup/pluginutils");
const fs_1 = require("fs");
const path_1 = __importDefault(require("path"));
const compiler_1 = require("svelte/compiler");
const svgo_1 = require("svgo");
const { readFile } = fs_1.promises;
function isCompileError(err) {
return err instanceof Error && 'code' in err && 'frame' in err;
}
const svgRegex = /<svg(.*?)>(.*)<\/svg>/s;
function color(start, end = '\u001b[0m') {
return (text) => `${start}${text}${end}`;
}
// const green = color('\u001b[32m')
const yellow = color('\u001b[33m');
const blue = color('\u001b[34m');
function toComponent(svg) {
const parts = svgRegex.exec(svg);
if (!parts) {
throw new Error('Invalid SVG');
}
const [, attributes, content] = parts;
// JSON.stringify escapes any characters that need to be escaped and
// surrounds `content` with double quotes
const contentStrLiteral = JSON.stringify(content);
const component = `<svg ${attributes} {...$$props}>{@html ${contentStrLiteral}}</svg>`;
return {
attributes,
content,
component,
};
}
function isSvgoOptimizeError(obj) {
return typeof obj === 'object' && obj !== null && !('data' in obj);
}
function hasCdata(code) {
return code.includes('<![CDATA[');
}
function isDevelopmentMode() {
return process.env['NODE_ENV'] === 'development';
}
function readSvg(options = { type: 'component' }) {
const resvg = /\.svg(?:\?(src|url|component|dataurl)(=(base64|(un)?enc))?)?$/;
if (options.includePaths) {
// Normalize the include paths prefixes ahead of time
options.includePaths = options.includePaths.map((pattern) => {
const filepath = path_1.default.resolve(path_1.default.normalize(pattern));
return (0, pluginutils_1.normalizePath)(filepath);
});
}
const isType = (str, type) => {
return (!str && options.type === type) || str === type;
};
const handlePath = (id) => {
if (!resvg.test(id)) {
return false;
}
if (options.includePaths) {
return options.includePaths.some((pattern) => {
return id.startsWith(pattern);
});
}
return true;
};
const hook = options.preCompileHook;
/**
* This contains URLs of generated static assets, that is SVGs that are
* imported as URLs, so we can optimize them before they are written to disk.
*/
const optmizeUrls = new Set();
const plugin = {
name: 'sveltekit-svg',
// NOTE: This will only run in production mode
renderChunk(_code, chunk) {
var _a;
// Check if this chunk is an SVG imported as an URL...
const mods = chunk.moduleIds.filter((id) => { var _a; return (_a = this.getModuleInfo(id)) === null || _a === void 0 ? void 0 : _a.meta['isSvgUrl']; });
// ...if so, we should have 1 module and one importAssets...
if (mods.length !== 1 || ((_a = chunk.viteMetadata) === null || _a === void 0 ? void 0 : _a.importedAssets.size) !== 1) {
return undefined;
}
// ...and store the output filename in the lookup so we can verify in
// `generateBundle()` that the SVG should be optimized before written
// to disk
const outputId = Array.from(chunk.viteMetadata.importedAssets)[0];
if (outputId) {
optmizeUrls.add(outputId);
}
},
// NOTE: This will only run in production mode
async generateBundle(_options, bundle) {
// Resolve assets that should be SVGO optimized before written to disk.
// Such assets should exist in the `optimizeUrl` lookup data structure.
for (const [k, v] of Object.entries(bundle)) {
if (!optmizeUrls.has(k) || v.type !== 'asset') {
continue;
}
const optSvg = (0, svgo_1.optimize)(v.source.toString(), options.svgoOptions || undefined);
v.source = Buffer.from(optSvg.data);
}
},
async transform(source, id, transformOptions) {
var _a, _b;
if (!handlePath(id)) {
return undefined;
}
const match = id.match(resvg);
if (!match) {
return undefined;
}
const isBuild = (_a = transformOptions === null || transformOptions === void 0 ? void 0 : transformOptions.ssr) !== null && _a !== void 0 ? _a : false;
const type = match[1];
if (isType(type, 'url')) {
if (isDevelopmentMode()) {
const msg = `"url" imports are not optimized in development mode`;
this.info({
id,
message: msg,
});
}
return { code: source, map: null, meta: { isSvgUrl: true } };
}
let svgo = options.svgoOptions;
let isSvgoDataUri = false;
if (svgo && typeof svgo === 'object') {
if (svgo.datauri) {
isSvgoDataUri = true;
}
}
if (isSvgoDataUri && type === 'component') {
console.warn(`%s Type %O can not be imported as a Svelte component ` +
`since "datauri" is set in vite.config`, yellow('[WARNING]'), id);
}
else if (type === 'dataurl') {
const t = (_b = match[3]) !== null && _b !== void 0 ? _b : 'base64';
if (!svgo) {
svgo = {};
}
svgo.datauri = t;
isSvgoDataUri = true;
}
try {
const filename = id.replace(/\.svg(\?.*)$/, '.svg');
const data = (await readFile(filename)).toString('utf-8');
const opt = svgo !== false
? (0, svgo_1.optimize)(data, { path: filename, ...svgo })
: { data };
if (isSvgoOptimizeError(opt)) {
console.error('Got optimize error from SVGO:', opt);
return undefined;
}
if (isType(type, 'src') || isSvgoDataUri) {
return {
code: `\nexport default \`${opt.data}\`;`,
map: null,
};
}
const comp = toComponent(opt.data);
opt.data = hook ? hook(opt.data, comp) : comp.component;
const { js } = (0, compiler_1.compile)(opt.data, {
css: 'external',
filename: id,
namespace: 'svg',
generate: isBuild ? 'server' : 'client',
});
// Remove query string from sources
js.map.sources.forEach((v, idx, a) => {
if (v.includes('?')) {
const t = v.split('?')[0];
if (t) {
a[idx] = t;
}
}
});
return js;
}
catch (err) {
if (isCompileError(err) && hasCdata(err.frame)) {
const msg = `\n%s The SVG file %O contains a %s section which is not ` +
`supported by Svelte. To make this SVG work with the %s ` +
`plugin, you need to remove all %s sections from the SVG.\n`;
console.warn(msg, yellow('[WARNING]'), id, blue('<![CDATA[...]]>'), blue('@poppanator/sveltekit-svg'), blue('<![CDATA[...]]>'));
}
else {
console.error('Failed reading SVG "%s": %s', id, err.message, err);
}
return undefined;
}
},
};
if (isDevelopmentMode()) {
delete plugin.renderChunk;
delete plugin.generateBundle;
}
return plugin;
}
module.exports = readSvg;