@vueland/utils-jit 0.2.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 ADDED
@@ -0,0 +1,1109 @@
1
+ <div align="center">
2
+ <img src="logo.png" style="max-width: 100%;">
3
+ </div>
4
+
5
+ # @vueland/utils-jit
6
+
7
+ A lightweight Vite JIT utility engine for generating CSS from arbitrary utility classes.
8
+
9
+ `@vueland/utils-jit` scans your project files, detects utility tokens such as `w-[320px]`, `px-[16px]`, `hover:bg-[#fff]`, and generates only the CSS that is actually used in your source code.
10
+
11
+ It is designed for Vueland and Vue 3 projects, but can be used in any Vite-based application.
12
+
13
+ ## Features
14
+ - JIT CSS generation for arbitrary utility classes
15
+ - Vite plugin integration
16
+ - Incremental updates during development
17
+ - File reference counting to remove unused generated rules
18
+ - Built-in utilities for sizing, spacing, radius, position, z-index, opacity and colors
19
+ - Built-in pseudo variants such as `hover:`, `focus:` and `active:`
20
+ - Responsive variants through configurable breakpoints
21
+ - Custom selector, attribute and media variants
22
+ - Custom rules through `defineRule`
23
+ - Safe value validation before CSS generation
24
+ - Configurable include / exclude patterns
25
+ - Configurable output file
26
+
27
+ ## Installation
28
+
29
+ ```bash
30
+ pnpm add -D @vueland/utils-jit
31
+ ```
32
+
33
+ ```bash
34
+ npm install -D @vueland/utils-jit
35
+ ```
36
+
37
+ ```bash
38
+ yarn add -D @vueland/utils-jit
39
+ ```
40
+
41
+ ## Usage
42
+
43
+ Add `utilsJIT()` to your `vite.config.ts`.
44
+
45
+ ```ts
46
+ import { defineConfig } from 'vite'
47
+ import vue from '@vitejs/plugins-vue'
48
+ import { utilsJIT } from '@vueland/utils-jit'
49
+
50
+ export default defineConfig({
51
+ plugins: [
52
+ vue(),
53
+ utilsJIT(),
54
+ ],
55
+ })
56
+ ```
57
+
58
+ By default, the plugin generates this file:
59
+
60
+ ```txt
61
+ src/.generated/utils-jit.css
62
+ ```
63
+
64
+ Import it in your application entry file:
65
+
66
+ ```ts
67
+ import './.generated/utils-jit.css'
68
+ ```
69
+
70
+ Now you can use arbitrary utility classes in your components:
71
+
72
+ ```vue
73
+ <template>
74
+ <div class="w-[320px] px-[16px] radius-[12px] hover:w-[360px] md:px-[24px]">
75
+ Card content
76
+ </div>
77
+ </template>
78
+ ```
79
+
80
+ The generated CSS will contain only the rules used in your source files.
81
+
82
+ ## Quick example
83
+
84
+ ```vue
85
+ <template>
86
+ <div class="w-[300px] h-[200px] px-[16px] radius-[12px] z-[10]">
87
+ Hello Vueland
88
+ </div>
89
+ </template>
90
+ ```
91
+
92
+ Generated CSS example:
93
+
94
+ ```css
95
+ /* @vueland/utils-jit: generated utilities */
96
+ .h-\[200px\]{height: 200px !important;}
97
+ .px-\[16px\]{padding-left: 16px !important;padding-right: 16px !important;}
98
+ .radius-\[12px\]{border-radius: 12px !important;}
99
+ .w-\[300px\]{width: 300px !important;}
100
+ .z-\[10\]{z-index: 10 !important;}
101
+ ```
102
+
103
+ Generated rules are sorted by utility token for stable output.
104
+
105
+ ## Built-in utilities
106
+
107
+ | Utility | CSS property | Example |
108
+ | --- | --- | --- |
109
+ | `w-[value]` | `width` | `w-[320px]` |
110
+ | `h-[value]` | `height` | `h-[200px]` |
111
+ | `min-w-[value]` | `min-width` | `min-w-[240px]` |
112
+ | `max-w-[value]` | `max-width` | `max-w-[1200px]` |
113
+ | `min-h-[value]` | `min-height` | `min-h-[100vh]` |
114
+ | `max-h-[value]` | `max-height` | `max-h-[600px]` |
115
+ | `ma-[value]` | `margin` | `ma-[16px]` |
116
+ | `mx-[value]` | `margin-left`, `margin-right` | `mx-[auto]` |
117
+ | `my-[value]` | `margin-top`, `margin-bottom` | `my-[24px]` |
118
+ | `mt-[value]` | `margin-top` | `mt-[16px]` |
119
+ | `mr-[value]` | `margin-right` | `mr-[12px]` |
120
+ | `mb-[value]` | `margin-bottom` | `mb-[24px]` |
121
+ | `ml-[value]` | `margin-left` | `ml-[12px]` |
122
+ | `pa-[value]` | `padding` | `pa-[20px]` |
123
+ | `px-[value]` | `padding-left`, `padding-right` | `px-[16px]` |
124
+ | `py-[value]` | `padding-top`, `padding-bottom` | `py-[12px]` |
125
+ | `pt-[value]` | `padding-top` | `pt-[10px]` |
126
+ | `pr-[value]` | `padding-right` | `pr-[8px]` |
127
+ | `pb-[value]` | `padding-bottom` | `pb-[20px]` |
128
+ | `pl-[value]` | `padding-left` | `pl-[16px]` |
129
+ | `left-[value]` | `left` | `left-[12px]` |
130
+ | `right-[value]` | `right` | `right-[12px]` |
131
+ | `top-[value]` | `top` | `top-[12px]` |
132
+ | `bottom-[value]` | `bottom` | `bottom-[12px]` |
133
+ | `inset-[value]` | `inset` | `inset-[0px]` |
134
+ | `radius-[value]` | `border-radius` | `radius-[12px]` |
135
+ | `radius-tl-[value]` | `border-top-left-radius` | `radius-tl-[8px]` |
136
+ | `radius-tr-[value]` | `border-top-right-radius` | `radius-tr-[8px]` |
137
+ | `radius-bl-[value]` | `border-bottom-left-radius` | `radius-bl-[8px]` |
138
+ | `radius-br-[value]` | `border-bottom-right-radius` | `radius-br-[8px]` |
139
+ | `z-[value]` | `z-index` | `z-[100]` |
140
+ | `opacity-[value]` | `opacity` | `opacity-[0.64]` |
141
+ | `color-[value]` | `color` | `color-[#111]` |
142
+ | `bg-[value]` | `background-color` | `bg-[#fff]` |
143
+
144
+ Built-in rules generate declarations with `!important`.
145
+
146
+ ## Supported values
147
+
148
+ Values are validated before CSS is generated. Invalid or unsafe values are ignored.
149
+
150
+ ### Size, padding, radius and position
151
+
152
+ The following utilities support length-like values:
153
+
154
+ - `w`
155
+ - `h`
156
+ - `min-w`
157
+ - `max-w`
158
+ - `min-h`
159
+ - `max-h`
160
+ - `pa`
161
+ - `px`
162
+ - `py`
163
+ - `pt`
164
+ - `pr`
165
+ - `pb`
166
+ - `pl`
167
+ - `radius`
168
+ - `left`
169
+ - `right`
170
+ - `top`
171
+ - `bottom`
172
+ - `inset`
173
+
174
+ Examples:
175
+
176
+ ```html
177
+ <div class="w-[320px]"></div>
178
+ <div class="h-[50%]"></div>
179
+ <div class="px-[1rem]"></div>
180
+ <div class="radius-[12px]"></div>
181
+ <div class="left-[10vw]"></div>
182
+ <div class="w-[calc(100%-32px)]"></div>
183
+ <div class="max-w-[clamp(320px,50vw,960px)]"></div>
184
+ <div class="h-[var(--panel-height)]"></div>
185
+ ```
186
+
187
+ Supported units:
188
+
189
+ ```txt
190
+ px, em, rem, %, vw, vh, svw, svh, lvw, lvh, dvw, dvh, vmin, vmax, ch, ex, cm, mm, in, pt, pc
191
+ ```
192
+
193
+ Supported functions:
194
+
195
+ ```txt
196
+ calc(), min(), max(), clamp(), var()
197
+ ```
198
+
199
+ ### Margin
200
+
201
+ Margin utilities support length-like values and `auto`.
202
+
203
+ ```html
204
+ <div class="ma-[16px]"></div>
205
+ <div class="mx-[auto]"></div>
206
+ <div class="mt-[2rem]"></div>
207
+ <div class="mb-[calc(100%-20px)]"></div>
208
+ <div class="ma-[10px 20px]"></div>
209
+ ```
210
+
211
+ ### Padding
212
+
213
+ Padding utilities support length-like values.
214
+
215
+ ```html
216
+ <div class="pa-[16px]"></div>
217
+ <div class="px-[12px]"></div>
218
+ <div class="py-[8px 12px]"></div>
219
+ ```
220
+
221
+ `auto` is not valid for padding and will be ignored.
222
+
223
+ ### Radius
224
+
225
+ Radius utilities support length-like values.
226
+
227
+ ```html
228
+ <div class="radius-[8px]"></div>
229
+ <div class="radius-[8px 12px]"></div>
230
+ <div class="radius-tl-[16px]"></div>
231
+ ```
232
+
233
+ ### Z-index
234
+
235
+ `z-[value]` supports numbers, `auto` and CSS variables.
236
+
237
+ ```html
238
+ <div class="z-[1]"></div>
239
+ <div class="z-[999]"></div>
240
+ <div class="z-[auto]"></div>
241
+ <div class="z-[var(--z-modal)]"></div>
242
+ ```
243
+
244
+ ### Opacity
245
+
246
+ `opacity-[value]` supports values from `0` to `1`, CSS variables and global CSS values.
247
+
248
+ ```html
249
+ <div class="opacity-[0]"></div>
250
+ <div class="opacity-[0.64]"></div>
251
+ <div class="opacity-[1]"></div>
252
+ <div class="opacity-[var(--opacity)]"></div>
253
+ ```
254
+
255
+ ### Color and background-color
256
+
257
+ `color-[value]` and `bg-[value]` support hex colors, CSS color functions, CSS variables and selected keyword values.
258
+
259
+ ```html
260
+ <div class="color-[#111]"></div>
261
+ <div class="bg-[#fff]"></div>
262
+ <div class="bg-[rgb(255,255,255)]"></div>
263
+ <div class="color-[oklch(60% 0.2 20)]"></div>
264
+ <div class="bg-[var(--vl-surface)]"></div>
265
+ <div class="color-[currentColor]"></div>
266
+ ```
267
+
268
+ Invalid values are ignored:
269
+
270
+ ```html
271
+ <div class="w-[;]"></div>
272
+ <div class="radius-[.]"></div>
273
+ <div class="px-[auto]"></div>
274
+ <div class="z-[10px]"></div>
275
+ <div class="opacity-[2]"></div>
276
+ ```
277
+
278
+ ## Variants
279
+
280
+ Variants are added before the utility name using `:`.
281
+
282
+ ```html
283
+ <div class="hover:w-[320px] md:px-[24px]"></div>
284
+ ```
285
+
286
+ ### Built-in pseudo variants
287
+
288
+ The following pseudo variants are available by default:
289
+
290
+ ```txt
291
+ hover
292
+ focus
293
+ focus-visible
294
+ focus-within
295
+ active
296
+ disabled
297
+ checked
298
+ visited
299
+ first
300
+ last
301
+ odd
302
+ even
303
+ ```
304
+
305
+ Example:
306
+
307
+ ```vue
308
+ <template>
309
+ <button class="w-[160px] hover:w-[180px] focus:px-[20px] active:radius-[10px]">
310
+ Button
311
+ </button>
312
+ </template>
313
+ ```
314
+
315
+ Generated CSS:
316
+
317
+ ```css
318
+ .hover\:w-\[180px\]:hover{width: 180px !important;}
319
+ .focus\:px-\[20px\]:focus{padding-left: 20px !important;padding-right: 20px !important;}
320
+ .active\:radius-\[10px\]:active{border-radius: 10px !important;}
321
+ ```
322
+
323
+ ## Responsive variants
324
+
325
+ The default breakpoints are:
326
+
327
+ ```ts
328
+ {
329
+ sm: 640,
330
+ md: 768,
331
+ lg: 1024,
332
+ xl: 1280,
333
+ '2xl': 1536,
334
+ }
335
+ ```
336
+
337
+ Example:
338
+
339
+ ```vue
340
+ <template>
341
+ <div class="w-[100%] md:w-[720px] lg:w-[960px] xl:w-[1200px] 2xl:w-[1440px]">
342
+ Container
343
+ </div>
344
+ </template>
345
+ ```
346
+
347
+ Generated CSS:
348
+
349
+ ```css
350
+ @media (min-width: 768px) { .md\:w-\[720px\]{width: 720px !important;} }
351
+ @media (min-width: 1024px) { .lg\:w-\[960px\]{width: 960px !important;} }
352
+ @media (min-width: 1280px) { .xl\:w-\[1200px\]{width: 1200px !important;} }
353
+ @media (min-width: 1536px) { .2xl\:w-\[1440px\]{width: 1440px !important;} }
354
+ ```
355
+
356
+ ## Custom variants
357
+
358
+ You can add custom selector, attribute and media variants.
359
+
360
+ ```ts
361
+ utilsJIT({
362
+ variants: {
363
+ hocus: {
364
+ kind: 'selector',
365
+ value: '&:hover,&:focus',
366
+ },
367
+ selected: {
368
+ kind: 'attribute',
369
+ value: '[aria-selected="true"]',
370
+ },
371
+ tablet: {
372
+ kind: 'media',
373
+ value: 900,
374
+ },
375
+ },
376
+ })
377
+ ```
378
+
379
+ Usage:
380
+
381
+ ```html
382
+ <div class="hocus:w-[320px] selected:bg-[#eee] tablet:px-[24px]"></div>
383
+ ```
384
+
385
+ Generated CSS:
386
+
387
+ ```css
388
+ .hocus\:w-\[320px\]:hover,.hocus\:w-\[320px\]:focus{width: 320px !important;}
389
+ .selected\:bg-\[\#eee\][aria-selected="true"]{background-color: #eee !important;}
390
+ @media (min-width: 900px) { .tablet\:px-\[24px\]{padding-left: 24px !important;padding-right: 24px !important;} }
391
+ ```
392
+
393
+ ### Theme variants
394
+ Dark mode is part of the application theme strategy. Different projects may implement it through `.dark`, `data-theme`, CSS variables, a provider, or a custom theme layer. The plugin does not enforce one specific approach.
395
+
396
+ If you need `dark:`, add it explicitly.
397
+
398
+ Using `data-theme`:
399
+
400
+ ```ts
401
+ utilsJIT({
402
+ variants: {
403
+ dark: {
404
+ kind: 'selector',
405
+ value: '[data-theme="dark"] &',
406
+ },
407
+ },
408
+ })
409
+ ```
410
+
411
+ Usage:
412
+
413
+ ```html
414
+ <div class="bg-[#fff] dark:bg-[#111] color-[#111] dark:color-[#fff]"></div>
415
+ ```
416
+
417
+ Generated CSS:
418
+
419
+ ```css
420
+ [data-theme="dark"] .dark\:bg-\[\#111\]{background-color: #111 !important;}
421
+ [data-theme="dark"] .dark\:color-\[\#fff\]{color: #fff !important;}
422
+ ```
423
+
424
+ Using `.dark`:
425
+
426
+ ```ts
427
+ utilsJIT({
428
+ variants: {
429
+ dark: {
430
+ kind: 'selector',
431
+ value: '.dark &',
432
+ },
433
+ },
434
+ })
435
+ ```
436
+
437
+ Usage:
438
+
439
+ ```html
440
+ <div class="dark:bg-[#111]"></div>
441
+ ```
442
+
443
+ Generated CSS:
444
+
445
+ ```css
446
+ .dark .dark\:bg-\[\#111\]{background-color: #111 !important;}
447
+ ```
448
+
449
+ ## Combining variants
450
+
451
+ Pseudo variants, custom selector variants and responsive variants can be combined.
452
+
453
+ `hocus:` is not built in. Add it first:
454
+
455
+ ```ts
456
+ utilsJIT({
457
+ variants: {
458
+ hocus: {
459
+ kind: 'selector',
460
+ value: '&:hover,&:focus',
461
+ },
462
+ },
463
+ })
464
+ ```
465
+
466
+ Then it can be combined with responsive variants:
467
+
468
+ ```vue
469
+ <template>
470
+ <button class="hover:md:w-[240px] focus:lg:px-[32px] hocus:xl:bg-[#eee]">
471
+ Responsive button
472
+ </button>
473
+ </template>
474
+ ```
475
+
476
+ Generated CSS:
477
+
478
+ ```css
479
+ @media (min-width: 768px) { .hover\:md\:w-\[240px\]:hover{width: 240px !important;} }
480
+ @media (min-width: 1024px) { .focus\:lg\:px-\[32px\]:focus{padding-left: 32px !important;padding-right: 32px !important;} }
481
+ @media (min-width: 1280px) { .hocus\:xl\:bg-\[\#eee\]:hover,.hocus\:xl\:bg-\[\#eee\]:focus{background-color: #eee !important;} }
482
+ ```
483
+
484
+ ## Configuration
485
+
486
+ `utilsJIT` accepts an options object.
487
+
488
+ ```ts
489
+ import { defineConfig } from 'vite'
490
+ import vue from '@vitejs/plugins-vue'
491
+ import { utilsJIT } from '@vueland/utils-jit'
492
+
493
+ export default defineConfig({
494
+ plugins: [
495
+ vue(),
496
+ utilsJIT({
497
+ outFile: 'src/.generated/utils-jit.css',
498
+ include: [/\.(vue|js|ts|jsx|tsx|html)$/],
499
+ exclude: [/src\/fixtures/],
500
+ breakpoints: {
501
+ xs: 480,
502
+ sm: 640,
503
+ md: 768,
504
+ lg: 1024,
505
+ xl: 1280,
506
+ '2xl': 1536,
507
+ },
508
+ debug: false,
509
+ }),
510
+ ],
511
+ })
512
+ ```
513
+
514
+ ## Options
515
+
516
+ | Option | Type | Default | Description |
517
+ | --- | --- | --- | --- |
518
+ | `include` | `Array<string \| RegExp>` | `[/\.(vue\|js\|ts\|jsx\|tsx\|html)$/]` | Files to scan. |
519
+ | `exclude` | `Array<string \| RegExp>` | Internal service directories | Files and directories to ignore. |
520
+ | `outFile` | `string` | `src/.generated/utils-jit.css` | Generated CSS path relative to the Vite root. |
521
+ | `breakpoints` | `Record<string, number>` | `sm`, `md`, `lg`, `xl`, `2xl` | Responsive variants. |
522
+ | `rules` | `UtilityRule[]` | `[]` | Custom utility rules. |
523
+ | `variants` | `VariantMap` | Built-in variants | Custom variants. |
524
+ | `banner` | `string` | `/* @vueland/utils-jit: generated utilities */` | Banner at the top of the generated CSS file. |
525
+ | `emitEmptyFile` | `boolean` | `true` | Create a file with a comment when no utilities are found. |
526
+ | `debug` | `boolean` | `false` | Print diagnostic messages. |
527
+
528
+ ### outFile
529
+
530
+ Path to the generated CSS file relative to the Vite project root.
531
+
532
+ ```ts
533
+ utilsJIT({
534
+ outFile: 'src/styles/generated/utils.css',
535
+ })
536
+ ```
537
+
538
+ Then update your import accordingly:
539
+
540
+ ```ts
541
+ import './styles/generated/utils.css'
542
+ ```
543
+
544
+ ### include
545
+
546
+ Patterns for files that should be scanned.
547
+
548
+ Default:
549
+
550
+ ```ts
551
+ [/\.(vue|js|ts|jsx|tsx|html)$/]
552
+ ```
553
+
554
+ Example:
555
+
556
+ ```ts
557
+ utilsJIT({
558
+ include: [/\.(vue|ts)$/],
559
+ })
560
+ ```
561
+
562
+ ### exclude
563
+
564
+ Patterns for files and directories that should be excluded from the initial scan, transform and HMR.
565
+
566
+ The following directories are excluded by default:
567
+
568
+ ```txt
569
+ node_modules
570
+ .git
571
+ dist
572
+ build
573
+ coverage
574
+ .output
575
+ .nuxt
576
+ .turbo
577
+ .generated
578
+ storybook-static
579
+ playwright-report
580
+ ```
581
+
582
+ Example:
583
+
584
+ ```ts
585
+ utilsJIT({
586
+ exclude: [
587
+ /src\/fixtures/,
588
+ /src\/legacy/,
589
+ 'storybook-static',
590
+ ],
591
+ })
592
+ ```
593
+
594
+ ### breakpoints
595
+
596
+ Responsive variants. The key is used as the class prefix, and the value is used as `min-width` in pixels.
597
+
598
+ ```ts
599
+ utilsJIT({
600
+ breakpoints: {
601
+ xs: 480,
602
+ sm: 640,
603
+ md: 768,
604
+ lg: 1024,
605
+ xl: 1280,
606
+ '2xl': 1536,
607
+ '3xl': 1920,
608
+ },
609
+ })
610
+ ```
611
+
612
+ Usage:
613
+
614
+ ```html
615
+ <div class="xs:w-[320px] 3xl:w-[1600px]"></div>
616
+ ```
617
+
618
+ ### variants
619
+
620
+ Custom variants extend the state and selector syntax.
621
+
622
+ ```ts
623
+ utilsJIT({
624
+ variants: {
625
+ hocus: {
626
+ kind: 'selector',
627
+ value: '&:hover,&:focus',
628
+ },
629
+ selected: {
630
+ kind: 'attribute',
631
+ value: '[aria-selected="true"]',
632
+ },
633
+ tablet: {
634
+ kind: 'media',
635
+ value: 900,
636
+ },
637
+ },
638
+ })
639
+ ```
640
+
641
+ ### emitEmptyFile
642
+
643
+ When `emitEmptyFile` is `true`, the plugin creates a file with this content if no utilities are found:
644
+
645
+ ```css
646
+ /* @vueland/utils-jit: no utilities found */
647
+ ```
648
+
649
+ When `emitEmptyFile` is `false`, the file is not created until at least one utility class is found.
650
+
651
+ ```ts
652
+ utilsJIT({
653
+ emitEmptyFile: false,
654
+ })
655
+ ```
656
+
657
+ ## Custom utility rules
658
+
659
+ The plugin can be extended with custom rules through `rules` and `defineRule`.
660
+
661
+ ```ts
662
+ import { defineConfig } from 'vite'
663
+ import vue from '@vitejs/plugins-vue'
664
+ import {
665
+ defineRule,
666
+ isColorValue,
667
+ isSizeValue,
668
+ utilsJIT,
669
+ } from '@vueland/utils-jit'
670
+
671
+ export default defineConfig({
672
+ plugins: [
673
+ vue(),
674
+ utilsJIT({
675
+ rules: [
676
+ defineRule({
677
+ name: 'surface',
678
+ matcher: /^surface-\[(.+)\]$/,
679
+ validate: isColorValue,
680
+ declaration: (value) => ({
681
+ backgroundColor: value,
682
+ }),
683
+ important: false,
684
+ }),
685
+
686
+ defineRule({
687
+ name: 'size',
688
+ matcher: /^size-\[(.+)\]$/,
689
+ validate: isSizeValue,
690
+ declaration: (value) => ({
691
+ width: value,
692
+ height: value,
693
+ }),
694
+ }),
695
+ ],
696
+ }),
697
+ ],
698
+ })
699
+ ```
700
+
701
+ Usage:
702
+
703
+ ```vue
704
+ <template>
705
+ <div class="surface-[#fff] size-[40px] hover:size-[48px]">
706
+ Custom utilities
707
+ </div>
708
+ </template>
709
+ ```
710
+
711
+ Generated CSS:
712
+
713
+ ```css
714
+ .surface-\[\#fff\]{background-color: #fff;}
715
+ .size-\[40px\]{width: 40px !important;height: 40px !important;}
716
+ .hover\:size-\[48px\]:hover{width: 48px !important;height: 48px !important;}
717
+ ```
718
+
719
+ ## defineRule API
720
+
721
+ ```ts
722
+ defineRule({
723
+ name: 'rule-name',
724
+ matcher: /^rule-name-\[(.+)\]$/,
725
+ validate: (value) => true,
726
+ declaration: (value) => ({
727
+ cssProperty: value,
728
+ }),
729
+ important: true,
730
+ })
731
+ ```
732
+
733
+ | Field | Type | Description |
734
+ | --- | --- | --- |
735
+ | `name` | `string` | Rule name for readability and debugging. |
736
+ | `matcher` | `RegExp` | Matcher for the utility part without variants. |
737
+ | `validate` | `(value: string) => boolean` | Validates the value inside `[]`. |
738
+ | `declaration` | `(value: string) => Record<string, string \| number> \| string[]` | Generates CSS declarations. |
739
+ | `important` | `boolean` | Adds `!important` to object-based declarations. Defaults to `true`. |
740
+
741
+ `declaration` usually returns a JS-style object:
742
+
743
+ ```ts
744
+ defineRule({
745
+ name: 'bg',
746
+ matcher: /^bg-\[(.+)\]$/,
747
+ validate: isColorValue,
748
+ declaration: (value) => ({
749
+ backgroundColor: value,
750
+ }),
751
+ })
752
+ ```
753
+
754
+ CamelCase CSS properties are converted to kebab-case:
755
+
756
+ ```ts
757
+ {
758
+ backgroundColor: '#fff',
759
+ borderTopLeftRadius: '8px',
760
+ }
761
+ ```
762
+
763
+ Result:
764
+
765
+ ```css
766
+ background-color: #fff !important;
767
+ border-top-left-radius: 8px !important;
768
+ ```
769
+
770
+ CSS variables are preserved:
771
+
772
+ ```ts
773
+ defineRule({
774
+ name: 'token',
775
+ matcher: /^token-\[(.+)\]$/,
776
+ declaration: (value) => ({
777
+ '--vl-token': value,
778
+ }),
779
+ important: false,
780
+ })
781
+ ```
782
+
783
+ Result:
784
+
785
+ ```css
786
+ --vl-token: #fff;
787
+ ```
788
+
789
+ If `declaration` returns `string[]`, the strings are treated as final CSS declarations. In this case, `!important` is not added automatically.
790
+
791
+ ```ts
792
+ defineRule({
793
+ name: 'raw',
794
+ matcher: /^raw-\[(.+)\]$/,
795
+ declaration: (value) => [
796
+ `--raw-value: ${value};`,
797
+ ],
798
+ })
799
+ ```
800
+
801
+ ## Validators
802
+
803
+ The package exports validators that can be reused in custom rules.
804
+
805
+ ```ts
806
+ import {
807
+ isColorValue,
808
+ isMarginValue,
809
+ isOpacityValue,
810
+ isPaddingValue,
811
+ isPositionValue,
812
+ isRadiusValue,
813
+ isSizeValue,
814
+ isZIndexValue,
815
+ } from '@vueland/utils-jit'
816
+ ```
817
+
818
+ Example:
819
+
820
+ ```ts
821
+ defineRule({
822
+ name: 'text',
823
+ matcher: /^text-\[(.+)\]$/,
824
+ validate: isColorValue,
825
+ declaration: (value) => ({
826
+ color: value,
827
+ }),
828
+ })
829
+ ```
830
+
831
+ For production rules, prefer strict validation.
832
+
833
+ ```ts
834
+ import { defineRule } from '@vueland/utils-jit'
835
+
836
+ const gridColsRule = defineRule({
837
+ name: 'grid-cols',
838
+ matcher: /^grid-cols-\[(.+)\]$/,
839
+ validate: (value) => /^\d+$/.test(value),
840
+ declaration: (value) => ({
841
+ gridTemplateColumns: `repeat(${value}, minmax(0, 1fr))`,
842
+ }),
843
+ })
844
+ ```
845
+
846
+ Usage:
847
+
848
+ ```html
849
+ <div class="grid-cols-[3]"></div>
850
+ ```
851
+
852
+ Generated CSS:
853
+
854
+ ```css
855
+ .grid-cols-\[3\]{grid-template-columns: repeat(3, minmax(0, 1fr)) !important;}
856
+ ```
857
+
858
+ ## Vue class scanning
859
+
860
+ The plugin first tries to extract content from `class="..."` and `:class="..."`, then tokenizes the found chunks.
861
+
862
+ Static strings inside `:class` are supported:
863
+
864
+ ```vue
865
+ <template>
866
+ <div :class="['w-[200px]', active && 'px-[16px]']"></div>
867
+ <div :class="{ 'radius-[12px]': rounded }"></div>
868
+ </template>
869
+ ```
870
+
871
+ Runtime-generated class names are not evaluated. The class must exist as a static token in the source code.
872
+
873
+ This will not work:
874
+
875
+ ```vue
876
+ <script setup lang="ts">
877
+ const width = 320
878
+ </script>
879
+
880
+ <template>
881
+ <div :class="`w-[${width}px]`"></div>
882
+ </template>
883
+ ```
884
+
885
+ This will work:
886
+
887
+ ```vue
888
+ <template>
889
+ <div :class="isWide ? 'w-[320px]' : 'w-[240px]'"></div>
890
+ </template>
891
+ ```
892
+
893
+ ## How generation works
894
+
895
+ During startup, the plugin:
896
+
897
+ 1. Walks through project files.
898
+ 2. Skips service directories like `node_modules`, `.git`, `dist`, `build` and `.generated`.
899
+ 3. Scans only files that match `include`.
900
+ 4. Extracts utility tokens.
901
+ 5. Validates values.
902
+ 6. Generates the final CSS file.
903
+
904
+ During development, the plugin updates CSS incrementally:
905
+
906
+ - adds rules for new tokens;
907
+ - removes rules when a token is no longer used anywhere;
908
+ - keeps a rule when the same token is still used in another file;
909
+ - reuses token parsing and CSS rule caches;
910
+ - notifies the Vite watcher when the generated CSS changes.
911
+
912
+ ## Safety limits
913
+
914
+ To avoid unsafe or invalid CSS, the plugin limits arbitrary values:
915
+
916
+ - minimum token length: `5`;
917
+ - maximum token length: `180`;
918
+ - maximum value length: `160`;
919
+ - forbidden characters: `;`, `{`, `}`, `<`, `>`;
920
+ - CSS comments inside values are forbidden;
921
+ - the value must contain at least one letter or digit;
922
+ - only a safe subset of CSS value characters is allowed.
923
+
924
+ The following classes are ignored:
925
+
926
+ ```html
927
+ <div class="w-[;]"></div>
928
+ <div class="w-[{}]"></div>
929
+ <div class="w-[<script>]"></div>
930
+ <div class="w-[...........................................]"></div>
931
+ ```
932
+
933
+ ## Recommendations
934
+
935
+ Use Utils JIT for precise one-off arbitrary values, not as a replacement for a design system.
936
+
937
+ Good:
938
+
939
+ ```vue
940
+ <template>
941
+ <c-card class="max-w-[720px] px-[24px] radius-[16px]">
942
+ Content
943
+ </c-card>
944
+ </template>
945
+ ```
946
+
947
+ If a value is repeated across the project, prefer moving it into a theme, preset, token or component variant.
948
+
949
+ ## Troubleshooting
950
+
951
+ ### The CSS file was not created
952
+
953
+ Check that:
954
+
955
+ - `utilsJIT()` is added to `vite.config.ts`;
956
+ - there is at least one supported utility class in the project;
957
+ - `outFile` is correct;
958
+ - the generated CSS file is imported by the application;
959
+ - the file matches `include`;
960
+ - the file is not ignored by `exclude`.
961
+
962
+ If no utilities are found and `emitEmptyFile` is `true`, the file is created with this content:
963
+
964
+ ```css
965
+ /* @vueland/utils-jit: no utilities found */
966
+ ```
967
+
968
+ If `emitEmptyFile` is `false`, the file appears only after at least one utility class is found.
969
+
970
+ ### A class exists, but CSS is not generated
971
+
972
+ Check that:
973
+
974
+ - the file matches `include`;
975
+ - the file is not ignored by `exclude`;
976
+ - the class is written statically and is not generated at runtime;
977
+ - the value passes validation;
978
+ - the utility is supported by built-in rules or added through `rules`;
979
+ - the variant exists in `breakpoints` or `variants`.
980
+
981
+ ### `xs:` or `3xl:` does not work
982
+
983
+ Add the breakpoint manually:
984
+
985
+ ```ts
986
+ utilsJIT({
987
+ breakpoints: {
988
+ xs: 480,
989
+ sm: 640,
990
+ md: 768,
991
+ lg: 1024,
992
+ xl: 1280,
993
+ '2xl': 1536,
994
+ '3xl': 1920,
995
+ },
996
+ })
997
+ ```
998
+
999
+ ### A custom rule does not work
1000
+
1001
+ Make sure `matcher` matches only the utility part without variants.
1002
+
1003
+ For this class:
1004
+
1005
+ ```html
1006
+ <div class="hover:surface-[#fff]"></div>
1007
+ ```
1008
+
1009
+ The matcher receives:
1010
+
1011
+ ```txt
1012
+ surface-[#fff]
1013
+ ```
1014
+
1015
+ Correct rule:
1016
+
1017
+ ```ts
1018
+ defineRule({
1019
+ name: 'surface',
1020
+ matcher: /^surface-\[(.+)\]$/,
1021
+ validate: isColorValue,
1022
+ declaration: (value) => ({
1023
+ backgroundColor: value,
1024
+ }),
1025
+ })
1026
+ ```
1027
+
1028
+ ## Full Vite config example
1029
+
1030
+ ```ts
1031
+ import { defineConfig } from 'vite'
1032
+ import vue from '@vitejs/plugins-vue'
1033
+ import {
1034
+ defineRule,
1035
+ isColorValue,
1036
+ isSizeValue,
1037
+ utilsJIT,
1038
+ } from '@vueland/utils-jit'
1039
+
1040
+ export default defineConfig({
1041
+ plugins: [
1042
+ vue(),
1043
+
1044
+ utilsJIT({
1045
+ outFile: 'src/.generated/utils-jit.css',
1046
+
1047
+ include: [
1048
+ /\.(vue|js|ts|jsx|tsx|html)$/,
1049
+ ],
1050
+
1051
+ exclude: [
1052
+ /src\/fixtures/,
1053
+ ],
1054
+
1055
+ breakpoints: {
1056
+ xs: 480,
1057
+ sm: 640,
1058
+ md: 768,
1059
+ lg: 1024,
1060
+ xl: 1280,
1061
+ '2xl': 1536,
1062
+ },
1063
+
1064
+ variants: {
1065
+ hocus: {
1066
+ kind: 'selector',
1067
+ value: '&:hover,&:focus',
1068
+ },
1069
+
1070
+ selected: {
1071
+ kind: 'attribute',
1072
+ value: '[aria-selected="true"]',
1073
+ },
1074
+
1075
+ dark: {
1076
+ kind: 'selector',
1077
+ value: '[data-theme="dark"] &',
1078
+ },
1079
+ },
1080
+
1081
+ rules: [
1082
+ defineRule({
1083
+ name: 'surface',
1084
+ matcher: /^surface-\[(.+)\]$/,
1085
+ validate: isColorValue,
1086
+ declaration: (value) => ({
1087
+ backgroundColor: value,
1088
+ }),
1089
+ important: false,
1090
+ }),
1091
+
1092
+ defineRule({
1093
+ name: 'size',
1094
+ matcher: /^size-\[(.+)\]$/,
1095
+ validate: isSizeValue,
1096
+ declaration: (value) => ({
1097
+ width: value,
1098
+ height: value,
1099
+ }),
1100
+ }),
1101
+ ],
1102
+ }),
1103
+ ],
1104
+ })
1105
+ ```
1106
+
1107
+ ## License
1108
+
1109
+ MIT