nuxt-svgo
Version:
Nuxt module to load optimized SVG files as Vue components
332 lines (313 loc) • 9.97 kB
JavaScript
import { basename, extname, join } from 'node:path';
import * as fs from 'node:fs';
import { defineNuxtModule, createResolver, addComponent, addVitePlugin, addTemplate, extendWebpackConfig, addComponentsDir } from '@nuxt/kit';
import { readFile } from 'node:fs/promises';
import { compileTemplate } from 'vue/compiler-sfc';
import { optimize } from 'svgo';
import urlEncodeSvg from 'mini-svg-data-uri';
function svgLoader(options) {
const { svgoConfig, svgo, defaultImport, explicitImportsOnly, autoImportPath, customComponent } = options || {};
const normalizedCustomComponent = customComponent.includes("-") ? customComponent.split("-").map((c) => c[0].toUpperCase() + c.substring(1).toLowerCase()).join("") : customComponent;
const autoImportPathNormalized = autoImportPath && autoImportPath.replaceAll(/^\.*(?=[/\\])/g, "");
const svgRegex = /\.svg(\?(url_encode|raw|raw_optimized|component|skipsvgo|componentext))?$/;
const explicitImportRegex = /\.svg(\?(url_encode|raw|raw_optimized|component|skipsvgo|componentext))+$/;
return {
name: "svg-loader",
enforce: "pre",
async load(id) {
if (!id.match(svgRegex) || id.startsWith("virtual:public")) {
return;
}
const [path, query] = id.split("?", 2);
if (explicitImportsOnly) {
const isExplicitlyQueried = id.match(explicitImportRegex);
if (!isExplicitlyQueried) {
if (autoImportPathNormalized) {
if (!path.includes(autoImportPathNormalized)) {
return;
}
} else {
return;
}
}
}
const importType = query || defaultImport;
if (importType === "url") {
return;
}
let svg;
try {
svg = await readFile(path, "utf-8");
} catch (ex) {
console.warn(
"\n",
`${id} couldn't be loaded by vite-svg-loader, fallback to default loader`
);
return;
}
if (importType === "raw") {
return `export default ${JSON.stringify(svg)}`;
}
if (svgo !== false && query !== "skipsvgo") {
svg = optimize(svg, {
...svgoConfig,
path
}).data;
}
if (importType === "url_encode") {
return `export default "${urlEncodeSvg(svg)}"`;
}
if (importType === "raw_optimized") {
return `export default ${JSON.stringify(svg)}`;
}
svg = svg.replace(/<style/g, '<component is="style"').replace(/<\/style/g, "</component");
const svgName = basename(path, extname(path));
let { code } = compileTemplate({
id: JSON.stringify(id),
source: svg,
filename: path,
transformAssetUrls: false
});
if (importType === "componentext") {
code = `import {${normalizedCustomComponent}} from "#components";
import {h} from "vue";
` + code;
code += `
export default { render() { return h(${normalizedCustomComponent}, {icon: {render}, name: "${svgName}"}) } }`;
return code;
} else {
return `${code}
export default { render: render }`;
}
}
};
}
function resolveDefaultImport({ defaultImport, customComponent }) {
switch (defaultImport) {
case "url_encode":
return `
// Default - loads optimized svg as data uri (uses svgo + \`mini-svg-data-uri\`)
declare module '*.svg' {
const dataUri: string;
export default dataUri;
}
`;
case "raw":
return `
// Default - loads contents as text
declare module '*.svg' {
const text: string;
export default text;
}
`;
case "raw_optimized":
return `
// Default - loads optimized svg as text
declare module '*.svg' {
const text: string;
export default text;
}
`;
case "skipsvgo":
return `
// Default - loads contents as a component (unoptimized, without <${customComponent}/>)
declare module '*.svg' {
import { DefineComponent, SVGAttributes, ReservedProps } from 'vue';
const component: DefineComponent<SVGAttributes & ReservedProps>;
export default component;
}
`;
case "component":
return `
// Default - loads optimized svg as a component
declare module '*.svg' {
import { DefineComponent, SVGAttributes, ReservedProps } from 'vue';
const component: DefineComponent<SVGAttributes & ReservedProps>;
export default component;
}
`;
case "componentext":
return `
// Default - loads optimized svg with <${customComponent}/> component
declare module '*.svg' {
import { DefineComponent } from 'vue';
import { ${customComponent} } from '#components';
type OmitIcon<T> = DefineComponent<Omit<ComponentProps<T>, 'icon'>>;
const component: OmitIcon<typeof ${customComponent}>;
export default component;
}
`;
default:
return ``;
}
}
function generateImportQueriesDts(options) {
return `// Generated by nuxt-svgo module
type ComponentProps<T> = T extends new (...args: any) => {
$props: infer P;
} ? NonNullable<P> : T extends (props: infer P, ...args: any) => any ? P : {};
${!options.explicitImportsOnly ? resolveDefaultImport(options) : ``}
// loads optimized svg as data uri (uses svgo + \`mini-svg-data-uri\`)
declare module '*.svg?url_encode' {
const dataUri: string;
export default dataUri;
}
// loads contents as text
declare module '*.svg?raw' {
const text: string;
export default text;
}
// loads optimized svg as text
declare module '*.svg?raw_optimized' {
const text: string;
export default text;
}
// loads contents as a component (unoptimized, without <${options.customComponent}/>)
declare module '*.svg?skipsvgo' {
import { DefineComponent, SVGAttributes, ReservedProps } from 'vue';
const component: DefineComponent<SVGAttributes & ReservedProps>;
export default component;
}
// loads optimized svg as a component
declare module '*.svg?component' {
import { DefineComponent, SVGAttributes, ReservedProps } from 'vue';
const component: DefineComponent<SVGAttributes & ReservedProps>;
export default component;
}
// loads optimized svg with <${options.customComponent}/> component
declare module '*.svg?componentext' {
import { DefineComponent } from 'vue';
import { ${options.customComponent} } from '#components';
type OmitIcon<T> = DefineComponent<Omit<ComponentProps<T>, 'icon'>>;
const component: OmitIcon<typeof ${options.customComponent}>;
export default component;
}
`;
}
function hashCode(str) {
let hash = 0;
for (let i = 0, len = str.length; i < len; i++) {
const chr = str.charCodeAt(i);
hash = (hash << 5) - hash + chr;
hash |= 0;
}
return hash;
}
const defaultSvgoConfig = {
plugins: [
{
name: "preset-default",
params: {
overrides: {
removeViewBox: false
}
}
},
"removeDimensions",
{
name: "prefixIds",
params: {
prefix(_, info) {
return "i" + hashCode(info.path);
}
}
}
]
};
const nuxtSvgo = defineNuxtModule({
meta: {
name: "nuxt-svgo",
configKey: "svgo",
compatibility: {
// Add -rc.0 due to issue described in https://github.com/nuxt/framework/issues/6699
nuxt: ">=3.0.0-rc.0"
}
},
defaults: {
svgo: true,
defaultImport: "componentext",
autoImportPath: "./assets/icons/",
svgoConfig: void 0,
global: true,
customComponent: "NuxtIcon",
componentPrefix: "svgo",
dts: false
},
async setup(options, nuxt) {
const { resolvePath, resolve } = createResolver(import.meta.url);
addComponent({
name: "nuxt-icon",
filePath: resolve("./runtime/components/nuxt-icon.vue")
});
addVitePlugin(
svgLoader({
...options,
svgoConfig: options.svgoConfig || defaultSvgoConfig
})
);
if (options.autoImportPath) {
const addIconComponentsDir = (path) => {
if (fs.existsSync(path)) {
addComponentsDir({
path,
global: options.global,
extensions: ["svg"],
prefix: options.componentPrefix || "svgo",
watch: true
});
}
};
const iconPaths = [];
try {
const iconPath = await resolvePath(options.autoImportPath);
iconPaths.push(iconPath);
} catch (e) {
console.error("Error resolving module path:", e);
}
const appDir = nuxt.options.srcDir || nuxt.options.rootDir;
iconPaths.push(join(appDir, options.autoImportPath.replace(/^\.\//, "")));
if (nuxt.options._layers) {
for (const layer of nuxt.options._layers) {
if (layer.config && layer.config.srcDir) {
iconPaths.push(join(layer.config.srcDir, options.autoImportPath.replace(/^\.\//, "")));
}
}
}
iconPaths.forEach(addIconComponentsDir);
}
if (options.dts && ["@nuxt/vite-builder", "vite"].includes(nuxt.options.builder)) {
addTemplate({
filename: "types/nuxt-svgo.d.ts",
getContents: () => generateImportQueriesDts(options)
});
nuxt.hook("prepare:types", ({ references }) => {
const builderEnvFilePath = resolve(nuxt.options.buildDir, "types", "builder-env.d.ts");
const fileIndex = references.findIndex(
(ref) => "path" in ref && ref.path === builderEnvFilePath
);
references.splice(fileIndex, 0, { path: "types/nuxt-svgo.d.ts" });
});
}
extendWebpackConfig((config) => {
const svgRule = config.module.rules.find((rule) => rule.test.test(".svg"));
svgRule.test = /\.(png|jpe?g|gif|webp)$/;
config.module.rules.push({
test: /\.svg$/,
use: [
"vue-loader",
{
loader: "vue-svg-loader",
options: {
svgo: false
}
},
options.svgo && {
loader: "svgo-loader",
options: options.svgoConfig || defaultSvgoConfig
}
].filter(Boolean)
});
});
}
});
export { nuxtSvgo as default, defaultSvgoConfig };