@open-xchange/vite-plugin-icon-sprite 0.0.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.
@@ -0,0 +1,3 @@
1
+ {
2
+ "typescript.tsdk": "node_modules/typescript/lib"
3
+ }
package/CHANGELOG.md ADDED
@@ -0,0 +1,5 @@
1
+ # Changelog
2
+
3
+ ## [0.0.1] - 2024-07-30
4
+
5
+ - initial release
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2024 OX Software GmbH, Germany <info@open-xchange.com>
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.
package/README.md ADDED
@@ -0,0 +1,324 @@
1
+ # @open-xchange/vite-plugin-icon-sprite
2
+
3
+ A Vite plugin that builds icon sprites from multiple SVG or PNG icon files.
4
+
5
+ ## Usage
6
+
7
+ This plugin collects multiple SVG or PNG image files, assembles them into a single icon sprite file, and provides virtual modules that can be imported in source code.
8
+
9
+ In Vite server mode, the icon sprite files will be generated on demand. When bundling the project, all icon sprite files referenced in source code will be generated and bundled.
10
+
11
+ It is possible to instantiate this plugin repeatedly, e.g. in order to generate an SVG sprite and a PNG sprite in the same project.
12
+
13
+ ### Common Options
14
+
15
+ Common configuration options for SVG sprites and PNG sprites.
16
+
17
+ #### Option `format`
18
+
19
+ - Type: `"svg" | "png"`
20
+ - _required_
21
+
22
+ The file format specifier of the icon sprite, and the source images. Depending on the value of this option, the plugin expects various file-format specific options (see below).
23
+
24
+ #### Option `imagesPath`
25
+
26
+ - Type: `string`
27
+ - _required_
28
+
29
+ Path to root directory with all image resource files to be processed.
30
+
31
+ #### Option `mappingPath`
32
+
33
+ - Type: `string`
34
+ - _required_
35
+
36
+ Path to the JSON or YAML configuration file containing the mapping between icon identifiers and SVG source file names (see below).
37
+
38
+ ### SVG Sprites
39
+
40
+ The plugin collects multiple SVG files, and generates a virtual import for an SVG sprite file with multiple `<symbol>` elements that can be imported from source code.
41
+
42
+ Example how to consume the SVG sprite in source code:
43
+
44
+ ```ts
45
+ // path/to/source.ts
46
+
47
+ import svgSprite from "virtual:svg/my-icons.svg"
48
+
49
+ // insert the SVG sprite into the DOM to be able to refer to the <symbol> elements
50
+ document.createElement("div").innerHTML = svgSprite
51
+
52
+ // create an SVG element referring to an icon in the sprite
53
+ function createIcon(id: string): SVGElement {
54
+ const useEl = document.createElementNS("http://www.w3.org/2000/svg", "use")
55
+ useEl.setAttribute("href", "#my-icon")
56
+ const iconEl = document.createElementNS("http://www.w3.org/2000/svg", "svg")
57
+ iconEl.append(useEl)
58
+ return iconEl
59
+ }
60
+ ```
61
+
62
+ #### Option `spriteName`
63
+
64
+ - Type: `string`
65
+ - _required_
66
+
67
+ The module name of the SVG sprite to be generated (with ".svg" extension).
68
+
69
+ The SVG markup of the sprite can be imported from the virtual module path `"virtual:svg/[spriteName]"`.
70
+
71
+ #### Option `idPrefix`
72
+
73
+ - Type: `string`
74
+ - _optional_
75
+
76
+ A prefix to be added to all icon identifiers declared in the mapping file. By default, no prefix will be added.
77
+
78
+ #### SVG Sprite Example
79
+
80
+ ```ts
81
+ // vite.config.ts
82
+
83
+ import { defineConfig } from "vite" // or "vitest/config"
84
+ import spritePlugin from "@open-xchange/vite-plugin-icon-sprite"
85
+
86
+ export default defineConfig(() => {
87
+ plugins: [
88
+ spritePlugin({
89
+ format: "svg",
90
+ imagesPath: "src/icons/images",
91
+ mappingPath: "src/icons/svg-mapping.yaml",
92
+ spriteName: "icons.svg",
93
+ idPrefix: "svg-",
94
+ }),
95
+ ],
96
+ })
97
+ ```
98
+
99
+ - Collects all SVG files in the directory `src/icons/images`.
100
+ - Uses the icon mapping in `src/icons/svg-mapping.yaml`.
101
+ - Creates a virtual import `"virtual:svg/icons.svg"`.
102
+ - Prefixes all icon identifiers with `svg-`, e.g. the icon key `my-icon` in the mapping file will result in the icon identifier `"svg-my-icon"` in source code.
103
+
104
+ ### PNG Sprites
105
+
106
+ The plugin collects multiple PNG files, and generates a virtual imports for one or more sprite files with different icon sizes and a CSS file that can be imported from source code.
107
+
108
+ #### Option `cssName`
109
+
110
+ - Type: `string`
111
+ - _required_
112
+
113
+ The module name for the CSS file (with ".css" extension). The generated CSS markup can be imported from `"virtual:png/[cssName]"`.
114
+
115
+ #### Option `cssIconSize`
116
+
117
+ - Type: `number`
118
+ - _required_
119
+
120
+ Base size of all icons, in CSS pixels.
121
+
122
+ #### Option `cssIconPadding`
123
+
124
+ - Type: `number`
125
+ - Default: `0`
126
+
127
+ Additional padding around all icons to be generated in the sprites, in CSS pixels.
128
+
129
+ #### Option `cssIconSelector`
130
+
131
+ - Type: `string`
132
+ - Default: `"i.png-icon"`
133
+
134
+ The CSS selector for a PNG icon element to be used in all generated CSS rules.
135
+
136
+ #### Option `rootLocaleAttr`
137
+
138
+ - Type: `string`
139
+ - Default: `"lang"`
140
+
141
+ Name of the root element's attribute containing the locale identifier. Needed to generate CSS selectors for localized icons.
142
+
143
+ #### Option `spriteColorType`
144
+
145
+ - Type: `"source" | "monochrome" | "alpha"`
146
+ - Default: `"source"`
147
+
148
+ Specifies how to generate the sprite PNG files.
149
+
150
+ | Value | Description |
151
+ | - | - |
152
+ | `"source"` | The source PNGs will be copied into the generated sprites unmodified. They will contain three color channels, and an alpha channel. |
153
+ | `"monochrome"` | The generated sprites will be converted to monochrome. They will contain a gray channel and an alpha channel. |
154
+ | `"alpha"` | Only the alpha channels of the source PNGs will be copied into the generated sprites. They will contain a single gray channel representing the original alpha channels. |
155
+
156
+ #### Option `spriteFillType`
157
+
158
+ - Type: `"background" | "mask"`
159
+ - Default: `"background"`
160
+
161
+ Specifies how the sprites are supposed to be used in CSS rules.
162
+
163
+ | Value | Description |
164
+ | `"background"` | The sprites will be attached via "background-image". |
165
+ | `"mask"` | The sprites will be attached via "mask-image". |
166
+
167
+ All related CSS properties (e.g. `background-position` vs. `mask-position` etc.) will be generated accordingly.
168
+
169
+ #### Option `sprites`
170
+
171
+ - Type: `Record<string, { factor: number; src: string }>`
172
+ - _required_
173
+
174
+ List of all icon sprites with different icon sizes to be generated.
175
+
176
+ - The keys of the dictionary are the module names of the PNG sprites (with ".png" extension). The generated PNG sprite can be imported from `"virtual:png/[key]"`.
177
+
178
+ - The values of the dictionary contain configuration options for the PNG sprite:
179
+
180
+ | Option | Type | Default | Description |
181
+ | - | - | - | - |
182
+ | `factor` | `number` | _required_ | Icon scaling factor (a multiplier for plugin option `cssIconSize`). All source PNG files must have the effective pixel size (`cssIconSize * factor`). |
183
+ | `src` | `string` | _required_ | The pattern used to build the path of the source PNG files. MUST contain the placeholder `[path]` that will be replaced with the base paths contained in the icon mapping file. |
184
+
185
+ #### PNG Sprite Example
186
+
187
+ ```ts
188
+ // vite.config.ts
189
+
190
+ import { defineConfig } from "vite" // or "vitest/config"
191
+ import spritePlugin from "@open-xchange/vite-plugin-icon-sprite"
192
+
193
+ export default defineConfig(() => {
194
+ plugins: [
195
+ spritePlugin({
196
+ format: "png",
197
+ imagesPath: "src/icons/images",
198
+ mappingPath: "src/icons/png-mapping.yaml",
199
+ cssName: "icons.css",
200
+ cssIconSize: 16,
201
+ cssIconPadding: 1,
202
+ cssIconSelector: "i.my-icon",
203
+ rootLocaleAttr: "data-icon-locale",
204
+ spriteColorType: "alpha",
205
+ spriteFillType: "mask",
206
+ sprites: {
207
+ "icons1.png": { factor: 1, src: "[path]_16.png" },
208
+ "icons2.png": { factor: 2, src: "[path]_32.png" },
209
+ },
210
+ }),
211
+ ]
212
+ })
213
+ ```
214
+
215
+ - Collects all PNG files in the directory `src/icons/images`. The images must exist in two sizes (16px and 32px), their file names must end with `_16.png` and `_32.png` respectively (according to the options `cssIconSize` and `sprites->factor`).
216
+ - Uses the icon mapping in `src/icons/png-mapping.yaml`.
217
+ - Creates the virtual imports `"virtual:svg/icons.css"`, `"virtual:svg/icons1.png"`, and `"virtual:svg/icons2.png"`.
218
+ - Adds one pixel padding around all icons in the PNG sprites.
219
+ - Generates CSS selectors for `<i>` elements with CSS class `my-icon`.
220
+ - Generates `:root[data-icon-locale]` CSS selectors for localized icons (i.e., the UI locale code must be stored in the root element's attribute "data-icon-locale").
221
+ - Generates PNG sprites consisting of an 8-bit alpha channel only.
222
+ - Generates CSS rules using CSS mask (instead of background).
223
+
224
+ ### Icon Mapping File
225
+
226
+ The plugin expects an icon mapping file (plugin option `mappingPath`) which is a JSON or YAML configuration file containing a mapping from arbitrary icon identifiers to the paths of the source images. The icon identifiers can be used later in source code to refer to a specific icon in the generated sprite.
227
+
228
+ - The name of the configuration file can be chosen freely.
229
+
230
+ - The configuration file must consist of a single object map.
231
+
232
+ - Each entry maps a unique icon identifier (used in source code) to a source image to be used for that icon.
233
+
234
+ - The source image file can be specified directly as string, or as dictionary for localized icons (more details below).
235
+
236
+ - Only the base name of the source image file must be specified relative to the configured root directory of the image files. It must not contain the image size suffix (_PNG only_), nor a file extension (`.svg` or `.png`). See examples below.
237
+
238
+ - Localized icons will be described by a dictionary mapping the ISO language identifiers (as comma-separated strings) to the image base names (as described above). The special locale code `"*"` is mandatory, and defines a default icon for unlisted locales. See examples below.
239
+
240
+ #### Icon Mapping Examples
241
+
242
+ In all examples, the configured image root directory (plugin option `imagesPath`) shall be `path/to/images`.
243
+
244
+ ##### Example 1: Simple SVG Icons
245
+
246
+ - Assign the icon with the identifier `my-icon` to the SVG image `path/to/images/commons/icon1.svg`.
247
+ - Assign the icon with the identifier `other-icon` to the SVG image `path/to/images/commons/icon2.svg`.
248
+
249
+ ```YAML
250
+ # svg-mapping.yaml
251
+
252
+ my-icon: commons/icon1
253
+ other-icon: commons/icon2
254
+ ```
255
+
256
+ In source code, the icons can be used with the identifiers `"my-icon"` and `"other-icon"`.
257
+
258
+ ##### Example 2: Simple PNG Icons
259
+
260
+ Assuming that the plugin will generate PNG sprites for icons with sizes of 16px and 32px.
261
+
262
+ - Assign the icon with the identifier `my-icon` to the PNG images `path/to/images/commons/icon1_16.png` (16x16 pixels) and `path/to/images/commons/icon1_32.png` (32x32 pixels).
263
+ - Respectively, assign an icon with the identifier `other-icon` to the PNG images `path/to/images/commons/icon2_*.png`.
264
+
265
+ The resulting mapping file looks exactly as the former mapping file for SVG icons:
266
+
267
+ ```YAML
268
+ # png-mapping.yaml
269
+
270
+ my-icon: commons/icon1
271
+ other-icon: commons/icon2
272
+ ```
273
+
274
+ ##### Example 3: Localized Icons
275
+
276
+ - Assign the icon with the identifier `my-icon` to the SVG image `path/to/images/commons/icon1.svg` by default.
277
+ - Use the SVG image `path/to/images/commons/icon2.svg` in German and French UI instead.
278
+ - Use the SVG image `path/to/images/commons/icon3.svg` in Swedish UI instead.
279
+
280
+ ```YAML
281
+ # svg-mapping.yaml
282
+
283
+ my-icon:
284
+ "*": commons/icon1
285
+ de,fr: commons/icon2
286
+ sv: commons/icon3
287
+ ```
288
+
289
+ The same applies to PNG icons as well.
290
+
291
+ #### Schema Validation
292
+
293
+ This package provides a JSON schema that can be used for validation in editors.
294
+
295
+ ##### JSON Mapping File
296
+
297
+ Add the path to the schema file as property `"$schema"` to the mapping file:
298
+
299
+ ```json
300
+ // mapping.json
301
+
302
+ {
303
+ "$schema": "../../node_modules/@open-xchange/vite-plugin-icon-sprite/dist/mapping-schema.json",
304
+ // ...
305
+ }
306
+ ```
307
+
308
+ Adjust the number of parent path fragments according to the location of the mapping file in the project.
309
+
310
+ ##### YAML Mapping File
311
+
312
+ Add the path to the schema file in a `yaml-language-server` directive to the mapping file:
313
+
314
+ ```yaml
315
+ # mapping.yaml
316
+
317
+ # yaml-language-server: $schema=../../node_modules/@open-xchange/vite-plugin-icon-sprite/dist/mapping-schema.json
318
+
319
+ # ...
320
+ ```
321
+
322
+ Adjust the number of parent path fragments according to the location of the mapping file in the project.
323
+
324
+ In VS Code, the plugin [redhat.vscode-yaml](https://marketplace.visualstudio.com/items?itemName=redhat.vscode-yaml) needs to be installed to support this directive.
@@ -0,0 +1,62 @@
1
+ import { type PluginHelperConfig, PluginHelper } from "@open-xchange/vite-helper";
2
+ /**
3
+ * Common configuration options (independent from the icon file format) for the
4
+ * plugin "@open-xchange/vite-plugin-icon-sprite".
5
+ *
6
+ * @template FormatT
7
+ * The file format specifier of the icon sprite, and the source images.
8
+ */
9
+ export interface SpritePluginBaseOptions<FormatT extends string> {
10
+ /**
11
+ * The file format specifier of the icon sprite, and the source images.
12
+ */
13
+ format: FormatT;
14
+ /**
15
+ * Path to root directory with all image resource files to be processed.
16
+ */
17
+ imagesPath: string;
18
+ /**
19
+ * Path to the JSON or YAML configuration file containing the mapping
20
+ * between icon identifiers and SVG source file names.
21
+ */
22
+ mappingPath: string;
23
+ }
24
+ /**
25
+ * Internal configuration of a `SpritePluginHelper` instance.
26
+ */
27
+ export type SpritePluginConfig = Required<Pick<PluginHelperConfig, "pluginIndex" | "virtualModules">>;
28
+ /**
29
+ * Represents a single entry in an icon mapping configuration file (a mapping
30
+ * between icon identifier, path to source image, and language code for
31
+ * localized icons).
32
+ */
33
+ export interface IconMappingEntry {
34
+ /** The icon identifier used in source code to select an icon. */
35
+ iconId: string;
36
+ /** The path to the source image file to be inserted into the sprite. */
37
+ iconPath: string;
38
+ /** Language code for a localized icon. */
39
+ iconLang?: string;
40
+ }
41
+ /**
42
+ * Plugin helper for all icon file formats.
43
+ *
44
+ * @template OptionsT
45
+ * Exact type of the options interface.
46
+ */
47
+ export declare class SpritePluginHelper<OptionsT extends SpritePluginBaseOptions<string>> extends PluginHelper {
48
+ /** Resolved configuration options. */
49
+ readonly options: Required<Readonly<OptionsT>>;
50
+ protected constructor(config: SpritePluginConfig, options: Required<OptionsT>);
51
+ /**
52
+ * Reads an icon mapping configuration file, and returns the entries as
53
+ * array.
54
+ *
55
+ * @param path
56
+ * The path to the icon mapping configuration file to be read.
57
+ *
58
+ * @returns
59
+ * The entries of the configuration file.
60
+ */
61
+ protected readIconMapping(): Promise<IconMappingEntry[]>;
62
+ }
package/dist/helper.js ADDED
@@ -0,0 +1,56 @@
1
+ import { dictEntries } from "@open-xchange/vite-helper/utils";
2
+ import { resolvePath } from "@open-xchange/vite-helper/file";
3
+ import { PluginHelper } from "@open-xchange/vite-helper";
4
+ // class SpritePluginHelper ===================================================
5
+ /**
6
+ * Plugin helper for all icon file formats.
7
+ *
8
+ * @template OptionsT
9
+ * Exact type of the options interface.
10
+ */
11
+ export class SpritePluginHelper extends PluginHelper {
12
+ /** Resolved configuration options. */
13
+ options;
14
+ // constructor ------------------------------------------------------------
15
+ constructor(config, options) {
16
+ super({
17
+ ...config,
18
+ virtualPrefix: options.format,
19
+ loggerPrefix: options.format,
20
+ logLevelEnvVar: "PLUGIN_ICON_SPRITE_LOGLEVEL",
21
+ cacheSrcFiles: [options.imagesPath + "/**/*." + options.format, options.mappingPath],
22
+ });
23
+ this.options = options;
24
+ }
25
+ // protected methods ------------------------------------------------------
26
+ /**
27
+ * Reads an icon mapping configuration file, and returns the entries as
28
+ * array.
29
+ *
30
+ * @param path
31
+ * The path to the icon mapping configuration file to be read.
32
+ *
33
+ * @returns
34
+ * The entries of the configuration file.
35
+ */
36
+ async readIconMapping() {
37
+ const schemaPath = resolvePath("./mapping-schema.json", import.meta.url);
38
+ const mappingDict = await this.readConfig(this.options.mappingPath, { schema: schemaPath });
39
+ // convert to array of `IconMappingEntry`
40
+ const iconMapping = [];
41
+ for (const [iconId, entry] of dictEntries(mappingDict)) {
42
+ const iconDict = (typeof entry === "string") ? { "*": entry } : entry;
43
+ for (const [languages, iconPath] of dictEntries(iconDict)) {
44
+ for (const iconLang of languages.split(",")) {
45
+ if (iconLang === "*") {
46
+ iconMapping.push({ iconId, iconPath });
47
+ }
48
+ else {
49
+ iconMapping.push({ iconId, iconPath, iconLang });
50
+ }
51
+ }
52
+ }
53
+ }
54
+ return iconMapping;
55
+ }
56
+ }
@@ -0,0 +1,19 @@
1
+ import type { Plugin } from "vite";
2
+ import { type SvgSpritePluginOptions } from "./plugin-svg.js";
3
+ import { type PngSpritePluginOptions } from "./plugin-png.js";
4
+ /**
5
+ * Configuration options for generating icon sprites from SVG or PNG source
6
+ * images.
7
+ */
8
+ export type IconSpritePluginOptions = SvgSpritePluginOptions | PngSpritePluginOptions;
9
+ /**
10
+ * A plugin for Vite to generate an icon sprite file from multiple SVG or PNG
11
+ * source files.
12
+ *
13
+ * @param options
14
+ * Plugin configuration options.
15
+ *
16
+ * @returns
17
+ * The plugin instance.
18
+ */
19
+ export default function iconSpritePlugin(options: IconSpritePluginOptions): Plugin;
package/dist/index.js ADDED
@@ -0,0 +1,19 @@
1
+ import { svgSpritePlugin } from "./plugin-svg.js";
2
+ import { pngSpritePlugin } from "./plugin-png.js";
3
+ // plugin =====================================================================
4
+ /**
5
+ * A plugin for Vite to generate an icon sprite file from multiple SVG or PNG
6
+ * source files.
7
+ *
8
+ * @param options
9
+ * Plugin configuration options.
10
+ *
11
+ * @returns
12
+ * The plugin instance.
13
+ */
14
+ export default function iconSpritePlugin(options) {
15
+ switch (options.format) {
16
+ case "svg": return svgSpritePlugin(import.meta.url, options);
17
+ case "png": return pngSpritePlugin(import.meta.url, options);
18
+ }
19
+ }
@@ -0,0 +1,32 @@
1
+ {
2
+ "$schema": "http://json-schema.org/draft-07/schema",
3
+ "title": "Icon mapping for SVG or PNG icon sprites",
4
+ "description": "A dictionary that maps icon identifiers to source SVG or PNG image files.",
5
+ "type": "object",
6
+ "properties": {
7
+ "$schema": {
8
+ "type": "string",
9
+ "format": "uri-reference"
10
+ }
11
+ },
12
+ "patternProperties": {
13
+ "^[a-z0-9]+(-[a-z0-9]+)*$": {
14
+ "anyOf": [
15
+ {
16
+ "type": "string"
17
+ },
18
+ {
19
+ "type": "object",
20
+ "patternProperties": {
21
+ "^(\\*|[a-z]+(,[a-z]+)*)$": {
22
+ "type": "string"
23
+ }
24
+ },
25
+ "required": ["*"],
26
+ "additionalProperties": false
27
+ }
28
+ ]
29
+ }
30
+ },
31
+ "additionalProperties": false
32
+ }
@@ -0,0 +1,88 @@
1
+ import type { Plugin } from "vite";
2
+ import { type Dict } from "@open-xchange/vite-helper/utils";
3
+ import { type SpritePluginBaseOptions } from "./helper.js";
4
+ /**
5
+ * Configuration options for generating PNG sprites from PNG source images.
6
+ */
7
+ export interface PngSpritePluginOptions extends SpritePluginBaseOptions<"png"> {
8
+ /**
9
+ * Module name for the CSS file (with ".css" extension). The generated CSS
10
+ * markup can be imported from `"virtual:png/[cssName]"`.
11
+ */
12
+ cssName: string;
13
+ /**
14
+ * Base size of all icons, in CSS pixels.
15
+ */
16
+ cssIconSize: number;
17
+ /**
18
+ * Additional padding around all icons to be generated in the sprites, in
19
+ * CSS pixels. Default value is 0.
20
+ */
21
+ cssIconPadding?: number;
22
+ /**
23
+ * The CSS selector for a PNG icon element to be used in all generated CSS
24
+ * rules. Default value is "i.png-icon".
25
+ */
26
+ cssIconSelector?: string;
27
+ /**
28
+ * Name of the root element's attribute containing the locale identifier.
29
+ * Needed to generate CSS selectors for localized icons. Default value is
30
+ * "lang".
31
+ */
32
+ rootLocaleAttr?: string;
33
+ /**
34
+ * Specifies how to generate the sprite PNG files.
35
+ * - `"source"`: The source PNGs will be copied into the generated sprites
36
+ * unmodified. They will contain three color channels, and an alpha
37
+ * channel.
38
+ * - `"monochrome"`: The generated sprites will be converted to monochrome.
39
+ * They will contain a gray channel and an alpha channel.
40
+ * - `"alpha"`: Only the alpha channels of the source PNGs will be copied
41
+ * into the generated sprites. They will contain a single gray channel
42
+ * representing the original alpha channels.
43
+ *
44
+ * Default value is "source".
45
+ */
46
+ spriteColorType?: "source" | "monochrome" | "alpha";
47
+ /**
48
+ * Specifies how the sprites are supposed to be used in CSS rules.
49
+ * - `"background"`: The sprites will be attached via "background-image".
50
+ * - `"mask"`: The sprites will be attached via "mask-image".
51
+ *
52
+ * All related CSS properties (e.g. "(background|mask)-position" etc.) will
53
+ * be generated accordingly. Default value is "background".
54
+ */
55
+ spriteFillType?: "background" | "mask";
56
+ /**
57
+ * List of all icon sprites to be generated.
58
+ *
59
+ * - The keys of the dictionary are the module names of the PNG sprites
60
+ * (with ".png" extension). The generated sprite PNG file can be imported
61
+ * from `"virtual:png/[key]"`.
62
+ * - "factor" is the scaling factor (a multiplier for "cssIconSize"). All
63
+ * source PNG files must have the effective pixel size (`cssIconSize *
64
+ * factor`).
65
+ * - "src" specifies the pattern used to build the path of the source PNG
66
+ * files. MUST contain the placeholder "[path]" that will be replaced
67
+ * with the base paths contained in the icon mapping file referred in the
68
+ * option "mappingFile".
69
+ */
70
+ sprites: Dict<{
71
+ factor: number;
72
+ src: string;
73
+ }>;
74
+ }
75
+ /**
76
+ * A plugin for Vite to generate PNG sprite files from multiple PNG source
77
+ * files.
78
+ *
79
+ * @param index
80
+ * URL of the index source file of this plugin.
81
+ *
82
+ * @param options
83
+ * Plugin configuration options.
84
+ *
85
+ * @returns
86
+ * The plugin instance.
87
+ */
88
+ export declare function pngSpritePlugin(index: string, options: PngSpritePluginOptions): Plugin;
@@ -0,0 +1,233 @@
1
+ import Jimp from "jimp";
2
+ import { onceFn } from "@open-xchange/vite-helper/utils";
3
+ import { Cache } from "@open-xchange/vite-helper/cache";
4
+ import { SpritePluginHelper } from "./helper.js";
5
+ // constants ==================================================================
6
+ const FILL_VENDOR_PREFIXES = {
7
+ background: [],
8
+ mask: ["webkit"],
9
+ };
10
+ // functions ==================================================================
11
+ /**
12
+ * Generates a CSS length string in pixels (omits unit for zero).
13
+ *
14
+ * @param value
15
+ * The length to be emitted.
16
+ *
17
+ * @returns
18
+ * The passed length with "px" unit.
19
+ */
20
+ function px(value) {
21
+ return value ? `${value}px` : "0";
22
+ }
23
+ // class PngSpritePluginHelper ================================================
24
+ class PngSpritePluginHelper extends SpritePluginHelper {
25
+ #parseMappingOnce;
26
+ // constructor ------------------------------------------------------------
27
+ constructor(index, options) {
28
+ super({
29
+ pluginIndex: index,
30
+ virtualModules: [options.cssName, ...Object.keys(options.sprites)],
31
+ }, {
32
+ cssIconPadding: 0,
33
+ cssIconSelector: "i.png-icon",
34
+ rootLocaleAttr: "lang",
35
+ spriteColorType: "source",
36
+ spriteFillType: "background",
37
+ ...options,
38
+ });
39
+ this.#parseMappingOnce = onceFn(() => this.#parseMapping());
40
+ }
41
+ // public methods ---------------------------------------------------------
42
+ /**
43
+ * Generates an ES source module with CSS markup code for all icons.
44
+ *
45
+ * @returns
46
+ * The source module with CSS markup for all icons.
47
+ */
48
+ async generateCssMarkupModule() {
49
+ // shortcuts to plugin options
50
+ const { cssName } = this.options;
51
+ // try to resolve cached version of generated file
52
+ return await this.generateModule(cssName, async () => {
53
+ this.info("generating CSS markup %f", cssName);
54
+ // parse mapping file which collects icon paths, CSS selectors, and entry positions
55
+ const cssMarkup = (await this.#parseMappingOnce()).cssMarkup;
56
+ this.info("CSS markup %f generated successfully", cssName);
57
+ return cssMarkup;
58
+ });
59
+ }
60
+ /**
61
+ * Generates the ES source module for a PNG sprite with a specific scaling
62
+ * factor.
63
+ *
64
+ * @param spriteName
65
+ * The name of the sprite.
66
+ *
67
+ * @param factor
68
+ * The scaling factor.
69
+ *
70
+ * @param srcPattern
71
+ * The pattern for the source PNG images.
72
+ *
73
+ * @returns
74
+ * The source module exporting the PNG sprite as base-64 encoded data URL.
75
+ */
76
+ async generateSpriteModule(spriteName, factor, srcPattern) {
77
+ // shortcuts to plugin options
78
+ const { imagesPath, cssIconSize, cssIconPadding, spriteColorType } = this.options;
79
+ // check configuration
80
+ this.ensure(factor > 0, "invalid scaling factor in configuration for sprite %f", spriteName);
81
+ this.ensure(srcPattern.includes("[path]"), "placeholder [path] expected in configuration for sprite %f", spriteName);
82
+ // try to resolve cached version of generated file
83
+ return await this.generateModule(spriteName, async () => {
84
+ this.info("generating sprite %f", spriteName);
85
+ // parse mapping file which collects icon paths, CSS selectors, and entry positions
86
+ const { cssSpriteWidth, cssSpriteHeight, entries } = await this.#parseMappingOnce();
87
+ // new image data is not clean out-of-the-box, explicitly fill with zeros
88
+ const sprite = new Jimp(cssSpriteWidth * factor, cssSpriteHeight * factor, 0);
89
+ // process all entries in the mapping configuration
90
+ for (const { iconPath, x, y } of entries) {
91
+ // expected pixel size for the current scaling factor
92
+ const size = cssIconSize * factor;
93
+ // load and parse the source image file
94
+ const path = imagesPath + "/" + srcPattern.replace("[path]", iconPath);
95
+ const buffer = await this.readBuffer(path);
96
+ const image = await Jimp.read(buffer);
97
+ // validate the image size
98
+ const { width, height } = image.bitmap;
99
+ this.ensure((width === size) && (height === size), "wrong image width in %f (expected %s but got %s)", path, `${size}x${size}`, `${width}x${height}`);
100
+ // insert source image into the sprite
101
+ sprite.blit(image, (x + cssIconPadding) * factor, (y + cssIconPadding) * factor);
102
+ }
103
+ // copy alpha of all pixels to RGB channels (alpha channel will be exported as greyscale without alpha)
104
+ if (spriteColorType === "alpha") {
105
+ for (let d = sprite.bitmap.data, i = 0, l = d.length; i < l; i += 4) {
106
+ d.fill(d[i + 3], i, i + 3); // copy A to RGB
107
+ d[i + 3] = 255; // set A to full opacity
108
+ }
109
+ }
110
+ // generate the binary PNG data (no preset constants for PNG color types in Jimp)
111
+ const pngColorType = { source: 6, monochrome: 4, alpha: 0 }[spriteColorType];
112
+ const spriteBuffer = await sprite.colorType(pngColorType).getBufferAsync(Jimp.MIME_PNG);
113
+ // convert to base64 encoded data URL
114
+ const spriteDataUrl = `data:image/png;base64,${spriteBuffer.toString("base64")}`;
115
+ const moduleSource = `export default ${JSON.stringify(spriteDataUrl)};`;
116
+ this.info("sprite %f generated successfully", spriteName);
117
+ return moduleSource;
118
+ });
119
+ }
120
+ // private methods --------------------------------------------------------
121
+ // parses the mapping file once, generates a map from short icon paths to `SpriteEntry` descriptor objects
122
+ async #parseMapping() {
123
+ // shortcuts to plugin options
124
+ const { cssIconSize, cssIconPadding, cssIconSelector, rootLocaleAttr, spriteFillType } = this.options;
125
+ // CSS selectors for all sprite entries, mapped by short icon path (different icons may refer to the same source PNG)
126
+ const selectorMap = new Cache();
127
+ // process all entries in the mapping configuration, collect CSS selectors for each entry in the sprite
128
+ for (const { iconId, iconPath, iconLang } of await this.readIconMapping()) {
129
+ const selectors = selectorMap.upsert(iconPath, () => []);
130
+ const selector = `${cssIconSelector}[data-icon-id="${iconId}"]`;
131
+ if (iconLang) {
132
+ selectors.push(`:root[${rootLocaleAttr}="${iconLang}"] ${selector}`);
133
+ selectors.push(`:root[${rootLocaleAttr}^="${iconLang}_"] ${selector}`);
134
+ }
135
+ else {
136
+ selectors.push(selector);
137
+ }
138
+ }
139
+ // the size of one icon occupied in the sprite including padding, for scaling factor 1
140
+ const cssTileSize = cssIconSize + 2 * cssIconPadding;
141
+ // number of distinct icons in the sprites
142
+ const iconCount = selectorMap.size;
143
+ // generate a square-shaped sprite
144
+ const rowLength = Math.ceil(Math.sqrt(iconCount));
145
+ // the resulting size of a sprite, for scaling factor 1
146
+ const cssSpriteWidth = cssTileSize * rowLength;
147
+ const cssSpriteHeight = cssTileSize * Math.ceil(iconCount / rowLength);
148
+ // additional vendor prefixes for background/fill attributes
149
+ const prefixes = FILL_VENDOR_PREFIXES[spriteFillType];
150
+ // generates a background/mask CSS property definition with correct vendor prefixes
151
+ const fillProp = (name, value) => {
152
+ const prop = `${spriteFillType}-${name}: ${value};`;
153
+ return prefixes.map(prefix => `-${prefix}-${prop} `).join("") + prop;
154
+ };
155
+ // the contents of the resulting CSS file with icon definitions
156
+ const cssLines = [
157
+ `${cssIconSelector} {`,
158
+ " display: inline-block;",
159
+ ` width: ${px(cssTileSize)};`,
160
+ ` height: ${px(cssTileSize)};`,
161
+ " flex: 0 0 auto;",
162
+ ` ${fillProp("size", `${px(cssSpriteWidth)} ${px(cssSpriteHeight)}`)}`,
163
+ ` ${fillProp("origin", "content-box")}`,
164
+ "}",
165
+ `${cssIconSelector}[data-icon-id="none"] { visibility: hidden; }`,
166
+ ];
167
+ // generate all sprite entries as an array
168
+ const entries = Array.from(selectorMap, ([iconPath, selectors], index) => {
169
+ const x = cssTileSize * (index % rowLength);
170
+ const y = cssTileSize * Math.floor(index / rowLength);
171
+ const prop = fillProp("position", `${px(-x)} ${px(-y)}`);
172
+ for (const selector of selectors) {
173
+ cssLines.push(`${selector} { ${prop} }`);
174
+ }
175
+ return { iconPath, x, y };
176
+ });
177
+ // create the resulting CSS markup as string
178
+ const cssMarkup = cssLines.join("\n") + "\n";
179
+ return { cssMarkup, cssSpriteWidth, cssSpriteHeight, entries };
180
+ }
181
+ }
182
+ // plugin =====================================================================
183
+ /**
184
+ * A plugin for Vite to generate PNG sprite files from multiple PNG source
185
+ * files.
186
+ *
187
+ * @param index
188
+ * URL of the index source file of this plugin.
189
+ *
190
+ * @param options
191
+ * Plugin configuration options.
192
+ *
193
+ * @returns
194
+ * The plugin instance.
195
+ */
196
+ export function pngSpritePlugin(index, options) {
197
+ // resolved configuration options
198
+ const { cssName, sprites } = options;
199
+ // helper instance for file system access, logging, etc.
200
+ const helper = new PngSpritePluginHelper(index, options);
201
+ // create and return the plugin object
202
+ return {
203
+ name: "@open-xchange/vite-plugin-icon-sprite/png",
204
+ // initialize file system cache for generated modules
205
+ async configResolved(viteConfig) {
206
+ await helper.initializeCache(viteConfig);
207
+ },
208
+ // pick matching imports
209
+ resolveId(source) {
210
+ return helper.resolveVirtualModuleId(source);
211
+ },
212
+ // load the PNG icon files, generate final PNG sprites and CSS file
213
+ async load(moduleId) {
214
+ // only handle the target modules specified in "resolveId"
215
+ const target = helper.matchVirtualModuleId(moduleId);
216
+ if (!target) {
217
+ return;
218
+ }
219
+ // generate the CSS markup
220
+ if (target === cssName) {
221
+ return await helper.generateCssMarkupModule();
222
+ }
223
+ // generate a PNG sprite (export binary data as plain Base64 encoded)
224
+ if (target in sprites) {
225
+ const { factor, src } = sprites[target];
226
+ return await helper.generateSpriteModule(target, factor, src);
227
+ }
228
+ // invalid module identifier
229
+ helper.fail("unknown output file %f", target);
230
+ return undefined;
231
+ },
232
+ };
233
+ }
@@ -0,0 +1,32 @@
1
+ import type { Plugin } from "vite";
2
+ import { type SpritePluginBaseOptions } from "./helper.js";
3
+ /**
4
+ * Configuration options for generating an SVG sprite from SVG source images.
5
+ */
6
+ export interface SvgSpritePluginOptions extends SpritePluginBaseOptions<"svg"> {
7
+ /**
8
+ * Module name of the SVG sprite to be generated (with ".svg" extension).
9
+ * The SVG markup of the sprite can be imported from the virtual module
10
+ * path `"virtual:svg/[spriteName]"`.
11
+ */
12
+ spriteName: string;
13
+ /**
14
+ * A prefix to be added to all icon identifiers declared in the mapping
15
+ * file. By default, no prefix will be added.
16
+ */
17
+ idPrefix?: string;
18
+ }
19
+ /**
20
+ * A plugin for Vite to generate an SVG sprite file from multiple SVG source
21
+ * files.
22
+ *
23
+ * @param index
24
+ * URL of the index source file of this plugin.
25
+ *
26
+ * @param options
27
+ * Plugin configuration options.
28
+ *
29
+ * @returns
30
+ * The plugin instance.
31
+ */
32
+ export declare function svgSpritePlugin(index: string, options: SvgSpritePluginOptions): Plugin;
@@ -0,0 +1,119 @@
1
+ import { basename } from "node:path";
2
+ import SVGSprite from "svg-sprite";
3
+ import { SpritePluginHelper } from "./helper.js";
4
+ // class SvgSpritePluginHelper ================================================
5
+ class SvgSpritePluginHelper extends SpritePluginHelper {
6
+ constructor(index, options) {
7
+ super({
8
+ pluginIndex: index,
9
+ virtualModules: options.spriteName,
10
+ }, {
11
+ idPrefix: "",
12
+ ...options,
13
+ });
14
+ }
15
+ // public methods ---------------------------------------------------------
16
+ /**
17
+ * Generates the ES source module for an SVG sprite.
18
+ *
19
+ * @returns
20
+ * The source module containing the SVG markup of the sprite.
21
+ */
22
+ async generateSpriteModule() {
23
+ // shortcuts to plugin options
24
+ const { imagesPath, idPrefix, spriteName } = this.options;
25
+ // try to resolve cached version of generated file
26
+ return await this.generateModule(spriteName, async () => {
27
+ this.info("compiling sprite %f", spriteName);
28
+ // create a reverse map from icon filenames to icon identifiers
29
+ const iconIdMap = new Map();
30
+ // create the core SVG spriter
31
+ const spriter = new SVGSprite({
32
+ shape: {
33
+ id: {
34
+ generator: name => iconIdMap.get(name) ?? "",
35
+ },
36
+ spacing: {
37
+ box: "icon",
38
+ },
39
+ dimension: {
40
+ maxWidth: 16,
41
+ maxHeight: 16,
42
+ },
43
+ },
44
+ svg: {
45
+ xmlDeclaration: false,
46
+ // - remove all <style> elements declaring a class with fill color
47
+ // - remove all class attribute referring to the class from <style>
48
+ // - add "fill" attribute to all <symbol> elements
49
+ transform: markup => markup
50
+ .replace(/<style>.*?<\/style>/g, "")
51
+ .replace(/class=".*?" ?/g, "")
52
+ .replace(/<symbol /g, '<symbol fill="currentColor" '),
53
+ },
54
+ mode: {
55
+ inline: true,
56
+ symbol: {
57
+ sprite: spriteName,
58
+ render: { less: false },
59
+ },
60
+ },
61
+ });
62
+ // load all source SVG images
63
+ for (const { iconId, iconPath, iconLang } of await this.readIconMapping()) {
64
+ const path = `${imagesPath}/${iconPath}.svg`;
65
+ // icon identifier with configured prefix and language suffix
66
+ const idSuffix = iconLang ? `;${iconLang}` : "";
67
+ iconIdMap.set(basename(path), idPrefix + iconId + idSuffix);
68
+ // read SVG source image, insert into spriter instance
69
+ const markup = await this.readText(path);
70
+ spriter.add(path, null, markup);
71
+ }
72
+ // compile the SVG sprite
73
+ const result = (await spriter.compileAsync()).result;
74
+ const spriteMarkup = result.symbol.sprite.contents.toString("utf8");
75
+ const moduleCode = `export default ${JSON.stringify(spriteMarkup)};`;
76
+ this.info("sprite %f generated successfully", spriteName);
77
+ return moduleCode;
78
+ });
79
+ }
80
+ }
81
+ // plugin =====================================================================
82
+ /**
83
+ * A plugin for Vite to generate an SVG sprite file from multiple SVG source
84
+ * files.
85
+ *
86
+ * @param index
87
+ * URL of the index source file of this plugin.
88
+ *
89
+ * @param options
90
+ * Plugin configuration options.
91
+ *
92
+ * @returns
93
+ * The plugin instance.
94
+ */
95
+ export function svgSpritePlugin(index, options) {
96
+ // helper instance for file system access, logging, etc.
97
+ const helper = new SvgSpritePluginHelper(index, options);
98
+ // create and return the plugin object
99
+ return {
100
+ name: "@open-xchange/vite-plugin-icon-sprite/svg",
101
+ // initialize file system cache for generated modules
102
+ async configResolved(viteConfig) {
103
+ await helper.initializeCache(viteConfig);
104
+ },
105
+ // pick matching imports
106
+ resolveId(source) {
107
+ return helper.resolveVirtualModuleId(source);
108
+ },
109
+ // load the SVG icon files, generate final SVG sprite
110
+ async load(moduleId) {
111
+ // only handle the output file specified in "resolveId"
112
+ if (!helper.matchVirtualModuleId(moduleId)) {
113
+ return null;
114
+ }
115
+ // create an ES module that exports the SVG markup of the sprite
116
+ return await helper.generateSpriteModule();
117
+ },
118
+ };
119
+ }
package/package.json ADDED
@@ -0,0 +1,45 @@
1
+ {
2
+ "name": "@open-xchange/vite-plugin-icon-sprite",
3
+ "version": "0.0.1",
4
+ "description": "Vite plugin that builds icon sprites from SVG or PNG icon files",
5
+ "repository": {
6
+ "url": "https://gitlab.open-xchange.com/fspd/npm-packages/vite-plugin-icon-sprite"
7
+ },
8
+ "license": "MIT",
9
+ "engines": {
10
+ "node": "18.18.0 || ^20.9.0 || >=21.1.0"
11
+ },
12
+ "packageManager": "yarn@4.4.0",
13
+ "type": "module",
14
+ "exports": "./dist/index.js",
15
+ "scripts": {
16
+ "prepare": "husky",
17
+ "prepack": "yarn build && yarn lint",
18
+ "build": "npx --yes rimraf dist && tsc && npx --yes cpy-cli --flat src/*.json dist/",
19
+ "lint": "eslint ."
20
+ },
21
+ "lint-staged": {
22
+ "*.{js,ts,json}": "yarn lint"
23
+ },
24
+ "dependencies": {
25
+ "@open-xchange/vite-helper": "0.1.0",
26
+ "jimp": "0.22.12",
27
+ "svg-sprite": "2.0.4"
28
+ },
29
+ "devDependencies": {
30
+ "@open-xchange/linter-presets": "0.4.3",
31
+ "@types/node": "22.1.0",
32
+ "@types/svg-sprite": "0.0.39",
33
+ "@types/vinyl": "2.0.12",
34
+ "eslint": "9.8.0",
35
+ "husky": "9.1.4",
36
+ "typescript": "5.5.4",
37
+ "vite": "5.4.0"
38
+ },
39
+ "peerDependencies": {
40
+ "vite": "^5.3"
41
+ },
42
+ "resolutions": {
43
+ "semver": "^7.6.2"
44
+ }
45
+ }