denji 0.1.2 → 0.3.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/README.md CHANGED
@@ -1,11 +1,17 @@
1
1
  # Denji
2
2
 
3
- CLI tool for managing SVG icons in React projects. Fetches icons from Iconify, converts to optimized React components, and maintains a centralized icons file.
3
+ CLI tool for managing SVG icons in frontend projects. Fetches icons from Iconify, converts to optimized components, and maintains a centralized icons file.
4
+
5
+ Supports **React**, **Preact**, and **Solid**.
6
+
7
+ ## Documentation
8
+
9
+ Visit https://denji-docs.vercel.app/docs to view the full documentation.
4
10
 
5
11
  ## Installation
6
12
 
7
13
  ```bash
8
- bun add -D denji
14
+ npm add -D denji
9
15
  ```
10
16
 
11
17
  ## Quick Start
@@ -39,7 +45,7 @@ Create `denji.json`:
39
45
  "typescript": true,
40
46
  "a11y": "hidden",
41
47
  "hooks": {
42
- "postAdd": ["bunx biome check --write ./src/icons.tsx"]
48
+ "postAdd": ["npx biome check --write ./src/icons.tsx"]
43
49
  }
44
50
  }
45
51
  ```
@@ -49,20 +55,110 @@ Create `denji.json`:
49
55
  | Option | Type | Default | Description |
50
56
  |--------|------|---------|-------------|
51
57
  | `output` | `string` | - | Output file path (e.g., `./src/icons.tsx`) |
52
- | `framework` | `"react"` | - | Target framework |
58
+ | `framework` | `"react"` \| `"preact"` \| `"solid"` | - | Target framework |
53
59
  | `typescript` | `boolean` | `true` | Generate TypeScript |
54
60
  | `a11y` | `"hidden"` \| `"img"` \| `"title"` \| `"presentation"` \| `false` | - | SVG accessibility strategy |
55
61
  | `hooks` | `object` | - | Lifecycle hooks |
56
62
 
57
- ### Accessibility Options
63
+ ## Frameworks
64
+
65
+ ### React
66
+
67
+ ```bash
68
+ denji init --framework react
69
+ ```
70
+
71
+ ```json
72
+ {
73
+ "framework": "react",
74
+ "react": {
75
+ "forwardRef": true
76
+ }
77
+ }
78
+ ```
79
+
80
+ **Usage:**
81
+
82
+ ```tsx
83
+ import { Icons } from "./icons";
84
+
85
+ function App() {
86
+ return <Icons.Check className="size-4 text-green-500" />;
87
+ }
88
+ ```
89
+
90
+ **Options:**
91
+
92
+ | Option | Type | Default | Description |
93
+ |--------|------|---------|-------------|
94
+ | `react.forwardRef` | `boolean` | `false` | Wrap icons with `forwardRef` |
95
+
96
+ ### Preact
97
+
98
+ ```bash
99
+ denji init --framework preact
100
+ ```
101
+
102
+ ```json
103
+ {
104
+ "framework": "preact",
105
+ "preact": {
106
+ "forwardRef": true
107
+ }
108
+ }
109
+ ```
110
+
111
+ **Usage:**
112
+
113
+ ```tsx
114
+ import { Icons } from "./icons";
115
+
116
+ function App() {
117
+ return <Icons.Check className="size-4 text-green-500" />;
118
+ }
119
+ ```
120
+
121
+ **Options:**
122
+
123
+ | Option | Type | Default | Description |
124
+ |--------|------|---------|-------------|
125
+ | `preact.forwardRef` | `boolean` | `false` | Wrap icons with `forwardRef` (uses `preact/compat`) |
126
+
127
+ ### Solid
128
+
129
+ ```bash
130
+ denji init --framework solid
131
+ ```
58
132
 
59
- - `hidden` - Adds `aria-hidden="true"` (decorative icons)
60
- - `img` - Adds `role="img"` with `aria-label`
61
- - `title` - Adds `<title>` element inside SVG
62
- - `presentation` - Adds `role="presentation"`
63
- - `false` - No accessibility attributes
133
+ ```json
134
+ {
135
+ "framework": "solid"
136
+ }
137
+ ```
64
138
 
65
- ### Hooks
139
+ **Usage:**
140
+
141
+ ```tsx
142
+ import { Icons } from "./icons";
143
+
144
+ function App() {
145
+ return <Icons.Check class="size-4 text-green-500" />;
146
+ }
147
+ ```
148
+
149
+ > Note: Solid uses `class` instead of `className`. Refs are passed as regular props - no `forwardRef` needed.
150
+
151
+ ## Accessibility
152
+
153
+ | Strategy | Description |
154
+ |----------|-------------|
155
+ | `hidden` | Adds `aria-hidden="true"` (decorative icons) |
156
+ | `img` | Adds `role="img"` with `aria-label` |
157
+ | `title` | Adds `<title>` element inside SVG |
158
+ | `presentation` | Adds `role="presentation"` |
159
+ | `false` | No accessibility attributes |
160
+
161
+ ## Hooks
66
162
 
67
163
  Available hooks: `preAdd`, `postAdd`, `preRemove`, `postRemove`, `preClear`, `postClear`, `preList`, `postList`
68
164
 
@@ -112,16 +208,6 @@ denji clear
112
208
  denji clear --yes # Skip confirmation
113
209
  ```
114
210
 
115
- ## Usage
116
-
117
- ```tsx
118
- import { Icons } from "./icons";
119
-
120
- function App() {
121
- return <Icons.Check className="size-4 text-green-500" />;
122
- }
123
- ```
124
-
125
211
  ## License
126
212
 
127
213
  MIT
