vite-plugin-svg-sprite 0.5.2 → 0.6.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (44) hide show
  1. package/README.md +57 -46
  2. package/cjs/index.d.ts +3 -1
  3. package/cjs/index.js +47 -34
  4. package/cjs/package.json +1 -1
  5. package/cjs/runtime/adapters/react.d.ts +2 -0
  6. package/cjs/runtime/adapters/react.js +15 -0
  7. package/cjs/runtime/adapters/vanilla.d.ts +2 -0
  8. package/cjs/runtime/adapters/vanilla.js +5 -0
  9. package/cjs/runtime/adapters/vue.d.ts +2 -0
  10. package/cjs/runtime/adapters/vue.js +18 -0
  11. package/cjs/runtime/inject.d.ts +3 -0
  12. package/cjs/{runtime.js → runtime/inject.js} +16 -5
  13. package/cjs/runtime/types.d.ts +1 -0
  14. package/cjs/runtime/types.js +2 -0
  15. package/cjs/runtime/utils.d.ts +1 -0
  16. package/cjs/runtime/utils.js +6 -0
  17. package/cjs/svg-to-symbol.d.ts +8 -0
  18. package/cjs/svg-to-symbol.js +54 -0
  19. package/esm/index.d.ts +3 -1
  20. package/esm/index.js +46 -33
  21. package/esm/runtime/adapters/react.d.ts +2 -0
  22. package/esm/runtime/adapters/react.js +11 -0
  23. package/esm/runtime/adapters/vanilla.d.ts +2 -0
  24. package/esm/runtime/adapters/vanilla.js +1 -0
  25. package/esm/runtime/adapters/vue.d.ts +2 -0
  26. package/esm/runtime/adapters/vue.js +14 -0
  27. package/esm/runtime/inject.d.ts +3 -0
  28. package/esm/{runtime.js → runtime/inject.js} +16 -5
  29. package/esm/runtime/types.d.ts +1 -0
  30. package/esm/runtime/types.js +1 -0
  31. package/esm/runtime/utils.d.ts +1 -0
  32. package/esm/runtime/utils.js +3 -0
  33. package/esm/svg-to-symbol.d.ts +8 -0
  34. package/esm/svg-to-symbol.js +48 -0
  35. package/package.json +26 -30
  36. package/typings/internal.d.ts +1 -0
  37. package/typings/react.d.ts +11 -0
  38. package/typings/vallina.d.ts +9 -0
  39. package/typings/vue.d.ts +11 -0
  40. package/cjs/runtime.d.ts +0 -2
  41. package/client.d.ts +0 -9
  42. package/esm/runtime.d.ts +0 -2
  43. package/src/index.ts +0 -85
  44. package/src/runtime.ts +0 -33
package/README.md CHANGED
@@ -1,15 +1,22 @@
1
1
  # vite-plugin-svg-sprite
2
2
 
3
- SVG sprite plugin for [Vite](https://github.com/vitejs/vite)
3
+ > A Vite plugin for importing SVG files as SVG sprite symbols or components.
4
4
 
5
- ## install
6
- ```
7
- npm i vite-plugin-svg-sprite -D
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
8
15
  ```
9
16
 
10
- ## Usage
17
+ ## How to Use
11
18
 
12
- Add the plugin to your `vite.config.js`:
19
+ To use the plugin, import and configure it in your Vite configuration file (`vite.config.js|ts`):
13
20
 
14
21
  ```javascript
15
22
  import createSvgSpritePlugin from 'vite-plugin-svg-sprite';
@@ -17,66 +24,70 @@ import createSvgSpritePlugin from 'vite-plugin-svg-sprite';
17
24
  const config = {
18
25
  plugins: [
19
26
  createSvgSpritePlugin({
20
- symbolId: 'icon-[name]-[hash]',
27
+ exportType: 'vanilla', // or 'react' or 'vue'
28
+ include: '**/icons/*.svg'
21
29
  }),
22
30
  ],
23
31
  }
24
32
  ```
25
33
 
26
- Then use it like that in your app code:
34
+ ### React
27
35
 
28
- ```jsx
29
- import appIconId from './path/to/icons/app.svg';
36
+ For React projects, set the `exportType` to `'react'` to import SVGs as components:
30
37
 
31
- // react or vue component, as you want
32
- export default function App() {
33
- return (
34
- <svg>
35
- <use
36
- xlinkHref={`#${appIconId}`}
37
- />
38
- </svg>
39
- );
40
- }
38
+ ```javascript
39
+ import IconFoo from './icons/foo.svg';
40
+
41
+ <IconFoo />
41
42
  ```
42
43
 
43
- You can also access the `width`/`height` attributes of the SVG with the `size` export:
44
+ This may seem similar to `svgr` but internally they are different.
44
45
 
45
- ```jsx
46
- import appIconId, { size } from './path/to/icons/app.svg';
46
+ `vite-plugin-svg-sprite` usually has a better render performance.
47
47
 
48
- // react or vue component, as you want
49
- export default function App() {
50
- return (
51
- <svg {...size}>
52
- <use
53
- xlinkHref={`#${appIconId}`}
54
- />
55
- </svg>
56
- );
57
- }
58
- ```
48
+ ### Vue
59
49
 
60
- If you're using TypeScript, add the following line to your `vite-env.d.ts`:
61
- ```diff
62
- /// <reference types="vite/client" />
63
- + /// <reference types="vite-plugin-svg-sprite/client" />
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
+ <IconFoo />
64
56
  ```
65
57
 
66
- ## options
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 `<use>`:
67
61
 
68
62
  ```javascript
