├── .editorconfig ├── .github └── assets │ └── devtools.png ├── .gitignore ├── .npmrc ├── .nuxtrc ├── CODE_OF_CONDUCT.md ├── LICENCE ├── README.md ├── build.config.ts ├── client ├── app.vue ├── components │ ├── SvgDetails.vue │ └── SvgPreview.vue ├── composables │ └── state.ts ├── nuxt.config.ts ├── package.json └── tsconfig.json ├── eslint.config.js ├── package.json ├── playground ├── assets │ └── svg │ │ ├── nuxt.svg │ │ └── vue.svg ├── nuxt.config.ts ├── package.json ├── pages │ └── index.vue └── tsconfig.json ├── pnpm-lock.yaml ├── pnpm-workspace.yaml ├── src ├── module.ts └── types.ts └── tsconfig.json /.editorconfig: -------------------------------------------------------------------------------- 1 | root = true 2 | 3 | [*] 4 | indent_size = 2 5 | indent_style = space 6 | end_of_line = lf 7 | charset = utf-8 8 | trim_trailing_whitespace = true 9 | insert_final_newline = true 10 | 11 | [*.md] 12 | trim_trailing_whitespace = false -------------------------------------------------------------------------------- /.github/assets/devtools.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Mini-ghost/nuxt-svgo-loader/ef00f1112d80b13ba3832af22a00b8cf9e0a4688/.github/assets/devtools.png -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | # Dependencies 2 | node_modules 3 | 4 | # Logs 5 | *.log* 6 | 7 | # Temp directories 8 | .temp 9 | .tmp 10 | .cache 11 | 12 | # Yarn 13 | **/.yarn/cache 14 | **/.yarn/*state* 15 | 16 | # Generated dirs 17 | dist 18 | 19 | # Nuxt 20 | .nuxt 21 | .output 22 | .vercel_build_output 23 | .build-* 24 | .env 25 | .netlify 26 | 27 | # Env 28 | .env 29 | 30 | # Testing 31 | reports 32 | coverage 33 | *.lcov 34 | .nyc_output 35 | 36 | # VSCode 37 | .vscode 38 | 39 | # Intellij idea 40 | *.iml 41 | .idea 42 | 43 | # OSX 44 | .DS_Store 45 | .AppleDouble 46 | .LSOverride 47 | .AppleDB 48 | .AppleDesktop 49 | Network Trash Folder 50 | Temporary Items 51 | .apdisk 52 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | shamefully-hoist=true 2 | strict-peer-dependencies=false 3 | -------------------------------------------------------------------------------- /.nuxtrc: -------------------------------------------------------------------------------- 1 | typescript.includeWorkspace=true 2 | -------------------------------------------------------------------------------- /CODE_OF_CONDUCT.md: -------------------------------------------------------------------------------- 1 | # Contributor Covenant Code of Conduct 2 | 3 | ## Our Pledge 4 | 5 | We as members, contributors, and leaders pledge to make participation in our 6 | community a harassment-free experience for everyone, regardless of age, body 7 | size, visible or invisible disability, ethnicity, sex characteristics, gender 8 | identity and expression, level of experience, education, socio-economic status, 9 | nationality, personal appearance, race, caste, color, religion, or sexual 10 | identity and orientation. 11 | 12 | We pledge to act and interact in ways that contribute to an open, welcoming, 13 | diverse, inclusive, and healthy community. 14 | 15 | ## Our Standards 16 | 17 | Examples of behavior that contributes to a positive environment for our 18 | community include: 19 | 20 | * Demonstrating empathy and kindness toward other people 21 | * Being respectful of differing opinions, viewpoints, and experiences 22 | * Giving and gracefully accepting constructive feedback 23 | * Accepting responsibility and apologizing to those affected by our mistakes, 24 | and learning from the experience 25 | * Focusing on what is best not just for us as individuals, but for the overall 26 | community 27 | 28 | Examples of unacceptable behavior include: 29 | 30 | * The use of sexualized language or imagery, and sexual attention or advances of 31 | any kind 32 | * Trolling, insulting or derogatory comments, and personal or political attacks 33 | * Public or private harassment 34 | * Publishing others' private information, such as a physical or email address, 35 | without their explicit permission 36 | * Other conduct which could reasonably be considered inappropriate in a 37 | professional setting 38 | 39 | ## Enforcement Responsibilities 40 | 41 | Community leaders are responsible for clarifying and enforcing our standards of 42 | acceptable behavior and will take appropriate and fair corrective action in 43 | response to any behavior that they deem inappropriate, threatening, offensive, 44 | or harmful. 45 | 46 | Community leaders have the right and responsibility to remove, edit, or reject 47 | comments, commits, code, wiki edits, issues, and other contributions that are 48 | not aligned to this Code of Conduct, and will communicate reasons for moderation 49 | decisions when appropriate. 50 | 51 | ## Scope 52 | 53 | This Code of Conduct applies within all community spaces, and also applies when 54 | an individual is officially representing the community in public spaces. 55 | Examples of representing our community include using an official e-mail address, 56 | posting via an official social media account, or acting as an appointed 57 | representative at an online or offline event. 58 | 59 | ## Enforcement 60 | 61 | Instances of abusive, harassing, or otherwise unacceptable behavior may be 62 | reported to the community leaders responsible for enforcement at 63 | . 64 | All complaints will be reviewed and investigated promptly and fairly. 65 | 66 | All community leaders are obligated to respect the privacy and security of the 67 | reporter of any incident. 68 | 69 | ## Enforcement Guidelines 70 | 71 | Community leaders will follow these Community Impact Guidelines in determining 72 | the consequences for any action they deem in violation of this Code of Conduct: 73 | 74 | ### 1. Correction 75 | 76 | **Community Impact**: Use of inappropriate language or other behavior deemed 77 | unprofessional or unwelcome in the community. 78 | 79 | **Consequence**: A private, written warning from community leaders, providing 80 | clarity around the nature of the violation and an explanation of why the 81 | behavior was inappropriate. A public apology may be requested. 82 | 83 | ### 2. Warning 84 | 85 | **Community Impact**: A violation through a single incident or series of 86 | actions. 87 | 88 | **Consequence**: A warning with consequences for continued behavior. No 89 | interaction with the people involved, including unsolicited interaction with 90 | those enforcing the Code of Conduct, for a specified period of time. This 91 | includes avoiding interactions in community spaces as well as external channels 92 | like social media. Violating these terms may lead to a temporary or permanent 93 | ban. 94 | 95 | ### 3. Temporary Ban 96 | 97 | **Community Impact**: A serious violation of community standards, including 98 | sustained inappropriate behavior. 99 | 100 | **Consequence**: A temporary ban from any sort of interaction or public 101 | communication with the community for a specified period of time. No public or 102 | private interaction with the people involved, including unsolicited interaction 103 | with those enforcing the Code of Conduct, is allowed during this period. 104 | Violating these terms may lead to a permanent ban. 105 | 106 | ### 4. Permanent Ban 107 | 108 | **Community Impact**: Demonstrating a pattern of violation of community 109 | standards, including sustained inappropriate behavior, harassment of an 110 | individual, or aggression toward or disparagement of classes of individuals. 111 | 112 | **Consequence**: A permanent ban from any sort of public interaction within the 113 | community. 114 | 115 | ## Attribution 116 | 117 | This Code of Conduct is adapted from the [Contributor Covenant][homepage], 118 | version 2.1, available at 119 | [https://www.contributor-covenant.org/version/2/1/code_of_conduct.html][v2.1]. 120 | 121 | Community Impact Guidelines were inspired by 122 | [Mozilla's code of conduct enforcement ladder][Mozilla CoC]. 123 | 124 | For answers to common questions about this code of conduct, see the FAQ at 125 | [https://www.contributor-covenant.org/faq][FAQ]. Translations are available at 126 | [https://www.contributor-covenant.org/translations][translations]. 127 | 128 | [homepage]: https://www.contributor-covenant.org 129 | [v2.1]: https://www.contributor-covenant.org/version/2/1/code_of_conduct.html 130 | [Mozilla CoC]: https://github.com/mozilla/diversity 131 | [FAQ]: https://www.contributor-covenant.org/faq 132 | [translations]: https://www.contributor-covenant.org/translations 133 | -------------------------------------------------------------------------------- /LICENCE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2023 Alex Liu 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Nuxt Svgo Loader 2 | 3 | [![npm version][npm-version-src]][npm-version-href] 4 | [![npm downloads][npm-downloads-src]][npm-downloads-href] 5 | [![License][license-src]][license-href] 6 | [![Nuxt][nuxt-src]][nuxt-href] 7 | 8 | Nuxt module to load SVG files as Vue components, using SVGO for optimization. 9 | 10 | ## Features 11 | 12 | - 📁 Load SVG files as Vue components. 13 | - 🎨 Optimize SVGs using SVGO. 14 | - 🛠️ Seamless integration with Nuxt DevTools. 15 | 16 | ## Installation 17 | 18 | Install and add nuxt-svgo-loader to your nuxt.config. 19 | 20 | ```bash 21 | npx nuxi@latest module add nuxt-svgo-loader 22 | ``` 23 | 24 | ```ts 25 | export default defineNuxtConfig({ 26 | modules: ['nuxt-svgo-loader'], 27 | svgoLoader: { 28 | // Options here will be passed to `vite-svg-loader` 29 | } 30 | }) 31 | ``` 32 | 33 | > [!NOTE] 34 | > Since `nuxt-svgo-loader` is a Nuxt module based on `vite-svg-loader`, the configuration for `svgoLoader` remains identical to that of `vite-svg-loader`. You can refer to the documentation of `vite-svg-loader` for the available options [here](https://github.com/jpkleemans/vite-svg-loader?tab=readme-ov-file#vite-svg-loader). 35 | 36 | ## Usage 37 | 38 | ### Component 39 | 40 | SVGs can be explicitly imported as Vue components using the `?component` suffix: 41 | 42 | ```ts 43 | import NuxtSvg from '~/assets/svg/nuxt.svg' 44 | // 45 | ``` 46 | 47 | ### URL 48 | 49 | SVGs can be imported as URLs using the `?url` suffix: 50 | 51 | ```ts 52 | import nuxtSvgUrl from '~/assets/svg/nuxt.svg?url' 53 | // nuxtSvgUrl === '/_nuxt/assets/svg/nuxt.svg' 54 | ``` 55 | 56 | ### Raw 57 | 58 | SVGs can be imported as raw strings using the `?raw` suffix: 59 | 60 | ```ts 61 | import nuxtSvgRaw from '~/assets/svg/nuxt.svg?raw' 62 | // nuxtSvgRaw === ' 72 | ``` 73 | 74 | ## DevTools 75 | 76 | This module adds a new tab to the Nuxt DevTools, which allows you to inspect the SVG files. 77 | 78 |

