@svgforge/svgforge-cli 2.0.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.
package/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Felix Müller
4
+ Based on svg-sprite — Copyright © 2018 Joschi Kuphal
5
+
6
+ Permission is hereby granted, free of charge, to any person obtaining a copy
7
+ of this software and associated documentation files (the "Software"), to deal
8
+ in the Software without restriction, including without limitation the rights
9
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
10
+ copies of the Software, and to permit persons to whom the Software is
11
+ furnished to do so, subject to the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be included in all
14
+ copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
17
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
18
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
19
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
20
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
21
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
22
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,115 @@
1
+ # @svgforge/svgforge-cli
2
+
3
+ [![npm version][npm-image]][npm-url] [![Build Status][ci-image]][ci-url] [![Coverage Status][coveralls-image]][coveralls-url] [![npm downloads][npm-downloads]][npm-url]
4
+
5
+ [Command line interface](https://github.com/svgforge/svgforge-cli) for [svgforge](https://github.com/svgforge/svgforge) — create optimized **SVG sprites** of several types along with accompanying stylesheet resources and example documents.
6
+
7
+ The configuration options are [documented in the svgforge](https://github.com/svgforge/svgforge/blob/main/docs/configuration.md) library.
8
+
9
+ ## Installation
10
+
11
+ Install globally to use `svgforge` anywhere:
12
+
13
+ ```bash
14
+ npm install --global @svgforge/svgforge-cli
15
+ ```
16
+
17
+ Or as a project dependency (e.g. for usage in npm scripts):
18
+
19
+ ```bash
20
+ npm install --save-dev @svgforge/svgforge-cli
21
+ ```
22
+
23
+ ## Usage
24
+
25
+ ```
26
+ svgforge [options] files
27
+ ```
28
+
29
+ Run `svgforge --help` to get a full list of all available options, or consult the [command line reference](docs/command-line.md).
30
+
31
+ ### Examples
32
+
33
+ Create a `view` sprite of all SVG files in `assets/` and write the result — along with an example HTML document — to the `out/` directory:
34
+
35
+ ```bash
36
+ svgforge --view --view-example --view-bust=false --dest=out assets/*.svg
37
+ ```
38
+
39
+ Create a `symbol` sprite with an accompanying CSS stylesheet and example document:
40
+
41
+ ```bash
42
+ svgforge --symbol --symbol-render-css --symbol-example --dest=out assets/*.svg
43
+ ```
44
+
45
+ Add a 10px padding around all shapes of a `stack` sprite:
46
+
47
+ ```bash
48
+ svgforge --stack -p 10 assets/*.svg
49
+ ```
50
+
51
+ Instead of passing options on the command line you can also use an external JSON config file:
52
+
53
+ ```bash
54
+ svgforge --config config.json assets/*.svg
55
+ ```
56
+
57
+ > **Tip:** A config file can be generated with AI DeepWiki [online configurator](https://deepwiki.com/svgforge/svgforge-cli).
58
+
59
+ ### Sprite modes
60
+
61
+ The CLI supports all four sprite modes of svgforge. Activate them individually or combine them in a single run (e.g. `--view --symbol`):
62
+
63
+ | Mode | Flag | Description |
64
+ | ---- | ---- | ----------- |
65
+ | `view` | `--view` / `-v` | SVG view-based sprite |
66
+ | `symbol` | `--symbol` / `-s` | Sprite of `<symbol>` elements for inline embedding |
67
+ | `stack` | `--stack` / `-S` | Stacked sprite with `:target` CSS |
68
+ | `defs` | `--defs` / `-d` | Sprite of `<defs>` elements (legacy) |
69
+
70
+ ### Advanced globbing
71
+
72
+ Some shells don't support the double-star character `**`. Wrap your glob expression in single quotes so that Node.js handles the matching instead:
73
+
74
+ ```bash
75
+ svgforge --config config.json 'assets/**/*.svg'
76
+ ```
77
+
78
+ The CLI typically derives shape IDs from the file basename. To start ID traversal from a base directory, add a symbolic link to that directory (`./`) to your pattern:
79
+
80
+ ```bash
81
+ svgforge --config config.json 'assets/./**/*.svg'
82
+ ```
83
+
84
+ This results in shape IDs like `path--to--source` (assuming the default shape ID generator is used).
85
+
86
+ ## Development
87
+
88
+ Requirements: Node.js >= 18, [pnpm](https://pnpm.io).
89
+
90
+ ```bash
91
+ pnpm install
92
+ pnpm lint # Run XO linter
93
+ pnpm test # Run the test suite (node --test)
94
+ ```
95
+
96
+ The test suite spawns the CLI as a child process against fixtures in `test/fixtures/` and asserts the generated sprite files and their contents.
97
+
98
+ ## Related
99
+
100
+ - [svgforge](https://github.com/svgforge/svgforge) — the underlying sprite creation library
101
+ - [svgforge docs](https://github.com/svgforge/svgforge/tree/main/docs) — detailed documentation of all sprite modes
102
+
103
+ ## License
104
+
105
+ [MIT](LICENSE) © Felix Müller
106
+ [npm-url]: https://www.npmjs.com/package/@svgforge/svgforge-cli
107
+
108
+ [npm-image]: https://img.shields.io/npm/v/@svgforge/svgforge-cli?logo=npm&logoColor=fff
109
+
110
+ [npm-downloads]: https://img.shields.io/npm/dm/@svgforge/svgforge-cli
111
+ [ci-url]: https://github.com/svgforge/svgforge-cli/actions/workflows/test.yml?query=branch%3Amain
112
+ [ci-image]: https://img.shields.io/github/actions/workflow/status/svgforge/svgforge-cli/test.yml?branch=main&label=CI&logo=github
113
+
114
+ [coveralls-url]: https://coveralls.io/github/svgforge/svgforge-cli?branch=main
115
+ [coveralls-image]: https://img.shields.io/coveralls/github/svgforge/svgforge-cli/main?logo=coveralls
@@ -0,0 +1,427 @@
1
+ dest:
2
+ description: Main output directory (base path)
3
+ alias: D
4
+ default: .
5
+ map: dest
6
+
7
+ config:
8
+ description: Path to external JSON config file
9
+ alias: C
10
+
11
+ log:
12
+ description: Logging verbosity ("info", "verbose" or "debug")
13
+ alias: l
14
+ map: log
15
+
16
+ shape:
17
+ id:
18
+ separator:
19
+ description: Separator for traversing a directory structure into a shape ID
20
+ default: --
21
+ map: shape.id.separator
22
+
23
+ generator:
24
+ description: ID generation callback [via CLI only template strings]
25
+ default: "%s"
26
+ map: shape.id.generator
27
+
28
+ pseudo:
29
+ description: Separator for CSS pseudo classes
30
+ default: "~"
31
+ map: shape.id.pseudo
32
+
33
+ whitespace:
34
+ description: Whitespace replacement string for shape IDs
35
+ default: "_"
36
+ map: shape.id.whitespace
37
+
38
+ dim:
39
+ width:
40
+ description: Maximum shape width in pixels
41
+ default: 2000
42
+ alias: w
43
+ map: shape.dimension.maxWidth
44
+
45
+ height:
46
+ description: Maximum shape height in pixels
47
+ default: 2000
48
+ alias: h
49
+ map: shape.dimension.maxHeight
50
+
51
+ precision:
52
+ description: Precision (decimal places) for dimension calculations
53
+ default: 2
54
+ map: shape.dimension.precision
55
+
56
+ attributes:
57
+ description: Whether to add width and height attributes to the shapes
58
+ default: false
59
+ map: shape.dimension.attributes
60
+
61
+ spacing:
62
+ padding:
63
+ description: Padding around shape (up to 4 x comma-separated)
64
+ default: 0,0,0,0
65
+ alias: p
66
+ map: shape.spacing.padding
67
+
68
+ box:
69
+ description: Box sizing strategy ("content", "padding" or "icon")
70
+ default: content
71
+ alias: b
72
+ map: shape.spacing.box
73
+
74
+ meta:
75
+ description: Path to YAML file with meta information
76
+ alias: m
77
+ map: shape.meta
78
+
79
+ align:
80
+ description: Path to YAML file with alignment information
81
+ alias: a
82
+ map: shape.align
83
+
84
+ dest:
85
+ description: Path to output directory for intermediate SVG files
86
+ alias: ims
87
+ map: shape.dest
88
+
89
+ transform:
90
+ description: Comma-separated list of predefined transformers (see docs)
91
+ default: svgo
92
+ map: shape.transform
93
+
94
+ "*":
95
+ description: External JSON config files for named transformers
96
+
97
+ svg:
98
+ xmldecl:
99
+ description: Whether to include an XML declaration in SVG files
100
+ default: true
101
+ map: svg.xmlDeclaration
102
+
103
+ doctype:
104
+ description: Whether to include a doctype declaration in SVG files
105
+ default: true
106
+ map: svg.doctypeDeclaration
107
+
108
+ namespace:
109
+ ids:
110
+ description: Whether to apply ID namespacing to the sprite
111
+ default: true
112
+ map: svg.namespaceIDs
113
+
114
+ prefix:
115
+ description: What, if any, prefix to apply to the automatically generated id
116
+ default: ""
117
+ map: svg.namespaceIDPrefix
118
+
119
+ classnames:
120
+ description: Whether to apply CSS class namespacing to the sprite
121
+ default: true
122
+ map: svg.namespaceClassnames
123
+
124
+ dimattrs:
125
+ description: Whether to add width and height attributes to the sprite
126
+ default: true
127
+ map: svg.dimensionAttributes
128
+
129
+ rootattrs:
130
+ description: Custom root attributes for the outermost <svg> element (external JSON file)
131
+ map: svg.rootAttributes
132
+
133
+ precision:
134
+ description: Floating point precision for CSS positioning values
135
+ default: -1
136
+ map: svg.precision
137
+
138
+ view:
139
+ description: Activates the «view» mode
140
+ default: false
141
+ alias: v
142
+
143
+ dest:
144
+ description: Mode specific output directory
145
+ default: view
146
+ map: mode.view.dest
147
+
148
+ sprite:
149
+ description: Sprite path and filename (relative to --view-dest)
150
+ default: svg/sprite.view.svg
151
+ alias: vs
152
+ map: mode.view.sprite
153
+
154
+ bust:
155
+ description: Enable cache busting
156
+ default: true
157
+ map: mode.view.bust
158
+
159
+ render:
160
+ css:
161
+ description: Whether to render a CSS stylesheet with shape dimension classes
162
+ default: false
163
+ alias: vcss
164
+
165
+ template:
166
+ description: CSS stylesheet Mustache template (relative to svgforge basedir)
167
+ default: tmpl/common/sprite.css
168
+ map: mode.view.render.css.template
169
+
170
+ dest:
171
+ description: CSS stylesheet destination (relative to the --view-dest)
172
+ default: sprite.css
173
+ map: mode.view.render.css.dest
174
+
175
+ "*":
176
+ description: Custom output renderings
177
+ template:
178
+ description: Custom output Mustache template (relative to svgforge basedir)
179
+
180
+ dest:
181
+ description: Custom output destination (relative to the --view-dest)
182
+
183
+ example:
184
+ description: Whether to render an example HTML document
185
+ default: false
186
+ alias: vx
187
+
188
+ template:
189
+ description: HTML document Mustache template (relative to svgforge basedir)
190
+ default: tmpl/view/sprite.html
191
+ map: mode.view.example.template
192
+
193
+ dest:
194
+ description: HTML document destination (relative to the --view-dest)
195
+ default: sprite.view.html
196
+ map: mode.view.example.dest
197
+
198
+ defs:
199
+ description: Activates the «defs» mode
200
+ default: false
201
+ alias: d
202
+
203
+ dest:
204
+ description: Mode specific output directory
205
+ default: defs
206
+ map: mode.defs.dest
207
+
208
+ prefix:
209
+ description: CSS selector prefix for all shapes (including placeholders)
210
+ default: .svg-%s
211
+ map: mode.defs.prefix
212
+
213
+ dimensions:
214
+ description: CSS selector suffix for shape dimension rules ("" for inline)
215
+ default: -dims
216
+ map: mode.defs.dimensions
217
+
218
+ sprite:
219
+ description: Sprite path and filename (relative to --defs-dest)
220
+ default: svg/sprite.css.svg
221
+ alias: ds
222
+ map: mode.defs.sprite
223
+
224
+ bust:
225
+ description: Enable cache busting
226
+ default: false
227
+ map: mode.defs.bust
228
+
229
+ render:
230
+ css:
231
+ description: Whether to render a CSS stylesheet
232
+ default: false
233
+ alias: dcss
234
+
235
+ template:
236
+ description: CSS stylesheet Mustache template (relative to svgforge basedir)
237
+ default: tmpl/common/sprite.css
238
+ map: mode.defs.render.css.template
239
+
240
+ dest:
241
+ description: CSS stylesheet destination (relative to the --defs-dest)
242
+ default: sprite.css
243
+ map: mode.defs.render.css.dest
244
+
245
+ "*":
246
+ description: Custom output renderings
247
+ template:
248
+ description: Custom output Mustache template (relative to svgforge basedir)
249
+
250
+ dest:
251
+ description: Custom output destination (relative to the --defs-dest)
252
+
253
+ inline:
254
+ description: Create sprite variant suitable for inline embedding
255
+ default: false
256
+ alias: di
257
+ map: mode.defs.inline
258
+
259
+ example:
260
+ description: Whether to render an example HTML document
261
+ default: false
262
+ alias: dx
263
+
264
+ template:
265
+ description: HTML document Mustache template (relative to svgforge basedir)
266
+ default: tmpl/defs/sprite.html
267
+ map: mode.defs.example.template
268
+
269
+ dest:
270
+ description: HTML document destination (relative to the --defs-dest)
271
+ default: sprite.defs.html
272
+ map: mode.defs.example.dest
273
+
274
+ symbol:
275
+ description: Activates the «symbol» mode
276
+ default: false
277
+ alias: s
278
+
279
+ dest:
280
+ description: Mode specific output directory
281
+ default: symbol
282
+ map: mode.symbol.dest
283
+
284
+ prefix:
285
+ description: CSS selector prefix for all shapes (including placeholders)
286
+ default: .svg-%s
287
+ map: mode.symbol.prefix
288
+
289
+ dimensions:
290
+ description: CSS selector suffix for shape dimension rules ("" for inline)
291
+ default: -dims
292
+ map: mode.symbol.dimensions
293
+
294
+ sprite:
295
+ description: Sprite path and filename (relative to --symbol-dest)
296
+ default: svg/sprite.css.svg
297
+ alias: ss
298
+ map: mode.symbol.sprite
299
+
300
+ bust:
301
+ description: Enable cache busting
302
+ default: false
303
+ map: mode.symbol.bust
304
+
305
+ render:
306
+ css:
307
+ description: Whether to render a CSS stylesheet
308
+ default: false
309
+ alias: sc
310
+
311
+ template:
312
+ description: CSS stylesheet Mustache template (relative to svgforge basedir)
313
+ default: tmpl/common/sprite.css
314
+ map: mode.symbol.render.css.template
315
+
316
+ dest:
317
+ description: CSS stylesheet destination (relative to the --symbol-dest)
318
+ default: sprite.css
319
+ map: mode.symbol.render.css.dest
320
+
321
+ "*":
322
+ description: Custom output renderings
323
+ template:
324
+ description: Custom output Mustache template (relative to svgforge basedir)
325
+
326
+ dest:
327
+ description: Custom output destination (relative to the --symbol-dest)
328
+
329
+ inline:
330
+ description: Create sprite variant suitable for inline embedding
331
+ default: false
332
+ alias: si
333
+ map: mode.symbol.inline
334
+
335
+ example:
336
+ description: Whether to render an example HTML document
337
+ default: false
338
+ alias: sx
339
+
340
+ template:
341
+ description: HTML document Mustache template (relative to svgforge basedir)
342
+ default: tmpl/symbol/sprite.html
343
+ map: mode.symbol.example.template
344
+
345
+ dest:
346
+ description: HTML document destination (relative to the --symbol-dest)
347
+ default: sprite.symbol.html
348
+ map: mode.symbol.example.dest
349
+
350
+ stack:
351
+ description: Activates the «stack» mode
352
+ default: false
353
+ alias: S
354
+
355
+ dest:
356
+ description: Mode specific output directory
357
+ default: stack
358
+ map: mode.stack.dest
359
+
360
+ prefix:
361
+ description: CSS selector prefix for all shapes (including placeholders)
362
+ default: .svg-%s
363
+ map: mode.stack.prefix
364
+
365
+ dimensions:
366
+ description: CSS selector suffix for shape dimension rules ("" for inline)
367
+ default: -dims
368
+ map: mode.stack.dimensions
369
+
370
+ sprite:
371
+ description: Sprite path and filename (relative to --stack-dest)
372
+ default: svg/sprite.css.svg
373
+ alias: Ss
374
+ map: mode.stack.sprite
375
+
376
+ bust:
377
+ description: Enable cache busting
378
+ default: false
379
+ map: mode.stack.bust
380
+
381
+ rootviewbox:
382
+ description: Add viewBox attribute to root svg automatically
383
+ default: true
384
+ map: mode.stack.rootviewbox
385
+
386
+ render:
387
+ css:
388
+ description: Whether to render a CSS stylesheet
389
+ default: false
390
+ alias: Sc
391
+
392
+ template:
393
+ description: CSS stylesheet Mustache template (relative to svgforge basedir)
394
+ default: tmpl/common/sprite.css
395
+ map: mode.stack.render.css.template
396
+
397
+ dest:
398
+ description: CSS stylesheet destination (relative to the --stack-dest)
399
+ default: sprite.css
400
+ map: mode.stack.render.css.dest
401
+
402
+ "*":
403
+ description: Custom output renderings
404
+ template:
405
+ description: Custom output Mustache template (relative to svgforge basedir)
406
+
407
+ dest:
408
+ description: Custom output destination (relative to the --stack-dest)
409
+
410
+ example:
411
+ description: Whether to render an example HTML document
412
+ default: false
413
+ alias: Sx
414
+
415
+ template:
416
+ description: HTML document Mustache template (relative to svgforge basedir)
417
+ default: tmpl/stack/sprite.html
418
+ map: mode.stack.example.template
419
+
420
+ dest:
421
+ description: HTML document destination (relative to the --stack-dest)
422
+ default: sprite.stack.html
423
+ map: mode.stack.example.dest
424
+
425
+ variables:
426
+ description: Path to external JSON file with Mustache variable definitions
427
+ map: "variables"
@@ -0,0 +1,523 @@
1
+ #!/usr/bin/env node
2
+
3
+ /**
4
+ Svgforge is a Node.js module for creating SVG sprites
5
+
6
+ Based on the command line interface originally written for svg-sprite
7
+ by Joschi Kuphal — this is a standalone fork/package of that CLI.
8
+
9
+ @see https://github.com/joeda1/svgforge-cli
10
+ @author Joschi Kuphal <joschi@kuphal.net> (https://github.com/jkphl)
11
+ @author Felix Müller
12
+ @copyright © 2018 Joschi Kuphal
13
+ @copyright © 2026 Felix Müller
14
+ @license MIT https://github.com/joeda1/svgforge-cli/blob/main/LICENSE
15
+ */
16
+
17
+ import fs from 'node:fs';
18
+ import path from 'node:path';
19
+ import process from 'node:process';
20
+ import {fileURLToPath} from 'node:url';
21
+ import {load} from 'js-yaml';
22
+ import yargs from 'yargs';
23
+ import SVGSpriter from '@svgforge/svgforge';
24
+ import {deepMerge, isObject, zipObject} from '@svgforge/svgforge/lib/svg-sprite/utils/index.js';
25
+
26
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
27
+ const {version} = JSON.parse(fs.readFileSync(path.resolve(__dirname, '..', 'package.json'), 'utf8'));
28
+
29
+ /**
30
+ All supported sprite modes
31
+
32
+ @type {string[]}
33
+ */
34
+ const MODES = ['view', 'defs', 'symbol', 'stack'];
35
+
36
+ /**
37
+ All supported stylesheet render types
38
+
39
+ @type {string[]}
40
+ */
41
+ const RENDER_TYPES = ['css'];
42
+
43
+ /**
44
+ Yargs argument parser instance
45
+ */
46
+
47
+ // eslint-disable-next-line jsdoc/imports-as-dependencies -- `yargs` is a declared dependency; dynamic import() is required in JSDoc types.
48
+ /** @typedef {import('yargs').Argv} Yargs */
49
+
50
+ /**
51
+ CLI option definition (subset of the bundled YAML configuration)
52
+
53
+ @typedef {object} OptionDefinition
54
+ @property {string} [description] Option description
55
+ @property {string} [alias] Short option alias
56
+ @property {unknown} [default] Default value applied when the option is omitted
57
+ @property {boolean} [required] Whether the option is mandatory
58
+ @property {string} [map] Dot-separated config key the option maps to
59
+ @property {object} [children] Nested option definitions keyed by their suffix
60
+ */
61
+
62
+ /**
63
+ Recursively nested map of vinyl files
64
+
65
+ @typedef {{[key: string]: File | FileMap}} FileMap
66
+ */
67
+
68
+ /**
69
+ Spriter configuration (subset of the SVGSpriter configuration object)
70
+
71
+ @typedef {object} SpriterConfig
72
+ @property {string} [dest] Main output directory
73
+ @property {string} [log] Logging verbosity ("info", "verbose" or "debug")
74
+ @property {object} [shape] SVG shape configuration
75
+ @property {object} [svg] SVG output configuration
76
+ @property {object} [mode] Sprite mode configuration
77
+ @property {unknown} [variables] Mustache template variables (or the path to their JSON file)
78
+ */
79
+
80
+ /**
81
+ Global spriter configuration accumulated from the CLI options
82
+
83
+ @type {SpriterConfig}
84
+ */
85
+ const config = {};
86
+
87
+ /**
88
+ External JSON configuration ("--config" file) used to detect explicitly
89
+ requested modes, render types and examples
90
+
91
+ @type {object}
92
+ */
93
+ let JSONConfig = {mode: {}};
94
+
95
+ /**
96
+ Maps dot-separated config keys to their CLI option names
97
+
98
+ @type {{[key: string]: string}}
99
+ */
100
+ const optionsMap = {};
101
+
102
+ /**
103
+ Resolve a path relative to the installed svgforge library
104
+
105
+ Default template files (e.g. "tmpl/stack/sprite.html") ship with the
106
+ svgforge library package, so they are resolved against its install
107
+ location rather than against this CLI package.
108
+
109
+ @param {string} target Path to resolve
110
+ @returns {string} Resolved absolute path
111
+ */
112
+ function resolveSvgForgePath(target) {
113
+ const libEntry = fileURLToPath(import.meta.resolve('@svgforge/svgforge'));
114
+ const libRoot = path.dirname(path.dirname(libEntry));
115
+ return path.resolve(libRoot, target);
116
+ }
117
+
118
+ /**
119
+ Add a command line option and recursively register all of its children
120
+
121
+ @param {Yargs} parser Yargs instance to extend
122
+ @param {string} name Hyphenated option name (e.g. "defs-render-css")
123
+ @param {OptionDefinition} option Option configuration
124
+ @returns {Yargs} The extended yargs instance
125
+ */
126
+ function addOption(parser, name, option) {
127
+ let alias = name;
128
+
129
+ // If this is an option itself
130
+ if ('description' in option) {
131
+ if ('alias' in option) {
132
+ alias = option.alias;
133
+ parser = parser.alias(alias, name);
134
+ }
135
+
136
+ parser = parser.describe(alias, option.description);
137
+
138
+ if ('default' in option) {
139
+ const templated = name.endsWith('-template');
140
+ const defaultValue = templated ? resolveSvgForgePath(option.default) : option.default;
141
+ parser = parser.default(alias, defaultValue);
142
+
143
+ if (option.default === true || option.default === false) {
144
+ parser = parser.boolean(name);
145
+ }
146
+ } else if (option.required) {
147
+ parser = parser.require(alias);
148
+ }
149
+
150
+ if ('map' in option) {
151
+ optionsMap[option.map] = name;
152
+ }
153
+ }
154
+
155
+ const {description, alias: optAlias, default: optDefault, map, required, ...children} = option;
156
+ for (const [key, child] of Object.entries(children)) {
157
+ if (isObject(child)) {
158
+ parser = addOption(parser, `${name}-${key}`, child);
159
+ }
160
+ }
161
+
162
+ return parser;
163
+ }
164
+
165
+ /**
166
+ Add a value at the given key path of a configuration object
167
+
168
+ @param {object} store Configuration object to modify
169
+ @param {string[]} keyPath Path segments ending at the target key
170
+ @param {unknown} value Value to store
171
+ @returns {void}
172
+ */
173
+ function addConfigMap(store, keyPath, value) {
174
+ const key = keyPath.shift();
175
+
176
+ if (keyPath.length > 0) {
177
+ if (!Object.hasOwn(store, key) || !isObject(store[key])) {
178
+ store[key] = {};
179
+ }
180
+
181
+ addConfigMap(store[key], keyPath, value);
182
+ } else {
183
+ store[key] = value;
184
+ }
185
+ }
186
+
187
+ /**
188
+ Recursively write files to disk
189
+
190
+ @param {FileMap} files Nested map of vinyl files
191
+ @returns {number} Number of written files
192
+ */
193
+ function writeFiles(files) {
194
+ let written = 0;
195
+
196
+ for (const file of Object.values(files)) {
197
+ if (!isObject(file)) {
198
+ continue;
199
+ }
200
+
201
+ if (typeof file.path === 'string') {
202
+ fs.mkdirSync(path.dirname(file.path), {recursive: true});
203
+ fs.writeFileSync(file.path, file.contents);
204
+ ++written;
205
+ } else {
206
+ written += writeFiles(file);
207
+ }
208
+ }
209
+
210
+ return written;
211
+ }
212
+
213
+ /**
214
+ Compile the spriter and write all generated files to disk
215
+
216
+ @param {SVGSpriter} spriter Spriter instance to compile
217
+ @returns {Promise<number>} Promise resolving to the number of written files
218
+ */
219
+ function compile(spriter) {
220
+ return new Promise((resolve, reject) => {
221
+ spriter.compile((error, result) => {
222
+ if (error) {
223
+ reject(error);
224
+ } else {
225
+ resolve(writeFiles(result));
226
+ }
227
+ });
228
+ });
229
+ }
230
+
231
+ /**
232
+ Register all CLI options from the bundled YAML configuration
233
+
234
+ @returns {Yargs} The fully configured yargs instance
235
+ */
236
+ function registerOptions() {
237
+ const parser = yargs(process.argv.slice(2))
238
+ .usage('Create one or multiple sprites of the given SVG files, optionally along with some stylesheet resources.\nUsage: $0 [options] files')
239
+ .version(version)
240
+ .help('help', 'Display this help information')
241
+ .wrap(null)
242
+ .example('$0 --view --view-example --dest=out assets/*.svg', 'Create a view sprite of the given SVG files including example document to the subdirectory "out"')
243
+ .example('$0 --defs --defs-render-css --defs-example --dest=out assets/*.svg', 'Create a defs sprite of the given SVG files including a CSS stylesheet and example document')
244
+ .example('$0 -S -p 10 assets/*.svg', 'Create a stack sprite and add 10px padding around all shapes')
245
+ .showHelpOnFail(true)
246
+ .demandCommand(1);
247
+
248
+ try {
249
+ const options = load(fs.readFileSync(path.resolve(__dirname, 'config.yaml'), 'utf8'));
250
+ for (const [key, option] of Object.entries(options)) {
251
+ addOption(parser, key, option);
252
+ }
253
+ } catch (error) {
254
+ console.log(error);
255
+ }
256
+
257
+ return parser;
258
+ }
259
+
260
+ /**
261
+ Apply all CLI arguments to the global configuration object
262
+
263
+ @param {SpriterConfig} cfg Configuration object to modify
264
+ @param {{[key: string]: string}} optMap Config keys mapped to option names
265
+ @param {object} argv Parsed command line arguments
266
+ @returns {void}
267
+ */
268
+ function applyCliOptions(cfg, optMap, argv) {
269
+ for (const [configKey, optionName] of Object.entries(optMap)) {
270
+ if (!Object.hasOwn(argv, optionName)) {
271
+ continue;
272
+ }
273
+
274
+ addConfigMap(cfg, configKey.split('.'), argv[optionName]);
275
+ }
276
+ }
277
+
278
+ /**
279
+ Load an external JSON configuration file ("--config") and merge it in
280
+
281
+ @param {SpriterConfig} cfg Configuration object to merge into
282
+ @param {object} argv Parsed command line arguments
283
+ @returns {void}
284
+ */
285
+ function loadExternalConfig(cfg, argv) {
286
+ if (!argv.config) {
287
+ return;
288
+ }
289
+
290
+ try {
291
+ const configFile = argv.config;
292
+ delete argv.config;
293
+ delete argv.C;
294
+
295
+ const JSONConfigContent = fs.readFileSync(path.resolve(configFile));
296
+ /** @type {SpriterConfig} */
297
+ const externalConfig = JSON.parse(JSONConfigContent);
298
+
299
+ // Keep an un-merged clone for the later option-removal checks
300
+ JSONConfig = JSON.parse(JSONConfigContent);
301
+ if (!('mode' in JSONConfig)) {
302
+ JSONConfig.mode = {};
303
+ }
304
+
305
+ // Expand shorthand mode definitions
306
+ if ('mode' in externalConfig && isObject(externalConfig.mode)) {
307
+ for (const [mode, modeConfig] of Object.entries(externalConfig.mode)) {
308
+ if (modeConfig !== true) {
309
+ continue;
310
+ }
311
+
312
+ externalConfig.mode[mode] = {render: {css: true}};
313
+ JSONConfig.mode[mode] = {render: {css: true}};
314
+ }
315
+ }
316
+
317
+ deepMerge(cfg, externalConfig);
318
+ } catch (error) {
319
+ console.error('[ERROR] Skipping --config file due to errors ("%s")', error.message.trim());
320
+ }
321
+ }
322
+
323
+ /**
324
+ Read a shape transform configuration file to a plain object
325
+
326
+ @param {string} file Path to the transform configuration file
327
+ @returns {object|undefined} Parsed configuration, or nothing on error
328
+ */
329
+ function readTransformConfig(file) {
330
+ try {
331
+ if (fs.existsSync(file)) {
332
+ const contents = fs.readFileSync(file, 'utf8');
333
+ return contents.trim() ? JSON.parse(contents) : {};
334
+ }
335
+ } catch {
336
+ return undefined;
337
+ }
338
+
339
+ return undefined;
340
+ }
341
+
342
+ /**
343
+ Refine the shape related configuration options
344
+
345
+ @param {SpriterConfig} cfg Configuration object to modify
346
+ @param {object} argv Parsed command line arguments
347
+ @returns {void}
348
+ */
349
+ function refineShapeConfig(cfg, argv) {
350
+ // Refine particular config options
351
+ cfg.shape.spacing.padding = String(cfg.shape.spacing.padding).trim();
352
+ cfg.shape.spacing.padding = cfg.shape.spacing.padding.length > 0
353
+ // eslint-disable-next-line unicorn/prefer-number-coercion -- Padding values may carry unit suffixes (e.g. "48px"); parseFloat is the required semantics.
354
+ ? cfg.shape.spacing.padding.split(',').map(dimension => Number.parseFloat(dimension || 0))
355
+ : [];
356
+
357
+ if (cfg.svg.rootAttributes && typeof cfg.svg.rootAttributes === 'string') {
358
+ try {
359
+ const attributesPath = path.resolve(cfg.svg.rootAttributes);
360
+ cfg.svg.rootAttributes = JSON.parse(fs.readFileSync(attributesPath, 'utf8'));
361
+ } catch (error) {
362
+ console.error('[ERROR] Skipping --svg-rootattrs file due to errors ("%s")', error.message.trim());
363
+ cfg.svg.rootAttributes = {};
364
+ }
365
+ }
366
+
367
+ // Expand transformation options
368
+ if (typeof cfg.shape.transform === 'string') {
369
+ const transforms = String(cfg.shape.transform).trim();
370
+ cfg.shape.transform = [];
371
+
372
+ if (transforms.length > 0) {
373
+ for (const raw of transforms.split(',')) {
374
+ const transform = String(raw).trim();
375
+ if (transform.length === 0) {
376
+ continue;
377
+ }
378
+
379
+ if (Object.hasOwn(argv, `shape-transform-${transform}`)) {
380
+ const transformConfig = readTransformConfig(argv[`shape-transform-${transform}`]);
381
+ // eslint-disable-next-line max-depth -- Per-transform config lookup nests the mode/shape refinement walk one level too deep.
382
+ if (transformConfig !== undefined) {
383
+ cfg.shape.transform.push(zipObject([transform], [transformConfig]));
384
+ }
385
+ } else {
386
+ cfg.shape.transform.push(transform);
387
+ }
388
+ }
389
+ }
390
+ }
391
+ }
392
+
393
+ /**
394
+ Refine the sprite mode configuration and remove inactive render types
395
+
396
+ @param {SpriterConfig} cfg Configuration object to modify
397
+ @param {object} argv Parsed command line arguments
398
+ @returns {void}
399
+ */
400
+ function refineSpriteModes(cfg, argv) {
401
+ // Run through all sprite modes
402
+ for (const mode of MODES) {
403
+ if (argv[mode] !== true && !Object.hasOwn(JSONConfig.mode, mode)) {
404
+ delete cfg.mode[mode];
405
+ continue;
406
+ }
407
+
408
+ const {render} = cfg.mode[mode];
409
+
410
+ // Remove excessive render types
411
+ if (render) {
412
+ for (const renderType of RENDER_TYPES) {
413
+ const arg = `${mode}-render-${renderType}`;
414
+ if (
415
+ Object.hasOwn(render, renderType)
416
+ && argv[arg] !== true
417
+ && (!Object.hasOwn(JSONConfig.mode, mode)
418
+ || !Object.hasOwn(JSONConfig.mode[mode], 'render')
419
+ || !Object.hasOwn(JSONConfig.mode[mode].render, renderType))
420
+ ) {
421
+ delete render[renderType];
422
+ }
423
+ }
424
+ }
425
+
426
+ if (Array.isArray(cfg.mode[mode].dimensions) && cfg.mode[mode].dimensions.length === 0) {
427
+ cfg.mode[mode].dimensions = true;
428
+ }
429
+ }
430
+ }
431
+
432
+ /**
433
+ Remove excessive example options from the sprite modes
434
+
435
+ @param {SpriterConfig} cfg Configuration object to modify
436
+ @param {object} argv Parsed command line arguments
437
+ @returns {void}
438
+ */
439
+ function removeExcessiveExamples(cfg, argv) {
440
+ for (const [mode, modeConfig] of Object.entries(cfg.mode)) {
441
+ const example = `${mode}-example`;
442
+ if (argv[example] !== true && (!Object.hasOwn(JSONConfig.mode, mode) || !Object.hasOwn(JSONConfig.mode[mode], 'example')) && Object.hasOwn(modeConfig, 'example')) {
443
+ delete modeConfig.example;
444
+ }
445
+ }
446
+ }
447
+
448
+ /**
449
+ Read and parse the Mustache variables JSON file
450
+
451
+ @param {SpriterConfig} cfg Configuration object to modify
452
+ @returns {void}
453
+ */
454
+ function loadVariables(cfg) {
455
+ if (!Object.hasOwn(cfg, 'variables')) {
456
+ return;
457
+ }
458
+
459
+ const variables = String(cfg.variables).trim();
460
+ delete cfg.variables;
461
+
462
+ if (variables.length > 0) {
463
+ const variablesFile = path.resolve(variables);
464
+ if (fs.existsSync(variablesFile)) {
465
+ try {
466
+ cfg.variables = JSON.parse(fs.readFileSync(variablesFile, 'utf8'));
467
+ } catch (error) {
468
+ console.error('[ERROR] Skipping --variables file due to errors ("%s")', error.message.trim());
469
+ }
470
+ }
471
+ }
472
+ }
473
+
474
+ /**
475
+ Run the command line interface
476
+
477
+ @returns {Promise<void>} Promise resolving after all sprites were written
478
+ */
479
+ async function main() {
480
+ const parser = registerOptions();
481
+ const argv = parser.parse();
482
+
483
+ // Map all arguments to a global configuration object
484
+ applyCliOptions(config, optionsMap, argv);
485
+
486
+ // Load external JSON config file
487
+ loadExternalConfig(config, argv);
488
+
489
+ // Refine particular config options
490
+ refineShapeConfig(config, argv);
491
+ refineSpriteModes(config, argv);
492
+ removeExcessiveExamples(config, argv);
493
+ loadVariables(config);
494
+
495
+ const spriter = new SVGSpriter(config);
496
+ const files = argv._.flatMap(filePattern => fs.globSync(filePattern).toSorted((a, b) => b.localeCompare(a)));
497
+
498
+ for (const filePattern of files) {
499
+ // Glob >= 9 returns paths without the "./" prefix, so detect relative
500
+ // patterns from the original glob result to preserve directory structure
501
+ // in the shape identifiers (e.g. "nested/leaf.svg" -> "nested--leaf")
502
+ const isRelative = !path.isAbsolute(filePattern) && !filePattern.startsWith('../');
503
+ const file = path.resolve(filePattern);
504
+ const stat = fs.lstatSync(file);
505
+ let basename;
506
+
507
+ if (stat.isSymbolicLink()) {
508
+ basename = path.basename(fs.readlinkSync(file));
509
+ } else {
510
+ basename = isRelative ? filePattern : path.basename(file);
511
+ }
512
+
513
+ spriter.add(file, basename, fs.readFileSync(file));
514
+ }
515
+
516
+ try {
517
+ await compile(spriter);
518
+ } catch (error) {
519
+ console.error(error);
520
+ }
521
+ }
522
+
523
+ await main();
package/package.json ADDED
@@ -0,0 +1,52 @@
1
+ {
2
+ "name": "@svgforge/svgforge-cli",
3
+ "version": "2.0.0",
4
+ "author": "Felix Müller (https://github.com/joeda1)",
5
+ "contributors": [
6
+ "Joschi Kuphal <joschi@kuphal.net> (https://github.com/jkphl)"
7
+ ],
8
+ "description": "Command line interface for svgforge — create SVG sprites & stacks with stylesheet resources (CSS, Sass, etc.)",
9
+ "homepage": "https://github.com/svgforge/svgforge-cli",
10
+ "repository": {
11
+ "type": "git",
12
+ "url": "git+https://github.com/svgforge/svgforge-cli.git"
13
+ },
14
+ "bugs": {
15
+ "url": "https://github.com/svgforge/svgforge-cli/issues"
16
+ },
17
+ "keywords": [
18
+ "svg",
19
+ "stack",
20
+ "generator",
21
+ "css",
22
+ "cli",
23
+ "command-line"
24
+ ],
25
+ "license": "MIT",
26
+ "type": "module",
27
+ "engines": {
28
+ "node": ">=24"
29
+ },
30
+ "bin": {
31
+ "svgforge": "./bin/svgforge.js"
32
+ },
33
+ "files": [
34
+ "bin"
35
+ ],
36
+ "dependencies": {
37
+ "js-yaml": "^5.4.1",
38
+ "@svgforge/svgforge": "^2.0.0",
39
+ "vinyl": "^3.0.1",
40
+ "yargs": "^18.1.0"
41
+ },
42
+ "devDependencies": {
43
+ "eslint-plugin-jsdoc": "^64.3.2",
44
+ "xo": "^4.0.0"
45
+ },
46
+ "scripts": {
47
+ "lint": "xo",
48
+ "fix": "xo --fix",
49
+ "test": "node --test test/*.test.js",
50
+ "test:coverage": "mkdir -p coverage && node --test --experimental-test-coverage --test-reporter=spec --test-reporter-destination=stdout --test-reporter=lcov --test-reporter-destination=coverage/lcov.info"
51
+ }
52
+ }