@@ -1,117 +1,198 @@
1
1
  {
2
2
  "$schema": "https://json-schema.org/draft/2020-12/schema",
3
- "type": "object",
4
- "properties": {
5
- "$schema": {
6
- "default": "https://denji-docs.vercel.app/configuration_schema.json",
7
- "description": "The URL of the JSON Schema for this configuration file (e.g., './node_modules/denji/configuration_schema.json')",
8
- "type": "string"
9
- },
10
- "output": {
11
- "type": "string",
12
- "description": "The output file path for generated icon components (e.g., './src/icons.tsx')"
13
- },
14
- "framework": {
15
- "type": "string",
16
- "enum": [
17
- "react"
18
- ],
19
- "description": "The framework to generate icon components for"
20
- },
21
- "typescript": {
22
- "default": true,
23
- "description": "Whether to generate TypeScript code",
24
- "type": "boolean"
25
- },
26
- "a11y": {
27
- "description": "Accessibility strategy for SVG icons (hidden: aria-hidden, img: role=img with aria-label, title: <title> element, presentation: role=presentation, false: no a11y attrs)",
28
- "anyOf": [
29
- {
30
- "type": "string",
31
- "enum": [
32
- "hidden",
33
- "img",
34
- "title",
35
- "presentation"
36
- ]
37
- },
38
- {
39
- "type": "boolean",
40
- "const": false
41
- }
42
- ]
43
- },
44
- "hooks": {
45
- "description": "Hooks to run at various stages",
3
+ "allOf": [
4
+ {
46
5
  "type": "object",
47
6
  "properties": {
48
- "preAdd": {
49
- "type": "array",
50
- "items": {
51
- "type": "string"
52
- },
53
- "description": "Scripts to run before adding icons"
7
+ "$schema": {
8
+ "default": "https://denji-docs.vercel.app/configuration_schema.json",
9
+ "description": "The URL of the JSON Schema for this configuration file (e.g., './node_modules/denji/configuration_schema.json')",
10
+ "type": "string"
54
11
  },
55
- "postAdd": {
56
- "type": "array",
57
- "items": {
58
- "type": "string"
59
- },
60
- "description": "Scripts to run after adding icons"
12
+ "output": {
13
+ "type": "string",
14
+ "description": "The output file path for generated icon components (e.g., './src/icons.tsx')"
61
15
  },
62
- "preRemove": {
63
- "type": "array",
64
- "items": {
65
- "type": "string"
66
- },
67
- "description": "Scripts to run before removing icons"
16
+ "typescript": {
17
+ "default": true,
18
+ "description": "Whether to generate TypeScript code",
19
+ "type": "boolean"
68
20
  },
69
- "postRemove": {
70
- "type": "array",
71
- "items": {
72
- "type": "string"
73
- },
74
- "description": "Scripts to run after removing icons"
21
+ "a11y": {
22
+ "anyOf": [
23
+ {
24
+ "type": "string",
25
+ "enum": [
26
+ "hidden",
27
+ "img",
28
+ "title",
29
+ "presentation"
30
+ ]
31
+ },
32
+ {
33
+ "type": "boolean",
34
+ "const": false
35
+ }
36
+ ],
37
+ "description": "Accessibility strategy for SVG icons (hidden: aria-hidden, img: role=img with aria-label, title: <title> element, presentation: role=presentation, false: no a11y attrs)"
75
38
  },
76
- "preClear": {
77
- "type": "array",
78
- "items": {
79
- "type": "string"
80
- },
81
- "description": "Scripts to run before clearing all icons"
39
+ "trackSource": {
40
+ "default": true,
41
+ "description": "Add data-icon attribute with Iconify source name (enables update command, debugging, and identifying icon collections)",
42
+ "type": "boolean"
82
43
  },
83
- "postClear": {
84
- "type": "array",
85
- "items": {
86
- "type": "string"
44
+ "hooks": {
45
+ "description": "Hooks to run at various stages",
46
+ "type": "object",
47
+ "properties": {
48
+ "preAdd": {
49
+ "type": "array",
50
+ "items": {
51
+ "type": "string"
52
+ },
53
+ "description": "Scripts to run before adding icons"
54
+ },
55
+ "postAdd": {
56
+ "type": "array",
57
+ "items": {
58
+ "type": "string"
59
+ },
60
+ "description": "Scripts to run after adding icons"
61
+ },
62
+ "preRemove": {
63
+ "type": "array",
64
+ "items": {
65
+ "type": "string"
66
+ },
67
+ "description": "Scripts to run before removing icons"
68
+ },
69
+ "postRemove": {
70
+ "type": "array",
71
+ "items": {
72
+ "type": "string"
73
+ },
74
+ "description": "Scripts to run after removing icons"
75
+ },
76
+ "preClear": {
77
+ "type": "array",
78
+ "items": {
79
+ "type": "string"
80
+ },
81
+ "description": "Scripts to run before clearing all icons"
82
+ },
83
+ "postClear": {
84
+ "type": "array",
85
+ "items": {
86
+ "type": "string"
87
+ },
88
+ "description": "Scripts to run after clearing all icons"
89
+ },
90
+ "preList": {
91
+ "type": "array",
92
+ "items": {
93
+ "type": "string"
94
+ },
95
+ "description": "Scripts to run before listing icons"
96
+ },
97
+ "postList": {
98
+ "type": "array",
99
+ "items": {
100
+ "type": "string"
101
+ },
102
+ "description": "Scripts to run after listing icons"
103
+ }
104
+ },
105
+ "additionalProperties": false
106
+ }
107
+ },
108
+ "required": [
109
+ "$schema",
110
+ "output",
111
+ "typescript",
112
+ "trackSource"
113
+ ]
114
+ },
115
+ {
116
+ "oneOf": [
117
+ {
118
+ "type": "object",
119
+ "properties": {
120
+ "framework": {
121
+ "type": "string",
122
+ "const": "react",
123
+ "description": "React framework"
124
+ },
125
+ "react": {
126
+ "type": "object",
127
+ "properties": {
128
+ "forwardRef": {
129
+ "default": false,
130
+ "description": "Wrap icon components with forwardRef",
131
+ "type": "boolean"
132
+ }
133
+ },
134
+ "required": [
135
+ "forwardRef"
136
+ ],
137
+ "additionalProperties": false,
138
+ "description": "React-specific configuration options"
139
+ }
87
140
  },
88
- "description": "Scripts to run after clearing all icons"
141
+ "required": [
142
+ "framework"
143
+ ]
89
144
  },
90
- "preList": {
91
- "type": "array",
92
- "items": {
93
- "type": "string"
145
+ {
146
+ "type": "object",
147
+ "properties": {
148
+ "framework": {
149
+ "type": "string",
150
+ "const": "preact",
151
+ "description": "Preact framework"
152
+ },
153
+ "preact": {
154
+ "type": "object",
155
+ "properties": {
156
+ "forwardRef": {
157
+ "default": false,
158
+ "description": "Wrap icon components with forwardRef",
159
+ "type": "boolean"
160
+ }
161
+ },
162
+ "required": [
163
+ "forwardRef"
164
+ ],
165
+ "additionalProperties": false,
166
+ "description": "Preact-specific configuration options"
167
+ }
94
168
  },
95
- "description": "Scripts to run before listing icons"
169
+ "required": [
170
+ "framework"
171
+ ]
96
172
  },
97
- "postList": {
98
- "type": "array",
99
- "items": {
100
- "type": "string"
173
+ {
174
+ "type": "object",
175
+ "properties": {
176
+ "framework": {
177
+ "type": "string",
178
+ "const": "solid",
179
+ "description": "Solid framework"
180
+ },
181
+ "solid": {
182
+ "type": "object",
183
+ "properties": {},
184
+ "additionalProperties": false,
185
+ "description": "Solid-specific configuration options"
186
+ }
101
187
  },
102
- "description": "Scripts to run after listing icons"
188
+ "required": [
189
+ "framework"
190
+ ]
103
191
  }
104
- },
105
- "additionalProperties": false
192
+ ]
106
193
  }
107
- },
108
- "required": [
109
- "$schema",
110
- "output",
111
- "framework",
112
- "typescript"
113
194
  ],
114
- "additionalProperties": false,
115
195
  "title": "Denji Configuration Schema",
116
- "description": "Schema for Denji configuration file"
196
+ "description": "Schema for Denji configuration file",
197
+ "unevaluatedProperties": false
117
198
  }
package/dist/index.mjs CHANGED
@@ -1,7 +1,2 @@
1
1
  #!/usr/bin/env node
2
- import{Command as e}from"commander";import t from"node:path";import{cancel as n,confirm as r,intro as i,isCancel as a,outro as o,select as s,text as c}from"@clack/prompts";import{z as l}from"zod";import u from"node:fs/promises";import d from"picocolors";import{spawn as f}from"node:child_process";import{transform as p}from"@svgr/core";import{parseSync as m}from"oxc-parser";var h=`denji`,g=`A CLI tool to manage your SVG icons`,_=`0.1.2`;const v=l.enum([`react`]),y=l.enum([`hidden`,`img`,`title`,`presentation`]).or(l.literal(!1)),b=l.object({$schema:l.string().optional().default(`https://denji-docs.vercel.app/configuration_schema.json`).describe(`The URL of the JSON Schema for this configuration file (e.g., './node_modules/denji/configuration_schema.json')`),output:l.string().describe(`The output file path for generated icon components (e.g., './src/icons.tsx')`),framework:v.describe(`The framework to generate icon components for`),typescript:l.boolean().default(!0).describe(`Whether to generate TypeScript code`),a11y:y.optional().describe(`Accessibility strategy for SVG icons (hidden: aria-hidden, img: role=img with aria-label, title: <title> element, presentation: role=presentation, false: no a11y attrs)`),hooks:l.object({preAdd:l.array(l.string()).describe(`Scripts to run before adding icons`),postAdd:l.array(l.string()).describe(`Scripts to run after adding icons`),preRemove:l.array(l.string()).describe(`Scripts to run before removing icons`),postRemove:l.array(l.string()).describe(`Scripts to run after removing icons`),preClear:l.array(l.string()).describe(`Scripts to run before clearing all icons`),postClear:l.array(l.string()).describe(`Scripts to run after clearing all icons`),preList:l.array(l.string()).describe(`Scripts to run before listing icons`),postList:l.array(l.string()).describe(`Scripts to run after listing icons`)}).partial().optional().describe(`Hooks to run at various stages`)}).meta({title:`Denji Configuration Schema`,description:`Schema for Denji configuration file`}),x=`denji.json`;var S=class{constructor(e){this.value=e}isOk(){return!0}isErr(){return!1}},C=class{constructor(e){this.error=e}isOk(){return!1}isErr(){return!0}};async function w(e,t,n){try{return await u.writeFile(e,t,n),new S(null)}catch{return new C(`Failed to write file.`)}}async function T(e,t){try{return new S(await u.readFile(e,t))}catch{return new C(`Failed to read file.`)}}async function ee(e,t){try{return await u.mkdir(e,t),new S(null)}catch{return new C(`Failed to create directory.`)}}async function E(e,t){try{return await u.access(e,t),!0}catch{return!1}}async function D(e){let n=t.join(e,x);if(!await E(n))return new C(`${x} not found. Run "denji init" first.`);let r=await T(n,`utf-8`);if(r.isErr())return new C(`Failed to read ${x}`);try{let e=b.safeParse(JSON.parse(r.value));return e.success?new S(e.data):new C(`Invalid ${x}: ${e.error.message}`)}catch{return new C(`Invalid JSON in ${x}`)}}const O=console,k={error(e){O.error(d.red(e))},warn(e){O.warn(d.yellow(e))},info(e){O.info(d.cyan(e))},success(e){O.info(d.green(e))},break(){O.info(``)}};function A(e){k.error(`Something went wrong. Please check the error below for more details.`),k.error(`If the problem persists, please open an issue on GitHub.`),k.break(),k.error(e),k.break(),process.exit(1)}async function j(e,t){if(!e||e.length===0)return new S(null);for(let n of e){k.info(`Running: ${n}`);let e=await te(n,t);if(e.isErr())return e}return new S(null)}function te(e,t){return new Promise(n=>{let r=f(e,{cwd:t,shell:!0,stdio:`inherit`});r.on(`error`,e=>{n(new C(`Hook failed: ${e.message}`))}),r.on(`close`,t=>{n(t===0?new S(null):new C(`Hook "${e}" exited with code ${t}`))})})}const M=/^[a-z0-9]+(-[a-z0-9]+)*$/;function ne(e){let t=e.split(`:`);if(t.length!==2)return new C(`Invalid icon format "${e}". Expected "prefix:name" (e.g., mdi:home)`);let[n,r]=t;return M.test(n)?M.test(r)?new S({prefix:n,name:r}):new C(`Invalid name "${r}". Must match: lowercase letters, numbers, hyphens`):new C(`Invalid prefix "${n}". Must match: lowercase letters, numbers, hyphens`)}const re=/[-_]/,N=/^\d/;function P(e){let t=e.split(`:`)[1];if(!t)throw Error(`Invalid icon format: ${e}`);let n=t.split(re).filter(Boolean).map(e=>e.at(0)?.toUpperCase()+e.slice(1).toLowerCase()),r=n[0];for(;r&&N.test(r);)n.push(n.shift()??``),r=n[0];return n.join(``)}async function F(e){try{let{loadIcon:t,buildIcon:n}=await import(`iconify-icon`),r=await t(e);if(!r)return new C(`Icon "${e}" not found`);let i=n(r,{height:`1em`});return i?new S(`<svg xmlns="http://www.w3.org/2000/svg" ${Object.entries(i.attributes).map(([e,t])=>`${e}="${t}"`).join(` `)}>${i.body}</svg>`):new C(`Failed to build icon "${e}"`)}catch{return new C(`Icon "${e}" not found`)}}const I=/<svg[\s\S]*<\/svg>/,L=/<svg([^>]*)>/;function R(e){return e.replace(/([a-z])([A-Z])/g,`$1 $2`)}function z(e,t){switch(e){case`hidden`:return{"aria-hidden":`true`};case`img`:return{role:`img`,"aria-label":R(t)};case`presentation`:return{role:`presentation`};default:return{}}}async function B(e,t,n={}){let{a11y:r}=n,i=(await p(e,{plugins:[`@svgr/plugin-svgo`,`@svgr/plugin-jsx`],svgoConfig:{plugins:[`preset-default`,`convertStyleToAttrs`,`sortAttrs`,`mergePaths`]},jsxRuntime:`automatic`,typescript:!1,expandProps:`end`,svgProps:z(r,t),titleProp:r===`title`},{componentName:`Icon`})).match(I);if(!i)throw Error(`Failed to extract SVG from SVGR output`);let a=i[0];if(r===`title`){let e=R(t);a=a.replace(L,`<svg$1><title>${e}</title>`)}return`${t}: (props) => (${a})`}function V(e){let t=m(`icons.tsx`,e).program,n=[],r=0,i=0;for(let e of t.body)if(e.type===`ExportNamedDeclaration`&&e.declaration?.type===`VariableDeclaration`){for(let t of e.declaration.declarations)if(t.id?.type===`Identifier`&&t.id.name===`Icons`){let e=t.init;if(e?.type===`TSSatisfiesExpression`&&(e=e.expression),e?.type===`TSAsExpression`&&(e=e.expression),e?.type===`ObjectExpression`){r=e.start+1,i=e.end-1;for(let t of e.properties)t.type!==`SpreadElement`&&t.key?.type===`Identifier`&&n.push({name:t.key.name,start:t.start,end:t.end})}}}return{icons:n,objectStart:r,objectEnd:i}}function H(e){let{icons:t}=V(e);return t.map(e=>e.name)}function U(e,t,n){let{icons:r,objectStart:i,objectEnd:a}=V(e),o=r.findIndex(e=>e.name.localeCompare(t)>0);if(r.length===0)return`${e.slice(0,i)}\n ${n},\n${e.slice(a)}`;if(o===-1){let t=r.at(-1);if(!t)throw Error(`Failed to find last icon`);return`${e.slice(0,t.end)},\n ${n}${e.slice(t.end)}`}if(o===0){let t=r[0];if(!t)throw Error(`Failed to find first icon`);return`${e.slice(0,t.start)}${n},\n ${e.slice(t.start)}`}let s=r[o];if(!s)throw Error(`Failed to find target icon`);let c=s.start;return`${e.slice(0,c)}${n},\n ${e.slice(c)}`}function W(e,t,n){let{icons:r}=V(e),i=r.find(e=>e.name===t);return i?`${e.slice(0,i.start)}${n}${e.slice(i.end)}`:e}function G(e,t){let{icons:n}=V(e),r=n.findIndex(e=>e.name===t);if(r===-1)return e;let i=n[r];if(!i)return e;let a=r===n.length-1;if(n.length===1)return`${e.slice(0,i.start).trimEnd()}${e.slice(i.end).trimStart()}`;if(a){let t=n[r-1];return t?e.slice(t.end,i.start).indexOf(`,`)===-1?`${e.slice(0,t.end)}\n${e.slice(i.end).trimStart()}`:`${e.slice(0,t.end)}${e.slice(i.end)}`:e}let o=n[r+1];return o?`${e.slice(0,i.start)}${e.slice(o.start)}`:e}const K=`Operation cancelled.`;async function q(e,t=K){let i=await r(e);return a(i)&&(n(t),process.exit(0)),i}async function J(e,t=K){let r=await s(e);return a(r)&&(n(t),process.exit(0)),r}async function Y(e,t=K){let r=await c({...e,placeholder:e.placeholder??e.defaultValue});return a(r)&&(n(t),process.exit(0)),r}const ie=new e().name(`add`).description(`Add icons to your project`).argument(`<icons...>`,`Icon names (e.g., mdi:home lucide:check)`).option(`--name <name>`,`Custom component name (single icon only)`).option(`--a11y <strategy>`,`Accessibility strategy (overrides config)`).option(`-c, --cwd <cwd>`,`The working directory. Defaults to the current directory.`,process.cwd()).action(async(e,t)=>{i(`denji add`);let n=await ae(e,t);n.isErr()&&A(n.error),o(`Added ${e.length} icon(s)`)});async function ae(e,n){if(!await E(n.cwd))return new C(`Directory does not exist: ${n.cwd}`);if(n.name&&e.length>1)return new C(`--name can only be used with a single icon`);for(let t of e){let e=ne(t);if(e.isErr())return e}let r;if(n.a11y!==void 0){let e=n.a11y===`false`?!1:n.a11y,t=y.safeParse(e);if(!t.success)return new C(`Invalid a11y strategy: ${n.a11y}. Use: hidden, img, title, presentation, false`);r=t.data}let i=await D(n.cwd);if(i.isErr())return i;let a=i.value,o=await j(a.hooks?.preAdd,n.cwd);if(o.isErr())return o;let s=t.resolve(n.cwd,a.output);if(!await E(s))return new C(`Icons file not found: ${a.output}. Run "denji init" first.`);let c=await T(s,`utf-8`);if(c.isErr())return new C(`Failed to read icons file: ${a.output}`);let l=c.value,u=H(l),d=0;for(let t of e){let e=n.name??P(t);if(u.includes(e)&&!await q({message:`Icon "${e}" already exists. Overwrite?`,initialValue:!1})){k.info(`Skipped ${e}`);continue}let i=await F(t);if(i.isErr()){k.error(`Failed to fetch ${t}: ${i.error}`);continue}let o=await B(i.value,e,{a11y:r??a.a11y});u.includes(e)?(l=W(l,e,o),k.success(`Replaced ${e}`)):(l=U(l,e,o),u.push(e),k.success(`Added ${e}`)),d++}if(d>0){if((await w(s,l)).isErr())return new C(`Failed to write icons file: ${a.output}`);let e=await j(a.hooks?.postAdd,n.cwd);if(e.isErr())return e}return new S(null)}function X(e){return e.framework===`react`?e.typescript?`export type IconProps = React.ComponentProps<"svg">;
3
- export type Icon = (props: IconProps) => React.JSX.Element;
4
-
5
- export const Icons = {} as const satisfies Record<string, Icon>;
6
- `:`export const Icons = {};
7
- `:``}const oe=new e().name(`clear`).description(`Remove all icons from your project`).aliases([`clr`,`reset`]).option(`-c, --cwd <cwd>`,`The working directory. Defaults to the current directory.`,process.cwd()).option(`-y, --yes`,`Skip confirmation prompt`,!1).action(async e=>{i(`denji clear`);let t=await se(e);t.isErr()&&A(t.error),o(`All icons removed`)});async function se(e){if(!await E(e.cwd))return new C(`Directory does not exist: ${e.cwd}`);let r=await D(e.cwd);if(r.isErr())return r;let i=r.value,a=t.resolve(e.cwd,i.output);if(!await E(a))return new C(`Icons file not found: ${i.output}. Run "denji init" first.`);let o=await T(a,`utf-8`);if(o.isErr())return new C(`Failed to read icons file: ${i.output}`);let s=o.value,c=H(s);if(c.length===0)return k.info(`No icons to remove`),new S(null);e.yes||await q({message:`Remove all ${c.length} icon(s)?`,initialValue:!1})||n(K);let l=await j(i.hooks?.preClear,e.cwd);if(l.isErr())return l;if((await w(a,X(i))).isErr())return new C(`Failed to write icons file: ${i.output}`);let u=await j(i.hooks?.postClear,e.cwd);return u.isErr()?u:new S(null)}const ce=new e().name(`init`).description(`Initialize a new denji project`).option(`-c, --cwd <cwd>`,`The working directory. Defaults to the current directory.`,process.cwd()).option(`--output <file>`,`Output file path for icons`).option(`--framework <framework>`,`Framework to use`).option(`--typescript`,`Use TypeScript`,!0).option(`--no-typescript`,`Use JavaScript`).option(`--a11y <strategy>`,`Accessibility strategy for SVG icons`).action(async e=>{i(`denji init`);let t=await le(e);t.isErr()&&A(t.error),o(`Project initialized successfully!`)});async function le(e){if(!await E(e.cwd))return new C(`Directory does not exist: ${e.cwd}`);let n=t.join(e.cwd,x);if(await E(n))return new C(`${x} already exists in ${e.cwd}`);let r=await ue(e);if(r.isErr())return r;let i=r.value,a=de(i);if(a.isErr())return a;let o=t.resolve(e.cwd,i.output);if(await E(o))return new C(`Output file already exists: ${o}`);let s=t.dirname(o);return!await E(s)&&(await ee(s,{recursive:!0})).isErr()?new C(`Failed to create directory: ${s}`):(await w(n,JSON.stringify(i,null,2))).isErr()?new C(`Failed to write ${x}`):(k.success(`Created ${x}`),(await w(o,X(i))).isErr()?new C(`Failed to write ${i.output}`):(k.success(`Created ${i.output}`),new S(null)))}async function ue(e){let t=e.output??await Y({message:`Where should icons be created?`,defaultValue:`./src/icons.tsx`}),n=e.framework??await J({message:`Which framework are you using?`,options:[{value:`react`,label:`React`}],initialValue:`react`}),r=v.safeParse(n);if(!r.success)return new C(`Invalid framework: ${n}. Use: react`);let i=r.data,a=e.typescript??await q({message:`Use TypeScript?`,initialValue:!0}),o=e.a11y??await J({message:`Which accessibility strategy should be used?`,options:[{value:`hidden`,label:`aria-hidden`,hint:`Hide from screen readers (decorative icons)`},{value:`img`,label:`role="img"`,hint:`Meaningful icon with aria-label`},{value:`title`,label:`title`,hint:`Add <title> element inside SVG`},{value:`presentation`,label:`presentation`,hint:`role=presentation (older pattern)`},{value:!1,label:`false`,hint:`No accessibility attributes`}],initialValue:`hidden`}),s=y.safeParse(o);if(!s.success)return new C(`Invalid a11y strategy: ${o}. Use: hidden, img, title, presentation, false`);let c=s.data;return new S(b.parse({output:t,framework:i,typescript:a,a11y:c}))}function de(e){let n=t.extname(e.output);if(e.framework===`react`){if(e.typescript&&n!==`.tsx`)return new C(`Invalid extension "${n}" for React + TypeScript. Use ".tsx"`);if(!e.typescript&&n!==`.jsx`)return new C(`Invalid extension "${n}" for React + JavaScript. Use ".jsx"`)}return new S(null)}const Z=new e().name(`list`).description(`List all icons in your project`).option(`-c, --cwd <cwd>`,`The working directory. Defaults to the current directory.`,process.cwd()).option(`--json`,`Output icons as JSON`).action(async e=>{e.json||i(`denji list`);let t=await fe(e);t.isErr()&&A(t.error),e.json||o(`Done`)});async function fe(e){if(!await E(e.cwd))return new C(`Directory does not exist: ${e.cwd}`);let n=await D(e.cwd);if(n.isErr())return n;let r=n.value,i=t.resolve(e.cwd,r.output);if(!await E(i))return new C(`Icons file not found: ${r.output}. Run "denji init" first.`);let a=await T(i,`utf-8`);if(a.isErr())return new C(`Failed to read icons file: ${r.output}`);let o=await j(r.hooks?.preList,e.cwd);if(o.isErr())return o;let s=a.value,{icons:c}=V(s);if(e.json){let t={count:c.length,output:r.output,icons:c.map(e=>e.name)};console.info(JSON.stringify(t,null,2));let n=await j(r.hooks?.postList,e.cwd);return n.isErr()?n:new S(null)}if(c.length===0){k.info(`No icons found in ${r.output}`);let t=await j(r.hooks?.postList,e.cwd);return t.isErr()?t:new S(null)}k.success(`Found ${c.length} icon(s) in ${r.output}`),k.break(),k.info(`Icons:`);for(let e of c)k.info(` • ${e.name}`);let l=await j(r.hooks?.postList,e.cwd);return l.isErr()?l:new S(null)}const pe=new e().name(`remove`).description(`Remove icons from your project`).argument(`<icons...>`,`Icon component names (e.g., Home Check)`).aliases([`rm`,`delete`,`del`]).option(`-c, --cwd <cwd>`,`The working directory. Defaults to the current directory.`,process.cwd()).action(async(e,t)=>{i(`denji remove`);let n=await me(e,t);n.isErr()&&A(n.error),o(`Removed ${e.length} icon(s)`)});async function me(e,n){if(!await E(n.cwd))return new C(`Directory does not exist: ${n.cwd}`);let r=await D(n.cwd);if(r.isErr())return r;let i=r.value,a=t.resolve(n.cwd,i.output);if(!await E(a))return new C(`Icons file not found: ${i.output}. Run "denji init" first.`);let o=await T(a,`utf-8`);if(o.isErr())return new C(`Failed to read icons file: ${i.output}`);let s=o.value,c=H(s),l=[];for(let t of e)c.includes(t)||l.push(t);if(l.length>0)return new C(`Icon(s) not found: ${l.join(`, `)}`);let u=await j(i.hooks?.preRemove,n.cwd);if(u.isErr())return u;if(c.length-e.length===0){if((await w(a,X(i))).isErr())return new C(`Failed to write icons file: ${i.output}`);for(let t of e)k.success(`Removed ${t}`);let t=await j(i.hooks?.postRemove,n.cwd);return t.isErr()?t:new S(null)}for(let t of e)s=G(s,t),k.success(`Removed ${t}`);if((await w(a,s)).isErr())return new C(`Failed to write icons file: ${i.output}`);let d=await j(i.hooks?.postRemove,n.cwd);return d.isErr()?d:new S(null)}const Q=()=>process.exit(0);process.on(`SIGINT`,Q),process.on(`SIGTERM`,Q);const $=new e().name(h).description(g).version(_,`-v, --version`,`Display the version number.`);$.addCommand(ie),$.addCommand(oe),$.addCommand(ce),$.addCommand(Z),$.addCommand(pe),$.parse();export{};
2
+ import{Command as e}from"commander";import t from"node:path";import{cancel as n,confirm as r,intro as i,isCancel as a,multiselect as o,outro as s,select as c,text as l}from"@clack/prompts";import{_default as u,array as d,boolean as f,describe as p,discriminatedUnion as m,enum as h,intersection as g,literal as _,meta as v,object as y,optional as b,partial as x,string as S,union as ee}from"zod/mini";import C from"node:fs/promises";import{spawn as te}from"node:child_process";import w from"picocolors";import{parseSync as ne}from"oxc-parser";var re=`denji`,ie=`A CLI tool to manage your SVG icons`,ae=`0.3.0`;const T=y({forwardRef:u(f(),!1).check(p(`Wrap icon components with forwardRef`))}).check(p(`Preact-specific configuration options`)),E=y({forwardRef:u(f(),!1).check(p(`Wrap icon components with forwardRef`))}).check(p(`React-specific configuration options`)),D=y({}).check(p(`Solid-specific configuration options`)),O=ee([h([`hidden`,`img`,`title`,`presentation`]),_(!1)]).check(p(`Accessibility strategy for SVG icons (hidden: aria-hidden, img: role=img with aria-label, title: <title> element, presentation: role=presentation, false: no a11y attrs)`)),k=g(y({$schema:u(b(S()),`https://denji-docs.vercel.app/configuration_schema.json`).check(p(`The URL of the JSON Schema for this configuration file (e.g., './node_modules/denji/configuration_schema.json')`)),output:S().check(p(`The output file path for generated icon components (e.g., './src/icons.tsx')`)),typescript:u(f(),!0).check(p(`Whether to generate TypeScript code`)),a11y:b(O),trackSource:u(f(),!0).check(p(`Add data-icon attribute with Iconify source name (enables update command, debugging, and identifying icon collections)`)),hooks:b(x(y({preAdd:d(S()).check(p(`Scripts to run before adding icons`)),postAdd:d(S()).check(p(`Scripts to run after adding icons`)),preRemove:d(S()).check(p(`Scripts to run before removing icons`)),postRemove:d(S()).check(p(`Scripts to run after removing icons`)),preClear:d(S()).check(p(`Scripts to run before clearing all icons`)),postClear:d(S()).check(p(`Scripts to run after clearing all icons`)),preList:d(S()).check(p(`Scripts to run before listing icons`)),postList:d(S()).check(p(`Scripts to run after listing icons`))}))).check(p(`Hooks to run at various stages`))}),m(`framework`,[y({framework:_(`react`).check(p(`React framework`)),react:b(E)}),y({framework:_(`preact`).check(p(`Preact framework`)),preact:b(T)}),y({framework:_(`solid`).check(p(`Solid framework`)),solid:b(D)})])).check(v({title:`Denji Configuration Schema`})).check(p(`Schema for Denji configuration file`)),oe=h([`react`,`preact`,`solid`]),A=`denji.json`;async function se(e){switch(e){case`react`:{let{reactStrategy:e}=await import(`./strategy-bLzeYhwz.mjs`);return e}case`preact`:{let{preactStrategy:e}=await import(`./strategy-HhTswc-Y.mjs`);return e}case`solid`:{let{solidStrategy:e}=await import(`./strategy-C4DjhM4A.mjs`);return e}default:throw Error(`Unknown framework: ${e}`)}}var j=class{constructor(e){this.value=e}isOk(){return!0}isErr(){return!1}},M=class{constructor(e){this.error=e}isOk(){return!1}isErr(){return!0}};async function ce(e,t,n){try{return await C.writeFile(e,t,n),new j(null)}catch{return new M(`Failed to write file.`)}}async function N(e,t){try{return new j(await C.readFile(e,t))}catch{return new M(`Failed to read file.`)}}async function le(e,t){try{return await C.mkdir(e,t),new j(null)}catch{return new M(`Failed to create directory.`)}}async function P(e,t){try{return await C.access(e,t),!0}catch{return!1}}async function ue(e){let n=t.join(e,A);if(!await P(n))return new M(`${A} not found. Run "denji init" first.`);let r=await N(n,`utf-8`);if(r.isErr())return new M(`Failed to read ${A}`);try{let e=k.safeParse(JSON.parse(r.value));return e.success?new j(e.data):new M(`Invalid ${A}: ${e.error.message}`)}catch{return new M(`Invalid JSON in ${A}`)}}const F=console,I={error(e){F.error(w.red(e))},warn(e){F.warn(w.yellow(e))},info(e){F.info(w.cyan(e))},success(e){F.info(w.green(e))},break(){F.info(``)}};async function de(e,t){if(!e||e.length===0)return new j(null);for(let n of e){I.info(`Running: ${n}`);let e=await fe(n,t);if(e.isErr())return e}return new j(null)}function fe(e,t){return new Promise(n=>{let r=te(e,{cwd:t,shell:!0,stdio:`inherit`});r.on(`error`,e=>{n(new M(`Hook failed: ${e.message}`))}),r.on(`close`,t=>{n(t===0?new j(null):new M(`Hook "${e}" exited with code ${t}`))})})}const L=/^[a-z0-9]+(-[a-z0-9]+)*$/;function pe(e){let t=e.split(`:`);if(t.length!==2)return new M(`Invalid icon format "${e}". Expected "prefix:name" (e.g., mdi:home)`);let[n,r]=t;return L.test(n)?L.test(r)?new j({prefix:n,name:r}):new M(`Invalid name "${r}". Must match: lowercase letters, numbers, hyphens`):new M(`Invalid prefix "${n}". Must match: lowercase letters, numbers, hyphens`)}const me=/[-_]/,R=/^\d/;function z(e){let t=e.split(`:`)[1];if(!t)throw Error(`Invalid icon format: ${e}`);let n=t.split(me).filter(Boolean).map(e=>e.at(0)?.toUpperCase()+e.slice(1).toLowerCase()),r=n[0];for(;r&&R.test(r);)n.push(n.shift()??``),r=n[0];return n.join(``)}async function he(e){try{let{loadIcon:t,buildIcon:n}=await import(`iconify-icon`),r=await t(e);if(!r)return new M(`Icon "${e}" not found`);let i=n(r,{height:`1em`});return i?new j(`<svg xmlns="http://www.w3.org/2000/svg" ${Object.entries(i.attributes).map(([e,t])=>`${e}="${t}"`).join(` `)}>${i.body}</svg>`):new M(`Failed to build icon "${e}"`)}catch{return new M(`Icon "${e}" not found`)}}function B(e){let t=ne(`icons.tsx`,e).program,n=[],r=0,i=0;for(let e of t.body)if(e.type===`ExportNamedDeclaration`&&e.declaration?.type===`VariableDeclaration`){for(let t of e.declaration.declarations)if(t.id?.type===`Identifier`&&t.id.name===`Icons`){let e=t.init;if(e?.type===`TSSatisfiesExpression`&&(e=e.expression),e?.type===`TSAsExpression`&&(e=e.expression),e?.type===`ObjectExpression`){r=e.start+1,i=e.end-1;for(let t of e.properties)t.type!==`SpreadElement`&&t.key?.type===`Identifier`&&n.push({name:t.key.name,start:t.start,end:t.end})}}}return{icons:n,objectStart:r,objectEnd:i}}function ge(e){let{icons:t}=B(e);return t.map(e=>e.name)}function _e(e,t,n){let{icons:r,objectStart:i,objectEnd:a}=B(e),o=r.findIndex(e=>e.name.localeCompare(t)>0);if(r.length===0)return`${e.slice(0,i)}\n ${n},\n${e.slice(a)}`;if(o===-1){let t=r.at(-1);if(!t)throw Error(`Failed to find last icon`);return`${e.slice(0,t.end)},\n ${n}${e.slice(t.end)}`}if(o===0){let t=r[0];if(!t)throw Error(`Failed to find first icon`);return`${e.slice(0,t.start)}${n},\n ${e.slice(t.start)}`}let s=r[o];if(!s)throw Error(`Failed to find target icon`);let c=s.start;return`${e.slice(0,c)}${n},\n ${e.slice(c)}`}function ve(e,t,n){let{icons:r}=B(e),i=r.find(e=>e.name===t);return i?`${e.slice(0,i.start)}${n}${e.slice(i.end)}`:e}function ye(e,t){let{icons:n}=B(e),r=n.findIndex(e=>e.name===t);if(r===-1)return e;let i=n[r];if(!i)return e;let a=r===n.length-1;if(n.length===1)return`${e.slice(0,i.start).trimEnd()}${e.slice(i.end).trimStart()}`;if(a){let t=n[r-1];return t?e.slice(t.end,i.start).indexOf(`,`)===-1?`${e.slice(0,t.end)}\n${e.slice(i.end).trimStart()}`:`${e.slice(0,t.end)}${e.slice(i.end)}`:e}let o=n[r+1];return o?`${e.slice(0,i.start)}${e.slice(o.start)}`:e}const V=`Operation cancelled.`;async function H(e,t=V){let i=await r(e);return a(i)&&(n(t),process.exit(0)),i}async function be(e,t=V){let r=await c(e);return a(r)&&(n(t),process.exit(0)),r}async function xe(e,t=V){let r=await o(e);return a(r)&&(n(t),process.exit(0)),r}async function Se(e,t=V){let r=await l({...e,placeholder:e.placeholder??e.defaultValue});return a(r)&&(n(t),process.exit(0)),r}const U={access:P,readFile:N,writeFile:ce,mkdir:le},W={loadConfig:ue},G={runHooks:de},K={fetchIcon:he,validateIconName:pe,toComponentName:z,parseIconsFile:B,getExistingIconNames:ge,insertIconAlphabetically:_e,replaceIcon:ve,removeIcon:ye},q={confirm:H,select:be,text:Se,multiselect:xe},J=I,Y={createStrategy:se},Ce={fs:U,config:W,hooks:G,icons:K,logger:J},we={fs:U,config:W,hooks:G,icons:K,prompts:q,logger:J,frameworks:Y},Te={fs:U,config:W,hooks:G,icons:K,prompts:q,logger:J,frameworks:Y},Ee={fs:U,prompts:q,logger:J,frameworks:Y},De={fs:U,config:W,hooks:G,icons:K,prompts:q,logger:J,frameworks:Y};function X(e){I.error(`Something went wrong. Please check the error below for more details.`),I.error(`If the problem persists, please open an issue on GitHub.`),I.break(),I.error(e),I.break(),process.exit(1)}var Oe=class{constructor(e){this.deps=e}async run(e,n){let{fs:r,config:i,hooks:a,icons:o,prompts:s,logger:c,frameworks:l}=this.deps;if(!await r.access(n.cwd))return new M(`Directory does not exist: ${n.cwd}`);if(n.name&&e.length>1)return new M(`--name can only be used with a single icon`);for(let t of e){let e=o.validateIconName(t);if(e.isErr())return e}let u;if(n.a11y!==void 0){let e=n.a11y===`false`?!1:n.a11y,t=O.safeParse(e);if(!t.success)return new M(`Invalid a11y strategy: ${n.a11y}. Use: hidden, img, title, presentation, false`);u=t.data}let d=await i.loadConfig(n.cwd);if(d.isErr())return d;let f=d.value,p=await l.createStrategy(f.framework),m=await a.runHooks(f.hooks?.preAdd,n.cwd);if(m.isErr())return m;let h=t.resolve(n.cwd,f.output);if(!await r.access(h))return new M(`Icons file not found: ${f.output}. Run "denji init" first.`);let g=await r.readFile(h,`utf-8`);if(g.isErr())return new M(`Failed to read icons file: ${f.output}`);let _=g.value,v=o.getExistingIconNames(_),y=0,b=f[p.getConfigKey()]??{},x=p.isForwardRefEnabled(b);for(let t of e){let e=n.name??o.toComponentName(t);if(v.includes(e)&&!await s.confirm({message:`Icon "${e}" already exists. Overwrite?`,initialValue:!1})){c.info(`Skipped ${e}`);continue}let r=await o.fetchIcon(t);if(r.isErr()){c.error(`Failed to fetch ${t}: ${r.error}`);continue}let i=await p.transformSvg(r.value,{a11y:u??f.a11y,trackSource:f.trackSource??!0,iconName:t,componentName:e},b);if(x&&v.length===0&&y===0){let e=p.getForwardRefImportSource();_.includes(`import { forwardRef } from "${e}"`)||(_=`import { forwardRef } from "${e}";\n\n${_}`)}v.includes(e)?(_=o.replaceIcon(_,e,i),c.success(`Replaced ${e}`)):(_=o.insertIconAlphabetically(_,e,i),v.push(e),c.success(`Added ${e}`)),y++}if(y>0){if((await r.writeFile(h,_)).isErr())return new M(`Failed to write icons file: ${f.output}`);let e=await a.runHooks(f.hooks?.postAdd,n.cwd);if(e.isErr())return e}return new j(null)}};function ke(){return new Oe(De)}const Ae=new e().name(`add`).description(`Add icons to your project`).argument(`<icons...>`,`Icon names (e.g., mdi:home lucide:check)`).option(`--name <name>`,`Custom component name (single icon only)`).option(`--a11y <strategy>`,`Accessibility strategy (overrides config)`).option(`-c, --cwd <cwd>`,`The working directory. Defaults to the current directory.`,process.cwd()).action(async(e,t)=>{i(`denji add`);let n=await ke().run(e,t);n.isErr()&&X(n.error),s(`Added ${e.length} icon(s)`)});var je=class{constructor(e){this.deps=e}async run(e){let{fs:r,config:i,hooks:a,icons:o,prompts:s,logger:c,frameworks:l}=this.deps;if(!await r.access(e.cwd))return new M(`Directory does not exist: ${e.cwd}`);let u=await i.loadConfig(e.cwd);if(u.isErr())return u;let d=u.value,f=await l.createStrategy(d.framework),p=t.resolve(e.cwd,d.output);if(!await r.access(p))return new M(`Icons file not found: ${d.output}. Run "denji init" first.`);let m=await r.readFile(p,`utf-8`);if(m.isErr())return new M(`Failed to read icons file: ${d.output}`);let h=m.value,g=o.getExistingIconNames(h);if(g.length===0)return c.info(`No icons to remove`),new j(null);e.yes||await s.confirm({message:`Remove all ${g.length} icon(s)?`,initialValue:!1})||n(V);let _=await a.runHooks(d.hooks?.preClear,e.cwd);if(_.isErr())return _;let v=d[f.getConfigKey()]??{},y=f.getIconsTemplate({typescript:d.typescript,frameworkOptions:v});if((await r.writeFile(p,y)).isErr())return new M(`Failed to write icons file: ${d.output}`);let b=await a.runHooks(d.hooks?.postClear,e.cwd);return b.isErr()?b:new j(null)}};function Me(){return new je(we)}const Ne=new e().name(`clear`).description(`Remove all icons from your project`).aliases([`clr`,`reset`]).option(`-c, --cwd <cwd>`,`The working directory. Defaults to the current directory.`,process.cwd()).option(`-y, --yes`,`Skip confirmation prompt`,!1).action(async e=>{i(`denji clear`);let t=await Me().run(e);t.isErr()&&X(t.error),s(`All icons removed`)}),Pe=[{value:`react`,label:`React`},{value:`preact`,label:`Preact`},{value:`solid`,label:`Solid`}];function Fe(){return Pe.map(({value:e,label:t})=>({value:e,label:t}))}var Ie=class{constructor(e){this.deps=e}async run(e){let{fs:n,logger:r}=this.deps;if(!await n.access(e.cwd))return new M(`Directory does not exist: ${e.cwd}`);let i=t.join(e.cwd,A);if(await n.access(i))return new M(`${A} already exists in ${e.cwd}`);let a=await this.resolveConfig(e);if(a.isErr())return a;let{config:o,strategy:s}=a.value,c=this.validateExtension(o,s);if(c.isErr())return c;let l=t.resolve(e.cwd,o.output);if(await n.access(l))return new M(`Output file already exists: ${l}`);let u=t.dirname(l);if(!await n.access(u)&&(await n.mkdir(u,{recursive:!0})).isErr())return new M(`Failed to create directory: ${u}`);let d=JSON.stringify(o,null,2);if((await n.writeFile(i,d)).isErr())return new M(`Failed to write ${A}`);r.success(`Created ${A}`);let f=o[s.getConfigKey()]??{},p=s.getIconsTemplate({typescript:o.typescript,frameworkOptions:f});return(await n.writeFile(l,p)).isErr()?new M(`Failed to write ${o.output}`):(r.success(`Created ${o.output}`),new j(null))}async resolveConfig(e){let{prompts:t,frameworks:n}=this.deps,r=e.output??await t.text({message:`Where should icons be created?`,defaultValue:`./src/icons.tsx`}),i=e.framework??await t.select({message:`Which framework are you using?`,options:Fe(),initialValue:`react`}),a=oe.safeParse(i);if(!a.success)return new M(`Invalid framework: ${i}. Use: react, preact`);let o=a.data,s=await n.createStrategy(o),c=e.typescript??await t.confirm({message:`Use TypeScript?`,initialValue:!0}),l=e.a11y??await t.select({message:`Which accessibility strategy should be used?`,options:[{value:`hidden`,label:`aria-hidden`,hint:`Hide from screen readers (decorative icons)`},{value:`img`,label:`role="img"`,hint:`Meaningful icon with aria-label`},{value:`title`,label:`title`,hint:`Add <title> element inside SVG`},{value:`presentation`,label:`presentation`,hint:`role=presentation (older pattern)`},{value:!1,label:`false`,hint:`No accessibility attributes`}],initialValue:`hidden`}),u=O.safeParse(l);if(!u.success)return new M(`Invalid a11y strategy: ${l}. Use: hidden, img, title, presentation, false`);let d=u.data,f=e.trackSource??await t.confirm({message:`Track Iconify source names? (for update command, debugging, identifying collections)`,initialValue:!0}),p=await s.promptOptions({forwardRef:e.forwardRef});return new j({config:k.parse({output:r,framework:o,typescript:c,a11y:d,trackSource:f,[s.getConfigKey()]:p}),strategy:s})}validateExtension(e,n){let r=t.extname(e.output),i=e.typescript?n.fileExtensions.typescript:n.fileExtensions.javascript;if(r!==i){let t=e.typescript?`TypeScript`:`JavaScript`;return new M(`Invalid extension "${r}" for ${n.name} + ${t}. Use "${i}"`)}return new j(null)}};function Z(){return new Ie(Ee)}const Le=new e().name(`init`).description(`Initialize a new denji project`).option(`-c, --cwd <cwd>`,`The working directory. Defaults to the current directory.`,process.cwd()).option(`--output <file>`,`Output file path for icons`).option(`--framework <framework>`,`Framework to use`).option(`--typescript`,`Use TypeScript`).option(`--no-typescript`,`Use JavaScript`).option(`--a11y <strategy>`,`Accessibility strategy for SVG icons`).option(`--track-source`,`Track Iconify source names`).option(`--no-track-source`,`Don't track Iconify source names`).option(`--forward-ref`,`Use forwardRef for React icon components`).option(`--no-forward-ref`,`Don't use forwardRef for React icon components`).action(async e=>{i(`denji init`);let t=await Z().run(e);t.isErr()&&X(t.error),s(`Project initialized successfully!`)});var Re=class{constructor(e){this.deps=e}async run(e){let{fs:n,config:r,hooks:i,icons:a,logger:o}=this.deps;if(!await n.access(e.cwd))return new M(`Directory does not exist: ${e.cwd}`);let s=await r.loadConfig(e.cwd);if(s.isErr())return s;let c=s.value,l=t.resolve(e.cwd,c.output);if(!await n.access(l))return new M(`Icons file not found: ${c.output}. Run "denji init" first.`);let u=await n.readFile(l,`utf-8`);if(u.isErr())return new M(`Failed to read icons file: ${c.output}`);let d=await i.runHooks(c.hooks?.preList,e.cwd);if(d.isErr())return d;let f=u.value,{icons:p}=a.parseIconsFile(f);if(e.json){let t={count:p.length,output:c.output,icons:p.map(e=>e.name)};console.info(JSON.stringify(t,null,2));let n=await i.runHooks(c.hooks?.postList,e.cwd);return n.isErr()?n:new j(null)}if(p.length===0){o.info(`No icons found in ${c.output}`);let t=await i.runHooks(c.hooks?.postList,e.cwd);return t.isErr()?t:new j(null)}o.success(`Found ${p.length} icon(s) in ${c.output}`),o.break(),o.info(`Icons:`);for(let e of p)o.info(` • ${e.name}`);let m=await i.runHooks(c.hooks?.postList,e.cwd);return m.isErr()?m:new j(null)}};function ze(){return new Re(Ce)}const Be=new e().name(`list`).description(`List all icons in your project`).option(`-c, --cwd <cwd>`,`The working directory. Defaults to the current directory.`,process.cwd()).option(`--json`,`Output icons as JSON`).action(async e=>{e.json||i(`denji list`);let t=await ze().run(e);t.isErr()&&X(t.error),e.json||s(`Done`)});var Ve=class{constructor(e){this.deps=e}async run(e,n){let{fs:r,config:i,hooks:a,icons:o,logger:s,frameworks:c}=this.deps;if(!await r.access(n.cwd))return new M(`Directory does not exist: ${n.cwd}`);let l=await i.loadConfig(n.cwd);if(l.isErr())return l;let u=l.value,d=await c.createStrategy(u.framework),f=t.resolve(n.cwd,u.output);if(!await r.access(f))return new M(`Icons file not found: ${u.output}. Run "denji init" first.`);let p=await r.readFile(f,`utf-8`);if(p.isErr())return new M(`Failed to read icons file: ${u.output}`);let m=p.value,h=o.getExistingIconNames(m),g=[];for(let t of e)h.includes(t)||g.push(t);if(g.length>0)return new M(`Icon(s) not found: ${g.join(`, `)}`);let _=await a.runHooks(u.hooks?.preRemove,n.cwd);if(_.isErr())return _;if(h.length-e.length===0){let t=u[d.getConfigKey()]??{},i=d.getIconsTemplate({typescript:u.typescript,frameworkOptions:t});if((await r.writeFile(f,i)).isErr())return new M(`Failed to write icons file: ${u.output}`);for(let t of e)s.success(`Removed ${t}`);let o=await a.runHooks(u.hooks?.postRemove,n.cwd);return o.isErr()?o:new j(null)}for(let t of e)m=o.removeIcon(m,t),s.success(`Removed ${t}`);if((await r.writeFile(f,m)).isErr())return new M(`Failed to write icons file: ${u.output}`);let v=await a.runHooks(u.hooks?.postRemove,n.cwd);return v.isErr()?v:new j(null)}};function He(){return new Ve(Te)}const Ue=new e().name(`remove`).description(`Remove icons from your project`).argument(`<icons...>`,`Icon component names (e.g., Home Check)`).aliases([`rm`,`delete`,`del`]).option(`-c, --cwd <cwd>`,`The working directory. Defaults to the current directory.`,process.cwd()).action(async(e,t)=>{i(`denji remove`);let n=await He().run(e,t);n.isErr()&&X(n.error),s(`Removed ${e.length} icon(s)`)}),Q=()=>process.exit(0);process.on(`SIGINT`,Q),process.on(`SIGTERM`,Q);const $=new e().name(re).description(ie).version(ae,`-v, --version`,`Display the version number.`);$.addCommand(Ae),$.addCommand(Ne),$.addCommand(Le),$.addCommand(Be),$.addCommand(Ue),$.parse();export{T as i,D as n,E as r,H as t};
@@ -0,0 +1,13 @@
1
+ import{n as e}from"./index.mjs";import{a as t,i as n,o as r,r as i,s as a,t as o}from"./svg-Dnt5ibPB.mjs";a.loadTemplate(`@solid/icons`,`<% if (it.typescript) { -%>
2
+ import type { ComponentProps, JSX } from "solid-js";
3
+
4
+ export type IconProps = ComponentProps<"svg">;
5
+ export type Icon = (props: IconProps) => JSX.Element;
6
+
7
+ export const Icons = {} as const satisfies Record<string, Icon>;
8
+
9
+ export type IconName = keyof typeof Icons;
10
+ <% } else { -%>
11
+ export const Icons = {};
12
+ <% } -%>
13
+ `);function s(e){return a.render(`@solid/icons`,{typescript:e.typescript})}function c(e,a){let{a11y:s,trackSource:c,iconName:l,componentName:u}=a,d=t(e),f=i(s,u);if(c&&l&&(f[`data-icon`]=l),Object.keys(f).length>0){let e=Object.entries(f).map(([e,t])=>`${e}="${t}"`).join(` `);d=d.replace(o,`<svg$1 ${e}>`)}if(s===`title`){let e=r(u);d=n(d,e)}return d=d.replace(o,`<svg$1 {...props}>`),Promise.resolve(`${u}: (props) => (${d})`)}const l={name:`solid`,fileExtensions:{typescript:`.tsx`,javascript:`.jsx`},optionsSchema:e,supportsRef:!0,getIconsTemplate:s,getImports(e){return[]},getForwardRefImportSource(){return`solid-js`},isForwardRefEnabled(e){return!1},promptOptions(){return Promise.resolve({})},getConfigKey(){return`solid`},transformSvg:c};export{l as solidStrategy};
@@ -0,0 +1,17 @@
1
+ import{i as e,t}from"./index.mjs";import{n,o as r,r as i,s as a,t as o}from"./svg-Dnt5ibPB.mjs";import{transform as s}from"@svgr/core";a.loadTemplate(`@preact/icons`,`<% if (it.typescript) { -%>
2
+ import type * as preact from "preact/compat";
3
+
4
+ export type IconProps = preact.ComponentProps<"svg">;
5
+ <% if (it.forwardRef) { -%>
6
+ export type Icon = preact.ForwardRefExoticComponent<IconProps & preact.RefAttributes<SVGSVGElement>>;
7
+ <% } else { -%>
8
+ export type Icon = (props: IconProps) => preact.JSX.Element;
9
+ <% } -%>
10
+
11
+ export const Icons = {} as const satisfies Record<string, Icon>;
12
+
13
+ export type IconName = keyof typeof Icons;
14
+ <% } else { -%>
15
+ export const Icons = {};
16
+ <% } -%>
17
+ `);function c(e){let t=e.frameworkOptions?.forwardRef??!1;return a.render(`@preact/icons`,{typescript:e.typescript,forwardRef:t})}async function l(e,t,a){let{a11y:c,trackSource:l,iconName:u,componentName:d}=t,f=a?.forwardRef??!1,p=i(c,d);l&&u&&(p[`data-icon`]=u);let m=(await s(e,{plugins:[`@svgr/plugin-svgo`,`@svgr/plugin-jsx`],svgoConfig:{plugins:[`preset-default`,`convertStyleToAttrs`,`sortAttrs`,`mergePaths`]},jsxRuntime:`automatic`,typescript:!1,expandProps:`end`,svgProps:p,titleProp:c===`title`},{componentName:`Icon`})).match(n);if(!m)throw Error(`Failed to extract SVG from SVGR output`);let h=m[0];if(c===`title`){let e=r(d);h=h.replace(o,`<svg$1><title>${e}</title>`)}return f?(h=h.replace(o,`<svg$1 ref={ref}>`),`${d}: forwardRef<SVGSVGElement, IconProps>((props, ref) => (${h}))`):`${d}: (props) => (${h})`}const u={name:`preact`,fileExtensions:{typescript:`.tsx`,javascript:`.jsx`},optionsSchema:e,supportsRef:!0,getIconsTemplate:c,getImports(e){return e?.forwardRef?[`import { forwardRef } from "preact/compat";`]:[]},getForwardRefImportSource(){return`preact/compat`},isForwardRefEnabled(e){return e?.forwardRef===!0},async promptOptions(e){return{forwardRef:e.forwardRef??await t({message:`Use forwardRef for icon components?`,initialValue:!1})}},getConfigKey(){return`preact`},transformSvg:l};export{u as preactStrategy};
@@ -0,0 +1,15 @@
1
+ import{r as e,t}from"./index.mjs";import{n,o as r,r as i,s as a,t as o}from"./svg-Dnt5ibPB.mjs";import{transform as s}from"@svgr/core";a.loadTemplate(`@react/icons`,`<% if (it.typescript) { -%>
2
+ export type IconProps = React.ComponentProps<"svg">;
3
+ <% if (it.forwardRef) { -%>
4
+ export type Icon = React.ForwardRefExoticComponent<IconProps & React.ComponentRef<"svg">>;
5
+ <% } else { -%>
6
+ export type Icon = (props: IconProps) => React.JSX.Element;
7
+ <% } -%>
8
+
9
+ export const Icons = {} as const satisfies Record<string, Icon>;
10
+
11
+ export type IconName = keyof typeof Icons;
12
+ <% } else { -%>
13
+ export const Icons = {};
14
+ <% } -%>
15
+ `);function c(e){let t=e.frameworkOptions?.forwardRef??!1;return a.render(`@react/icons`,{typescript:e.typescript,forwardRef:t})}async function l(e,t,a){let{a11y:c,trackSource:l,iconName:u,componentName:d}=t,f=a?.forwardRef??!1,p=i(c,d);l&&u&&(p[`data-icon`]=u);let m=(await s(e,{plugins:[`@svgr/plugin-svgo`,`@svgr/plugin-jsx`],svgoConfig:{plugins:[`preset-default`,`convertStyleToAttrs`,`sortAttrs`,`mergePaths`]},jsxRuntime:`automatic`,typescript:!1,expandProps:`end`,svgProps:p,titleProp:c===`title`},{componentName:`Icon`})).match(n);if(!m)throw Error(`Failed to extract SVG from SVGR output`);let h=m[0];if(c===`title`){let e=r(d);h=h.replace(o,`<svg$1><title>${e}</title>`)}return f?(h=h.replace(o,`<svg$1 ref={ref}>`),`${d}: forwardRef<SVGSVGElement, IconProps>((props, ref) => (${h}))`):`${d}: (props) => (${h})`}const u={name:`react`,fileExtensions:{typescript:`.tsx`,javascript:`.jsx`},optionsSchema:e,supportsRef:!0,getIconsTemplate:c,getImports(e){return e?.forwardRef?[`import { forwardRef } from "react";`]:[]},getForwardRefImportSource(){return`react`},isForwardRefEnabled(e){return e?.forwardRef===!0},async promptOptions(e){return{forwardRef:e.forwardRef??await t({message:`Use forwardRef for icon components?`,initialValue:!1})}},getConfigKey(){return`react`},transformSvg:l};export{u as reactStrategy};
@@ -0,0 +1 @@
1
+ import{Eta as e}from"eta";import{optimize as t}from"svgo";const n=new e({cache:!0,autoEscape:!1,autoTrim:!1}),r={plugins:[`preset-default`,`convertStyleToAttrs`,`sortAttrs`,`mergePaths`]};function i(e){return t(e,r).data}function a(e){return e.replace(/([a-z])([A-Z])/g,`$1 $2`)}function o(e,t){switch(e){case`hidden`:return{"aria-hidden":`true`};case`img`:return{role:`img`,"aria-label":a(t)};case`presentation`:return{role:`presentation`};default:return{}}}const s=/<svg[\s\S]*<\/svg>/,c=/<svg([^>]*)>/;function l(e,t){return e.replace(c,`<svg$1><title>${t}</title>`)}export{i as a,l as i,s as n,a as o,o as r,n as s,c as t};
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "description": "A CLI tool to manage your SVG icons",
4
4
  "type": "module",
5
5
  "private": false,
6
- "version": "0.1.2",
6
+ "version": "0.3.0",
7
7
  "author": {
8
8
  "name": "Fellipe Utaka",
9
9
  "email": "fellipeutaka@gmail.com",
@@ -12,7 +12,8 @@
12
12
  "license": "MIT",
13
13
  "repository": {
14
14
  "type": "git",
15
- "url": "git+https://github.com/fellipeutaka/denji.git"
15
+ "url": "git+https://github.com/fellipeutaka/denji.git",
16
+ "directory": "apps/cli"
16
17
  },
17
18
  "main": "./dist/index.mjs",
18
19
  "module": "./dist/index.mjs",
@@ -37,18 +38,20 @@
37
38
  "lint:doctor": "ultracite doctor"
38
39
  },
39
40
  "dependencies": {
40
- "@clack/prompts": "^0.11.0",
41
+ "@clack/prompts": "^1.0.0",
41
42
  "@svgr/core": "^8.1.0",
42
43
  "@svgr/plugin-jsx": "^8.1.0",
43
44
  "@svgr/plugin-svgo": "^8.1.0",
44
- "commander": "^14.0.2",
45
+ "commander": "^14.0.3",
46
+ "eta": "^4.5.0",
45
47
  "iconify-icon": "^3.0.2",
46
- "oxc-parser": "^0.108.0",
48
+ "oxc-parser": "^0.112.0",
47
49
  "picocolors": "^1.1.1",
48
- "zod": "^4.3.5"
50
+ "svgo": "^4.0.0",
51
+ "zod": "^4.3.6"
49
52
  },
50
53
  "devDependencies": {
51
54
  "@iconify/types": "^2.0.0",
52
- "tsdown": "^0.20.0-beta.1"
55
+ "tsdown": "^0.20.1"
53
56
  }
54
57
  }