• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

SpiriitLabs / vite-plugin-svg-spritemap / 13546879406

26 Feb 2025 03:01PM UTC coverage: 96.979% (+0.05%) from 96.926%
13546879406

push

github

Applelo
fix lint issue

223 of 238 branches covered (93.7%)

Branch coverage included in aggregate %.

6 of 6 new or added lines in 1 file covered. (100.0%)

2 existing lines in 1 file now uncovered.

740 of 755 relevant lines covered (98.01%)

297.18 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

93.36
/src/svgManager.ts
1
import type { Config as SvgoConfig } from 'svgo'
2
import type { ResolvedConfig } from 'vite'
3
import type { Options, Pattern, SvgMapObject } from './types'
4
import { promises as fs } from 'node:fs'
1✔
5
import { basename, resolve } from 'node:path'
1✔
6
import { DOMImplementation, DOMParser, XMLSerializer } from '@xmldom/xmldom'
1✔
7
import fg from 'fast-glob'
1✔
8
import hash_sum from 'hash-sum'
1✔
9
import { calculateY } from './helpers/calculateY'
1✔
10
import { cleanAttributes } from './helpers/cleanAttributes'
1✔
11
import { getOptimize, getOptions } from './helpers/svgo'
1✔
12
import { Styles } from './styles/styles'
1✔
13