79 | 80 |

81 | 82 | ## License 83 | 84 | [MIT](./LICENSE) License © 2023-PRESENT [Alex Liu](https://github.com/Mini-ghost) 85 | 86 | 87 | 88 | [npm-version-src]: https://img.shields.io/npm/v/nuxt-svgo-loader?style=flat&colorA=18181B&colorB=28CF8D 89 | [npm-version-href]: https://npmjs.com/package/nuxt-svgo-loader 90 | [npm-downloads-src]: https://img.shields.io/npm/dt/nuxt-svgo-loader?style=flat&colorA=18181B&colorB=28CF8D 91 | [npm-downloads-href]: https://npmjs.com/package/nuxt-svgo-loader 92 | [license-src]: https://img.shields.io/npm/l/nuxt-svgo-loader.svg?style=flat&colorA=18181B&colorB=28CF8D 93 | [license-href]: https://npmjs.com/package/nuxt-svgo-loader 94 | [nuxt-src]: https://img.shields.io/badge/Nuxt-18181B?logo=nuxt.js 95 | [nuxt-href]: https://nuxt.com 96 | -------------------------------------------------------------------------------- /build.config.ts: -------------------------------------------------------------------------------- 1 | import { defineBuildConfig } from 'unbuild' 2 | 3 | export default defineBuildConfig({ 4 | rollup: { emitCJS: true }, 5 | externals: [ 6 | 'vite-svg-loader', 7 | ], 8 | }) 9 | -------------------------------------------------------------------------------- /client/app.vue: -------------------------------------------------------------------------------- 1 | 45 | 46 | 72 | 73 | 91 | -------------------------------------------------------------------------------- /client/components/SvgDetails.vue: -------------------------------------------------------------------------------- 1 | 31 | 32 | 129 | -------------------------------------------------------------------------------- /client/components/SvgPreview.vue: -------------------------------------------------------------------------------- 1 | 7 | 8 | 13 | -------------------------------------------------------------------------------- /client/composables/state.ts: -------------------------------------------------------------------------------- 1 | import type { AsyncDataOptions } from '#app' 2 | import type { BirpcReturn } from 'birpc' 3 | import type { ClientFunctions, ServerFunctions } from '../../src/types' 4 | import { onDevtoolsClientConnected } from '@nuxt/devtools-kit/iframe-client' 5 | 6 | let rpc: BirpcReturn 7 | 8 | export const clientFunctions = { 9 | // will be added in app.vue 10 | } as ClientFunctions 11 | 12 | onDevtoolsClientConnected((client) => { 13 | rpc = client.devtools.extendClientRpc('NUXT_SVGO_LOADER', clientFunctions) 14 | }) 15 | 16 | function useAsyncState(key: string, fn: () => Promise, options?: AsyncDataOptions) { 17 | const nuxt = useNuxtApp() 18 | 19 | const unique = (nuxt.payload.unique = nuxt.payload.unique || ({} as any)) 20 | if (!unique[key]) 21 | unique[key] = useAsyncData(key, fn, options) 22 | 23 | return unique[key].data as Ref 24 | } 25 | 26 | export function useStaticSvgFiles() { 27 | return useAsyncState('getStaticSvgFiles', () => rpc.getStaticSvgFiles()) 28 | } 29 | -------------------------------------------------------------------------------- /client/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import DevtoolsUIKit from '@nuxt/devtools-ui-kit' 2 | import { resolve } from 'pathe' 3 | 4 | export default defineNuxtConfig({ 5 | ssr: false, 6 | modules: [ 7 | DevtoolsUIKit, 8 | ], 9 | devtools: { 10 | enabled: false, 11 | }, 12 | nitro: { 13 | output: { 14 | publicDir: resolve(__dirname, '../dist/client'), 15 | }, 16 | }, 17 | app: { 18 | baseURL: '/__nuxt-svgo-loader', 19 | }, 20 | experimental: { 21 | componentIslands: true, 22 | }, 23 | }) 24 | -------------------------------------------------------------------------------- /client/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "private": true, 3 | "scripts": { 4 | "build": "pnpm prepare && nuxt build", 5 | "dev": "nuxt dev", 6 | "generate": "nuxt generate", 7 | "preview": "nuxt preview" 8 | }, 9 | "devDependencies": { 10 | "@nuxt/devtools-ui-kit": "^1.0.5", 11 | "nuxt": "^3.8.2", 12 | "unocss": "^0.58.0", 13 | "vite-hot-client": "^0.2.3" 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /client/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.nuxt/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /eslint.config.js: -------------------------------------------------------------------------------- 1 | import antfu from '@antfu/eslint-config' 2 | 3 | export default await antfu() 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-svgo-loader", 3 | "type": "module", 4 | "version": "0.5.0", 5 | "packageManager": "pnpm@10.6.1", 6 | "description": "Nuxt module to load SVG files as Vue components, using SVGO for optimization.", 7 | "author": { 8 | "name": "Alex Liu ", 9 | "url": "https://github.com/Mini-ghost" 10 | }, 11 | "license": "MIT", 12 | "homepage": "https://github.com/Mini-ghost/nuxt-svgo-loader#readme", 13 | "repository": { 14 | "type": "git", 15 | "url": "git+https://github.com/Mini-ghost/nuxt-svgo-loader.git" 16 | }, 17 | "bugs": { 18 | "url": "https://github.com/Mini-ghost/nuxt-svgo-loader/issues" 19 | }, 20 | "keywords": [ 21 | "nuxt", 22 | "module", 23 | "nuxt-module", 24 | "svgo", 25 | "svg" 26 | ], 27 | "exports": { 28 | ".": { 29 | "types": "./dist/types.d.ts", 30 | "import": "./dist/module.mjs", 31 | "require": "./dist/module.cjs" 32 | } 33 | }, 34 | "main": "./dist/module.cjs", 35 | "types": "./dist/types.d.ts", 36 | "files": [ 37 | "dist" 38 | ], 39 | "scripts": { 40 | "stub": "nuxt-build-module build --stub", 41 | "build": "pnpm dev:prepare && pnpm build:module && pnpm build:client", 42 | "build:client": "nuxi generate client", 43 | "build:module": "nuxt-build-module build", 44 | "dev": "nuxi dev playground", 45 | "dev:prepare": "nuxt-module-build build --stub && nuxi prepare playground", 46 | "lint": "eslint .", 47 | "prepack": "pnpm build", 48 | "prepare": "husky install", 49 | "release": "bumpp && npm publish" 50 | }, 51 | "dependencies": { 52 | "@nuxt/devtools-kit": "^2.2.1", 53 | "@nuxt/kit": "^3.16.0", 54 | "birpc": "^2.2.0", 55 | "pathe": "^2.0.3", 56 | "perfect-debounce": "^1.0.0", 57 | "scule": "^1.3.0", 58 | "sirv": "^3.0.1", 59 | "svgo": "^3.3.2", 60 | "tinyglobby": "^0.2.12", 61 | "vite-svg-loader": "^5.1.0" 62 | }, 63 | "devDependencies": { 64 | "@antfu/eslint-config": "^4.8.1", 65 | "@nuxt/module-builder": "^0.8.4", 66 | "@nuxt/schema": "^3.16.0", 67 | "@typescript-eslint/eslint-plugin": "^8.26.0", 68 | "bumpp": "^10.0.3", 69 | "eslint": "^9.22.0", 70 | "husky": "^9.1.7", 71 | "lint-staged": "^15.4.3", 72 | "nuxt": "^3.16.0", 73 | "typescript": "^5.8.2" 74 | }, 75 | "resolutions": { 76 | "nuxt-svgo-loader": "link:." 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /playground/assets/svg/nuxt.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /playground/assets/svg/vue.svg: -------------------------------------------------------------------------------- 1 | 2 | -------------------------------------------------------------------------------- /playground/nuxt.config.ts: -------------------------------------------------------------------------------- 1 | import { resolve } from 'node:path' 2 | import process from 'node:process' 3 | import { startSubprocess } from '@nuxt/devtools-kit' 4 | import { defineNuxtModule } from '@nuxt/kit' 5 | import NuxtSvgoLoader from '../src/module' 6 | 7 | export default defineNuxtConfig({ 8 | compatibilityDate: '2025-03-09', 9 | 10 | modules: [ 11 | NuxtSvgoLoader, 12 | 13 | /** 14 | * Start a sub Nuxt Server for developing the client 15 | * The terminal output can be found in the Terminals tab of the devtools. 16 | */ 17 | defineNuxtModule({ 18 | setup(_, nuxt) { 19 | if (!nuxt.options.dev) 20 | return 21 | 22 | const subprocess = startSubprocess( 23 | { 24 | command: 'npx', 25 | args: ['nuxi', 'dev', '--port', '3030'], 26 | cwd: resolve(__dirname, '../client'), 27 | }, 28 | { 29 | id: 'nuxt-svgo-loader:client', 30 | name: 'Nuxt SVGO Loader Client Dev', 31 | }, 32 | ) 33 | subprocess.getProcess().stdout?.on('data', (data) => { 34 | console.log(` sub: ${data.toString()}`) 35 | }) 36 | 37 | process.on('exit', () => { 38 | subprocess.terminate() 39 | }) 40 | }, 41 | }), 42 | ], 43 | 44 | devtools: { 45 | enabled: true, 46 | }, 47 | 48 | svgoLoader: { 49 | svgoConfig: { 50 | // Options here will be passed to svgo 51 | }, 52 | }, 53 | }) 54 | -------------------------------------------------------------------------------- /playground/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "nuxt-svgo-loader-playground", 3 | "type": "module", 4 | "private": true, 5 | "scripts": { 6 | "dev": "nuxi dev", 7 | "build": "nuxi build", 8 | "generate": "nuxi generate" 9 | }, 10 | "devDependencies": { 11 | "nuxt-svgo-loader": "latest" 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /playground/pages/index.vue: -------------------------------------------------------------------------------- 1 | 5 | 6 | 12 | -------------------------------------------------------------------------------- /playground/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./.nuxt/tsconfig.json" 3 | } 4 | -------------------------------------------------------------------------------- /pnpm-workspace.yaml: -------------------------------------------------------------------------------- 1 | packages: 2 | - playground 3 | - client 4 | -------------------------------------------------------------------------------- /src/module.ts: -------------------------------------------------------------------------------- 1 | import type { Config } from 'svgo' 2 | import type { ClientFunctions, ServerFunctions, SvgFilesInfo } from './types' 3 | 4 | import { existsSync } from 'node:fs' 5 | import fsp from 'node:fs/promises' 6 | import { extendServerRpc, onDevToolsInitialized } from '@nuxt/devtools-kit' 7 | import { addTemplate, addVitePlugin, createResolver, defineNuxtModule } from '@nuxt/kit' 8 | import { basename } from 'pathe' 9 | import { debounce } from 'perfect-debounce' 10 | import { glob } from 'tinyglobby' 11 | import SvgLoader from 'vite-svg-loader' 12 | 13 | interface SvgLoaderOptions { 14 | svgoConfig?: Config 15 | svgo?: boolean 16 | defaultImport?: 'url' | 'raw' | 'component' 17 | } 18 | 19 | const DEVTOOLS_CLIENT_PATH = '/__nuxt-svgo-loader' 20 | const DEVTOOLS_CLIENT_PORT = 3030 21 | 22 | export default defineNuxtModule({ 23 | meta: { 24 | name: 'nuxt-svgo-loader', 25 | configKey: 'svgoLoader', 26 | }, 27 | async setup(options, nuxt) { 28 | const { resolve } = createResolver(import.meta.url) 29 | 30 | const { srcDir } = nuxt.options 31 | 32 | addVitePlugin(SvgLoader(options)) 33 | 34 | addTemplate({ 35 | filename: 'nuxt-svgo-loader.d.ts', 36 | getContents() { 37 | return `declare module '*.svg?component' { 38 | import { FunctionalComponent, SVGAttributes } from 'vue' 39 | const src: FunctionalComponent 40 | export default src 41 | } 42 | 43 | declare module '*.svg?url' { 44 | const src: string 45 | export default src 46 | } 47 | 48 | declare module '*.svg?raw' { 49 | const src: string 50 | export default src 51 | } 52 | 53 | declare module '*.svg?skipsvgo' { 54 | import { FunctionalComponent, SVGAttributes } from 'vue' 55 | const src: FunctionalComponent 56 | export default src 57 | } 58 | ` 59 | }, 60 | }) 61 | 62 | nuxt.hook('prepare:types', ({ tsConfig }) => { 63 | tsConfig.include?.push('./nuxt-svgo-loader.d.ts') 64 | }) 65 | 66 | if (nuxt.options.dev) { 67 | const clientPath = resolve('./client') 68 | const isProductionBuild = existsSync(clientPath) 69 | 70 | if (isProductionBuild) { 71 | nuxt.hook('vite:serverCreated', async (server) => { 72 | const sirv = await import('sirv').then(r => r.default || r) 73 | 74 | server.middlewares.use( 75 | DEVTOOLS_CLIENT_PATH, 76 | sirv(resolve('./client'), { 77 | single: true, 78 | dev: true, 79 | }), 80 | ) 81 | 82 | server.middlewares.use(`${DEVTOOLS_CLIENT_PATH}/svg`, async (req, res, next) => { 83 | if (!req.url) 84 | return next() 85 | 86 | if (req.url.endsWith('.svg')) { 87 | try { 88 | res.setHeader('Content-Type', 'image/svg+xml') 89 | res.end(await fsp.readFile(resolve(srcDir, req.url.slice(1)), 'utf-8')) 90 | return 91 | } 92 | catch {} 93 | } 94 | 95 | next() 96 | }) 97 | }) 98 | } 99 | else { 100 | nuxt.hook('vite:extendConfig', (config) => { 101 | config.server ||= {} 102 | config.server.proxy ||= {} 103 | config.server.proxy[`^${DEVTOOLS_CLIENT_PATH}/svg/.*`] = { 104 | target: `http://localhost:${DEVTOOLS_CLIENT_PORT}${DEVTOOLS_CLIENT_PATH}`, 105 | changeOrigin: true, 106 | followRedirects: true, 107 | configure(proxy) { 108 | proxy.on('proxyReq', async (_proxyReq, req, res) => { 109 | if (!req.url) 110 | return 111 | 112 | const path = req.url.slice(`${DEVTOOLS_CLIENT_PATH}/svg/`.length) ?? '' 113 | 114 | res.setHeader('Content-Type', 'image/svg+xml') 115 | res.end(await fsp.readFile(resolve(srcDir, path), 'utf-8')) 116 | }) 117 | }, 118 | } 119 | 120 | config.server.proxy[DEVTOOLS_CLIENT_PATH] = { 121 | target: `http://localhost:${DEVTOOLS_CLIENT_PORT}${DEVTOOLS_CLIENT_PATH}`, 122 | changeOrigin: true, 123 | followRedirects: true, 124 | rewrite: path => path.replace(DEVTOOLS_CLIENT_PATH, ''), 125 | } 126 | }) 127 | } 128 | 129 | nuxt.hook('devtools:customTabs', (tabs) => { 130 | tabs.push({ 131 | title: 'Nuxt SVG Loader', 132 | name: 'nuxt-svgo-loader', 133 | view: { 134 | type: 'iframe', 135 | src: DEVTOOLS_CLIENT_PATH, 136 | }, 137 | }) 138 | }) 139 | 140 | onDevToolsInitialized(() => { 141 | const serverFunctions = {} as ServerFunctions 142 | const rpc = extendServerRpc('NUXT_SVGO_LOADER', serverFunctions) 143 | 144 | let cache: SvgFilesInfo[] | null = null 145 | 146 | const refreshDebounced = debounce(() => { 147 | cache = null 148 | rpc.broadcast.refresh.asEvent('getStaticSvgFiles') 149 | }, 500) 150 | 151 | nuxt.hook('builder:watch', (event) => { 152 | if (event === 'add' || event === 'unlink') 153 | refreshDebounced() 154 | }) 155 | 156 | async function scan() { 157 | if (cache) 158 | return cache 159 | 160 | const files = await glob(['**/*.svg'], { 161 | cwd: srcDir, 162 | onlyFiles: true, 163 | ignore: ['**/node_modules/**', '**/dist/**'], 164 | }) 165 | 166 | cache = await Promise.all( 167 | files.map(async (path) => { 168 | const filePath = resolve(srcDir, path) 169 | const stat = await fsp.lstat(filePath) 170 | 171 | return { 172 | path, 173 | filePath, 174 | name: basename(path), 175 | size: stat.size, 176 | mtime: stat.mtimeMs, 177 | } 178 | }), 179 | ) 180 | 181 | return cache 182 | } 183 | 184 | serverFunctions.getStaticSvgFiles = async () => { 185 | return await scan() 186 | } 187 | }) 188 | } 189 | }, 190 | }) 191 | -------------------------------------------------------------------------------- /src/types.ts: -------------------------------------------------------------------------------- 1 | export interface SvgFilesInfo { 2 | path: string 3 | filePath: string 4 | name: string 5 | size: number 6 | mtime: number 7 | } 8 | 9 | export interface ServerFunctions { 10 | getStaticSvgFiles: () => Promise 11 | } 12 | 13 | export interface ClientFunctions { 14 | refresh: (event: ClientUpdateEvent) => void 15 | } 16 | 17 | export type ClientUpdateEvent = keyof ServerFunctions 18 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "extends": "./playground/.nuxt/tsconfig.json" 3 | } 4 | --------------------------------------------------------------------------------