@onedarnleyroad/vite-plugin-svg-sprite 2.0.0-beta.8 → 2.1.0

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 (3) hide show
  1. package/README.md +46 -13
  2. package/package.json +14 -3
  3. package/src/index.js +9 -5
package/README.md CHANGED
@@ -1,6 +1,9 @@
1
1
  # @onedarnleyroad/vite-plugin-svg-sprite
2
2
 
3
- A Vite plugin that builds SVG sprite sheets from a directory of SVG files — with content-hashed filenames and full Vite manifest integration.
3
+ [![npm](https://img.shields.io/npm/v/@onedarnleyroad/vite-plugin-svg-sprite)](https://www.npmjs.com/package/@onedarnleyroad/vite-plugin-svg-sprite)
4
+ [![license](https://img.shields.io/npm/l/@onedarnleyroad/vite-plugin-svg-sprite)](./LICENSE)
5
+
6
+ A Vite plugin that builds **content-hashed SVG sprite sheets** at build time and registers them in **Vite's manifest** — built for server-rendered apps like **Craft CMS** (via [nystudio107's Vite plugin](https://nystudio107.com/docs/vite/)), and works with any manifest consumer. Organise icons into folders to get one sprite per folder, and reference them from templates with no JavaScript.
4
7
 
5
8
  ## Features
6
9
 
@@ -109,28 +112,32 @@ With Craft CMS + [nystudio107/craft-vite](https://nystudio107.com/docs/vite/):
109
112
 
110
113
  #### A reusable, accessible macro
111
114
 
112
- Rather than hand-writing the `<svg><use>` wrapper each time, wrap it in a Twig macro. Pass `folder:` for a subfolder spriteit maps to the `sprite-{folder}.svg` manifest key for you, so templates keep the folder mental model. Icons default to decorative (`aria-hidden="true"`), and switch to a labelled `role="img"` when the icon carries meaning on its own:
115
+ Rather than hand-writing the `<svg><use>` wrapper each time, wrap it in a Twig macro. The two things you set most often the icon **name** and its **classes** are positional; the rest (subfolder, accessible label, extra attributes) goes in an `options` hash. Icons default to decorative (`aria-hidden="true"`), and switch to a labelled `role="img"` when the icon carries meaning on its own:
113
116
 
114
117
  ```twig
115
- {% macro icon(name, folder = null, label = null, prefix = 'sprite', class = 'icon', extra = {}) %}
116
- {% set key = folder ? "#{prefix}-#{folder}.svg" : "#{prefix}.svg" %}
117
- {% set a11y = label
118
+ {% macro icon(name, classList = '', options = {}) %}
119
+ {% set folder = options.folder ?? null %}
120
+ {% set label = options.label ?? null %}
121
+ {% set file = folder ? "sprite-#{folder}.svg" : 'sprite.svg' %}
122
+ {% set a11y = label
118
123
  ? { role: 'img', 'aria-label': label }
119
124
  : { 'aria-hidden': 'true', focusable: 'false' } %}
120
- <svg{{ attr({ class: class }|merge(a11y)|merge(extra)) }}>
121
- <use href="{{ craft.vite.entry(key) }}#svg-{{ name }}"></use>
125
+ <svg{{ attr({ class: classList }|merge(a11y)|merge(options.attrs ?? {})) }}>
126
+ <use href="{{ craft.vite.entry(file) }}#svg-{{ name }}"></use>
122
127
  </svg>
123
128
  {% endmacro %}
124
129
  ```
125
130
 
126
131
  ```twig
127
- {{ icon('arrow') }} {# root sprite → entry('sprite.svg') #}
128
- {{ icon('sun', folder: 'light') }} {# subfolder → entry('sprite-light.svg') #}
129
- {{ icon('search', label: 'Search') }} {# meaningful → role="img" + aria-label #}
130
- <button aria-label="Close">{{ icon('x') }}</button> {# icon-only control: label the button #}
132
+ {{ icon('arrow', 'icon') }} {# root sprite → entry('sprite.svg') #}
133
+ {{ icon('sun', 'icon', { folder: 'light' }) }} {# subfolder → entry('sprite-light.svg') #}
134
+ {{ icon('search', 'icon', { label: 'Search' }) }} {# meaningful → role="img" + aria-label #}
135
+ <button aria-label="Close">{{ icon('x', 'icon') }}</button> {# icon-only control: label the button #}
131
136
  ```
132
137
 
133
- Reach for `aria-hidden="true"` only when the icon is decorative — i.e. sitting next to visible text. A standalone, meaning-bearing icon needs an accessible name (via `label`, or on the enclosing control such as a button), or it's invisible to assistive tech. (`attr()` is Craft's built-in attribute renderer.)
138
+ Reach for `aria-hidden="true"` only when the icon is decorative — i.e. sitting next to visible text. A standalone, meaning-bearing icon needs an accessible name (via `options.label`, or on the enclosing control such as a button), or it's invisible to assistive tech. (`'icon'` is a CSS class you define to size the `<svg>` — sprite symbols have no intrinsic dimensions; the walkthrough below covers sizing and colour. `attr()` is Craft's built-in attribute renderer.)
139
+
140
+ > **Full Craft CMS walkthrough:** [SVG icon sprites in Craft CMS with Vite](https://github.com/onedarnleyroad/craftcms/wiki/SVG-icon-sprites-in-Craft-CMS-with-Vite) — end-to-end setup, the macro, sizing & colour gotchas, and sprites vs. inlining.
134
141
 
135
142
  ## Development workflow
136
143
 
@@ -144,7 +151,7 @@ Every edit under `inputDir` triggers a rebuild (a second or two), updates `manif
144
151
 
145
152
  ### Without manifest
146
153
 
147
- You can reference the sprite directly if you don't use a manifest but the filename will be hashed, so you'd need to read the hash yourself or disable hashing manually. The manifest-based flow is strongly recommended.
154
+ You can reference a sprite file directly without a manifest, but its filename is always content-hashed — there's no option to turn hashing off — so you'd have to discover the emitted name yourself (e.g. by globbing `assets/sprite-*.svg`). The manifest-based flow is strongly recommended.
148
155
 
149
156
  ## Migrating from v1
150
157
 
@@ -167,6 +174,32 @@ svgSprite({
167
174
 
168
175
  Passing `outputFile` in v2 throws an error. Update your templates to read the sprite URL from Vite's manifest instead of hard-coding the path.
169
176
 
177
+ ## How this differs from other SVG sprite plugins
178
+
179
+ Several Vite plugins generate SVG sprites; they mostly split by *how the sprite reaches the page*:
180
+
181
+ - **Runtime injection** (e.g. [`vite-plugin-svg-icons`](https://github.com/vbenjs/vite-plugin-svg-icons)) injects the sprite into the DOM via a virtual import. Great for SPAs; needs JavaScript to run.
182
+ - **Import-based** plugins expose each icon as a JS module or framework component. Great when your icons live in JS/JSX.
183
+ - **This plugin is build-time and manifest-driven.** It emits content-hashed `.svg` files and registers them in Vite's manifest under stable logical keys, so a **server-rendered** template (Craft/Twig, Laravel/Blade, etc.) resolves the hashed URL with no JavaScript — e.g. `craft.vite.entry('sprite.svg')`. It also emits one sprite per subfolder.
184
+
185
+ If you want runtime/HMR injection or a virtual import, one of the others will suit you better. If you render HTML on the server and resolve assets through a Vite manifest, this is built for that.
186
+
187
+ ## FAQ
188
+
189
+ **Does it work with Craft CMS?** Yes — it's the primary target. With [nystudio107's Craft Vite plugin](https://nystudio107.com/docs/vite/), reference a sprite with `craft.vite.entry('sprite.svg')` — see [Using the sprite](#using-the-sprite), or the full [Craft + Vite walkthrough](https://github.com/onedarnleyroad/craftcms/wiki/SVG-icon-sprites-in-Craft-CMS-with-Vite).
190
+
191
+ **How do I reference an icon in a Twig template?** Use `<use href="{{ craft.vite.entry('sprite.svg') }}#svg-{name}">`, or the reusable [`icon()` macro](#a-reusable-accessible-macro).
192
+
193
+ **Can I have more than one sprite?** Yes — each subfolder of `inputDir` becomes its own sprite, keyed `sprite-{folder}.svg`. See [Subfolders](#subfolders).
194
+
195
+ **Can two icons share a filename across folders?** Yes. `arrow.svg` and `brand/arrow.svg` both become `<symbol id="svg-arrow">`, but in *separate* sprite files — and you reference each by its own sprite URL (`entry('sprite.svg')#svg-arrow` vs `entry('sprite-brand.svg')#svg-arrow`), so the file URL disambiguates them and they render fine on the same page. The ids only repeat *across* files, never within one; the only thing to avoid is inlining multiple sprites into a single document and referencing by bare `#svg-arrow`.
196
+
197
+ **Does it run during `vite dev` / support HMR?** No — it's build-only. Run `vite build --watch` alongside your dev server for a live-ish loop.
198
+
199
+ **Does it optimise SVGs?** No — it does light cleanup (strips the XML prolog / comments / empty `<defs>`, collapses whitespace, namespaces IDs), but it doesn't touch path data, so bring source SVGs that are already production-ready. Each icon needs a `viewBox` — the plugin strips `width`/`height` and scales from it.
200
+
201
+ **Why are the manifest keys `sprite.svg` and `sprite-light.svg`, not paths?** Because nystudio107's `entry()` resolves keys with a substring match, and a path-style `light/sprite.svg` would collide with the root `sprite.svg`. Basename keys avoid that.
202
+
170
203
  ## License
171
204
 
172
205
  MIT © [One Darnley Road](https://onedarnleyroad.com)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@onedarnleyroad/vite-plugin-svg-sprite",
3
- "version": "2.0.0-beta.8",
3
+ "version": "2.1.0",
4
4
  "description": "Vite plugin that builds an SVG sprite sheet from a directory of SVG files",
5
5
  "type": "module",
6
6
  "exports": {
@@ -15,14 +15,25 @@
15
15
  "vite-plugin",
16
16
  "svg",
17
17
  "sprite",
18
- "svg-sprite"
18
+ "svg-sprite",
19
+ "svg-symbols",
20
+ "icons",
21
+ "manifest",
22
+ "craft-cms",
23
+ "nystudio107"
19
24
  ],
20
25
  "author": "One Darnley Road <hello@onedarnleyroad.com>",
21
26
  "license": "MIT",
27
+ "repository": {
28
+ "type": "git",
29
+ "url": "git+https://github.com/onedarnleyroad/vite-plugin-svg-sprite.git"
30
+ },
31
+ "homepage": "https://github.com/onedarnleyroad/vite-plugin-svg-sprite#readme",
32
+ "bugs": "https://github.com/onedarnleyroad/vite-plugin-svg-sprite/issues",
22
33
  "publishConfig": {
23
34
  "access": "public"
24
35
  },
25
36
  "peerDependencies": {
26
- "vite": ">=4.0.0"
37
+ "vite": ">=3.0.0"
27
38
  }
28
39
  }
package/src/index.js CHANGED
@@ -142,11 +142,15 @@ export default function svgSpritePlugin(options = {}) {
142
142
  writeBundle(outputOptions) {
143
143
  if (!viteConfig?.build?.manifest) return
144
144
 
145
- const manifestName = typeof viteConfig.build.manifest === 'string'
146
- ? viteConfig.build.manifest
147
- : '.vite/manifest.json'
148
- const manifestPath = resolve(outputOptions.dir, manifestName)
149
- if (!existsSync(manifestPath)) return
145
+ // Vite 5+ writes the default manifest to .vite/manifest.json; Vite 3/4 write
146
+ // manifest.json at the outDir root. Try both when no custom path is configured.
147
+ const manifestNames = typeof viteConfig.build.manifest === 'string'
148
+ ? [viteConfig.build.manifest]
149
+ : ['.vite/manifest.json', 'manifest.json']
150
+ const manifestPath = manifestNames
151
+ .map(name => resolve(outputOptions.dir, name))
152
+ .find(path => existsSync(path))
153
+ if (!manifestPath) return
150
154
 
151
155
  const manifest = JSON.parse(readFileSync(manifestPath, 'utf-8'))
152
156