14
export class SVGManager {
1✔
15
  private _options: Options
16
  private _parser: DOMParser
17
  private _svgs: Map<string, SvgMapObject>
18
  private _iconsPattern: Pattern
19
  private _config: ResolvedConfig
20
  public hash: string | null = null
1✔
21
  private _optimizeOptions: SvgoConfig | false = false
1✔
22
  private _optimize: Awaited<ReturnType<typeof getOptimize>> | null = null
1✔
23

24
  constructor(iconsPattern: Pattern, options: Options, config: ResolvedConfig) {
1✔
25
    this._parser = new DOMParser()
62✔
26
    this._options = options
62✔
27
    this._svgs = new Map()
62✔
28
    this._iconsPattern = iconsPattern
62✔
29
    this._config = config
62✔
30
    this._optimizeOptions = getOptions(typeof this._options.svgo === 'undefined' ? true : this._options.svgo, this._options.prefix)
62✔
31
  }
62✔
32

33
  async update(filePath: string, loop = false) {
1✔
34
    const name = basename(filePath, '.svg')
347✔
35
    if (!name)
347✔
36
      return false
347!
37

38
    let svg: string = await fs.readFile(filePath, 'utf8')
347✔
39
    const document = this._parser.parseFromString(svg, 'image/svg+xml')
347✔
40
    const documentElement = document.documentElement
347✔
41
    let viewBox = (
347✔
42
      documentElement?.getAttribute('viewBox')
347✔
43
      || documentElement?.getAttribute('viewbox')
112✔
44
    )
45
      ?.split(' ')
347✔
46
      .map(a => Number.parseFloat(a))
347✔
47
    const widthAttr = documentElement?.getAttribute('width')
347✔
48
    const heightAttr = documentElement?.getAttribute('height')
347✔
49
    let width = widthAttr ? Number.parseFloat(widthAttr) : undefined
347✔
50
    let height = heightAttr ? Number.parseFloat(heightAttr) : undefined
347✔
51

52
    if (viewBox && viewBox.length !== 4 && (!width || !height)) {
347!
53
      this._config.logger.warn(`[vite-plugin-svg-spritemap] Sprite '${filePath}' is invalid, it's lacking both a viewBox and width/height attributes.`)
×
54

55
      return
×
56
    }
×
57
    if ((!viewBox || viewBox.length !== 4) && width && height)
347✔
58
      viewBox = [0, 0, width, height]
347✔
59

60
    if (!width && viewBox)
347✔
61
      width = viewBox[2]
347✔
62

63
    if (!height && viewBox)
347✔
64
      height = viewBox[3]
347✔
65

66
    if (!width || !height || !viewBox)
347✔
67
      return
347✔
68

69
    if (this._optimize === null) {
347✔
70
      this._optimize = await getOptimize()
61✔
71
      if (this._options.svgo && !this._optimize) {
61✔
72
        this._config.logger.warn(`[vite-plugin-svg-spritemap] You need to install SVGO to be able to optimize your SVG with it.`)
2✔
73
      }
2✔
74
    }
61✔
75

76
    if (this._optimize) {
247✔
77
      if (this._optimizeOptions) {
223✔
78
        const optimizedSvg = this._optimize(svg, this._optimizeOptions)
219✔
79
        if ('data' in optimizedSvg)
219✔
80
          svg = optimizedSvg.data
219✔
81
      }
219✔
82
    }
223✔
83

84
    this._svgs.set(name, {
235✔
85
      width,
235✔
86
      height,
235✔
87
      viewBox,
235✔
88
      filePath,
235✔
89
      source: svg,
235✔
90
    })
235✔
91

92
    if (!loop) {
347!
93
      this.hash = hash_sum(this.spritemap)
×
94
      await this.createFileStyle()
×
95
    }
×
96
  }
347✔
97

98
  async updateAll() {
1✔
99
    const iconsPath = await fg(this._iconsPattern)
62✔
100

101
    for (let index = 0; index < iconsPath.length; index++) {
62✔
102
      const iconPath = iconsPath[index]
347✔
103
      await this.update(iconPath, true)
347✔
104
    }
347✔
105

106
    this.hash = hash_sum(this.spritemap)
62✔
107
    await this.createFileStyle()
62✔
108
  }
62✔
109

110
  get spritemap() {
1✔
111
    const DOM = new DOMImplementation().createDocument(null, '', null)
184✔
112
    const Serializer = new XMLSerializer()
184✔
113
    const spritemap = DOM.createElement('svg')
184✔
114
    spritemap.setAttribute('xmlns', 'http://www.w3.org/2000/svg')
184✔
115

116
    if (this._options.output && this._options.output.use)
184✔
117
      spritemap.setAttribute('xmlns:xlink', 'http://www.w3.org/1999/xlink')
184✔
118

119
    // return empty spritemap
120
    if (!this._svgs.size)
184✔
121
      return Serializer.serializeToString(spritemap)
184✔
122

123
    const sizes: { width: number[], height: number[] } = {
181✔
124
      width: [],
181✔
125
      height: [],
181✔
126
    }
181✔
127
    const parser = new DOMParser()
181✔
128

129
    this._svgs.forEach((svg, name) => {
181✔
130
      const symbol = DOM.createElement('symbol')
703✔
131
      const document = parser.parseFromString(svg.source, 'image/svg+xml')
703✔
132
      const documentElement = document.documentElement
703✔
133
      let attributes = documentElement
703✔
134
        ? cleanAttributes(
703✔
135
            Array.from(documentElement.attributes),
703✔
136
            'symbol',
703✔
137
          )
703!
UNCOV
138
        : []
×
139

140
      // spritemap attributes
141
      attributes.forEach((attr) => {
703✔
142
        // console.log(attr.name, attr.value)
143
        if (attr.name.toLowerCase().startsWith('xmlns:'))
193✔
144
          spritemap.setAttribute(attr.name, attr.value)
193!
145
      })
703✔
146

147
      // symbol attributes
148
      attributes.forEach((attr) => {
703✔
149
        symbol.setAttribute(attr.name, attr.value)
193✔
150
      })
703✔
151
      symbol.setAttribute('id', this._options.idify(name, svg))
703✔
152
      symbol.setAttribute('viewBox', svg.viewBox.join(' '))
703✔
153

154
      // add childs
155
      if (documentElement) {
703✔
156
        Array.from(documentElement.childNodes).forEach((child) => {
703✔
157
          if (child)
977✔
158
            symbol.appendChild(child)
977✔
159
        })
703✔
160
      }
703✔
161

162
      spritemap.appendChild(symbol)
703✔
163
      const y = calculateY(sizes.height)
703✔
164

165
      // use
166
      if (this._options.output && this._options.output.use) {
703✔
167
        const use = DOM.createElement('use')
687✔
168
        use.setAttribute('xlink:href', `#${this._options.prefix}${name}`)
687✔
169
        use.setAttribute('width', svg.width.toString())
687✔
170
        use.setAttribute('height', svg.height.toString())
687✔
171
        use.setAttribute('y', y.toString())
687✔
172
        spritemap.appendChild(use)
687✔
173
      }
687✔
174

175
      // view
176
      if (this._options.output && this._options.output.view) {
703✔
177
        const view = DOM.createElement('view')
679✔
178
        attributes = documentElement && documentElement.attributes
679✔
179
          ? cleanAttributes(
679✔
180
              Array.from(documentElement.attributes),
679✔
181
              'view',
679✔
182
            )
679!
UNCOV
183
          : []
×
184
        attributes.forEach((attr) => {
679✔
185
          view.setAttribute(attr.name, attr.value)
185✔
186
        })
679✔
187
        view.setAttribute('id', `${this._options.prefix + name}-view`)
679✔
188
        view.setAttribute(
679✔
189
          'viewBox',
679✔
190
          `0 ${Math.max(0, y)} ${svg.width} ${svg.height}`,
679✔
191
        )
679✔
192
        spritemap.appendChild(view)
679✔
193
      }
679✔
194

195
      sizes.width.push(svg.width)
703✔
196
      sizes.height.push(svg.height)
703✔
197
    })
181✔
198

199
    return Serializer.serializeToString(spritemap)
181✔
200
  }
184✔
201

202
  private async createFileStyle() {
1✔
203
    if (typeof this._options.styles !== 'object')
62✔
204
      return
62✔
205
    const styleGen: Styles = new Styles(this._svgs, this._options)
32✔
206
    const content = await styleGen.generate()
32✔
207
    const path = resolve(this._config.root, this._options.styles.filename)
32✔
208

209
    await fs.writeFile(path, content, 'utf8')
32✔
210
  }
62✔
211

212
  public get svgs() {
1✔
213
    return this._svgs
8✔
214
  }
8✔
215
}
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc