UNPKG

nuxt-svgo

Version:

Nuxt module to load optimized SVG files as Vue components

332 lines (313 loc) 9.97 kB
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 };