@rolandjlevy/css-ghost 1.1.2 → 1.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/README.md +32 -273
  2. package/package.json +1 -1
  3. package/src/parser.js +3 -3
package/README.md CHANGED
@@ -6,26 +6,11 @@ Find and remove unused CSS selectors and orphaned CSS custom properties — sele
6
6
 
7
7
  Scans one or more directories for style files, extracts every class selector defined, then cross-references them against usages in your component files. Anything defined but never used is reported as an orphan — with its line number and the actual CSS printed inline.
8
8
 
9
- **Supported style files:**
9
+ **Supported style files:** `.css`, `.module.css`, `.scss`, `.sass`, `.less`, `.js`/`.ts` using `StyleSheet.create({})` / `makeStyles({})` (MUI), `.css.ts`/`.css.js` using `style()` or `styleVariants()` (vanilla-extract)
10
10
 
11
- - `.css`, `.module.css`, `.scss`, `.sass`, `.less`
12
- - `.js` / `.ts` files using `StyleSheet.create({})`, `makeStyles({})` (MUI), or named style objects
13
- - `.css.ts` / `.css.js` files using `style()` or `styleVariants()` (vanilla-extract)
11
+ **Detected usage patterns:** `className="foo"`, `className={styles.foo}`, `styles['foo']`, ternary patterns, `cx()` / `clsx()` / `classnames()` (strings, objects and arrays), `classes.foo` (MUI), vanilla-extract named imports
14
12
 
15
- **Detected usage patterns:**
16
-
17
- - `className="foo"`
18
- - `className={styles.foo}`
19
- - `styles['foo']`
20
- - `styles[condition ? 'foo' : 'bar']` — both branches extracted
21
- - `cx('foo')`, `clsx('foo')`, `classnames('foo')`
22
- - `cx({ active: isOn, disabled: true })` — object keys extracted
23
- - `cx(['foo', condition && 'bar'])` — array string literals extracted
24
- - `classes.foo` — MUI `makeStyles` / `useStyles` convention
25
- - `import { foo } from './styles.css'` — vanilla-extract named imports
26
-
27
- **Dynamic class detection:**
28
- Patterns where the class name is truly unknowable at parse time — `styles[variable]` or template literals containing `${}` — are flagged with a ⚠️ warning. All statically resolvable patterns (string literals, ternaries, object keys) are analysed without warnings.
13
+ **Dynamic class detection:** Patterns where the class name can't be resolved statically — `styles[variable]` or template literals with `${}` — are flagged with a ⚠️ warning instead of being reported as orphans.
29
14
 
30
15
  ## How is this different from PurgeCSS?
31
16
 
@@ -37,16 +22,16 @@ css-ghost is a **source code auditor** — it finds dead selectors in your actua
37
22
  npx @rolandjlevy/css-ghost ./src
38
23
  ```
39
24
 
40
- | | PurgeCSS | css-ghost |
41
- |---|---|---|
42
- | Works on source files | ❌ Build output only | ✅ |
43
- | Zero config, no build step | ❌ Requires webpack / PostCSS | ✅ |
44
- | CSS custom property orphans | ❌ | ✅ |
45
- | Git history per orphan | ❌ | ✅ |
46
- | Pre-commit hook | ❌ | ✅ |
47
- | Deletes dead code for you | ❌ | ✅ |
48
- | MUI / vanilla-extract aware | ❌ | ✅ |
49
- | Safe with dynamic classes | ❌ Silently removes them | ✅ Flags with ⚠️ |
25
+ | | PurgeCSS | css-ghost |
26
+ | --------------------------- | ----------------------------- | ---------------- |
27
+ | Works on source files | ❌ Build output only | ✅ |
28
+ | Zero config, no build step | ❌ Requires webpack / PostCSS | ✅ |
29
+ | CSS custom property orphans | ❌ | ✅ |
30
+ | Git history per orphan | ❌ | ✅ |
31
+ | Pre-commit hook | ❌ | ✅ |
32
+ | Deletes dead code for you | ❌ | ✅ |
33
+ | MUI / vanilla-extract aware | ❌ | ✅ |
34
+ | Safe with dynamic classes | ❌ Silently removes them | ✅ Flags with ⚠️ |
50
35
 
51
36
  The two tools are complementary: PurgeCSS for trimming framework output at build time; css-ghost for auditing your own hand-written CSS throughout development.
52
37
 
@@ -54,19 +39,14 @@ The two tools are complementary: PurgeCSS for trimming framework output at build
54
39
 
55
40
  ## To enable AI features
56
41
 
57
- css-ghost can generate a plain English summary of your orphaned selectors using a free [OpenRouter](https://openrouter.ai) API key. Run the setup wizard once:
42
+ Run the setup wizard once to save your free [OpenRouter](https://openrouter.ai) API key ([openrouter.ai/keys](https://openrouter.ai/keys)), then pass `--ai` to any scan:
58
43
 
59
44
  ```bash
60
45
  css-ghost --setup
61
- ```
62
-
63
- It will prompt you to paste your key (free at [openrouter.ai/keys](https://openrouter.ai/keys)) and save it to `~/.css-ghost`. Then pass `--ai` to any scan:
64
-
65
- ```bash
66
46
  css-ghost ./src --ai
67
47
  ```
68
48
 
69
- The AI summary appears in both the terminal output and the markdown report. The core scan always runs without a key — `--ai` is entirely optional.
49
+ The AI summary appears in both the terminal output and the markdown report. The core scan always runs without a key.
70
50
 
71
51
  ## Install
72
52
 
@@ -102,44 +82,18 @@ css-ghost [directory] [directory2] ... [options]
102
82
 
103
83
  ## CSS variable orphan detection
104
84
 
105
- css-ghost also catches **orphaned CSS custom properties** — variables defined with `--my-token: value` but never consumed anywhere via `var(--my-token)`.
106
-
107
- Usages are detected across all file types: CSS, SCSS, JS, TS, JSX, TSX, Vue, Svelte, HTML. A variable defined in `variables.css` and used in a React component is correctly seen as _used_.
108
-
109
- ```
110
- 🔮 --colour-danger-hover (line 8)
111
- --colour-danger-hover: #dc2626;
112
-
113
- 🔮 --spacing-xl (line 26)
114
- --spacing-xl: 64px;
115
- ```
116
-
117
- Orphaned variables appear under a separate **🔮 Orphaned CSS variables** section in both the terminal output and the markdown report.
118
-
119
- ---
85
+ css-ghost also catches **orphaned CSS custom properties** — variables defined with `--my-token: value` but never consumed via `var(--my-token)` anywhere in the codebase. Usages are detected across all file types: CSS, SCSS, JS, TS, JSX, TSX, Vue, Svelte, HTML. Orphaned variables appear under a **🔮 Orphaned CSS variables** section in both the terminal output and the markdown report.
120
86
 
121
87
  ## Ghost trail — git history for orphans
122
88
 
123
- Pass `--ghost-trail` to fetch the last git commit that changed each orphan's occurrence count in the codebase. For class-name orphans the search is limited to component file types (`.tsx`, `.jsx`, `.ts`, `.js`, etc.) so the result reflects _usage_ history rather than the style-file definition.
89
+ Pass `--ghost-trail` to fetch the last git commit that changed each orphan's occurrence count:
124
90
 
125
91
  ```bash
126
92
  css-ghost ./src --ghost-trail
127
93
  ```
128
94
 
129
- ```
130
- 👻 .iconRight (line 28)
131
- 🕵️ b5c6efb · Roland Levy · 3 weeks ago — "Refactor Button component"
132
- .iconRight {
133
- margin-left: 6px;
134
- }
135
- ```
136
-
137
- For CSS variables the search looks for `var(--name` across all file types.
138
-
139
95
  > **Note:** ghost trail runs one `git log -S` per orphan in parallel. On large histories with many orphans it may take a few seconds.
140
96
 
141
- ---
142
-
143
97
  ## Pre-commit hook
144
98
 
145
99
  Install a hook that blocks commits when orphaned selectors are present:
@@ -148,255 +102,60 @@ Install a hook that blocks commits when orphaned selectors are present:
148
102
  css-ghost --install-hook ./src
149
103
  ```
150
104
 
151
- This writes `.git/hooks/pre-commit` (executable, `chmod 755`) containing:
152
-
153
- ```sh
154
- #!/bin/sh
155
- echo "👻 css-ghost — checking for orphaned CSS selectors..."
156
- npx --yes @rolandjlevy/css-ghost ./src --strict --output /tmp/css-ghost-hook-report.md
157
-
158
- if [ $? -ne 0 ]; then
159
- echo "❌ Commit blocked: orphaned CSS selectors found"
160
- echo " Run 'css-ghost ./src --delete' to remove them"
161
- echo " Or skip: git commit --no-verify"
162
- exit 1
163
- fi
164
- ```
165
-
166
- Pass multiple directories to the hook installer the same way you would to a normal scan:
167
-
168
- ```bash
169
- css-ghost --install-hook ./src ./components ./pages
170
- ```
171
-
172
- To uninstall: `rm .git/hooks/pre-commit`
105
+ This writes `.git/hooks/pre-commit` (executable, `chmod 755`). Pass multiple directories the same way you would to a normal scan. To uninstall: `rm .git/hooks/pre-commit`
173
106
 
174
107
  ### `--strict` flag
175
108
 
176
- `--strict` makes css-ghost exit with code 1 when any orphans are found — the mechanism the hook depends on, but also useful in CI workflows independently:
109
+ Exit with code 1 when any orphans are found — the mechanism the hook depends on, also useful in CI:
177
110
 
178
111
  ```bash
179
- # In CI: fail the build if orphans exist
180
112
  css-ghost ./src --strict
181
113
  ```
182
114
 
183
- ---
184
-
185
115
  ## Interactive folder picker
186
116
 
187
- Run `css-ghost` with no arguments and an arrow-key picker opens in your terminal:
188
-
189
- ```
190
- 📂 Directory picker — navigate into a folder, then press [a] to add it
191
-
192
- 📍 /home/user/my-project
193
-
194
- 📁 components
195
- ▶ 📁 src
196
- 📁 styles
197
-
198
- ✅ Added to scan:
199
- /home/user/my-project/src
200
-
201
- [↑↓/jk] navigate [↵/→] enter [←] parent [a] add current dir [d] done [^C] quit
202
- ```
203
-
204
- | Key | Action |
205
- | ---------------------- | --------------------------------------- |
206
- | `↑` / `↓` or `k` / `j` | Move up and down the folder list |
207
- | `↵` or `→` | Enter the highlighted folder |
208
- | `←` or Backspace | Go up to the parent folder |
209
- | `a` | Add the current folder to the scan list |
210
- | `d` | Done — start the scan |
211
- | `Ctrl+C` | Quit without scanning |
212
-
213
- You can add **multiple folders** before pressing `d` — just navigate to each one and press `a`. If you press `d` without adding anything, the folder you're currently browsing is used.
117
+ Run `css-ghost` with no arguments to open an arrow-key picker. Navigate with `↑↓`, enter with `↵`, press `a` to add the current folder to the scan list, `d` to start the scan. Add multiple folders before pressing `d`.
214
118
 
215
119
  ## Deleting orphans
216
120
 
217
- Pass `--delete` after a scan to remove every flagged selector directly from its source file:
218
-
219
121
  ```bash
220
- css-ghost ./src --delete
122
+ css-ghost ./src --delete # prints report, then asks for confirmation
123
+ css-ghost ./src --delete --yes # skip the prompt (CI / scripts)
221
124
  ```
222
125
 
223
- css-ghost prints the full report first, then lists what will be deleted and asks for confirmation before touching anything. Pass `--yes` to skip the prompt (useful in CI or scripts):
224
-
225
- ```bash
226
- css-ghost ./src --delete --yes
227
- ```
228
-
229
- Works across all supported file types — `.css`, `.scss`, `.less`, MUI `makeStyles`, and vanilla-extract exports.
230
-
231
126
  > **Tip:** commit or stash your work before running `--delete`. There is no undo.
232
127
 
233
128
  ## Examples
234
129
 
235
130
  ```bash
236
- # Interactive folder picker (no args)
237
- css-ghost
238
-
239
- # Scan your src folder
240
- css-ghost ./src
241
-
242
- # Show git history for each orphan
243
- css-ghost ./src --ghost-trail
244
-
245
- # Fail with exit code 1 when orphans exist (CI)
246
- css-ghost ./src --strict
247
-
248
- # Install a pre-commit hook that scans ./src
249
- css-ghost --install-hook ./src
250
-
251
- # Scan and delete all orphans (with confirmation prompt)
252
- css-ghost ./src --delete
253
-
254
- # Scan and delete without prompting
255
- css-ghost ./src --delete --yes
256
-
257
- # Scan multiple directories
258
- css-ghost ./src ./components ./pages
259
-
260
- # With AI summary
261
- css-ghost ./src --ai
262
-
263
- # Write report to a custom path
264
- css-ghost ./src --output ./reports/css-audit.md
265
- ```
266
-
267
- ## CLI output
268
-
269
- Running `css-ghost ./src` prints a live summary to your terminal:
270
-
131
+ css-ghost # interactive folder picker
132
+ css-ghost ./src # scan a folder
133
+ css-ghost ./src --ghost-trail # show git history per orphan
134
+ css-ghost ./src --strict # CI: exit 1 on any orphan
135
+ css-ghost --install-hook ./src # install pre-commit hook
136
+ css-ghost ./src --delete # remove orphans (with prompt)
137
+ css-ghost ./src --delete --yes # remove without prompting
138
+ css-ghost ./src ./components ./pages # scan multiple directories
139
+ css-ghost ./src --ai # with AI summary
140
+ css-ghost ./src --output ./reports/audit.md
271
141
  ```
272
- ✅ Report written to orphan-report.md
273
- 📊 3 orphaned selectors across 1 file
274
-
275
- 📄 src/components/Button/Button.module.css
276
- 👻 .iconLeft (line 4)
277
- .iconLeft {
278
- margin-right: 6px;
279
- flex-shrink: 0;
280
- }
281
-
282
- 👻 .loadingSpinner (line 12)
283
- .loadingSpinner {
284
- animation: spin 0.8s linear infinite;
285
- }
286
-
287
- 👻 .disabled (line 18)
288
- .disabled {
289
- opacity: 0.4;
290
- pointer-events: none;
291
- }
292
-
293
- 📁 1 style file scanned
294
- 💾 Potential savings: 3 selectors removed
295
- ```
296
-
297
- Each orphan shows:
298
-
299
- - the selector name and the **exact line number** where it's defined
300
- - the **full CSS rule** so you can see what it does before deciding to delete it
301
-
302
- ## Markdown report
303
-
304
- A full report is also written to `orphan-report.md` (or your `--output` path):
305
-
306
- ---
307
-
308
- ### 👻 Orphaned selectors
309
-
310
- #### `.iconLeft` — line 4
311
-
312
- ```css
313
- .iconLeft {
314
- margin-right: 6px;
315
- flex-shrink: 0;
316
- }
317
- ```
318
-
319
- #### `.loadingSpinner` — line 12
320
-
321
- ```css
322
- .loadingSpinner {
323
- animation: spin 0.8s linear infinite;
324
- }
325
- ```
326
-
327
- ---
328
142
 
329
143
  ## Using with npm scripts
330
144
 
331
- If you're working inside the project itself, add a `scan` script to `package.json`:
332
-
333
145
  ```json
334
146
  "scripts": {
335
147
  "scan": "css-ghost ./src"
336
148
  }
337
149
  ```
338
150
 
339
- Then run:
340
-
341
151
  ```bash
342
- npm run scan
343
-
344
- # Pass extra args with --
345
152
  npm run scan -- --output ./reports/audit.md
346
153
  ```
347
154
 
348
- ## Troubleshooting
349
-
350
- ### `css-ghost: command not found` after global install
351
-
352
- Your npm global bin directory may not be on your PATH. Run:
353
-
354
- ```bash
355
- npm bin -g
356
- ```
357
-
358
- Then add that path to your shell config (`~/.zshrc`, `~/.bashrc`, etc.):
359
-
360
- ```bash
361
- export PATH="$(npm bin -g):$PATH"
362
- ```
363
-
364
- Reload with `source ~/.zshrc`. This is a one-time fix — it won't be needed again after that.
365
-
366
- > This is common when Node was installed via the official installer rather than nvm or Homebrew.
367
-
368
155
  ## Dependencies
369
156
 
370
157
  One runtime dependency: [`picocolors`](https://github.com/alexeyraspopov/picocolors) for terminal colours. Everything else uses Node.js built-ins (`fs`, `path`, `readline`).
371
158
 
372
- ## Origin — built with OpenRouter free models via Claude Code
373
-
374
- This tool was created as a test project to explore [using OpenRouter's free model tier with Claude Code](https://www.mindstudio.ai/blog/open-router-free-models-claude-code-cost-reduction) as a way to cut AI development costs.
375
-
376
- Claude Code normally calls Anthropic's API directly. By overriding two environment variables you can route it through OpenRouter's free tier instead — meaning you can run agentic coding sessions at no cost.
377
-
378
- If you've already run `css-ghost --setup`, you have an OpenRouter key. You can also export it as an environment variable — css-ghost picks it up automatically, so you don't even need `--setup`:
379
-
380
- ```bash
381
- export OPENROUTER_API_KEY="sk-or-v1-YOUR_KEY"
382
- ```
383
-
384
- And you can reuse the same key with Claude Code by adding these aliases to your `~/.zshrc`:
385
-
386
- ```bash
387
- alias claude-free='ANTHROPIC_BASE_URL="https://openrouter.ai/api/v1" \
388
- ANTHROPIC_API_KEY="$OPENROUTER_API_KEY" \
389
- claude --model "nvidia/nemotron-3.5-lightning:free"'
390
-
391
- alias claude-r1='ANTHROPIC_BASE_URL="https://openrouter.ai/api/v1" \
392
- ANTHROPIC_API_KEY="$OPENROUTER_API_KEY" \
393
- claude --model "qwen/qwen3.8-27b:free"'
394
- ```
395
-
396
- Then `source ~/.zshrc`. Use `claude-free` or `claude-r1` in place of `claude` to start a free session.
397
-
398
- > **Note:** Free model availability changes frequently. Browse current options at [openrouter.ai/models](https://openrouter.ai/models?q=free) — filter by `:free`.
399
-
400
159
  ## License
401
160
 
402
161
  [MIT](./LICENSE)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rolandjlevy/css-ghost",
3
- "version": "1.1.2",
3
+ "version": "1.1.3",
4
4
  "description": "Find and remove unused CSS selectors and orphaned CSS custom properties — a CLI linter and auditor for React, Vue, Svelte, SCSS, and CSS Modules",
5
5
  "type": "module",
6
6
  "bin": {
package/src/parser.js CHANGED
@@ -259,14 +259,14 @@ export function extractUsages(content) {
259
259
  used.add(m[1]);
260
260
  }
261
261
 
262
- // styles['foo'] or styles["foo"]
263
- const bracketStringRegex = /styles?\[['"]([a-zA-Z_$][a-zA-Z0-9_$]*)['"]]/g;
262
+ // styles['foo'] or styles["foo"] — includes hyphenated names (e.g. styles['btn-primary'])
263
+ const bracketStringRegex = /styles?\[['"]([a-zA-Z_$][a-zA-Z0-9_$-]*)['"]]/g;
264
264
  while ((m = bracketStringRegex.exec(content)) !== null) {
265
265
  used.add(m[1]);
266
266
  }
267
267
 
268
268
  // styles[condition ? 'foo' : 'bar'] — ternary with string literals: extract both branches
269
- const ternaryBracketRegex = /styles?\[\s*[^?]+\?\s*['"]([a-zA-Z_$][a-zA-Z0-9_$]*)["']\s*:\s*['"]([a-zA-Z_$][a-zA-Z0-9_$]*)["']\s*\]/g;
269
+ const ternaryBracketRegex = /styles?\[\s*[^?]+\?\s*['"]([a-zA-Z_$][a-zA-Z0-9_$-]*)["']\s*:\s*['"]([a-zA-Z_$][a-zA-Z0-9_$-]*)["']\s*\]/g;
270
270
  while ((m = ternaryBracketRegex.exec(content)) !== null) {
271
271
  used.add(m[1]);
272
272
  used.add(m[2]);