code-auditor-mcp 3.4.2 → 3.4.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 (46) hide show
  1. package/.claude-plugin/marketplace.json +1 -1
  2. package/CHANGELOG.md +67 -0
  3. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.d.ts.map +1 -1
  4. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js +67 -34
  5. package/dist/analyzers/crossDomain/CrossDomainAnalyzer.js.map +1 -1
  6. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts +9 -14
  7. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.d.ts.map +1 -1
  8. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js +41 -56
  9. package/dist/analyzers/universal/UniversalDataAccessAnalyzer.js.map +1 -1
  10. package/dist/analyzers/universal/UniversalSchemaAnalyzer.d.ts.map +1 -1
  11. package/dist/analyzers/universal/UniversalSchemaAnalyzer.js +6 -0
  12. package/dist/analyzers/universal/UniversalSchemaAnalyzer.js.map +1 -1
  13. package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts +6 -0
  14. package/dist/analyzers/universal/UniversalStylesAnalyzer.d.ts.map +1 -1
  15. package/dist/analyzers/universal/UniversalStylesAnalyzer.js +44 -24
  16. package/dist/analyzers/universal/UniversalStylesAnalyzer.js.map +1 -1
  17. package/dist/auditRunner.d.ts +4 -0
  18. package/dist/auditRunner.d.ts.map +1 -1
  19. package/dist/auditRunner.js +19 -8
  20. package/dist/auditRunner.js.map +1 -1
  21. package/dist/config/defaults.d.ts +0 -18
  22. package/dist/config/defaults.d.ts.map +1 -1
  23. package/dist/config/defaults.js +1 -19
  24. package/dist/config/defaults.js.map +1 -1
  25. package/dist/conventions/conventionMiner.d.ts +12 -0
  26. package/dist/conventions/conventionMiner.d.ts.map +1 -1
  27. package/dist/conventions/conventionMiner.js +13 -1
  28. package/dist/conventions/conventionMiner.js.map +1 -1
  29. package/dist/reporting/jsonReportGenerator.d.ts.map +1 -1
  30. package/dist/reporting/jsonReportGenerator.js +5 -0
  31. package/dist/reporting/jsonReportGenerator.js.map +1 -1
  32. package/dist/styles/styleExtractor.js +6 -3
  33. package/dist/styles/styleExtractor.js.map +1 -1
  34. package/dist/styles/tailwindProbe.d.ts +101 -0
  35. package/dist/styles/tailwindProbe.d.ts.map +1 -0
  36. package/dist/styles/tailwindProbe.js +637 -0
  37. package/dist/styles/tailwindProbe.js.map +1 -0
  38. package/dist/styles/tailwindUtilityExpander.d.ts +77 -53
  39. package/dist/styles/tailwindUtilityExpander.d.ts.map +1 -1
  40. package/dist/styles/tailwindUtilityExpander.js +146 -764
  41. package/dist/styles/tailwindUtilityExpander.js.map +1 -1
  42. package/dist/types.d.ts +5 -0
  43. package/dist/types.d.ts.map +1 -1
  44. package/dist/types.js.map +1 -1
  45. package/package.json +1 -1
  46. package/plugin/.claude-plugin/plugin.json +1 -1
@@ -1,755 +1,139 @@
1
1
  /**
2
- * Tailwind Utility Class Expander — Spec 22 R1
2
+ * Tailwind Utility Class Expander — Spec 22 R1.2 (compile-probe)
3
3
  *
4
- * Generates the full set of valid Tailwind utility classes from default theme
5
- * scales plus arbitrary-value grammar, variant prefix stripping, and project
6
- * config resolution. Replaces the hand-curated ~30-class Set previously in
7
- * UniversalStylesAnalyzer.detectUndefinedClasses.
4
+ * Validates Tailwind class names against the project's own installed
5
+ * `tailwindcss` package as the oracle. Zero hand-curated dictionaries
6
+ * the project's compiler IS the authority on what classes exist.
8
7
  *
9
- * Fail-open rule (per Spec 22 R1.3): if project Tailwind config resolution
10
- * fails, the undefined-class detector disables for that project with one
11
- * visible warning naming the failure. A claim that a class "does not exist"
12
- * may not ship on a known-incomplete dictionary.
13
- */
14
- import { loadTailwindConfig } from './tailwindConfigLoader.js';
15
- // ─── Variant Prefixes ──────────────────────────────────────────────────────
16
- /**
17
- * Tailwind variant prefixes that modify utilities.
18
- * When stripped, the underlying class should match a known utility or
19
- * arbitrary-value pattern.
20
- */
21
- const VARIANT_PREFIXES = [
22
- // Responsive
23
- 'sm', 'md', 'lg', 'xl', '2xl',
24
- // State
25
- 'hover', 'focus', 'active', 'disabled', 'visited',
26
- 'focus-visible', 'focus-within', 'focus-visible',
27
- // Form
28
- 'checked', 'indeterminate', 'required', 'valid', 'invalid',
29
- 'in-range', 'out-of-range', 'placeholder-shown', 'autofill', 'read-only',
30
- // Structure
31
- 'first', 'last', 'only', 'odd', 'even', 'first-of-type', 'last-of-type', 'only-of-type',
32
- 'empty',
33
- // Mode
34
- 'dark', 'light',
35
- // Group
36
- 'group-hover', 'group-focus', 'group-focus-visible', 'group-focus-within',
37
- 'group-active', 'group-disabled', 'group-visited', 'group-checked',
38
- // Peer
39
- 'peer-hover', 'peer-focus', 'peer-focus-visible', 'peer-active',
40
- 'peer-disabled', 'peer-checked', 'peer-invalid', 'peer-required',
41
- // Motion
42
- 'motion-safe', 'motion-reduce',
43
- // Other
44
- 'before', 'after', 'first-letter', 'first-line',
45
- 'marker', 'selection', 'target', 'file', 'placeholder',
46
- 'rtl', 'ltr',
47
- 'open', 'closed',
48
- 'portrait', 'landscape',
49
- 'print',
50
- // Aria
51
- 'aria-checked', 'aria-disabled',
52
- 'has-[:checked]', 'has-[:disabled]',
53
- // Redundant but safe: already covered by individual state modifiers
54
- ];
55
- // ─── Utility Prefixes (for arbitrary-value grammar) ─────────────────────────
56
- /**
57
- * Known Tailwind utility prefixes that can accept arbitrary values via
58
- * the `prefix-[...]` syntax. Covers spacing, color, sizing, and other
59
- * commonly-arbitrary-value'd families.
8
+ * **Primary (v4):** Uses `compile()` with `@apply` probe stylesheets.
9
+ * **Fallback (v3):** Uses `resolveConfig()` + theme generation.
10
+ * **Structural parsing:** Arbitrary values, variant prefixes, opacity
11
+ * modifiers, and negative utilities are parsed via structural regex
12
+ * patterns — never enumerated.
13
+ *
14
+ * **Fail-open rule:** If the probe can't initialize (no tailwindcss found,
15
+ * compile fails), `configFailed` is true. The caller MUST emit a visible
16
+ * warning and disable the undefined-class detector.
60
17
  */
61
- const ARBITRARY_VALUE_PREFIXES = new Set([
62
- // Spacing-based
63
- 'p', 'px', 'py', 'pt', 'pr', 'pb', 'pl',
64
- 'm', 'mx', 'my', 'mt', 'mr', 'mb', 'ml',
65
- 'gap', 'gap-x', 'gap-y',
66
- 'w', 'min-w', 'max-w', 'h', 'min-h', 'max-h',
67
- 'top', 'right', 'bottom', 'left', 'inset',
68
- 'inset-x', 'inset-y',
69
- 'translate-x', 'translate-y', 'translate',
70
- 'skew-x', 'skew-y', 'skew',
71
- 'rotate',
72
- 'scale', 'scale-x', 'scale-y',
73
- 'space-x', 'space-y',
74
- 'leading', 'tracking', 'indent',
75
- 'scroll-m', 'scroll-mx', 'scroll-my', 'scroll-mt', 'scroll-mr', 'scroll-mb', 'scroll-ml',
76
- 'scroll-p', 'scroll-px', 'scroll-py', 'scroll-pt', 'scroll-pr', 'scroll-pb', 'scroll-pl',
77
- // Color-based
78
- 'bg', 'text', 'border', 'ring', 'ring-offset', 'shadow',
79
- 'fill', 'stroke', 'outline', 'accent', 'caret',
80
- 'placeholder', 'divide',
81
- 'from', 'via', 'to', // gradient stops
82
- 'decoration',
83
- // Sizing
84
- 'basis', 'grow', 'shrink',
85
- 'order',
86
- 'z', // z-index
87
- // Border
88
- 'rounded', 'rounded-t', 'rounded-r', 'rounded-b', 'rounded-l',
89
- 'rounded-tl', 'rounded-tr', 'rounded-br', 'rounded-bl',
90
- 'rounded-s', 'rounded-e', 'rounded-ss', 'rounded-se', 'rounded-es', 'rounded-ee',
91
- 'border', 'border-t', 'border-r', 'border-b', 'border-l',
92
- 'border-x', 'border-y',
93
- 'outline',
94
- 'outline-offset',
95
- // Text
96
- 'text', 'text-wrap',
97
- 'underline-offset',
98
- // Opacity
99
- 'opacity',
100
- 'bg-opacity', 'text-opacity', 'border-opacity', 'ring-opacity',
101
- 'divide-opacity', 'placeholder-opacity',
102
- // Shadow
103
- 'shadow',
104
- // Duration / timing
105
- 'duration', 'delay', 'easing',
106
- // Animation
107
- 'animate',
108
- // Grid
109
- 'grid-cols', 'grid-rows', 'auto-cols', 'auto-rows',
110
- 'col', 'col-start', 'col-end', 'col-span',
111
- 'row', 'row-start', 'row-end', 'row-span',
112
- // Columns
113
- 'columns',
114
- // Filter
115
- 'blur', 'brightness', 'contrast', 'grayscale', 'hue-rotate',
116
- 'invert', 'saturate', 'sepia', 'drop-shadow',
117
- 'backdrop-blur', 'backdrop-brightness', 'backdrop-contrast',
118
- 'backdrop-grayscale', 'backdrop-hue-rotate', 'backdrop-invert',
119
- 'backdrop-opacity', 'backdrop-saturate', 'backdrop-sepia',
120
- // Aspect ratio
121
- 'aspect',
122
- // Break
123
- 'break',
124
- // Container queries
125
- '@',
126
- ]);
18
+ import { TailwindProbe } from './tailwindProbe.js';
19
+ // ---------------------------------------------------------------------------
20
+ // Structural patterns (regex NOT enumeration)
21
+ // ---------------------------------------------------------------------------
127
22
  /**
128
- * Prefixes that allow a negative sign to be prepended to the utility.
129
- * e.g., -mt-1 (negative margin-top), -top-2 (negative position).
23
+ * Regex matching Tailwind variant prefix syntax.
24
+ * Covers: responsive (sm/md/lg/xl/2xl), state (hover/focus/active/...),
25
+ * group/peer modifiers, dark/light, motion, aria, has, supports, etc.
26
+ *
27
+ * Pattern: `word` or `word-word` or `[...]` followed by `:`.
28
+ * This is a structural check — it does not enumerate valid variant names.
130
29
  */
131
- const NEGATIVE_ALLOWED_PREFIXES = new Set([
132
- // Margin — negative margins pull elements closer / overlap
133
- 'm', 'mx', 'my', 'mt', 'mr', 'mb', 'ml',
134
- // Position — negative offsets move element beyond container edge
135
- 'top', 'right', 'bottom', 'left',
136
- // Inset
137
- 'inset', 'inset-x', 'inset-y',
138
- // Z-index
139
- 'z',
140
- // Transform
141
- 'translate-x', 'translate-y',
142
- 'rotate',
143
- 'scale', 'scale-x', 'scale-y',
144
- 'skew-x', 'skew-y',
145
- // Indent (negative text indent)
146
- 'indent',
147
- ]);
148
- // ─── Static Utility Classes ─────────────────────────────────────────────────
30
+ const VARIANT_PREFIX_RE = /^(?:[a-z][a-z0-9]*(?:-[a-z][a-z0-9]*)*|\[.+?\]):/;
149
31
  /**
150
- * Standard Tailwind utility classes that don't depend on a theme scale.
151
- * These are the fixed-name classes: display, flex, grid, alignment, etc.
32
+ * Regex matching arbitrary-value syntax: `prefix-[...]`.
33
+ * Any word followed by `-[...]` is structurally a valid Tailwind
34
+ * arbitrary value. The brackets DO need to be balanced.
152
35
  */
153
- function getStaticUtilityClasses() {
154
- return [
155
- // Display
156
- 'block', 'inline-block', 'inline', 'flex', 'inline-flex', 'grid',
157
- 'inline-grid', 'hidden', 'flow-root', 'contents', 'table', 'table-row',
158
- 'table-cell', 'table-caption', 'table-column', 'table-column-group',
159
- 'table-footer-group', 'table-header-group', 'table-row-group', 'list-item',
160
- // Position
161
- 'static', 'fixed', 'absolute', 'relative', 'sticky',
162
- // Flex direction
163
- 'flex-row', 'flex-row-reverse', 'flex-col', 'flex-col-reverse',
164
- 'flex-wrap', 'flex-nowrap', 'flex-wrap-reverse',
165
- 'flex-1', 'flex-auto', 'flex-initial', 'flex-none',
166
- 'grow', 'grow-0', 'shrink', 'shrink-0',
167
- 'flex-grow', 'flex-grow-0', 'flex-shrink', 'flex-shrink-0',
168
- 'group',
169
- // Grid
170
- 'grid-cols-1', 'grid-cols-2', 'grid-cols-3', 'grid-cols-4',
171
- 'grid-cols-5', 'grid-cols-6', 'grid-cols-7', 'grid-cols-8',
172
- 'grid-cols-9', 'grid-cols-10', 'grid-cols-11', 'grid-cols-12',
173
- 'grid-cols-none', 'grid-cols-subgrid',
174
- 'grid-rows-1', 'grid-rows-2', 'grid-rows-3', 'grid-rows-4',
175
- 'grid-rows-5', 'grid-rows-6', 'grid-rows-none', 'grid-rows-subgrid',
176
- 'grid-flow-row', 'grid-flow-col', 'grid-flow-dense', 'grid-flow-row-dense', 'grid-flow-col-dense',
177
- 'auto-cols-auto', 'auto-cols-min', 'auto-cols-max', 'auto-cols-fr',
178
- 'auto-rows-auto', 'auto-rows-min', 'auto-rows-max', 'auto-rows-fr',
179
- // Alignment
180
- 'items-start', 'items-end', 'items-center', 'items-baseline', 'items-stretch',
181
- 'justify-start', 'justify-end', 'justify-center', 'justify-between',
182
- 'justify-around', 'justify-evenly', 'justify-stretch', 'justify-normal',
183
- 'justify-items-start', 'justify-items-end', 'justify-items-center', 'justify-items-stretch',
184
- 'content-center', 'content-start', 'content-end', 'content-between',
185
- 'content-around', 'content-evenly', 'content-baseline', 'content-normal', 'content-stretch',
186
- 'place-content-center', 'place-content-start', 'place-content-end',
187
- 'place-content-between', 'place-content-around', 'place-content-evenly',
188
- 'place-items-center', 'place-items-start', 'place-items-end', 'place-items-stretch',
189
- 'self-auto', 'self-start', 'self-end', 'self-center', 'self-stretch', 'self-baseline',
190
- 'place-self-auto', 'place-self-start', 'place-self-end', 'place-self-center', 'place-self-stretch',
191
- // Text alignment
192
- 'text-left', 'text-center', 'text-right', 'text-justify', 'text-start', 'text-end',
193
- // Text decoration
194
- 'underline', 'line-through', 'no-underline', 'overline',
195
- 'decoration-solid', 'decoration-double', 'decoration-dotted', 'decoration-dashed', 'decoration-wavy',
196
- // Text transform
197
- 'uppercase', 'lowercase', 'capitalize', 'normal-case',
198
- // Text overflow
199
- 'truncate', 'text-ellipsis', 'text-clip',
200
- // Whitespace
201
- 'whitespace-normal', 'whitespace-nowrap', 'whitespace-pre', 'whitespace-pre-line', 'whitespace-pre-wrap',
202
- 'whitespace-break-spaces',
203
- // Word break
204
- 'break-normal', 'break-words', 'break-all', 'break-keep', 'hyphens-none', 'hyphens-manual', 'hyphens-auto',
205
- // Font weight
206
- 'font-thin', 'font-extralight', 'font-light', 'font-normal', 'font-medium',
207
- 'font-semibold', 'font-bold', 'font-extrabold', 'font-black',
208
- // Font style
209
- 'italic', 'not-italic',
210
- // Font variant
211
- 'ordinal', 'slashed-zero', 'lining-nums', 'oldstyle-nums', 'proportional-nums',
212
- 'tabular-nums', 'diagonal-fractions', 'stacked-fractions',
213
- // Width
214
- 'w-auto', 'w-full', 'w-screen', 'w-svw', 'w-dvw', 'w-lvw', 'w-min', 'w-max', 'w-fit',
215
- 'w-0', 'w-px', 'w-0.5', 'w-1', 'w-2', 'w-3', 'w-4', 'w-5', 'w-6', 'w-7', 'w-8',
216
- 'w-9', 'w-10', 'w-11', 'w-12', 'w-14', 'w-16', 'w-20', 'w-24', 'w-28', 'w-32',
217
- 'w-36', 'w-40', 'w-44', 'w-48', 'w-52', 'w-56', 'w-60', 'w-64', 'w-72', 'w-80', 'w-96',
218
- 'w-1/2', 'w-1/3', 'w-2/3', 'w-1/4', 'w-2/4', 'w-3/4', 'w-1/5', 'w-2/5', 'w-3/5', 'w-4/5',
219
- 'w-1/6', 'w-2/6', 'w-3/6', 'w-4/6', 'w-5/6',
220
- // Height
221
- 'h-auto', 'h-full', 'h-screen', 'h-svh', 'h-dvh', 'h-lvh', 'h-min', 'h-max', 'h-fit',
222
- 'h-0', 'h-px', 'h-0.5', 'h-1', 'h-2', 'h-3', 'h-4', 'h-5', 'h-6', 'h-7', 'h-8',
223
- 'h-9', 'h-10', 'h-11', 'h-12', 'h-14', 'h-16', 'h-20', 'h-24', 'h-28', 'h-32',
224
- 'h-36', 'h-40', 'h-44', 'h-48', 'h-52', 'h-56', 'h-60', 'h-64', 'h-72', 'h-80', 'h-96',
225
- 'h-1/2', 'h-1/3', 'h-2/3', 'h-1/4', 'h-2/4', 'h-3/4', 'h-1/5', 'h-2/5', 'h-3/5', 'h-4/5',
226
- 'h-1/6', 'h-2/6', 'h-3/6', 'h-4/6', 'h-5/6',
227
- // Min/max sizing
228
- 'min-w-0', 'min-w-full', 'min-w-min', 'min-w-max', 'min-w-fit',
229
- 'max-w-0', 'max-w-none', 'max-w-xs', 'max-w-sm', 'max-w-md', 'max-w-lg', 'max-w-xl',
230
- 'max-w-2xl', 'max-w-3xl', 'max-w-4xl', 'max-w-5xl', 'max-w-6xl', 'max-w-7xl',
231
- 'max-w-full', 'max-w-min', 'max-w-max', 'max-w-fit', 'max-w-prose',
232
- 'max-w-screen-sm', 'max-w-screen-md', 'max-w-screen-lg', 'max-w-screen-xl', 'max-w-screen-2xl',
233
- 'min-h-0', 'min-h-full', 'min-h-screen', 'min-h-min', 'min-h-max', 'min-h-fit',
234
- 'max-h-0', 'max-h-full', 'max-h-screen', 'max-h-min', 'max-h-max', 'max-h-fit',
235
- // Size (both w and h)
236
- 'size-auto', 'size-full', 'size-min', 'size-max', 'size-fit',
237
- // Overflow
238
- 'overflow-auto', 'overflow-hidden', 'overflow-visible', 'overflow-scroll',
239
- 'overflow-x-auto', 'overflow-x-hidden', 'overflow-x-visible', 'overflow-x-scroll',
240
- 'overflow-y-auto', 'overflow-y-hidden', 'overflow-y-visible', 'overflow-y-scroll',
241
- // Cursor
242
- 'cursor-auto', 'cursor-default', 'cursor-pointer', 'cursor-wait', 'cursor-text',
243
- 'cursor-move', 'cursor-help', 'cursor-not-allowed', 'cursor-none',
244
- 'cursor-context-menu', 'cursor-progress', 'cursor-cell', 'cursor-crosshair',
245
- 'cursor-vertical-text', 'cursor-alias', 'cursor-copy', 'cursor-grab', 'cursor-grabbing',
246
- 'cursor-no-drop', 'cursor-zoom-in', 'cursor-zoom-out',
247
- // Visibility
248
- 'visible', 'invisible', 'collapse',
249
- // Border
250
- 'border', 'border-0', 'border-2', 'border-4', 'border-8',
251
- 'border-t', 'border-r', 'border-b', 'border-l',
252
- 'border-t-0', 'border-r-0', 'border-b-0', 'border-l-0',
253
- 'border-x', 'border-y',
254
- 'border-solid', 'border-dashed', 'border-dotted', 'border-double', 'border-hidden', 'border-none',
255
- // Shadow
256
- 'shadow', 'shadow-sm', 'shadow-md', 'shadow-lg', 'shadow-xl', 'shadow-2xl',
257
- 'shadow-inner', 'shadow-none',
258
- // Ring
259
- 'ring', 'ring-0', 'ring-1', 'ring-2', 'ring-4', 'ring-8', 'ring-inset',
260
- 'ring-offset-0', 'ring-offset-1', 'ring-offset-2', 'ring-offset-4', 'ring-offset-8',
261
- // Opacity
262
- 'opacity-0', 'opacity-5', 'opacity-10', 'opacity-20', 'opacity-25', 'opacity-30',
263
- 'opacity-40', 'opacity-50', 'opacity-60', 'opacity-70', 'opacity-75', 'opacity-80',
264
- 'opacity-90', 'opacity-95', 'opacity-100',
265
- // Z-index
266
- 'z-0', 'z-10', 'z-20', 'z-30', 'z-40', 'z-50', 'z-auto',
267
- // Object fit
268
- 'object-contain', 'object-cover', 'object-fill', 'object-none', 'object-scale-down',
269
- // Object position
270
- 'object-bottom', 'object-center', 'object-left', 'object-left-bottom',
271
- 'object-left-top', 'object-right', 'object-right-bottom', 'object-right-top', 'object-top',
272
- // Transition
273
- 'transition-none', 'transition-all', 'transition', 'transition-colors',
274
- 'transition-opacity', 'transition-shadow', 'transition-transform',
275
- // Duration
276
- 'duration-0', 'duration-75', 'duration-100', 'duration-150', 'duration-200',
277
- 'duration-300', 'duration-500', 'duration-700', 'duration-1000',
278
- // Delay
279
- 'delay-0', 'delay-75', 'delay-100', 'delay-150', 'delay-200', 'delay-300',
280
- 'delay-500', 'delay-700', 'delay-1000',
281
- // Timing
282
- 'ease-linear', 'ease-in', 'ease-out', 'ease-in-out',
283
- // Transform
284
- 'transform', 'transform-gpu', 'transform-none',
285
- 'scale-0', 'scale-50', 'scale-75', 'scale-90', 'scale-95', 'scale-100',
286
- 'scale-105', 'scale-110', 'scale-125', 'scale-150',
287
- 'scale-x-0', 'scale-x-50', 'scale-x-75', 'scale-x-90', 'scale-x-95', 'scale-x-100',
288
- 'scale-x-105', 'scale-x-110', 'scale-x-125', 'scale-x-150',
289
- 'scale-y-0', 'scale-y-50', 'scale-y-75', 'scale-y-90', 'scale-y-95', 'scale-y-100',
290
- 'scale-y-105', 'scale-y-110', 'scale-y-125', 'scale-y-150',
291
- 'rotate-0', 'rotate-1', 'rotate-2', 'rotate-3', 'rotate-6', 'rotate-12',
292
- 'rotate-45', 'rotate-90', 'rotate-180',
293
- 'origin-center', 'origin-top', 'origin-top-right', 'origin-right',
294
- 'origin-bottom-right', 'origin-bottom', 'origin-bottom-left', 'origin-left', 'origin-top-left',
295
- // Box sizing
296
- 'box-border', 'box-content',
297
- // Container
298
- 'container',
299
- // Pointer events
300
- 'pointer-events-none', 'pointer-events-auto',
301
- // Resize
302
- 'resize-none', 'resize', 'resize-y', 'resize-x',
303
- // User select
304
- 'select-none', 'select-text', 'select-all', 'select-auto',
305
- // Screen readers
306
- 'sr-only', 'not-sr-only',
307
- // Background
308
- 'bg-auto', 'bg-cover', 'bg-contain',
309
- 'bg-bottom', 'bg-center', 'bg-left', 'bg-left-bottom', 'bg-left-top',
310
- 'bg-right', 'bg-right-bottom', 'bg-right-top', 'bg-top',
311
- 'bg-fixed', 'bg-local', 'bg-scroll',
312
- 'bg-no-repeat', 'bg-repeat', 'bg-repeat-x', 'bg-repeat-y', 'bg-repeat-round', 'bg-repeat-space',
313
- 'bg-none', 'bg-origin-border', 'bg-origin-padding', 'bg-origin-content',
314
- 'bg-clip-border', 'bg-clip-padding', 'bg-clip-content', 'bg-clip-text',
315
- // Gradient
316
- 'bg-gradient-to-t', 'bg-gradient-to-tr', 'bg-gradient-to-r', 'bg-gradient-to-br',
317
- 'bg-gradient-to-b', 'bg-gradient-to-bl', 'bg-gradient-to-l', 'bg-gradient-to-tl',
318
- 'bg-none',
319
- // List style
320
- 'list-none', 'list-disc', 'list-decimal', 'list-image-none',
321
- 'list-inside', 'list-outside',
322
- // Table
323
- 'table-auto', 'table-fixed',
324
- 'border-collapse', 'border-separate',
325
- 'border-spacing-0', 'border-spacing-1', 'border-spacing-2',
326
- 'border-spacing-x-0', 'border-spacing-y-0',
327
- 'table-layout-auto', 'table-layout-fixed',
328
- 'caption-top', 'caption-bottom',
329
- // Vertical align
330
- 'align-baseline', 'align-top', 'align-middle', 'align-bottom',
331
- 'align-text-top', 'align-text-bottom', 'align-sub', 'align-super',
332
- // Aspect ratio
333
- 'aspect-auto', 'aspect-square', 'aspect-video',
334
- // Columns
335
- 'columns-1', 'columns-2', 'columns-3', 'columns-4', 'columns-5', 'columns-6',
336
- 'columns-7', 'columns-8', 'columns-9', 'columns-10', 'columns-11', 'columns-12',
337
- 'columns-auto', 'columns-3xs', 'columns-2xs', 'columns-xs', 'columns-sm',
338
- 'columns-md', 'columns-lg', 'columns-xl', 'columns-2xl', 'columns-3xl',
339
- 'columns-4xl', 'columns-5xl', 'columns-6xl', 'columns-7xl',
340
- 'break-before-auto', 'break-before-avoid', 'break-before-all', 'break-before-avoid-page', 'break-before-page',
341
- 'break-before-left', 'break-before-right', 'break-before-column',
342
- 'break-after-auto', 'break-after-avoid', 'break-after-all', 'break-after-avoid-page', 'break-after-page',
343
- 'break-after-left', 'break-after-right', 'break-after-column',
344
- 'break-inside-auto', 'break-inside-avoid', 'break-inside-avoid-page', 'break-inside-avoid-column',
345
- // Float
346
- 'float-right', 'float-left', 'float-none', 'float-start', 'float-end',
347
- 'clear-left', 'clear-right', 'clear-both', 'clear-none', 'clear-start', 'clear-end',
348
- // Box decoration
349
- 'decoration-slice', 'decoration-clone',
350
- 'box-decoration-slice', 'box-decoration-clone',
351
- // Isolation
352
- 'isolate', 'isolation-auto',
353
- // Overscroll
354
- 'overscroll-auto', 'overscroll-contain', 'overscroll-none',
355
- 'overscroll-x-auto', 'overscroll-x-contain', 'overscroll-x-none',
356
- 'overscroll-y-auto', 'overscroll-y-contain', 'overscroll-y-none',
357
- 'overscroll-behavior-auto', 'overscroll-behavior-contain', 'overscroll-behavior-none',
358
- // Scroll snap
359
- 'snap-none', 'snap-x', 'snap-y', 'snap-both', 'snap-mandatory', 'snap-proximity',
360
- 'snap-start', 'snap-end', 'snap-center', 'snap-align-none', 'snap-normal', 'snap-always',
361
- // Scroll behavior
362
- 'scroll-auto', 'scroll-smooth',
363
- // Appearance
364
- 'appearance-none', 'appearance-auto',
365
- // Touch
366
- 'touch-auto', 'touch-none', 'touch-pan-x', 'touch-pan-left', 'touch-pan-right',
367
- 'touch-pan-y', 'touch-pan-up', 'touch-pan-down', 'touch-pinch-zoom',
368
- 'touch-manipulation',
369
- // Will change
370
- 'will-change-auto', 'will-change-scroll', 'will-change-contents', 'will-change-transform',
371
- // Content
372
- 'content-none',
373
- // Blend mode
374
- 'mix-blend-normal', 'mix-blend-multiply', 'mix-blend-screen', 'mix-blend-overlay',
375
- 'mix-blend-darken', 'mix-blend-lighten', 'mix-blend-color-dodge', 'mix-blend-color-burn',
376
- 'mix-blend-hard-light', 'mix-blend-soft-light', 'mix-blend-difference',
377
- 'mix-blend-exclusion', 'mix-blend-hue', 'mix-blend-saturation', 'mix-blend-color', 'mix-blend-luminosity',
378
- 'mix-blend-plus-darker', 'mix-blend-plus-lighter',
379
- 'bg-blend-normal', 'bg-blend-multiply', 'bg-blend-screen', 'bg-blend-overlay',
380
- 'bg-blend-darken', 'bg-blend-lighten', 'bg-blend-color-dodge', 'bg-blend-color-burn',
381
- // Gap
382
- 'gap-0', 'gap-px', 'gap-0.5', 'gap-1', 'gap-2', 'gap-3', 'gap-4', 'gap-5',
383
- 'gap-6', 'gap-7', 'gap-8', 'gap-9', 'gap-10', 'gap-11', 'gap-12',
384
- 'gap-14', 'gap-16', 'gap-20', 'gap-24', 'gap-28', 'gap-32', 'gap-36', 'gap-40',
385
- 'gap-44', 'gap-48', 'gap-52', 'gap-56', 'gap-60', 'gap-64', 'gap-72', 'gap-80', 'gap-96',
386
- 'gap-x-0', 'gap-y-0',
387
- // Space
388
- 'space-x-0', 'space-y-0',
389
- // Divide
390
- 'divide-x', 'divide-y',
391
- 'divide-x-0', 'divide-y-0',
392
- 'divide-solid', 'divide-dashed', 'divide-dotted', 'divide-double', 'divide-none',
393
- 'divide-x-reverse', 'divide-y-reverse',
394
- // Ring
395
- 'ring-inset',
396
- // Line clamp
397
- 'line-clamp-1', 'line-clamp-2', 'line-clamp-3', 'line-clamp-4', 'line-clamp-5', 'line-clamp-6', 'line-clamp-none',
398
- // Animation
399
- 'animate-none', 'animate-spin', 'animate-ping', 'animate-pulse', 'animate-bounce',
400
- // Font family
401
- 'font-sans', 'font-serif', 'font-mono',
402
- // Letter spacing (tracking)
403
- 'tracking-tighter', 'tracking-tight', 'tracking-normal',
404
- 'tracking-wide', 'tracking-wider', 'tracking-widest',
405
- // Backdrop filters
406
- 'backdrop-blur', 'backdrop-blur-sm', 'backdrop-blur-md', 'backdrop-blur-lg',
407
- 'backdrop-blur-xl', 'backdrop-blur-2xl', 'backdrop-blur-3xl', 'backdrop-blur-none',
408
- 'backdrop-opacity-0', 'backdrop-opacity-50', 'backdrop-opacity-100',
409
- ];
410
- }
411
- // ─── Generated Utility Classes (from theme scales) ──────────────────────────
36
+ const ARBITRARY_VALUE_RE = /^[a-z][a-z0-9]*(?:-[a-z][a-z0-9]*)*-\[.+\]$/;
412
37
  /**
413
- * Generate all valid Tailwind utility classes by combining known prefixes with
414
- * default theme scales + common string values for non-scale prefixes.
38
+ * Regex matching opacity-modifier syntax: `utility/opacity`.
39
+ * e.g., `bg-red-500/50`, `text-blue-100/75`.
40
+ * The part before the last `/` should be a valid utility,
41
+ * and the suffix should be a numeric opacity (0-100).
415
42
  */
416
- function generateScaleBasedClasses() {
417
- const classes = [];
418
- // Color prefixes — generate bg-{color}, text-{color}, etc.
419
- const colorPrefixes = [
420
- 'bg', 'text', 'border', 'ring', 'shadow', 'fill', 'stroke',
421
- 'accent', 'caret', 'outline', 'placeholder', 'divide', 'from', 'via', 'to',
422
- 'decoration',
423
- ];
424
- // Color names from the default Tailwind palette
425
- const colorNames = [
426
- 'inherit', 'current', 'transparent',
427
- 'white', 'black',
428
- // Slate
429
- 'slate-50', 'slate-100', 'slate-200', 'slate-300', 'slate-400', 'slate-500',
430
- 'slate-600', 'slate-700', 'slate-800', 'slate-900', 'slate-950',
431
- // Gray
432
- 'gray-50', 'gray-100', 'gray-200', 'gray-300', 'gray-400', 'gray-500',
433
- 'gray-600', 'gray-700', 'gray-800', 'gray-900', 'gray-950',
434
- // Zinc
435
- 'zinc-50', 'zinc-100', 'zinc-200', 'zinc-300', 'zinc-400', 'zinc-500',
436
- 'zinc-600', 'zinc-700', 'zinc-800', 'zinc-900', 'zinc-950',
437
- // Neutral
438
- 'neutral-50', 'neutral-100', 'neutral-200', 'neutral-300', 'neutral-400', 'neutral-500',
439
- 'neutral-600', 'neutral-700', 'neutral-800', 'neutral-900', 'neutral-950',
440
- // Stone
441
- 'stone-50', 'stone-100', 'stone-200', 'stone-300', 'stone-400', 'stone-500',
442
- 'stone-600', 'stone-700', 'stone-800', 'stone-900', 'stone-950',
443
- // Red
444
- 'red-50', 'red-100', 'red-200', 'red-300', 'red-400', 'red-500',
445
- 'red-600', 'red-700', 'red-800', 'red-900', 'red-950',
446
- // Orange
447
- 'orange-50', 'orange-100', 'orange-200', 'orange-300', 'orange-400', 'orange-500',
448
- 'orange-600', 'orange-700', 'orange-800', 'orange-900', 'orange-950',
449
- // Amber
450
- 'amber-50', 'amber-100', 'amber-200', 'amber-300', 'amber-400', 'amber-500',
451
- 'amber-600', 'amber-700', 'amber-800', 'amber-900', 'amber-950',
452
- // Yellow
453
- 'yellow-50', 'yellow-100', 'yellow-200', 'yellow-300', 'yellow-400', 'yellow-500',
454
- 'yellow-600', 'yellow-700', 'yellow-800', 'yellow-900', 'yellow-950',
455
- // Lime
456
- 'lime-50', 'lime-100', 'lime-200', 'lime-300', 'lime-400', 'lime-500',
457
- 'lime-600', 'lime-700', 'lime-800', 'lime-900', 'lime-950',
458
- // Green
459
- 'green-50', 'green-100', 'green-200', 'green-300', 'green-400', 'green-500',
460
- 'green-600', 'green-700', 'green-800', 'green-900', 'green-950',
461
- // Emerald
462
- 'emerald-50', 'emerald-100', 'emerald-200', 'emerald-300', 'emerald-400', 'emerald-500',
463
- 'emerald-600', 'emerald-700', 'emerald-800', 'emerald-900', 'emerald-950',
464
- // Teal
465
- 'teal-50', 'teal-100', 'teal-200', 'teal-300', 'teal-400', 'teal-500',
466
- 'teal-600', 'teal-700', 'teal-800', 'teal-900', 'teal-950',
467
- // Cyan
468
- 'cyan-50', 'cyan-100', 'cyan-200', 'cyan-300', 'cyan-400', 'cyan-500',
469
- 'cyan-600', 'cyan-700', 'cyan-800', 'cyan-900', 'cyan-950',
470
- // Sky
471
- 'sky-50', 'sky-100', 'sky-200', 'sky-300', 'sky-400', 'sky-500',
472
- 'sky-600', 'sky-700', 'sky-800', 'sky-900', 'sky-950',
473
- // Blue
474
- 'blue-50', 'blue-100', 'blue-200', 'blue-300', 'blue-400', 'blue-500',
475
- 'blue-600', 'blue-700', 'blue-800', 'blue-900', 'blue-950',
476
- // Indigo
477
- 'indigo-50', 'indigo-100', 'indigo-200', 'indigo-300', 'indigo-400', 'indigo-500',
478
- 'indigo-600', 'indigo-700', 'indigo-800', 'indigo-900', 'indigo-950',
479
- // Violet
480
- 'violet-50', 'violet-100', 'violet-200', 'violet-300', 'violet-400', 'violet-500',
481
- 'violet-600', 'violet-700', 'violet-800', 'violet-900', 'violet-950',
482
- // Purple
483
- 'purple-50', 'purple-100', 'purple-200', 'purple-300', 'purple-400', 'purple-500',
484
- 'purple-600', 'purple-700', 'purple-800', 'purple-900', 'purple-950',
485
- // Fuchsia
486
- 'fuchsia-50', 'fuchsia-100', 'fuchsia-200', 'fuchsia-300', 'fuchsia-400', 'fuchsia-500',
487
- 'fuchsia-600', 'fuchsia-700', 'fuchsia-800', 'fuchsia-900', 'fuchsia-950',
488
- // Pink
489
- 'pink-50', 'pink-100', 'pink-200', 'pink-300', 'pink-400', 'pink-500',
490
- 'pink-600', 'pink-700', 'pink-800', 'pink-900', 'pink-950',
491
- // Rose
492
- 'rose-50', 'rose-100', 'rose-200', 'rose-300', 'rose-400', 'rose-500',
493
- 'rose-600', 'rose-700', 'rose-800', 'rose-900', 'rose-950',
494
- ];
495
- // Special opacity variants for color prefixes that support it
496
- const dividerColors = new Set(['divide', 'border', 'ring', 'placeholder', 'text']);
497
- const opacityNumbers = ['0', '5', '10', '20', '25', '30', '40', '50', '60', '70', '75', '80', '90', '95', '100'];
498
- for (const prefix of colorPrefixes) {
499
- for (const colorName of colorNames) {
500
- classes.push(`${prefix}-${colorName}`);
501
- // Opacity modifiers for supported prefixes
502
- if (dividerColors.has(prefix)) {
503
- for (const opacity of opacityNumbers) {
504
- classes.push(`${prefix}-${colorName}/${opacity}`);
505
- }
506
- }
507
- }
508
- }
509
- // Font size prefixes
510
- const fontSizePrefixes = ['text'];
511
- const fontSizeNames = [
512
- 'xs', 'sm', 'base', 'lg', 'xl', '2xl', '3xl', '4xl', '5xl', '6xl', '7xl', '8xl', '9xl',
513
- ];
514
- for (const prefix of fontSizePrefixes) {
515
- for (const sizeName of fontSizeNames) {
516
- classes.push(`${prefix}-${sizeName}`);
517
- }
518
- }
519
- // Border radius prefixes
520
- const radiusPrefixes = [
521
- 'rounded', 'rounded-t', 'rounded-r', 'rounded-b', 'rounded-l',
522
- 'rounded-tl', 'rounded-tr', 'rounded-br', 'rounded-bl',
523
- 'rounded-s', 'rounded-e', 'rounded-ss', 'rounded-se', 'rounded-es', 'rounded-ee',
524
- ];
525
- const radiusNames = ['none', 'sm', '', 'md', 'lg', 'xl', '2xl', '3xl', 'full'];
526
- for (const prefix of radiusPrefixes) {
527
- for (const radiusName of radiusNames) {
528
- if (radiusName === '') {
529
- classes.push(prefix); // "rounded" alone
530
- }
531
- else {
532
- classes.push(`${prefix}-${radiusName}`);
533
- }
534
- }
535
- }
536
- // ── Spacing-scale utilities ─────────────────────────────────────────
537
- // Padding, margin, position, inset, gap-x, gap-y, space-x, space-y,
538
- // size, leading, indent, scroll-margin/padding — all accept the default
539
- // spacing scale as values.
540
- const spacingScaleValues = [
541
- '0', 'px', '0.5', '1', '1.5', '2', '2.5', '3', '3.5', '4', '5', '6', '7',
542
- '8', '9', '10', '11', '12', '14', '16', '20', '24', '28', '32', '36', '40',
543
- '44', '48', '52', '56', '60', '64', '72', '80', '96',
544
- ];
545
- // Spacing-based prefixes — utilities that accept spacing scale values
546
- const spacingPrefixes = [
547
- // Padding
548
- 'p', 'px', 'py', 'pt', 'pr', 'pb', 'pl',
549
- // Margin
550
- 'm', 'mx', 'my', 'mt', 'mr', 'mb', 'ml',
551
- // Position
552
- 'top', 'right', 'bottom', 'left',
553
- // Inset
554
- 'inset', 'inset-x', 'inset-y',
555
- // Gap (gap-{x,y} — full scale; gap base already in static)
556
- 'gap-x', 'gap-y',
557
- // Space (space-{x,y} — full scale)
558
- 'space-x', 'space-y',
559
- // Size (both width and height)
560
- 'size',
561
- // Scroll margin / padding
562
- 'scroll-m', 'scroll-mx', 'scroll-my', 'scroll-mt', 'scroll-mr', 'scroll-mb', 'scroll-ml',
563
- 'scroll-p', 'scroll-px', 'scroll-py', 'scroll-pt', 'scroll-pr', 'scroll-pb', 'scroll-pl',
564
- ];
565
- for (const prefix of spacingPrefixes) {
566
- for (const value of spacingScaleValues) {
567
- classes.push(`${prefix}-${value}`);
568
- }
569
- }
570
- // Special non-scale values for margin: auto
571
- for (const prefix of ['m', 'mx', 'my', 'mt', 'mr', 'mb', 'ml']) {
572
- classes.push(`${prefix}-auto`);
573
- }
574
- // Special non-scale values for position: auto, fractions, full
575
- for (const prefix of ['top', 'right', 'bottom', 'left']) {
576
- classes.push(`${prefix}-auto`);
577
- classes.push(`${prefix}-full`);
578
- for (const frac of ['1/2', '1/3', '2/3', '1/4', '3/4']) {
579
- classes.push(`${prefix}-${frac}`);
580
- }
581
- }
582
- // Special non-scale values for inset: auto, full, fractions
583
- for (const prefix of ['inset', 'inset-x', 'inset-y']) {
584
- classes.push(`${prefix}-auto`);
585
- classes.push(`${prefix}-full`);
586
- for (const frac of ['1/2', '1/3', '2/3', '1/4', '3/4']) {
587
- classes.push(`${prefix}-${frac}`);
588
- }
589
- }
590
- // Leading (line-height) — spacing values + text keywords
591
- for (const value of spacingScaleValues) {
592
- classes.push(`leading-${value}`);
593
- }
594
- for (const kw of ['none', 'tight', 'snug', 'normal', 'relaxed', 'loose']) {
595
- classes.push(`leading-${kw}`);
596
- }
597
- // Indent — spacing scale values
598
- for (const value of spacingScaleValues) {
599
- classes.push(`indent-${value}`);
600
- }
601
- // Border-width directional — only major widths for border-{side}-{width}
602
- for (const side of ['t', 'r', 'b', 'l', 'x', 'y']) {
603
- for (const w of ['0', '2', '4', '8']) {
604
- classes.push(`border-${side}-${w}`);
605
- }
606
- }
607
- // Opacity modifiers for bg/text/shadow/ring (non-divider color prefixes)
608
- // — divider colors (border/ring/placeholder/divide/text) already generated
609
- const nonDividerOpacityPrefixes = ['bg', 'shadow'];
610
- const opacityValues = ['0', '5', '10', '20', '25', '30', '40', '50', '60', '70', '75', '80', '90', '95', '100'];
611
- for (const prefix of nonDividerOpacityPrefixes) {
612
- for (const op of opacityValues) {
613
- classes.push(`${prefix}-opacity-${op}`);
614
- }
615
- }
616
- // Translate — fractions and full
617
- for (const prefix of ['translate-x', 'translate-y']) {
618
- for (const frac of ['1/2', '1/3', '2/3', '1/4', '3/4', 'full']) {
619
- classes.push(`${prefix}-${frac}`);
620
- }
621
- }
622
- // Grid column spans — col-span-{1..12}, col-start-{1..13}, col-end-{1..13}
623
- for (let i = 1; i <= 12; i++)
624
- classes.push(`col-span-${i}`);
625
- for (let i = 1; i <= 13; i++) {
626
- classes.push(`col-start-${i}`);
627
- classes.push(`col-end-${i}`);
628
- }
629
- // Grid row spans — row-span-{1..6}, row-start-{1..7}, row-end-{1..7}
630
- for (let i = 1; i <= 6; i++)
631
- classes.push(`row-span-${i}`);
632
- for (let i = 1; i <= 7; i++) {
633
- classes.push(`row-start-${i}`);
634
- classes.push(`row-end-${i}`);
635
- }
636
- // Outline utilities
637
- classes.push('outline-none', 'outline', 'outline-dashed', 'outline-dotted', 'outline-double');
638
- return classes;
639
- }
640
- // ─── Main Expander API ──────────────────────────────────────────────────────
641
- /** Cached bundled utility classes. */
642
- let _bundledClasses = null;
43
+ const OPACITY_MODIFIER_RE = /^(.+)\/(\d{1,3})$/;
643
44
  /**
644
- * Get the full set of bundled Tailwind utility classes.
645
- * Generated from the default theme scales plus static utilities.
646
- * Cached after first call callers get the same Set instance.
45
+ * Regex matching negative-utility syntax: `-utility`.
46
+ * Any utility prefixed with a leading `-` is structurally a negative
47
+ * Tailwind utility. We strip the `-` and check the positive utility.
647
48
  */
648
- export function getBundledUtilityClasses() {
649
- if (_bundledClasses)
650
- return _bundledClasses;
651
- _bundledClasses = new Set();
652
- // Static utilities
653
- for (const cls of getStaticUtilityClasses()) {
654
- _bundledClasses.add(cls);
655
- }
656
- // Scale-generated utilities
657
- for (const cls of generateScaleBasedClasses()) {
658
- _bundledClasses.add(cls);
659
- }
660
- return _bundledClasses;
661
- }
49
+ const NEGATIVE_UTILITY_RE = /^-(.+)$/;
50
+ // ---------------------------------------------------------------------------
51
+ // TailwindUtilityExpander
52
+ // ---------------------------------------------------------------------------
662
53
  /**
663
- * Tailwind utility class expander.
54
+ * Tailwind utility class expander backed by compile-probe.
664
55
  *
665
- * Validates a class name against the full expansion path:
666
- * 1. Bundled default theme dictionary
667
- * 2. Project config custom classes (if resolved)
668
- * 3. Arbitrary-value grammar (`prefix-[...]`)
669
- * 4. Variant prefix stripping + retry against all of the above
56
+ * Validation pipeline:
57
+ * 1. Probe cache (compile-probe or config-generation validated classes)
58
+ * 2. User-supplied custom classes
59
+ * 3. Opacity-modifier syntax (`utility/opacity`)
60
+ * 4. Arbitrary-value grammar (`prefix-[...]`)
61
+ * 5. Negative-utility syntax (`-utility`)
62
+ * 6. Variant prefix stripping + retry against all of the above
670
63
  *
671
- * Fail-open: if project config resolution is requested but fails, the
672
- * expander enters fail-open mode where project-config tier is silently
673
- * empty. The CALLER is responsible for emitting the visible warning and
64
+ * Fail-open: if the probe fails to initialize, configFailed is true.
65
+ * The caller is responsible for emitting a visible warning and
674
66
  * potentially disabling the undefined-class detector.
675
67
  */
676
68
  export class TailwindUtilityExpander {
677
- bundled = null;
678
- projectClasses = null;
69
+ probe = null;
70
+ customClasses = null;
679
71
  _configFailed = false;
680
72
  _configFailureReason = null;
681
73
  /**
682
74
  * Initialize the expander with optional project config.
683
- * Call once at the start of a project audit.
75
+ * Call once per audit. Async because it may compile-probe Tailwind.
684
76
  */
685
- init(config = {}) {
686
- this.bundled = getBundledUtilityClasses();
687
- if (config.customClasses) {
688
- this.projectClasses = new Set(config.customClasses);
689
- return;
690
- }
77
+ async init(config = {}) {
78
+ this.customClasses = config.customClasses ?? null;
691
79
  if (config.useProjectConfig && config.projectRoot) {
692
- try {
693
- const result = loadTailwindConfig(config.projectRoot);
694
- if (result && result.tokens) {
695
- this.projectClasses = new Set();
696
- // Generate utility classes from project-specific theme tokens
697
- const colorPrefixes = ['bg', 'text', 'border', 'ring', 'shadow', 'fill', 'stroke', 'accent', 'caret'];
698
- for (const colorName of Object.keys(result.tokens.colors)) {
699
- for (const prefix of colorPrefixes) {
700
- this.projectClasses.add(`${prefix}-${colorName}`);
701
- }
702
- }
703
- for (const spaceName of Object.keys(result.tokens.spacing)) {
704
- for (const prefix of [
705
- 'p', 'px', 'py', 'pt', 'pr', 'pb', 'pl',
706
- 'm', 'mx', 'my', 'mt', 'mr', 'mb', 'ml',
707
- 'w', 'h', 'min-w', 'min-h', 'max-w', 'max-h',
708
- 'gap', 'gap-x', 'gap-y', 'space-x', 'space-y',
709
- 'top', 'right', 'bottom', 'left', 'inset',
710
- 'inset-x', 'inset-y',
711
- 'leading', 'indent',
712
- ]) {
713
- this.projectClasses.add(`${prefix}-${spaceName}`);
714
- }
715
- }
716
- for (const sizeName of Object.keys(result.tokens.fontSize)) {
717
- this.projectClasses.add(`text-${sizeName}`);
718
- }
719
- for (const radiusName of Object.keys(result.tokens.borderRadius)) {
720
- this.projectClasses.add(`rounded-${radiusName}`);
721
- }
722
- }
723
- }
724
- catch (err) {
80
+ this.probe = new TailwindProbe();
81
+ const result = await this.probe.init(config.projectRoot);
82
+ if (!result.ok) {
725
83
  this._configFailed = true;
726
- this._configFailureReason = err instanceof Error ? err.message : String(err);
84
+ this._configFailureReason = result.error ?? 'Unknown probe init failure';
85
+ this.probe = null;
727
86
  }
728
87
  }
729
88
  }
730
- /** Did the project config resolution fail? */
89
+ /** Did the probe initialization fail? */
731
90
  get configFailed() {
732
91
  return this._configFailed;
733
92
  }
734
- /** Why did project config resolution fail? */
93
+ /** Why did probe initialization fail? */
735
94
  get configFailureReason() {
736
95
  return this._configFailureReason;
737
96
  }
97
+ /** Whether the probe is ready (initialized successfully). */
98
+ get probeReady() {
99
+ return this.probe?.ready ?? false;
100
+ }
101
+ /** Human-readable source of validation data. */
102
+ get probeSource() {
103
+ return this.probe?.source ?? null;
104
+ }
738
105
  /**
739
- * Resolve a single class name against the expansion path.
740
- * Returns the highest-priority matching tier, or 'none'.
106
+ * Validate a batch of unknown class names against the compile-probe.
107
+ * Call this BEFORE resolve() to pre-populate the validation cache.
108
+ *
109
+ * Classes that pass validation are cached internally; resolve() will
110
+ * find them on subsequent calls.
111
+ *
112
+ * Returns the full set of all validated classes (including previously
113
+ * cached ones).
114
+ */
115
+ async validateBatch(candidates) {
116
+ if (!this.probe?.ready) {
117
+ // No probe — use custom classes only
118
+ return this.customClasses ?? new Set();
119
+ }
120
+ return this.probe.validateBatch(candidates);
121
+ }
122
+ /**
123
+ * Resolve a single class name against the validation pipeline.
124
+ * Synchronous — only checks cached data + structural patterns.
125
+ * Call validateBatch() first to populate the cache for unknown classes.
741
126
  */
742
127
  resolve(className) {
743
- const bundled = this.bundled ?? getBundledUtilityClasses();
744
- // 1. Check raw class against bundled dictionary
745
- if (bundled.has(className)) {
746
- return { valid: true, tier: 'bundled' };
128
+ // 1. Check probe cache
129
+ if (this.probe?.isCached(className)) {
130
+ return { valid: true, tier: 'probe' };
747
131
  }
748
- // 2. Check raw class against project config
749
- if (this.projectClasses?.has(className)) {
750
- return { valid: true, tier: 'project-config' };
132
+ // 2. Check custom (user-supplied) classes
133
+ if (this.customClasses?.has(className)) {
134
+ return { valid: true, tier: 'probe' };
751
135
  }
752
- // 3. Check opacity-modifier syntax: {color-utility}/{opacity}
136
+ // 3. Check opacity-modifier syntax: {utility}/{opacity}
753
137
  if (className.includes('/')) {
754
138
  const result = this.checkOpacityModifier(className);
755
139
  if (result)
@@ -759,79 +143,70 @@ export class TailwindUtilityExpander {
759
143
  if (this.matchesArbitraryValue(className)) {
760
144
  return { valid: true, tier: 'arbitrary-value' };
761
145
  }
762
- // 5. Check negative-value utility: -{prefix}-{value}
146
+ // 5. Check negative-utility syntax: -{utility}
763
147
  const negResult = this.checkNegativeUtility(className);
764
148
  if (negResult)
765
149
  return negResult;
766
150
  // 6. Strip variant prefix and retry
767
151
  const stripped = this.stripVariantPrefix(className);
768
152
  if (stripped !== className) {
769
- return this.resolve(stripped); // Recurse once with the stripped class
153
+ return this.resolve(stripped);
770
154
  }
771
155
  return { valid: false, tier: 'none' };
772
156
  }
773
157
  /**
774
158
  * Check whether a class matches the arbitrary-value grammar:
775
- * `{known-prefix}-[...]`
159
+ * `{prefix}-[...]` — any word prefix followed by `-[...]`.
160
+ *
161
+ * Structural check only — does not enumerate valid prefixes.
162
+ * The Tailwind compiler is the authority on whether a prefix
163
+ * actually accepts arbitrary values; our job is to not false-positive
164
+ * on obviously-arbitrary syntax.
776
165
  */
777
166
  matchesArbitraryValue(className) {
778
- const bracketIdx = className.indexOf('[');
779
- if (bracketIdx <= 0)
780
- return false;
781
- // Extract the prefix before the [
782
- const prefix = className.substring(0, bracketIdx);
783
- // Handle prefix-[value] where prefix ends with '-'
784
- if (prefix.endsWith('-')) {
785
- const normalizedPrefix = prefix.slice(0, -1);
786
- return ARBITRARY_VALUE_PREFIXES.has(normalizedPrefix);
787
- }
788
- return false;
167
+ return ARBITRARY_VALUE_RE.test(className);
789
168
  }
790
169
  /**
791
- * Check the opacity-modifier syntax: `{utility}/{opacity}`.
792
- * e.g., `bg-red-500/50` → `bg-red-500` must be a valid utility AND
793
- * `50` must be a valid opacity value.
170
+ * Check opacity-modifier syntax: `{utility}/{opacity}`.
794
171
  *
795
- * We split on the LAST `/` to avoid confusing fractional widths
796
- * (e.g. `w-1/2`) those are in the dictionary directly and won't
797
- * reach this check.
172
+ * Split on the LAST `/` to avoid confusing fractional widths
173
+ * (e.g. `w-1/2`). The prefix must be a known utility, suffix
174
+ * must be a numeric opacity (0-100).
798
175
  */
799
176
  checkOpacityModifier(className) {
800
- const lastSlash = className.lastIndexOf('/');
801
- if (lastSlash <= 0)
802
- return null;
803
- const prefix = className.substring(0, lastSlash);
804
- const suffix = className.substring(lastSlash + 1);
805
- // Suffix must be a numeric opacity value (0-100)
806
- if (!/^\d{1,3}$/.test(suffix))
177
+ const m = OPACITY_MODIFIER_RE.exec(className);
178
+ if (!m)
807
179
  return null;
180
+ const prefix = m[1];
181
+ const suffix = m[2];
808
182
  const opacity = parseInt(suffix, 10);
809
183
  if (opacity < 0 || opacity > 100)
810
184
  return null;
811
- // Prefix must be a known utility
812
- const bundled = this.bundled ?? getBundledUtilityClasses();
813
- if (bundled.has(prefix) || this.projectClasses?.has(prefix)) {
814
- return { valid: true, tier: 'project-config' };
185
+ // Prefix must be a known utility — check against probe cache
186
+ // and custom classes (structural check isn't enough here because
187
+ // "foo-bar/50" should not match unless "foo-bar" is valid).
188
+ if (this.probe?.isCached(prefix) || this.customClasses?.has(prefix)) {
189
+ return { valid: true, tier: 'probe' };
190
+ }
191
+ // Also check if the prefix itself resolves (handles variant-prefixed
192
+ // utilities with opacity: hover:bg-red-500/50)
193
+ const prefixResolution = this.resolve(prefix);
194
+ if (prefixResolution.valid) {
195
+ return { valid: true, tier: prefixResolution.tier };
815
196
  }
816
197
  return null;
817
198
  }
818
199
  /**
819
- * Check a negative utility: `-{prefix}-{value}`.
820
- * e.g., `-mt-1`, `-top-2`, `-translate-x-1/2`, `-z-10`.
200
+ * Check negative-utility syntax: `-{utility}`.
821
201
  *
822
- * Strips the leading `-` and checks whether the remainder is a valid
823
- * utility whose prefix is in the negative-allowed set.
202
+ * Structural check — strips the leading `-` and verifies the positive
203
+ * utility itself is valid. No hand-curated allowed-prefixes list.
824
204
  */
825
205
  checkNegativeUtility(className) {
826
- if (!className.startsWith('-'))
827
- return null;
828
- const positive = className.slice(1); // strip leading '-'
829
- // Determine the prefix of the positive utility
830
- // e.g., "top-2" → prefix="top", "translate-x-1/2" → prefix="translate-x"
831
- const lastDash = positive.lastIndexOf('-');
832
- const prefix = lastDash > 0 ? positive.substring(0, lastDash) : positive;
833
- if (!NEGATIVE_ALLOWED_PREFIXES.has(prefix))
206
+ const m = NEGATIVE_UTILITY_RE.exec(className);
207
+ if (!m)
834
208
  return null;
209
+ const positive = m[1];
835
210
  // Recurse: is the positive utility itself valid?
836
211
  const result = this.resolve(positive);
837
212
  if (result.valid) {
@@ -841,14 +216,19 @@ export class TailwindUtilityExpander {
841
216
  }
842
217
  /**
843
218
  * Strip a Tailwind variant prefix from a class name.
219
+ * Uses structural regex — matches any `word:` or `word-word:` prefix.
220
+ *
844
221
  * E.g., "hover:bg-blue-500" → "bg-blue-500"
222
+ * "md:w-full" → "w-full"
223
+ * "group-hover:text-red" → "text-red"
224
+ * "[&_>_a]:text-blue" → "text-blue" (arbitrary variant)
225
+ *
845
226
  * Returns the original if no variant prefix is matched.
846
227
  */
847
228
  stripVariantPrefix(className) {
848
- for (const prefix of VARIANT_PREFIXES) {
849
- if (className.startsWith(prefix + ':')) {
850
- return className.slice(prefix.length + 1);
851
- }
229
+ const m = VARIANT_PREFIX_RE.exec(className);
230
+ if (m) {
231
+ return className.slice(m[0].length);
852
232
  }
853
233
  return className;
854
234
  }
@@ -856,17 +236,19 @@ export class TailwindUtilityExpander {
856
236
  * Check if a class has a variant prefix at all.
857
237
  */
858
238
  hasVariantPrefix(className) {
859
- return className.includes(':') && this.stripVariantPrefix(className) !== className;
239
+ return VARIANT_PREFIX_RE.test(className);
860
240
  }
861
241
  /** Reset state (useful for testing). */
862
242
  reset() {
863
- this.bundled = null;
864
- this.projectClasses = null;
243
+ this.probe = null;
244
+ this.customClasses = null;
865
245
  this._configFailed = false;
866
246
  this._configFailureReason = null;
867
247
  }
868
248
  }
869
- /** Singleton instance for the analyzer layer. */
249
+ // ---------------------------------------------------------------------------
250
+ // Singleton
251
+ // ---------------------------------------------------------------------------
870
252
  let _instance = null;
871
253
  export function getTailwindExpander() {
872
254
  if (!_instance) {