@rolandjlevy/css-ghost 1.1.1 → 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 +44 -259
  2. package/package.json +3 -2
  3. package/src/parser.js +3 -3
package/README.md CHANGED
@@ -6,42 +6,47 @@ 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:**
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.
16
14
 
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
15
+ ## How is this different from PurgeCSS?
26
16
 
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.
17
+ [PurgeCSS](https://www.npmjs.com/package/purgecss) is a **build-time bundler** — it hooks into webpack, PostCSS, or Gulp to strip unused CSS from your production output. It's designed for removing large framework utility classes (Bootstrap, Tailwind) that you never use.
29
18
 
30
- ## To enable AI features
31
-
32
- 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:
19
+ css-ghost is a **source code auditor** — it finds dead selectors in your actual codebase, with no build step and no config file required. Just point it at a folder:
33
20
 
34
21
  ```bash
35
- css-ghost --setup
22
+ npx @rolandjlevy/css-ghost ./src
36
23
  ```
37
24
 
38
- 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:
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 ⚠️ |
35
+
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.
37
+
38
+ ---
39
+
40
+ ## To enable AI features
41
+
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:
39
43
 
40
44
  ```bash
45
+ css-ghost --setup
41
46
  css-ghost ./src --ai
42
47
  ```
43
48
 
44
- 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.
45
50
 
46
51
  ## Install
47
52
 
@@ -77,44 +82,18 @@ css-ghost [directory] [directory2] ... [options]
77
82
 
78
83
  ## CSS variable orphan detection
79
84
 
80
- css-ghost also catches **orphaned CSS custom properties** — variables defined with `--my-token: value` but never consumed anywhere via `var(--my-token)`.
81
-
82
- 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_.
83
-
84
- ```
85
- 🔮 --colour-danger-hover (line 8)
86
- --colour-danger-hover: #dc2626;
87
-
88
- 🔮 --spacing-xl (line 26)
89
- --spacing-xl: 64px;
90
- ```
91
-
92
- Orphaned variables appear under a separate **🔮 Orphaned CSS variables** section in both the terminal output and the markdown report.
93
-
94
- ---
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.
95
86
 
96
87
  ## Ghost trail — git history for orphans
97
88
 
98
- 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:
99
90
 
100
91
  ```bash
101
92
  css-ghost ./src --ghost-trail
102
93
  ```
103
94
 
104
- ```
105
- 👻 .iconRight (line 28)
106
- 🕵️ b5c6efb · Roland Levy · 3 weeks ago — "Refactor Button component"
107
- .iconRight {
108
- margin-left: 6px;
109
- }
110
- ```
111
-
112
- For CSS variables the search looks for `var(--name` across all file types.
113
-
114
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.
115
96
 
116
- ---
117
-
118
97
  ## Pre-commit hook
119
98
 
120
99
  Install a hook that blocks commits when orphaned selectors are present:
@@ -123,254 +102,60 @@ Install a hook that blocks commits when orphaned selectors are present:
123
102
  css-ghost --install-hook ./src
124
103
  ```
125
104
 
126
- This writes `.git/hooks/pre-commit` (executable, `chmod 755`) containing:
127
-
128
- ```sh
129
- #!/bin/sh
130
- echo "👻 css-ghost — checking for orphaned CSS selectors..."
131
- npx --yes @rolandjlevy/css-ghost ./src --strict --output /tmp/css-ghost-hook-report.md
132
-
133
- if [ $? -ne 0 ]; then
134
- echo "❌ Commit blocked: orphaned CSS selectors found"
135
- echo " Run 'css-ghost ./src --delete' to remove them"
136
- echo " Or skip: git commit --no-verify"
137
- exit 1
138
- fi
139
- ```
140
-
141
- Pass multiple directories to the hook installer the same way you would to a normal scan:
142
-
143
- ```bash
144
- css-ghost --install-hook ./src ./components ./pages
145
- ```
146
-
147
- 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`
148
106
 
149
107
  ### `--strict` flag
150
108
 
151
- `--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:
152
110
 
153
111
  ```bash
154
- # In CI: fail the build if orphans exist
155
112
  css-ghost ./src --strict
156
113
  ```
157
114
 
158
- ---
159
-
160
115
  ## Interactive folder picker
161
116
 
162
- Run `css-ghost` with no arguments and an arrow-key picker opens in your terminal:
163
-
164
- ```
165
- 📂 Directory picker — navigate into a folder, then press [a] to add it
166
-
167
- 📍 /home/user/my-project
168
-
169
- 📁 components
170
- ▶ 📁 src
171
- 📁 styles
172
-
173
- ✅ Added to scan:
174
- /home/user/my-project/src
175
-
176
- [↑↓/jk] navigate [↵/→] enter [←] parent [a] add current dir [d] done [^C] quit
177
- ```
178
-
179
- | Key | Action |
180
- | ---------------------- | --------------------------------------- |
181
- | `↑` / `↓` or `k` / `j` | Move up and down the folder list |
182
- | `↵` or `→` | Enter the highlighted folder |
183
- | `←` or Backspace | Go up to the parent folder |
184
- | `a` | Add the current folder to the scan list |
185
- | `d` | Done — start the scan |
186
- | `Ctrl+C` | Quit without scanning |
187
-
188
- 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`.
189
118
 
190
119
  ## Deleting orphans
191
120
 
192
- Pass `--delete` after a scan to remove every flagged selector directly from its source file:
193
-
194
121
  ```bash
195
- 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)
196
124
  ```
197
125
 
198
- 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):
199
-
200
- ```bash
201
- css-ghost ./src --delete --yes
202
- ```
203
-
204
- Works across all supported file types — `.css`, `.scss`, `.less`, MUI `makeStyles`, and vanilla-extract exports.
205
-
206
126
  > **Tip:** commit or stash your work before running `--delete`. There is no undo.
207
127
 
208
128
  ## Examples
209
129
 
210
130
  ```bash
211
- # Interactive folder picker (no args)
212
- css-ghost
213
-
214
- # Scan your src folder
215
- css-ghost ./src
216
-
217
- # Show git history for each orphan
218
- css-ghost ./src --ghost-trail
219
-
220
- # Fail with exit code 1 when orphans exist (CI)
221
- css-ghost ./src --strict
222
-
223
- # Install a pre-commit hook that scans ./src
224
- css-ghost --install-hook ./src
225
-
226
- # Scan and delete all orphans (with confirmation prompt)
227
- css-ghost ./src --delete
228
-
229
- # Scan and delete without prompting
230
- css-ghost ./src --delete --yes
231
-
232
- # Scan multiple directories
233
- css-ghost ./src ./components ./pages
234
-
235
- # With AI summary
236
- css-ghost ./src --ai
237
-
238
- # Write report to a custom path
239
- css-ghost ./src --output ./reports/css-audit.md
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
240
141
  ```
241
142
 
242
- ## CLI output
243
-
244
- Running `css-ghost ./src` prints a live summary to your terminal:
245
-
246
- ```
247
- ✅ Report written to orphan-report.md
248
- 📊 3 orphaned selectors across 1 file
249
-
250
- 📄 src/components/Button/Button.module.css
251
- 👻 .iconLeft (line 4)
252
- .iconLeft {
253
- margin-right: 6px;
254
- flex-shrink: 0;
255
- }
256
-
257
- 👻 .loadingSpinner (line 12)
258
- .loadingSpinner {
259
- animation: spin 0.8s linear infinite;
260
- }
261
-
262
- 👻 .disabled (line 18)
263
- .disabled {
264
- opacity: 0.4;
265
- pointer-events: none;
266
- }
267
-
268
- 📁 1 style file scanned
269
- ```
270
-
271
- Each orphan shows:
272
-
273
- - the selector name and the **exact line number** where it's defined
274
- - the **full CSS rule** so you can see what it does before deciding to delete it
275
-
276
- ## Markdown report
277
-
278
- A full report is also written to `orphan-report.md` (or your `--output` path):
279
-
280
- ---
281
-
282
- ### 👻 Orphaned selectors
283
-
284
- #### `.iconLeft` — line 4
285
-
286
- ```css
287
- .iconLeft {
288
- margin-right: 6px;
289
- flex-shrink: 0;
290
- }
291
- ```
292
-
293
- #### `.loadingSpinner` — line 12
294
-
295
- ```css
296
- .loadingSpinner {
297
- animation: spin 0.8s linear infinite;
298
- }
299
- ```
300
-
301
- ---
302
-
303
143
  ## Using with npm scripts
304
144
 
305
- If you're working inside the project itself, add a `scan` script to `package.json`:
306
-
307
145
  ```json
308
146
  "scripts": {
309
147
  "scan": "css-ghost ./src"
310
148
  }
311
149
  ```
312
150
 
313
- Then run:
314
-
315
151
  ```bash
316
- npm run scan
317
-
318
- # Pass extra args with --
319
152
  npm run scan -- --output ./reports/audit.md
320
153
  ```
321
154
 
322
- ## Troubleshooting
323
-
324
- ### `css-ghost: command not found` after global install
325
-
326
- Your npm global bin directory may not be on your PATH. Run:
327
-
328
- ```bash
329
- npm bin -g
330
- ```
331
-
332
- Then add that path to your shell config (`~/.zshrc`, `~/.bashrc`, etc.):
333
-
334
- ```bash
335
- export PATH="$(npm bin -g):$PATH"
336
- ```
337
-
338
- Reload with `source ~/.zshrc`. This is a one-time fix — it won't be needed again after that.
339
-
340
- > This is common when Node was installed via the official installer rather than nvm or Homebrew.
341
-
342
155
  ## Dependencies
343
156
 
344
157
  One runtime dependency: [`picocolors`](https://github.com/alexeyraspopov/picocolors) for terminal colours. Everything else uses Node.js built-ins (`fs`, `path`, `readline`).
345
158
 
346
- ## Origin — built with OpenRouter free models via Claude Code
347
-
348
- 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.
349
-
350
- 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.
351
-
352
- 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`:
353
-
354
- ```bash
355
- export OPENROUTER_API_KEY="sk-or-v1-YOUR_KEY"
356
- ```
357
-
358
- And you can reuse the same key with Claude Code by adding these aliases to your `~/.zshrc`:
359
-
360
- ```bash
361
- alias claude-free='ANTHROPIC_BASE_URL="https://openrouter.ai/api/v1" \
362
- ANTHROPIC_API_KEY="$OPENROUTER_API_KEY" \
363
- claude --model "nvidia/nemotron-3.5-lightning:free"'
364
-
365
- alias claude-r1='ANTHROPIC_BASE_URL="https://openrouter.ai/api/v1" \
366
- ANTHROPIC_API_KEY="$OPENROUTER_API_KEY" \
367
- claude --model "qwen/qwen3.8-27b:free"'
368
- ```
369
-
370
- Then `source ~/.zshrc`. Use `claude-free` or `claude-r1` in place of `claude` to start a free session.
371
-
372
- > **Note:** Free model availability changes frequently. Browse current options at [openrouter.ai/models](https://openrouter.ai/models?q=free) — filter by `:free`.
373
-
374
159
  ## License
375
160
 
376
161
  [MIT](./LICENSE)
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@rolandjlevy/css-ghost",
3
- "version": "1.1.1",
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": {
@@ -36,7 +36,8 @@
36
36
  "vue",
37
37
  "svelte",
38
38
  "scss",
39
- "css-modules"
39
+ "css-modules",
40
+ "purgecss"
40
41
  ],
41
42
  "author": "Roland Levy",
42
43
  "license": "MIT",
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]);