├── .gitignore ├── .npmrc ├── LICENSE ├── README.md ├── eslint.config.mjs ├── examples ├── vite-svg-sprite-example-react │ ├── .gitignore │ ├── .npmrc │ ├── index.html │ ├── package.json │ ├── public │ │ └── favicon.ico │ ├── src │ │ ├── App.tsx │ │ ├── assets │ │ │ └── icons │ │ │ │ ├── alarm.svg │ │ │ │ ├── car.svg │ │ │ │ ├── email.svg │ │ │ │ └── grade.svg │ │ ├── index.css │ │ └── main.ts │ ├── tsconfig.json │ └── vite.config.js ├── vite-svg-sprite-example-vue │ ├── .gitignore │ ├── .npmrc │ ├── index.html │ ├── package.json │ ├── public │ │ └── favicon.ico │ ├── src │ │ ├── App.vue │ │ ├── assets │ │ │ └── icons │ │ │ │ ├── alarm.svg │ │ │ │ ├── car.svg │ │ │ │ ├── email.svg │ │ │ │ └── grade.svg │ │ ├── index.css │ │ └── main.js │ ├── tsconfig.json │ └── vite.config.js └── vite-svg-sprite-example │ ├── .gitignore │ ├── .npmrc │ ├── index.html │ ├── package.json │ ├── public │ └── favicon.ico │ ├── src │ ├── App.vue │ ├── assets │ │ ├── icons │ │ │ ├── alarm.svg │ │ │ ├── car.svg │ │ │ ├── cube.svg │ │ │ ├── email.svg │ │ │ └── grade.svg │ │ └── logo.svg │ ├── components │ │ ├── Dynamic.vue │ │ └── Icon.vue │ ├── index.css │ └── main.js │ ├── tsconfig.json │ └── vite.config.js ├── package-cjs.json ├── package.json ├── src ├── index.ts ├── runtime │ ├── adapters │ │ ├── react.ts │ │ ├── vanilla.ts │ │ └── vue.ts │ ├── inject.ts │ ├── types.ts │ └── utils.ts └── svg-to-symbol.ts ├── tsconfig.json └── typings ├── internal.d.ts ├── react.d.ts ├── vanilla.d.ts └── vue.d.ts /.gitignore: -------------------------------------------------------------------------------- 1 | # Logs 2 | logs 3 | *.log 4 | npm-debug.log* 5 | yarn-debug.log* 6 | yarn-error.log* 7 | lerna-debug.log* 8 | 9 | # Diagnostic reports (https://nodejs.org/api/report.html) 10 | report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json 11 | 12 | # Runtime data 13 | pids 14 | *.pid 15 | *.seed 16 | *.pid.lock 17 | 18 | # Directory for instrumented libs generated by jscoverage/JSCover 19 | lib-cov 20 | 21 | # Coverage directory used by tools like istanbul 22 | coverage 23 | *.lcov 24 | 25 | # nyc test coverage 26 | .nyc_output 27 | 28 | # Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files) 29 | .grunt 30 | 31 | # Bower dependency directory (https://bower.io/) 32 | bower_components 33 | 34 | # node-waf configuration 35 | .lock-wscript 36 | 37 | # Compiled binary addons (https://nodejs.org/api/addons.html) 38 | build/Release 39 | 40 | # Dependency directories 41 | node_modules/ 42 | jspm_packages/ 43 | 44 | # TypeScript cache 45 | *.tsbuildinfo 46 | 47 | # Optional npm cache directory 48 | .npm 49 | 50 | # Optional eslint cache 51 | .eslintcache 52 | 53 | # Microbundle cache 54 | .rpt2_cache/ 55 | .rts2_cache_cjs/ 56 | .rts2_cache_es/ 57 | .rts2_cache_umd/ 58 | 59 | # Optional REPL history 60 | .node_repl_history 61 | 62 | # Output of 'npm pack' 63 | *.tgz 64 | 65 | # Yarn Integrity file 66 | .yarn-integrity 67 | 68 | # dotenv environment variables file 69 | .env 70 | .env.test 71 | 72 | # parcel-bundler cache (https://parceljs.org/) 73 | .cache 74 | 75 | # Next.js build output 76 | .next 77 | 78 | # Nuxt.js build / generate output 79 | .nuxt 80 | dist 81 | 82 | # Gatsby files 83 | .cache/ 84 | # Comment in the public line in if your project uses Gatsby and *not* Next.js 85 | # https://nextjs.org/blog/next-9-1#public-directory-support 86 | # public 87 | 88 | # vuepress build output 89 | .vuepress/dist 90 | 91 | # Serverless directories 92 | .serverless/ 93 | 94 | # FuseBox cache 95 | .fusebox/ 96 | 97 | # DynamoDB Local files 98 | .dynamodb/ 99 | 100 | # TernJS port file 101 | .tern-port 102 | 103 | esm 104 | cjs 105 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2020 Berton Zhu 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 | # vite-plugin-svg-sprite 2 | 3 | > A Vite plugin for importing SVG files as SVG sprite symbols or components. 4 | 5 | ## Installation 6 | 7 | You can install the plugin using npm, pnpm, or yarn: 8 | 9 | ```bash 10 | npm install vite-plugin-svg-sprite --save-dev 11 | # or 12 | pnpm add vite-plugin-svg-sprite --save-dev 13 | # or 14 | yarn add vite-plugin-svg-sprite --dev 15 | ``` 16 | 17 | ## How to Use 18 | 19 | To use the plugin, import and configure it in your Vite configuration file (`vite.config.js|ts`): 20 | 21 | ```javascript 22 | import createSvgSpritePlugin from 'vite-plugin-svg-sprite'; 23 | 24 | const config = { 25 | plugins: [ 26 | createSvgSpritePlugin({ 27 | exportType: 'vanilla', // or 'react' or 'vue' 28 | include: '**/icons/*.svg' 29 | }), 30 | ], 31 | } 32 | ``` 33 | 34 | ### React 35 | 36 | For React projects, set the `exportType` to `'react'` to import SVGs as components: 37 | 38 | ```javascript 39 | import IconFoo from './icons/foo.svg'; 40 | 41 | 42 | ``` 43 | 44 | This may seem similar to `svgr` but internally they are different. 45 | 46 | `vite-plugin-svg-sprite` usually has a better render performance. 47 | 48 | ### Vue 49 | 50 | For Vue projects, set the `exportType` to `'vue'` to import SVGs as components: 51 | 52 | ```javascript 53 | import IconFoo from './icons/foo.svg'; 54 | 55 | 56 | ``` 57 | 58 | ### Non-React / Non-Vue 59 | 60 | For users not using React or Vue, set the `exportType` to `'vanilla'`. The imported value will be the `symbolId`, which can be used with SVG ``: 61 | 62 | ```javascript 63 | import IconFoo from './icons/foo.svg'; 64 | const html = ` 65 | 66 | 67 | 68 | `; 69 | ``` 70 | 71 | ### TypeScript Users 72 | 73 | To get proper type hints in TypeScript, include the appropriate type definitions in your `tsconfig.json`: 74 | 75 | ```json 76 | "types": [ 77 | // or "vite-plugin-svg-sprite/typings/react" | "vite-plugin-svg-sprite/typings/vue" 78 | "vite-plugin-svg-sprite/typings/vanilla" 79 | ], 80 | ``` 81 | 82 | ## API Configuration Options 83 | 84 | - **symbolId**: (`string`, optional) Controls the generated symbol ID. Default is `'icon-[name]'`. 85 | 86 | - **exportType**: (`'vanilla' | 'react' | 'vue'`, optional) Determines the type of the exported value. Default is `'vanilla'`. 87 | - If set to `'vanilla'`, the value will be the `symbolId`. 88 | - If set to `'react'`, the value will be a React component. 89 | - If set to `'vue'`, the value will be a Vue component. 90 | 91 | - **svgo**: (object, optional) Configuration for SVGO, refer to the [SVGO documentation](https://github.com/svg/svgo) for details. 92 | 93 | - **include**: (string | string[], optional) Paths to match SVG files that should be processed. Default is `'**/icons/*.svg'`, following [micromatch](https://github.com/micromatch/micromatch) rules. 94 | -------------------------------------------------------------------------------- /eslint.config.mjs: -------------------------------------------------------------------------------- 1 | import eslint from '@eslint/js'; 2 | import tsEslint from 'typescript-eslint'; 3 | 4 | export default tsEslint.config( 5 | eslint.configs.recommended, 6 | ...tsEslint.configs.recommended, 7 | 8 | { 9 | ignores: ['**/cjs', '**/esm', 'examples'], 10 | }, 11 | // 通用规则 12 | ); 13 | -------------------------------------------------------------------------------- /examples/vite-svg-sprite-example-react/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | *.local 5 | -------------------------------------------------------------------------------- /examples/vite-svg-sprite-example-react/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /examples/vite-svg-sprite-example-react/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/vite-svg-sprite-example-react/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-svg-sprite-example", 3 | "type": "module", 4 | "private": true, 5 | "version": "0.0.0", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build" 9 | }, 10 | "devDependencies": { 11 | "@types/react": "^18.3.12", 12 | "@types/react-dom": "^18.3.1", 13 | "@vitejs/plugin-react": "^4.3.4", 14 | "react": "^18.3.1", 15 | "react-dom": "^18.3.1", 16 | "shx": "^0.3.4", 17 | "vite": "^6.0.3", 18 | "vite-plugin-svg-sprite": "file:../../" 19 | } 20 | } 21 | -------------------------------------------------------------------------------- /examples/vite-svg-sprite-example-react/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meowtec/vite-plugin-svg-sprite/7f433076d79bffac2ef8b6280050fdbe0e59436e/examples/vite-svg-sprite-example-react/public/favicon.ico -------------------------------------------------------------------------------- /examples/vite-svg-sprite-example-react/src/App.tsx: -------------------------------------------------------------------------------- 1 | import IconAlarm, { attributes } from './assets/icons/alarm.svg'; 2 | import IconCar from './assets/icons/car.svg'; 3 | import IconEmail from './assets/icons/email.svg'; 4 | import IconGrade from './assets/icons/grade.svg'; 5 | 6 | export default function App() { 7 | return ( 8 |
9 | ) 10 | } 11 | -------------------------------------------------------------------------------- /examples/vite-svg-sprite-example-react/src/assets/icons/alarm.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/vite-svg-sprite-example-react/src/assets/icons/car.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/vite-svg-sprite-example-react/src/assets/icons/email.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/vite-svg-sprite-example-react/src/assets/icons/grade.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/vite-svg-sprite-example-react/src/index.css: -------------------------------------------------------------------------------- 1 | #app { 2 | text-align: center; 3 | color: #2c3e50; 4 | margin-top: 60px; 5 | } 6 | -------------------------------------------------------------------------------- /examples/vite-svg-sprite-example-react/src/main.ts: -------------------------------------------------------------------------------- 1 | import React from 'react'; 2 | import { createRoot } from 'react-dom/client'; 3 | import App from './App' 4 | import './index.css' 5 | 6 | createRoot(document.getElementById('app')!).render(React.createElement(App)) -------------------------------------------------------------------------------- /examples/vite-svg-sprite-example-react/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016", 4 | "jsx": "react-jsx", 5 | "module": "commonjs", 6 | "types": [ 7 | "vite-plugin-svg-sprite/typings/react" 8 | ], 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "strict": true, 12 | "skipLibCheck": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/vite-svg-sprite-example-react/vite.config.js: -------------------------------------------------------------------------------- 1 | import pluginReact from '@vitejs/plugin-react'; 2 | import createSvgSpritePlugin from 'vite-plugin-svg-sprite'; 3 | 4 | /** 5 | * @type { import('vite').UserConfig } 6 | */ 7 | const config = { 8 | plugins: [ 9 | pluginReact(), 10 | createSvgSpritePlugin({ 11 | include: '**/icons/**/*.svg', 12 | exportType: 'react', 13 | symbolId: 'icon-[name]-[hash]', 14 | })], 15 | }; 16 | 17 | export default config; 18 | -------------------------------------------------------------------------------- /examples/vite-svg-sprite-example-vue/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | *.local 5 | -------------------------------------------------------------------------------- /examples/vite-svg-sprite-example-vue/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /examples/vite-svg-sprite-example-vue/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/vite-svg-sprite-example-vue/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-svg-sprite-example", 3 | "type": "module", 4 | "private": true, 5 | "version": "0.0.0", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build" 9 | }, 10 | "dependencies": { 11 | "vue": "^3.0.7" 12 | }, 13 | "devDependencies": { 14 | "@vitejs/plugin-vue": "^5.2.1", 15 | "vite": "^6.0.3", 16 | "vite-plugin-svg-sprite": "file:../../" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/vite-svg-sprite-example-vue/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meowtec/vite-plugin-svg-sprite/7f433076d79bffac2ef8b6280050fdbe0e59436e/examples/vite-svg-sprite-example-vue/public/favicon.ico -------------------------------------------------------------------------------- /examples/vite-svg-sprite-example-vue/src/App.vue: -------------------------------------------------------------------------------- 1 | 4 | 5 | 11 | -------------------------------------------------------------------------------- /examples/vite-svg-sprite-example-vue/src/assets/icons/alarm.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/vite-svg-sprite-example-vue/src/assets/icons/car.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/vite-svg-sprite-example-vue/src/assets/icons/email.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/vite-svg-sprite-example-vue/src/assets/icons/grade.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/vite-svg-sprite-example-vue/src/index.css: -------------------------------------------------------------------------------- 1 | #app { 2 | text-align: center; 3 | color: #2c3e50; 4 | margin-top: 60px; 5 | } 6 | -------------------------------------------------------------------------------- /examples/vite-svg-sprite-example-vue/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import './index.css' 4 | 5 | // const modules = import.meta.glob('./assets/icons/delay/*.svg') 6 | // Object.entries(modules).forEach(async ([key, el]) => { 7 | // await el() 8 | // }) 9 | 10 | createApp(App).mount('#app') 11 | -------------------------------------------------------------------------------- /examples/vite-svg-sprite-example-vue/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016", 4 | "jsx": "react-jsx", 5 | "module": "commonjs", 6 | "types": [ 7 | "vite-plugin-svg-sprite/typings/vue" 8 | ], 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "strict": true, 12 | "skipLibCheck": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/vite-svg-sprite-example-vue/vite.config.js: -------------------------------------------------------------------------------- 1 | import pluginVue from '@vitejs/plugin-vue'; 2 | import createSvgSpritePlugin from 'vite-plugin-svg-sprite'; 3 | 4 | /** 5 | * @type { import('vite').UserConfig } 6 | */ 7 | const config = { 8 | plugins: [ 9 | pluginVue(), 10 | createSvgSpritePlugin({ 11 | include: '**/icons/**/*.svg', 12 | exportType: 'vue', 13 | symbolId: 'icon-[name]-[hash]', 14 | })], 15 | }; 16 | 17 | export default config; 18 | -------------------------------------------------------------------------------- /examples/vite-svg-sprite-example/.gitignore: -------------------------------------------------------------------------------- 1 | node_modules 2 | .DS_Store 3 | dist 4 | *.local -------------------------------------------------------------------------------- /examples/vite-svg-sprite-example/.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false -------------------------------------------------------------------------------- /examples/vite-svg-sprite-example/index.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | Vite App 8 | 9 | 10 |
11 | 12 | 13 | 14 | -------------------------------------------------------------------------------- /examples/vite-svg-sprite-example/package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-svg-sprite-example", 3 | "type": "module", 4 | "private": true, 5 | "version": "0.0.0", 6 | "scripts": { 7 | "dev": "vite", 8 | "build": "vite build" 9 | }, 10 | "dependencies": { 11 | "vue": "^3.5.13" 12 | }, 13 | "devDependencies": { 14 | "@vitejs/plugin-vue": "^5.2.1", 15 | "vite": "^6.0.3", 16 | "vite-plugin-svg-sprite": "file:../../" 17 | } 18 | } 19 | -------------------------------------------------------------------------------- /examples/vite-svg-sprite-example/public/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/meowtec/vite-plugin-svg-sprite/7f433076d79bffac2ef8b6280050fdbe0e59436e/examples/vite-svg-sprite-example/public/favicon.ico -------------------------------------------------------------------------------- /examples/vite-svg-sprite-example/src/App.vue: -------------------------------------------------------------------------------- 1 | 10 | 11 | 31 | -------------------------------------------------------------------------------- /examples/vite-svg-sprite-example/src/assets/icons/alarm.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/vite-svg-sprite-example/src/assets/icons/car.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/vite-svg-sprite-example/src/assets/icons/cube.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | -------------------------------------------------------------------------------- /examples/vite-svg-sprite-example/src/assets/icons/email.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/vite-svg-sprite-example/src/assets/icons/grade.svg: -------------------------------------------------------------------------------- 1 | -------------------------------------------------------------------------------- /examples/vite-svg-sprite-example/src/assets/logo.svg: -------------------------------------------------------------------------------- 1 | 2 | 3 | -------------------------------------------------------------------------------- /examples/vite-svg-sprite-example/src/components/Dynamic.vue: -------------------------------------------------------------------------------- 1 | 15 | 16 | 41 | -------------------------------------------------------------------------------- /examples/vite-svg-sprite-example/src/components/Icon.vue: -------------------------------------------------------------------------------- 1 | 11 | 12 | 20 | 21 | 31 | -------------------------------------------------------------------------------- /examples/vite-svg-sprite-example/src/index.css: -------------------------------------------------------------------------------- 1 | #app { 2 | font-family: Avenir, Helvetica, Arial, sans-serif; 3 | -webkit-font-smoothing: antialiased; 4 | -moz-osx-font-smoothing: grayscale; 5 | text-align: center; 6 | color: #2c3e50; 7 | margin-top: 60px; 8 | } 9 | -------------------------------------------------------------------------------- /examples/vite-svg-sprite-example/src/main.js: -------------------------------------------------------------------------------- 1 | import { createApp } from 'vue' 2 | import App from './App.vue' 3 | import './index.css' 4 | 5 | // const modules = import.meta.glob('./assets/icons/delay/*.svg') 6 | // Object.entries(modules).forEach(async ([key, el]) => { 7 | // await el() 8 | // }) 9 | 10 | createApp(App).mount('#app') 11 | -------------------------------------------------------------------------------- /examples/vite-svg-sprite-example/tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | "target": "es2016", 4 | "jsx": "react-jsx", 5 | "module": "commonjs", 6 | "types": [ 7 | "vite-plugin-svg-sprite/typings/vanilla" 8 | ], 9 | "esModuleInterop": true, 10 | "forceConsistentCasingInFileNames": true, 11 | "strict": true, 12 | "skipLibCheck": true 13 | } 14 | } 15 | -------------------------------------------------------------------------------- /examples/vite-svg-sprite-example/vite.config.js: -------------------------------------------------------------------------------- 1 | // @ts-check 2 | 3 | import pluginVue from '@vitejs/plugin-vue'; 4 | import createSvgSpritePlugin from 'vite-plugin-svg-sprite'; 5 | 6 | /** 7 | * @type { import('vite').UserConfig } 8 | */ 9 | const config = { 10 | plugins: [ 11 | pluginVue(), 12 | createSvgSpritePlugin({ 13 | include: '**/icons/**/*.svg', 14 | symbolId: 'icon-[name]-[hash]', 15 | })], 16 | }; 17 | 18 | export default config; 19 | -------------------------------------------------------------------------------- /package-cjs.json: -------------------------------------------------------------------------------- 1 | { 2 | "type": "commonjs" 3 | } 4 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "vite-plugin-svg-sprite", 3 | "version": "0.6.2", 4 | "description": "SVG sprite plugin for Vite", 5 | "type": "module", 6 | "exports": { 7 | ".": { 8 | "import": { 9 | "types": "./esm/index.d.ts", 10 | "default": "./esm/index.js" 11 | }, 12 | "require": { 13 | "types": "./cjs/index.d.ts", 14 | "default": "./cjs/index.js" 15 | } 16 | }, 17 | "./runtime/*.js": "./esm/runtime/*.js", 18 | "./typings/*": { 19 | "types": "./typings/*.d.ts" 20 | } 21 | }, 22 | "main": "cjs/index.js", 23 | "types": "cjs/index.d.ts", 24 | "files": [ 25 | "cjs", 26 | "esm", 27 | "typings", 28 | "LICENSE" 29 | ], 30 | "scripts": { 31 | "lint": "eslint src", 32 | "build": "shx rm -rf cjs esm && tsc && tsc -m commonjs --outDir cjs && shx cp ./package-cjs.json cjs/package.json", 33 | "prepublishOnly": "npm run build" 34 | }, 35 | "keywords": [ 36 | "vite", 37 | "svg" 38 | ], 39 | "author": "meowtec", 40 | "homepage": "https://github.com/meowtec/vite-plugin-svg-sprite", 41 | "bugs": { 42 | "url": "https://github.com/meowtec/vite-plugin-svg-sprite/issues" 43 | }, 44 | "license": "MIT", 45 | "devDependencies": { 46 | "@types/micromatch": "^4.0.1", 47 | "@types/node": "^20.4.2", 48 | "@types/react": "^18.3.12", 49 | "eslint": "^9.17.0", 50 | "shx": "^0.3.4", 51 | "typescript": "^5.1.6", 52 | "typescript-eslint": "^8.18.0", 53 | "vite": "^6.0.3", 54 | "vue": "^3.5.13" 55 | }, 56 | "dependencies": { 57 | "@xmldom/xmldom": "^0.9.4", 58 | "micromatch": "^4.0.2", 59 | "svgo": "^3.0.2" 60 | }, 61 | "peerDependencies": { 62 | "react": "17 || 18 || 19", 63 | "vite": "2 || 3 || 4 || 5 || 6", 64 | "vue": "3" 65 | }, 66 | "peerDependenciesMeta": { 67 | "react": { 68 | "optional": true 69 | }, 70 | "vue": { 71 | "optional": true 72 | } 73 | } 74 | } 75 | -------------------------------------------------------------------------------- /src/index.ts: -------------------------------------------------------------------------------- 1 | import p from 'node:path'; 2 | import fs from 'node:fs'; 3 | import crypto from 'node:crypto'; 4 | import micromatch from 'micromatch'; 5 | import { optimize, Config as SvgoOptimizeOptions } from 'svgo'; 6 | import { Plugin } from 'vite'; 7 | import { svgToSymbol } from './svg-to-symbol.js'; 8 | 9 | const { stringify } = JSON; 10 | 11 | const exportTypes = ['vanilla', 'react', 'vue'] as const; 12 | 13 | export type { SvgoOptimizeOptions }; 14 | export interface SvgSpriteOptions { 15 | include?: string[] | string; 16 | symbolId?: string; 17 | svgo?: SvgoOptimizeOptions; 18 | exportType?: (typeof exportTypes)[number]; 19 | moduleSideEffects?: boolean; 20 | } 21 | 22 | function getHash(content: string) { 23 | const h = crypto.createHash('sha256'); 24 | h.update(content); 25 | return h.digest('hex'); 26 | } 27 | 28 | export default (options?: SvgSpriteOptions) => { 29 | const match = options?.include ?? '**.svg'; 30 | const svgoOptions = options?.svgo; 31 | 32 | const plugin: Plugin = { 33 | name: 'svg-sprite', 34 | 35 | async transform(src, filepath) { 36 | if (!micromatch.isMatch(filepath, match, { 37 | dot: true, 38 | })) { 39 | return undefined; 40 | } 41 | 42 | const code = await fs.promises.readFile(filepath, 'utf-8'); 43 | 44 | const hash = getHash(code).slice(0, 8); 45 | 46 | const { name } = p.parse(filepath); 47 | 48 | const optimizedSvg = optimize(code, { 49 | ...svgoOptions, 50 | plugins: [ 51 | { 52 | name: 'prefixIds', 53 | params: { 54 | prefix: hash, 55 | }, 56 | }, 57 | ...svgoOptions?.plugins ?? [], 58 | ], 59 | }).data; 60 | 61 | const symbolId = (options?.symbolId ?? 'icon-[name]').replace(/\[hash\]/g, hash).replace(/\[name\]/g, name); 62 | 63 | const symbolResults = svgToSymbol(optimizedSvg, symbolId); 64 | 65 | if (!symbolResults) { 66 | throw new Error(`invalid svg file: ${filepath}`); 67 | } 68 | 69 | const { symbolXml, attributes } = symbolResults; 70 | 71 | let exportType = options?.exportType; 72 | if (!exportType || !exportTypes.includes(exportType)) { 73 | exportType = 'vanilla'; 74 | } 75 | 76 | const codeToReturn = ` 77 | import addSymbol from 'vite-plugin-svg-sprite/runtime/inject.js'; 78 | import { adapter } from 'vite-plugin-svg-sprite/runtime/adapters/${exportType}.js'; 79 | const id = ${stringify(symbolId)}; 80 | const name = ${stringify(name)}; 81 | const symbolXml = ${stringify(symbolXml)}; 82 | const { dispose } = addSymbol(symbolXml, id); 83 | 84 | export default adapter(id, name); 85 | export const attributes = ${stringify(attributes)} 86 | 87 | if (import.meta.hot) { 88 | import.meta.hot.dispose(dispose); 89 | import.meta.hot.accept(); 90 | } 91 | `; 92 | 93 | return { 94 | code: codeToReturn, 95 | moduleSideEffects: options?.moduleSideEffects ?? true, 96 | map: { mappings: '' }, 97 | }; 98 | }, 99 | }; 100 | 101 | return plugin; 102 | }; 103 | -------------------------------------------------------------------------------- /src/runtime/adapters/react.ts: -------------------------------------------------------------------------------- 1 | import { memo, createElement } from 'react'; 2 | import { type Adapter } from '../types.js'; 3 | import { capitalizeFirstLetter } from '../utils.js'; 4 | 5 | export const adapter: Adapter = (id, name) => { 6 | const Icon = memo((props) => ( 7 | createElement('svg', { 8 | width: '1em', 9 | height: '1em', 10 | ...props, 11 | }, createElement('use', { xlinkHref: `#${id}` })) 12 | )); 13 | 14 | Icon.displayName = `Icon${capitalizeFirstLetter(name)}`; 15 | 16 | return Icon; 17 | }; 18 | -------------------------------------------------------------------------------- /src/runtime/adapters/vanilla.ts: -------------------------------------------------------------------------------- 1 | import { type Adapter } from '../types.js'; 2 | 3 | export const adapter: Adapter = (id) => id; 4 | -------------------------------------------------------------------------------- /src/runtime/adapters/vue.ts: -------------------------------------------------------------------------------- 1 | import { defineComponent, h } from 'vue'; 2 | import { type Adapter } from '../types.js'; 3 | import { capitalizeFirstLetter } from '../utils.js'; 4 | 5 | export const adapter: Adapter = (id, name) => defineComponent({ 6 | name: `Icon${capitalizeFirstLetter(name)}`, 7 | setup(props, { attrs }) { 8 | return () => h('svg', { 9 | width: '1em', 10 | height: '1em', 11 | ...attrs, 12 | }, [ 13 | h('use', { 'xlink:href': `#${id}` }), 14 | ]); 15 | }, 16 | }); 17 | -------------------------------------------------------------------------------- /src/runtime/inject.ts: -------------------------------------------------------------------------------- 1 | type AddSymbol = (symbol: string, id: string) => () => void; 2 | 3 | function createAddSymbol(): AddSymbol { 4 | if (typeof window === 'undefined' || typeof document === 'undefined') { 5 | return () => () => {}; 6 | } 7 | 8 | const idSet: Set = ( 9 | // eslint-disable-next-line @typescript-eslint/no-explicit-any 10 | (window as any)._SVG_SPRITE_IDS_ = (window as any)._SVG_SPRITE_IDS_ || new Set() 11 | ); 12 | 13 | const root = document.createElementNS('http://www.w3.org/2000/svg', 'svg') as SVGSVGElement; 14 | root.style.position = 'absolute'; 15 | root.style.width = '0'; 16 | root.style.height = '0'; 17 | root.style.overflow = 'hidden'; 18 | root.ariaHidden = 'true'; 19 | 20 | // DO NOT SET THIS 21 | // root.style.visibility = 'hidden'; 22 | 23 | function insertRoot() { 24 | document.body.insertBefore(root, document.body.firstChild); 25 | } 26 | 27 | if (document.readyState === 'loading') { 28 | document.addEventListener('DOMContentLoaded', insertRoot); 29 | } else { 30 | insertRoot(); 31 | } 32 | 33 | return function addSymbol(symbol: string, id: string) { 34 | if (idSet.has(id) || document.getElementById(id)) { 35 | console.warn(`Icon #${id} was repeatedly registered. It must be globally unique.`); 36 | } 37 | idSet.add(id); 38 | 39 | root.insertAdjacentHTML('beforeend', symbol); 40 | 41 | const el = root.lastChild; 42 | 43 | return function dispose() { 44 | idSet.delete(id); 45 | el?.remove(); 46 | }; 47 | }; 48 | } 49 | 50 | export default createAddSymbol(); 51 | -------------------------------------------------------------------------------- /src/runtime/types.ts: -------------------------------------------------------------------------------- 1 | export type Adapter = (id: string, name: string) => unknown; 2 | -------------------------------------------------------------------------------- /src/runtime/utils.ts: -------------------------------------------------------------------------------- 1 | export function capitalizeFirstLetter(text: string) { 2 | return text.charAt(0).toUpperCase() + text.slice(1); 3 | } 4 | -------------------------------------------------------------------------------- /src/svg-to-symbol.ts: -------------------------------------------------------------------------------- 1 | import { 2 | DOMParser, XMLSerializer, Document, Element, 3 | } from '@xmldom/xmldom'; 4 | import micromatch from 'micromatch'; 5 | 6 | const preserveAttrs = [ 7 | 'viewBox', 8 | 'preserveAspectRatio', 9 | 'class', 10 | 'overflow', 11 | 'stroke?(-*)', 12 | 'fill?(-*)', 13 | 'xmlns?(:*)', 14 | 'role', 15 | 'aria-*', 16 | ]; 17 | 18 | function findSvgNode(doc: Document): Element | undefined { 19 | return Array.from(doc.childNodes).find( 20 | (node) => node.nodeType === doc.ELEMENT_NODE && (node as Element).tagName === 'svg', 21 | ) as Element | undefined; 22 | } 23 | 24 | export function svgToSymbol(xml: string, id: string) { 25 | const domParser = new DOMParser(); 26 | const doc = domParser.parseFromString(xml.trim(), 'text/xml'); 27 | 28 | const svg = findSvgNode(doc); 29 | 30 | if (!svg) { 31 | return null; 32 | } 33 | 34 | const symbol = doc.createElement('symbol'); 35 | symbol.setAttribute('id', id); 36 | 37 | Array.from(svg.attributes).forEach((attr) => { 38 | if (micromatch.isMatch(attr.name, preserveAttrs)) { 39 | symbol.setAttribute(attr.name, attr.value); 40 | } 41 | }); 42 | 43 | Array.from(svg.childNodes).forEach((node) => { 44 | symbol.appendChild(node); 45 | }); 46 | 47 | const width = svg.getAttribute('width'); 48 | const height = svg.getAttribute('height'); 49 | 50 | if (!symbol.hasAttribute('viewBox') && width && height) { 51 | symbol.setAttribute('viewBox', `0 0 ${width} ${height}`); 52 | } 53 | 54 | const serializer = new XMLSerializer(); 55 | 56 | return { 57 | symbolXml: serializer.serializeToString(symbol), 58 | attributes: { 59 | width, 60 | height, 61 | viewBox: svg.getAttribute('viewBox'), 62 | }, 63 | }; 64 | } 65 | -------------------------------------------------------------------------------- /tsconfig.json: -------------------------------------------------------------------------------- 1 | { 2 | "compilerOptions": { 3 | /* Visit https://aka.ms/tsconfig.json to read more about this file */ 4 | 5 | /* Basic Options */ 6 | // "incremental": true, /* Enable incremental compilation */ 7 | "target": "es2018", /* Specify ECMAScript target version: 'ES3' (default), 'ES5', 'ES2015', 'ES2016', 'ES2017', 'ES2018', 'ES2019', 'ES2020', or 'ESNEXT'. */ 8 | "module": "ES2015", /* Specify module code generation: 'none', 'commonjs', 'amd', 'system', 'umd', 'es2015', 'es2020', or 'ESNext'. */ 9 | // "lib": [], /* Specify library files to be included in the compilation. */ 10 | "allowJs": true, /* Allow javascript files to be compiled. */ 11 | // "checkJs": true, /* Report errors in .js files. */ 12 | // "jsx": "preserve", /* Specify JSX code generation: 'preserve', 'react-native', or 'react'. */ 13 | "declaration": true, /* Generates corresponding '.d.ts' file. */ 14 | // "declarationMap": true, /* Generates a sourcemap for each corresponding '.d.ts' file. */ 15 | // "sourceMap": true, /* Generates corresponding '.map' file. */ 16 | // "outFile": "./", /* Concatenate and emit output to single file. */ 17 | "outDir": "./esm", /* Redirect output structure to the directory. */ 18 | "rootDir": "./src", /* Specify the root directory of input files. Use to control the output directory structure with --outDir. */ 19 | // "composite": true, /* Enable project compilation */ 20 | // "tsBuildInfoFile": "./", /* Specify file to store incremental compilation information */ 21 | // "removeComments": true, /* Do not emit comments to output. */ 22 | // "noEmit": true, /* Do not emit outputs. */ 23 | // "importHelpers": true, /* Import emit helpers from 'tslib'. */ 24 | // "downlevelIteration": true, /* Provide full support for iterables in 'for-of', spread, and destructuring when targeting 'ES5' or 'ES3'. */ 25 | // "isolatedModules": true, /* Transpile each file as a separate module (similar to 'ts.transpileModule'). */ 26 | 27 | /* Strict Type-Checking Options */ 28 | "strict": true, /* Enable all strict type-checking options. */ 29 | // "noImplicitAny": true, /* Raise error on expressions and declarations with an implied 'any' type. */ 30 | // "strictNullChecks": true, /* Enable strict null checks. */ 31 | // "strictFunctionTypes": true, /* Enable strict checking of function types. */ 32 | // "strictBindCallApply": true, /* Enable strict 'bind', 'call', and 'apply' methods on functions. */ 33 | // "strictPropertyInitialization": true, /* Enable strict checking of property initialization in classes. */ 34 | // "noImplicitThis": true, /* Raise error on 'this' expressions with an implied 'any' type. */ 35 | // "alwaysStrict": true, /* Parse in strict mode and emit "use strict" for each source file. */ 36 | 37 | /* Additional Checks */ 38 | // "noUnusedLocals": true, /* Report errors on unused locals. */ 39 | // "noUnusedParameters": true, /* Report errors on unused parameters. */ 40 | // "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */ 41 | // "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */ 42 | // "noUncheckedIndexedAccess": true, /* Include 'undefined' in index signature results */ 43 | 44 | /* Module Resolution Options */ 45 | "moduleResolution": "node", /* Specify module resolution strategy: 'node' (Node.js) or 'classic' (TypeScript pre-1.6). */ 46 | // "baseUrl": "./", /* Base directory to resolve non-absolute module names. */ 47 | // "paths": {}, /* A series of entries which re-map imports to lookup locations relative to the 'baseUrl'. */ 48 | // "rootDirs": [], /* List of root folders whose combined content represents the structure of the project at runtime. */ 49 | // "typeRoots": [], /* List of folders to include type definitions from. */ 50 | // "types": [], /* Type declaration files to be included in compilation. */ 51 | // "allowSyntheticDefaultImports": true, /* Allow default imports from modules with no default export. This does not affect code emit, just typechecking. */ 52 | "esModuleInterop": true, /* Enables emit interoperability between CommonJS and ES Modules via creation of namespace objects for all imports. Implies 'allowSyntheticDefaultImports'. */ 53 | // "preserveSymlinks": true, /* Do not resolve the real path of symlinks. */ 54 | // "allowUmdGlobalAccess": true, /* Allow accessing UMD globals from modules. */ 55 | 56 | /* Source Map Options */ 57 | // "sourceRoot": "", /* Specify the location where debugger should locate TypeScript files instead of source locations. */ 58 | // "mapRoot": "", /* Specify the location where debugger should locate map files instead of generated locations. */ 59 | // "inlineSourceMap": true, /* Emit a single file with source maps instead of having a separate file. */ 60 | // "inlineSources": true, /* Emit the source alongside the sourcemaps within a single file; requires '--inlineSourceMap' or '--sourceMap' to be set. */ 61 | 62 | /* Experimental Options */ 63 | // "experimentalDecorators": true, /* Enables experimental support for ES7 decorators. */ 64 | // "emitDecoratorMetadata": true, /* Enables experimental support for emitting type metadata for decorators. */ 65 | 66 | /* Advanced Options */ 67 | "skipLibCheck": true, /* Skip type checking of declaration files. */ 68 | "forceConsistentCasingInFileNames": true /* Disallow inconsistently-cased references to the same file. */ 69 | }, 70 | "include": ["src", "typings/internals"], 71 | } 72 | -------------------------------------------------------------------------------- /typings/internal.d.ts: -------------------------------------------------------------------------------- 1 | declare module 'domready'; 2 | -------------------------------------------------------------------------------- /typings/react.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | import { SVGAttributes, ComponentClass } from 'react'; 3 | 4 | const Component: ComponentClass>; 5 | export default Component; 6 | export const attributes: { 7 | width?: string; 8 | height?: string; 9 | viewBox?: string; 10 | }; 11 | } 12 | -------------------------------------------------------------------------------- /typings/vanilla.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | const symbolId: string; 3 | export default symbolId; 4 | export const attributes: { 5 | width?: string; 6 | height?: string; 7 | viewBox?: string; 8 | }; 9 | } 10 | -------------------------------------------------------------------------------- /typings/vue.d.ts: -------------------------------------------------------------------------------- 1 | declare module '*.svg' { 2 | import { Component } from 'vue'; 3 | 4 | const component: Component>; 5 | export default component; 6 | export const attributes: { 7 | width?: string; 8 | height?: string; 9 | viewBox?: string; 10 | }; 11 | } 12 | --------------------------------------------------------------------------------