69
- const plugin = createSvgSpritePlugin(options);
63
+ import IconFoo from './icons/foo.svg';
64
+ const html = `
65
+ <svg>
66
+ <use xlink:href="#${IconFoo}" />
67
+ </svg>
68
+ `;
70
69
  ```
71
70
 
72
- ### `options.symbolId: string`
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
+ ```
73
81
 
74
- For generating the `id` attribute of `<symbol>` element. Defaults to `[name]`
82
+ ## API Configuration Options
75
83
 
76
- ### `options.include: string | string[]`
84
+ - **symbolId**: (`string`, optional) Controls the generated symbol ID. Default is `'icon-[name]'`.
77
85
 
78
- Match files that will be transformed. Defaults to `'**.svg'`. See [micromatch](https://github.com/micromatch/micromatch) for the syntax document.
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.
79
90
 
80
- ### `options.svgo: boolean | SvgoOptions`
91
+ - **svgo**: (object, optional) Configuration for SVGO, refer to the [SVGO documentation](https://github.com/svg/svgo) for details.
81
92
 
82
- Enable [SVGO](https://github.com/svg/svgo) for optimizing SVG. Defaults to `true`.
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.
package/cjs/index.d.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  import { Config as SvgoOptimizeOptions } from 'svgo';
2
2
  import { Plugin } from 'vite';
3
+ declare const exportTypes: readonly ["vanilla", "react", "vue"];
3
4
  export type { SvgoOptimizeOptions };
4
5
  export interface SvgSpriteOptions {
5
6
  include?: string[] | string;
6
7
  symbolId?: string;
7
- svgo?: boolean | SvgoOptimizeOptions;
8
+ svgo?: SvgoOptimizeOptions;
9
+ exportType?: (typeof exportTypes)[number];
8
10
  moduleSideEffects?: boolean;
9
11
  }
10
12
  declare const _default: (options?: SvgSpriteOptions) => Plugin<any>;
package/cjs/index.js CHANGED
@@ -3,17 +3,21 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
3
3
  return (mod && mod.__esModule) ? mod : { "default": mod };
4
4
  };
5
5
  Object.defineProperty(exports, "__esModule", { value: true });
6
- const path_1 = __importDefault(require("path"));
7
- const fs_1 = __importDefault(require("fs"));
8
- const crypto_1 = __importDefault(require("crypto"));
6
+ const node_path_1 = __importDefault(require("node:path"));
7
+ const node_fs_1 = __importDefault(require("node:fs"));
8
+ const node_crypto_1 = __importDefault(require("node:crypto"));
9
9
  const micromatch_1 = __importDefault(require("micromatch"));
10
- const svg_baker_1 = __importDefault(require("svg-baker"));
11
- const svg_parser_1 = require("svg-parser");
12
10
  const svgo_1 = require("svgo");
11
+ const svg_to_symbol_js_1 = require("./svg-to-symbol.js");
13
12
  const { stringify } = JSON;
13
+ const exportTypes = ['vanilla', 'react', 'vue'];
14
+ function getHash(content) {
15
+ const h = node_crypto_1.default.createHash('sha256');
16
+ h.update(content);
17
+ return h.digest('hex');
18
+ }
14
19
  exports.default = (options) => {
15
20
  var _a;
16
- const svgCompiler = new svg_baker_1.default();
17
21
  const match = (_a = options === null || options === void 0 ? void 0 : options.include) !== null && _a !== void 0 ? _a : '**.svg';
18
22
  const svgoOptions = options === null || options === void 0 ? void 0 : options.svgo;
19
23
  const plugin = {
@@ -25,37 +29,46 @@ exports.default = (options) => {
25
29
  })) {
26
30
  return undefined;
27
31
  }
28
- let code = await fs_1.default.promises.readFile(filepath, 'utf-8');
29
- const root = (0, svg_parser_1.parse)(code);
30
- let topLevelAttributes = {};
31
- if (((_a = root.children[0]) === null || _a === void 0 ? void 0 : _a.type) === 'element') {
32
- topLevelAttributes = (_b = root.children[0].properties) !== null && _b !== void 0 ? _b : {};
32
+ const code = await node_fs_1.default.promises.readFile(filepath, 'utf-8');
33
+ const hash = getHash(code).slice(0, 8);
34
+ const { name } = node_path_1.default.parse(filepath);
35
+ const optimizedSvg = (0, svgo_1.optimize)(code, {
36
+ ...svgoOptions,
37
+ plugins: [
38
+ {
39
+ name: 'prefixIds',
40
+ params: {
41
+ prefix: hash,
42
+ },
43
+ },
44
+ ...(_a = svgoOptions === null || svgoOptions === void 0 ? void 0 : svgoOptions.plugins) !== null && _a !== void 0 ? _a : [],
45
+ ],
46
+ }).data;
47
+ const symbolId = ((_b = options === null || options === void 0 ? void 0 : options.symbolId) !== null && _b !== void 0 ? _b : 'icon-[name]').replace(/\[hash\]/g, hash).replace(/\[name\]/g, name);
48
+ const symbolResults = (0, svg_to_symbol_js_1.svgToSymbol)(optimizedSvg, symbolId);
49
+ if (!symbolResults) {
50
+ throw new Error(`invalid svg file: ${filepath}`);
33
51
  }
34
- const { name } = path_1.default.parse(filepath);
35
- if (svgoOptions !== false) {
36
- const result = ((0, svgo_1.optimize)(code, svgoOptions === true ? undefined : svgoOptions));
37
- code = result.data;
52
+ const { symbolXml, attributes } = symbolResults;
53
+ let exportType = options === null || options === void 0 ? void 0 : options.exportType;
54
+ if (!exportType || !exportTypes.includes(exportType)) {
55
+ exportType = 'vanilla';
38
56
  }
39
- let id = name;
40
- if (options === null || options === void 0 ? void 0 : options.symbolId) {
41
- id = options.symbolId;
42
- if (id.includes('[hash]')) {
43
- const hash = crypto_1.default.createHash('sha256');
44
- hash.update(code);
45
- id = id.replace(/\[hash\]/g, hash.digest('hex').slice(0, 6));
46
- }
47
- id = id.replace(/\[name\]/g, name);
48
- }
49
- const symbol = await svgCompiler.addSymbol({
50
- id,
51
- content: code,
52
- path: filepath,
53
- });
54
57
  const codeToReturn = `
55
- import addSymbol from 'vite-plugin-svg-sprite/runtime';
56
- addSymbol(${stringify(symbol.render())}, ${stringify(id)});
57
- export default ${stringify(id)};
58
- export const size = { width: ${stringify(topLevelAttributes.width)}, height: ${stringify(topLevelAttributes.height)} };
58
+ import addSymbol from 'vite-plugin-svg-sprite/runtime/inject.js';
59
+ import { adapter } from 'vite-plugin-svg-sprite/runtime/adapters/${exportType}.js';
60
+ const id = ${stringify(symbolId)};
61
+ const name = ${stringify(name)};
62
+ const symbolXml = ${stringify(symbolXml)};
63
+ const { dispose } = addSymbol(symbolXml, id);
64
+
65
+ export default adapter(id, name);
66
+ export const attributes = ${stringify(attributes)}
67
+
68
+ if (import.meta.hot) {
69
+ import.meta.hot.dispose(dispose);
70
+ import.meta.hot.accept();
71
+ }
59
72
  `;
60
73
  return {
61
74
  code: codeToReturn,
package/cjs/package.json CHANGED
@@ -1,3 +1,3 @@
1
1
  {
2
2
  "type": "commonjs"
3
- }
3
+ }
@@ -0,0 +1,2 @@
1
+ import { type Adapter } from '../types.js';
2
+ export declare const adapter: Adapter;
@@ -0,0 +1,15 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.adapter = void 0;
4
+ const react_1 = require("react");
5
+ const utils_js_1 = require("../utils.js");
6
+ const adapter = (id, name) => {
7
+ const Icon = (0, react_1.memo)((props) => ((0, react_1.createElement)('svg', {
8
+ width: '1em',
9
+ height: '1em',
10
+ ...props,
11
+ }, (0, react_1.createElement)('use', { xlinkHref: `#${id}` }))));
12
+ Icon.displayName = `Icon${(0, utils_js_1.capitalizeFirstLetter)(name)}`;
13
+ return Icon;
14
+ };
15
+ exports.adapter = adapter;
@@ -0,0 +1,2 @@
1
+ import { type Adapter } from '../types.js';
2
+ export declare const adapter: Adapter;
@@ -0,0 +1,5 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.adapter = void 0;
4
+ const adapter = (id) => id;
5
+ exports.adapter = adapter;
@@ -0,0 +1,2 @@
1
+ import { type Adapter } from '../types.js';
2
+ export declare const adapter: Adapter;
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.adapter = void 0;
4
+ const vue_1 = require("vue");
5
+ const utils_js_1 = require("../utils.js");
6
+ const adapter = (id, name) => (0, vue_1.defineComponent)({
7
+ name: `Icon${(0, utils_js_1.capitalizeFirstLetter)(name)}`,
8
+ setup(props, { attrs }) {
9
+ return () => (0, vue_1.h)('svg', {
10
+ width: '1em',
11
+ height: '1em',
12
+ ...attrs,
13
+ }, [
14
+ (0, vue_1.h)('use', { 'xlink:href': `#${id}` }),
15
+ ]);
16
+ },
17
+ });
18
+ exports.adapter = adapter;
@@ -0,0 +1,3 @@
1
+ type AddSymbol = (symbol: string, id: string) => () => void;
2
+ declare const _default: AddSymbol;
3
+ export default _default;
@@ -2,13 +2,19 @@
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  function createAddSymbol() {
4
4
  if (typeof window === 'undefined' || typeof document === 'undefined') {
5
- return () => { };
5
+ return () => () => { };
6
6
  }
7
- const idSet = window._SVG_SPRITE_IDS_ = window._SVG_SPRITE_IDS_ || [];
7
+ const idSet = (
8
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
9
+ window._SVG_SPRITE_IDS_ = window._SVG_SPRITE_IDS_ || new Set());
8
10
  const root = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
9
11
  root.style.position = 'absolute';
10
12
  root.style.width = '0';
11
13
  root.style.height = '0';
14
+ root.style.overflow = 'hidden';
15
+ root.ariaHidden = 'true';
16
+ // DO NOT SET THIS
17
+ // root.style.visibility = 'hidden';
12
18
  function insertRoot() {
13
19
  document.body.insertBefore(root, document.body.firstChild);
14
20
  }
@@ -19,11 +25,16 @@ function createAddSymbol() {
19
25
  insertRoot();
20
26
  }
21
27
  return function addSymbol(symbol, id) {
22
- if (idSet.indexOf(id) > -1 || document.getElementById(id)) {
23
- console.warn(`Icon #${id} was duplicately registered. It must be globally unique.`);
28
+ if (idSet.has(id) || document.getElementById(id)) {
29
+ console.warn(`Icon #${id} was repeatedly registered. It must be globally unique.`);
24
30
  }
25
- idSet.push(id);
31
+ idSet.add(id);
26
32
  root.insertAdjacentHTML('beforeend', symbol);
33
+ const el = root.lastChild;
34
+ return function dispose() {
35
+ idSet.delete(id);
36
+ el === null || el === void 0 ? void 0 : el.remove();
37
+ };
27
38
  };
28
39
  }
29
40
  exports.default = createAddSymbol();
@@ -0,0 +1 @@
1
+ export type Adapter = (id: string, name: string) => unknown;
@@ -0,0 +1,2 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
@@ -0,0 +1 @@
1
+ export declare function capitalizeFirstLetter(text: string): string;
@@ -0,0 +1,6 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.capitalizeFirstLetter = capitalizeFirstLetter;
4
+ function capitalizeFirstLetter(text) {
5
+ return text.charAt(0).toUpperCase() + text.slice(1);
6
+ }
@@ -0,0 +1,8 @@
1
+ export declare function svgToSymbol(xml: string, id: string): {
2
+ symbolXml: string;
3
+ attributes: {
4
+ width: string | null;
5
+ height: string | null;
6
+ viewBox: string | null;
7
+ };
8
+ } | null;
@@ -0,0 +1,54 @@
1
+ "use strict";
2
+ var __importDefault = (this && this.__importDefault) || function (mod) {
3
+ return (mod && mod.__esModule) ? mod : { "default": mod };
4
+ };
5
+ Object.defineProperty(exports, "__esModule", { value: true });
6
+ exports.svgToSymbol = svgToSymbol;
7
+ const xmldom_1 = require("@xmldom/xmldom");
8
+ const micromatch_1 = __importDefault(require("micromatch"));
9
+ const preserveAttrs = [
10
+ 'viewBox',
11
+ 'preserveAspectRatio',
12
+ 'class',
13
+ 'overflow',
14
+ 'stroke?(-*)',
15
+ 'fill?(-*)',
16
+ 'xmlns?(:*)',
17
+ 'role',
18
+ 'aria-*',
19
+ ];
20
+ function findSvgNode(doc) {
21
+ return Array.from(doc.childNodes).find((node) => node.nodeType === doc.ELEMENT_NODE && node.tagName === 'svg');
22
+ }
23
+ function svgToSymbol(xml, id) {
24
+ const domParser = new xmldom_1.DOMParser();
25
+ const doc = domParser.parseFromString(xml.trim(), 'text/xml');
26
+ const svg = findSvgNode(doc);
27
+ if (!svg) {
28
+ return null;
29
+ }
30
+ const symbol = doc.createElement('symbol');
31
+ symbol.setAttribute('id', id);
32
+ Array.from(svg.attributes).forEach((attr) => {
33
+ if (micromatch_1.default.isMatch(attr.name, preserveAttrs)) {
34
+ symbol.setAttribute(attr.name, attr.value);
35
+ }
36
+ });
37
+ Array.from(svg.childNodes).forEach((node) => {
38
+ symbol.appendChild(node);
39
+ });
40
+ const width = svg.getAttribute('width');
41
+ const height = svg.getAttribute('height');
42
+ if (!symbol.hasAttribute('viewBox') && width && height) {
43
+ symbol.setAttribute('viewBox', `0 0 ${width} ${height}`);
44
+ }
45
+ const serializer = new xmldom_1.XMLSerializer();
46
+ return {
47
+ symbolXml: serializer.serializeToString(symbol),
48
+ attributes: {
49
+ width,
50
+ height,
51
+ viewBox: svg.getAttribute('viewBox'),
52
+ },
53
+ };
54
+ }
package/esm/index.d.ts CHANGED
@@ -1,10 +1,12 @@
1
1
  import { Config as SvgoOptimizeOptions } from 'svgo';
2
2
  import { Plugin } from 'vite';
3
+ declare const exportTypes: readonly ["vanilla", "react", "vue"];
3
4
  export type { SvgoOptimizeOptions };
4
5
  export interface SvgSpriteOptions {
5
6
  include?: string[] | string;
6
7
  symbolId?: string;
7
- svgo?: boolean | SvgoOptimizeOptions;
8
+ svgo?: SvgoOptimizeOptions;
9
+ exportType?: (typeof exportTypes)[number];
8
10
  moduleSideEffects?: boolean;
9
11
  }
10
12
  declare const _default: (options?: SvgSpriteOptions) => Plugin<any>;
package/esm/index.js CHANGED
@@ -1,14 +1,18 @@
1
- import p from 'path';
2
- import fs from 'fs';
3
- import crypto from 'crypto';
1
+ import p from 'node:path';
2
+ import fs from 'node:fs';
3
+ import crypto from 'node:crypto';
4
4
  import micromatch from 'micromatch';
5
- import SVGCompiler from 'svg-baker';
6
- import { parse } from 'svg-parser';
7
5
  import { optimize } from 'svgo';
6
+ import { svgToSymbol } from './svg-to-symbol.js';
8
7
  const { stringify } = JSON;
8
+ const exportTypes = ['vanilla', 'react', 'vue'];
9
+ function getHash(content) {
10
+ const h = crypto.createHash('sha256');
11
+ h.update(content);
12
+ return h.digest('hex');
13
+ }
9
14
  export default (options) => {
10
15
  var _a;
11
- const svgCompiler = new SVGCompiler();
12
16
  const match = (_a = options === null || options === void 0 ? void 0 : options.include) !== null && _a !== void 0 ? _a : '**.svg';
13
17
  const svgoOptions = options === null || options === void 0 ? void 0 : options.svgo;
14
18
  const plugin = {
@@ -20,37 +24,46 @@ export default (options) => {
20
24
  })) {
21
25
  return undefined;
22
26
  }
23
- let code = await fs.promises.readFile(filepath, 'utf-8');
24
- const root = parse(code);
25
- let topLevelAttributes = {};
26
- if (((_a = root.children[0]) === null || _a === void 0 ? void 0 : _a.type) === 'element') {
27
- topLevelAttributes = (_b = root.children[0].properties) !== null && _b !== void 0 ? _b : {};
28
- }
27
+ const code = await fs.promises.readFile(filepath, 'utf-8');
28
+ const hash = getHash(code).slice(0, 8);
29
29
  const { name } = p.parse(filepath);
30
- if (svgoOptions !== false) {
31
- const result = (optimize(code, svgoOptions === true ? undefined : svgoOptions));
32
- code = result.data;
30
+ const optimizedSvg = optimize(code, {
31
+ ...svgoOptions,
32
+ plugins: [
33
+ {
34
+ name: 'prefixIds',
35
+ params: {
36
+ prefix: hash,
37
+ },
38
+ },
39
+ ...(_a = svgoOptions === null || svgoOptions === void 0 ? void 0 : svgoOptions.plugins) !== null && _a !== void 0 ? _a : [],
40
+ ],
41
+ }).data;
42
+ const symbolId = ((_b = options === null || options === void 0 ? void 0 : options.symbolId) !== null && _b !== void 0 ? _b : 'icon-[name]').replace(/\[hash\]/g, hash).replace(/\[name\]/g, name);
43
+ const symbolResults = svgToSymbol(optimizedSvg, symbolId);
44
+ if (!symbolResults) {
45
+ throw new Error(`invalid svg file: ${filepath}`);
33
46
  }
34
- let id = name;
35
- if (options === null || options === void 0 ? void 0 : options.symbolId) {
36
- id = options.symbolId;
37
- if (id.includes('[hash]')) {
38
- const hash = crypto.createHash('sha256');
39
- hash.update(code);
40
- id = id.replace(/\[hash\]/g, hash.digest('hex').slice(0, 6));
41
- }
42
- id = id.replace(/\[name\]/g, name);
47
+ const { symbolXml, attributes } = symbolResults;
48
+ let exportType = options === null || options === void 0 ? void 0 : options.exportType;
49
+ if (!exportType || !exportTypes.includes(exportType)) {
50
+ exportType = 'vanilla';
43
51
  }
44
- const symbol = await svgCompiler.addSymbol({
45
- id,
46
- content: code,
47
- path: filepath,
48
- });
49
52
  const codeToReturn = `
50
- import addSymbol from 'vite-plugin-svg-sprite/runtime';
51
- addSymbol(${stringify(symbol.render())}, ${stringify(id)});
52
- export default ${stringify(id)};
53
- export const size = { width: ${stringify(topLevelAttributes.width)}, height: ${stringify(topLevelAttributes.height)} };
53
+ import addSymbol from 'vite-plugin-svg-sprite/runtime/inject.js';
54
+ import { adapter } from 'vite-plugin-svg-sprite/runtime/adapters/${exportType}.js';
55
+ const id = ${stringify(symbolId)};
56
+ const name = ${stringify(name)};
57
+ const symbolXml = ${stringify(symbolXml)};
58
+ const { dispose } = addSymbol(symbolXml, id);
59
+
60
+ export default adapter(id, name);
61
+ export const attributes = ${stringify(attributes)}
62
+
63
+ if (import.meta.hot) {
64
+ import.meta.hot.dispose(dispose);
65
+ import.meta.hot.accept();
66
+ }
54
67
  `;
55
68
  return {
56
69
  code: codeToReturn,
@@ -0,0 +1,2 @@
1
+ import { type Adapter } from '../types.js';
2
+ export declare const adapter: Adapter;
@@ -0,0 +1,11 @@
1
+ import { memo, createElement } from 'react';
2
+ import { capitalizeFirstLetter } from '../utils.js';
3
+ export const adapter = (id, name) => {
4
+ const Icon = memo((props) => (createElement('svg', {
5
+ width: '1em',
6
+ height: '1em',
7
+ ...props,
8
+ }, createElement('use', { xlinkHref: `#${id}` }))));
9
+ Icon.displayName = `Icon${capitalizeFirstLetter(name)}`;
10
+ return Icon;
11
+ };
@@ -0,0 +1,2 @@
1
+ import { type Adapter } from '../types.js';
2
+ export declare const adapter: Adapter;
@@ -0,0 +1 @@
1
+ export const adapter = (id) => id;
@@ -0,0 +1,2 @@
1
+ import { type Adapter } from '../types.js';
2
+ export declare const adapter: Adapter;
@@ -0,0 +1,14 @@
1
+ import { defineComponent, h } from 'vue';
2
+ import { capitalizeFirstLetter } from '../utils.js';
3
+ export const adapter = (id, name) => defineComponent({
4
+ name: `Icon${capitalizeFirstLetter(name)}`,
5
+ setup(props, { attrs }) {
6
+ return () => h('svg', {
7
+ width: '1em',
8
+ height: '1em',
9
+ ...attrs,
10
+ }, [
11
+ h('use', { 'xlink:href': `#${id}` }),
12
+ ]);
13
+ },
14
+ });
@@ -0,0 +1,3 @@
1
+ type AddSymbol = (symbol: string, id: string) => () => void;
2
+ declare const _default: AddSymbol;
3
+ export default _default;
@@ -1,12 +1,18 @@
1
1
  function createAddSymbol() {
2
2
  if (typeof window === 'undefined' || typeof document === 'undefined') {
3
- return () => { };
3
+ return () => () => { };
4
4
  }
5
- const idSet = window._SVG_SPRITE_IDS_ = window._SVG_SPRITE_IDS_ || [];
5
+ const idSet = (
6
+ // eslint-disable-next-line @typescript-eslint/no-explicit-any
7
+ window._SVG_SPRITE_IDS_ = window._SVG_SPRITE_IDS_ || new Set());
6
8
  const root = document.createElementNS('http://www.w3.org/2000/svg', 'svg');
7
9
  root.style.position = 'absolute';
8
10
  root.style.width = '0';
9
11
  root.style.height = '0';
12
+ root.style.overflow = 'hidden';
13
+ root.ariaHidden = 'true';
14
+ // DO NOT SET THIS
15
+ // root.style.visibility = 'hidden';
10
16
  function insertRoot() {
11
17
  document.body.insertBefore(root, document.body.firstChild);
12
18
  }
@@ -17,11 +23,16 @@ function createAddSymbol() {
17
23
  insertRoot();
18
24
  }
19
25
  return function addSymbol(symbol, id) {
20
- if (idSet.indexOf(id) > -1 || document.getElementById(id)) {
21
- console.warn(`Icon #${id} was duplicately registered. It must be globally unique.`);
26
+ if (idSet.has(id) || document.getElementById(id)) {
27
+ console.warn(`Icon #${id} was repeatedly registered. It must be globally unique.`);
22
28
  }
23
- idSet.push(id);
29
+ idSet.add(id);
24
30
  root.insertAdjacentHTML('beforeend', symbol);
31
+ const el = root.lastChild;
32
+ return function dispose() {
33
+ idSet.delete(id);
34
+ el === null || el === void 0 ? void 0 : el.remove();
35
+ };
25
36
  };
26
37
  }
27
38
  export default createAddSymbol();
@@ -0,0 +1 @@
1
+ export type Adapter = (id: string, name: string) => unknown;
@@ -0,0 +1 @@
1
+ export {};
@@ -0,0 +1 @@
1
+ export declare function capitalizeFirstLetter(text: string): string;
@@ -0,0 +1,3 @@
1
+ export function capitalizeFirstLetter(text) {
2
+ return text.charAt(0).toUpperCase() + text.slice(1);
3
+ }
@@ -0,0 +1,8 @@
1
+ export declare function svgToSymbol(xml: string, id: string): {
2
+ symbolXml: string;
3
+ attributes: {
4
+ width: string | null;
5
+ height: string | null;
6
+ viewBox: string | null;
7
+ };
8
+ } | null;
@@ -0,0 +1,48 @@
1
+ import { DOMParser, XMLSerializer, } from '@xmldom/xmldom';
2
+ import micromatch from 'micromatch';
3
+ const preserveAttrs = [
4
+ 'viewBox',
5
+ 'preserveAspectRatio',
6
+ 'class',
7
+ 'overflow',
8
+ 'stroke?(-*)',
9
+ 'fill?(-*)',
10
+ 'xmlns?(:*)',
11
+ 'role',
12
+ 'aria-*',
13
+ ];
14
+ function findSvgNode(doc) {
15
+ return Array.from(doc.childNodes).find((node) => node.nodeType === doc.ELEMENT_NODE && node.tagName === 'svg');
16
+ }
17
+ export function svgToSymbol(xml, id) {
18
+ const domParser = new DOMParser();
19
+ const doc = domParser.parseFromString(xml.trim(), 'text/xml');
20
+ const svg = findSvgNode(doc);
21
+ if (!svg) {
22
+ return null;
23
+ }
24
+ const symbol = doc.createElement('symbol');
25
+ symbol.setAttribute('id', id);
26
+ Array.from(svg.attributes).forEach((attr) => {
27
+ if (micromatch.isMatch(attr.name, preserveAttrs)) {
28
+ symbol.setAttribute(attr.name, attr.value);
29
+ }
30
+ });
31
+ Array.from(svg.childNodes).forEach((node) => {
32
+ symbol.appendChild(node);
33
+ });
34
+ const width = svg.getAttribute('width');
35
+ const height = svg.getAttribute('height');
36
+ if (!symbol.hasAttribute('viewBox') && width && height) {
37
+ symbol.setAttribute('viewBox', `0 0 ${width} ${height}`);
38
+ }
39
+ const serializer = new XMLSerializer();
40
+ return {
41
+ symbolXml: serializer.serializeToString(symbol),
42
+ attributes: {
43
+ width,
44
+ height,
45
+ viewBox: svg.getAttribute('viewBox'),
46
+ },
47
+ };
48
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vite-plugin-svg-sprite",
3
- "version": "0.5.2",
3
+ "version": "0.6.1",
4
4
  "description": "SVG sprite plugin for Vite",
5
5
  "type": "module",
6
6
  "exports": {
@@ -14,18 +14,9 @@
14
14
  "default": "./cjs/index.js"
15
15
  }
16
16
  },
17
- "./runtime": {
18
- "import": {
19
- "types": "./esm/runtime.d.ts",
20
- "default": "./esm/runtime.js"
21
- },
22
- "require": {
23
- "types": "./cjs/runtime.d.ts",
24
- "default": "./cjs/runtime.js"
25
- }
26
- },
27
- "./client": {
28
- "types": "./client.d.ts"
17
+ "./runtime/*.js": "./esm/runtime/*.js",
18
+ "./react": {
19
+ "types": "./react.d.ts"
29
20
  }
30
21
  },
31
22
  "main": "cjs/index.js",
@@ -33,14 +24,12 @@
33
24
  "files": [
34
25
  "cjs",
35
26
  "esm",
36
- "src",
37
- "client.d.ts",
38
- "index.mjs",
27
+ "typings",
39
28
  "LICENSE"
40
29
  ],
41
30
  "scripts": {
42
31
  "lint": "eslint src",
43
- "build": "rm -rf cjs esm && tsc && npx tsc -m commonjs --outDir cjs && cp ./package-cjs.json cjs/package.json",
32
+ "build": "shx rm -rf cjs esm && tsc && tsc -m commonjs --outDir cjs && shx cp ./package-cjs.json cjs/package.json",
44
33
  "prepublishOnly": "npm run build"
45
34
  },
46
35
  "keywords": [
@@ -56,24 +45,31 @@
56
45
  "devDependencies": {
57
46
  "@types/micromatch": "^4.0.1",
58
47
  "@types/node": "^20.4.2",
59
- "@types/svgo": "^3.0.0",
60
- "@types/svg-parser": "^2.0.3",
61
-
62
- "@typescript-eslint/eslint-plugin": "^7.7.1",
63
- "eslint": "^8.45.0",
64
- "eslint-config-airbnb-base": "^15.0.0",
65
- "eslint-config-airbnb-typescript": "^18.0.0",
66
- "eslint-plugin-import": "^2.22.1",
48
+ "@types/react": "^18.3.12",
49
+ "eslint": "^9.17.0",
50
+ "shx": "^0.3.4",
67
51
  "typescript": "^5.1.6",
68
- "vite": "^5.0.0"
52
+ "typescript-eslint": "^8.18.0",
53
+ "vite": "^6.0.3",
54
+ "vue": "^3.5.13"
69
55
  },
70
56
  "dependencies": {
57
+ "@xmldom/xmldom": "^0.9.4",
71
58
  "micromatch": "^4.0.2",
72
- "svg-baker": "~1.7.0",
73
- "svg-parser": "^2.0.4",
74
59
  "svgo": "^3.0.2"
75
60
  },
76
61
  "peerDependencies": {
77
- "vite": "^2 || ^3 || ^4 || ^5"
78
- }
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
+ "packageManager": "pnpm@9.12.3+sha512.cce0f9de9c5a7c95bef944169cc5dfe8741abfb145078c0d508b868056848a87c81e626246cb60967cbd7fd29a6c062ef73ff840d96b3c86c40ac92cf4a813ee"
79
75
  }
@@ -0,0 +1 @@
1
+ declare module 'domready';
@@ -0,0 +1,11 @@
1
+ declare module '*.svg' {
2
+ import { SVGAttributes, ComponentClass } from 'react';
3
+
4
+ const Component: ComponentClass<SVGAttributes<SVGElement>>;
5
+ export default Component;
6
+ export const attributes: {
7
+ width?: string;
8
+ height?: string;
9
+ viewBox?: string;
10
+ };
11
+ }
@@ -0,0 +1,9 @@
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
+ }
@@ -0,0 +1,11 @@
1
+ declare module '*.svg' {
2
+ import { Component } from 'vue';
3
+
4
+ const component: Component<SVGAttributes<SVGElement>>;
5
+ export default component;
6
+ export const attributes: {
7
+ width?: string;
8
+ height?: string;
9
+ viewBox?: string;
10
+ };
11
+ }
package/cjs/runtime.d.ts DELETED
@@ -1,2 +0,0 @@
1
- declare const _default: (symbol: string, id: string) => void;
2
- export default _default;
package/client.d.ts DELETED
@@ -1,9 +0,0 @@
1
- declare module "*.svg" {
2
- const symbol: string;
3
- export default symbol;
4
-
5
- export const size: {
6
- width: string | number | undefined,
7
- height: string | number | undefined
8
- };
9
- }
package/esm/runtime.d.ts DELETED
@@ -1,2 +0,0 @@
1
- declare const _default: (symbol: string, id: string) => void;
2
- export default _default;
package/src/index.ts DELETED
@@ -1,85 +0,0 @@
1
- import p from 'path';
2
- import fs from 'fs';
3
- import crypto from 'crypto';
4
- import micromatch from 'micromatch';
5
- import SVGCompiler from 'svg-baker';
6
- import { parse } from 'svg-parser';
7
- import { optimize, Config as SvgoOptimizeOptions } from 'svgo';
8
- import { Plugin } from 'vite';
9
-
10
- const { stringify } = JSON;
11
-
12
- export type { SvgoOptimizeOptions };
13
- export interface SvgSpriteOptions {
14
- include?: string[] | string;
15
- symbolId?: string;
16
- svgo?: boolean | SvgoOptimizeOptions;
17
- moduleSideEffects?: boolean;
18
- }
19
-
20
- export default (options?: SvgSpriteOptions) => {
21
- const svgCompiler = new SVGCompiler();
22
- const match = options?.include ?? '**.svg';
23
- const svgoOptions = options?.svgo;
24
-
25
- const plugin: Plugin = {
26
- name: 'svg-sprite',
27
-
28
- async transform(src, filepath) {
29
- if (!micromatch.isMatch(filepath, match, {
30
- dot: true,
31
- })) {
32
- return undefined;
33
- }
34
-
35
- let code = await fs.promises.readFile(filepath, 'utf-8');
36
-
37
- const root = parse(code);
38
- let topLevelAttributes: Record<string, string | number> = {};
39
- if (root.children[0]?.type === 'element') {
40
- topLevelAttributes = root.children[0].properties ?? {};
41
- }
42
-
43
- const { name } = p.parse(filepath);
44
- if (svgoOptions !== false) {
45
- const result = (optimize(code, svgoOptions === true ? undefined : svgoOptions));
46
- code = result.data;
47
- }
48
-
49
- let id = name;
50
-
51
- if (options?.symbolId) {
52
- id = options.symbolId;
53
-
54
- if (id.includes('[hash]')) {
55
- const hash = crypto.createHash('sha256');
56
- hash.update(code);
57
- id = id.replace(/\[hash\]/g, hash.digest('hex').slice(0, 6));
58
- }
59
-
60
- id = id.replace(/\[name\]/g, name);
61
- }
62
-
63
- const symbol = await svgCompiler.addSymbol({
64
- id,
65
- content: code,
66
- path: filepath,
67
- });
68
-
69
- const codeToReturn = `
70
- import addSymbol from 'vite-plugin-svg-sprite/runtime';
71
- addSymbol(${stringify(symbol.render())}, ${stringify(id)});
72
- export default ${stringify(id)};
73
- export const size = { width: ${stringify(topLevelAttributes.width)}, height: ${stringify(topLevelAttributes.height)} };
74
- `;
75
-
76
- return {
77
- code: codeToReturn,
78
- moduleSideEffects: options?.moduleSideEffects ?? true,
79
- map: { mappings: '' },
80
- };
81
- },
82
- };
83
-
84
- return plugin;
85
- };
package/src/runtime.ts DELETED
@@ -1,33 +0,0 @@
1
- function createAddSymbol() {
2
- if (typeof window === 'undefined' || typeof document === 'undefined') {
3
- return () => {};
4
- }
5
-
6
- const idSet = (window as any)._SVG_SPRITE_IDS_ = (window as any)._SVG_SPRITE_IDS_ || [];
7
-
8
- const root = document.createElementNS('http://www.w3.org/2000/svg', 'svg') as SVGSVGElement;
9
- root.style.position = 'absolute';
10
- root.style.width = '0';
11
- root.style.height = '0';
12
-
13
- function insertRoot() {
14
- document.body.insertBefore(root, document.body.firstChild);
15
- }
16
-
17
- if (document.readyState === 'loading') {
18
- document.addEventListener('DOMContentLoaded', insertRoot);
19
- } else {
20
- insertRoot();
21
- }
22
-
23
- return function addSymbol(symbol: string, id: string) {
24
- if (idSet.indexOf(id) > -1 || document.getElementById(id)) {
25
- console.warn(`Icon #${id} was duplicately registered. It must be globally unique.`);
26
- }
27
- idSet.push(id);
28
-
29
- root.insertAdjacentHTML('beforeend', symbol);
30
- };
31
- }
32
-
33
- export default createAddSymbol();