@cwcss/crosswind 0.1.4

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 (64) hide show
  1. package/PLUGIN.md +235 -0
  2. package/benchmark/framework-comparison.bench.ts +850 -0
  3. package/bin/cli.ts +365 -0
  4. package/bin/crosswind +0 -0
  5. package/bin/headwind +0 -0
  6. package/build.ts +8 -0
  7. package/crosswind.config.ts +9 -0
  8. package/example/comprehensive.html +70 -0
  9. package/example/index.html +21 -0
  10. package/example/output.css +236 -0
  11. package/examples/plugin/README.md +112 -0
  12. package/examples/plugin/build.ts +32 -0
  13. package/examples/plugin/src/index.html +34 -0
  14. package/examples/plugin/src/index.ts +7 -0
  15. package/headwind +2 -0
  16. package/package.json +92 -0
  17. package/src/build.ts +101 -0
  18. package/src/config.ts +529 -0
  19. package/src/generator.ts +2173 -0
  20. package/src/index.ts +10 -0
  21. package/src/parser.ts +1471 -0
  22. package/src/plugin.ts +118 -0
  23. package/src/preflight-forms.ts +229 -0
  24. package/src/preflight.ts +388 -0
  25. package/src/rules-advanced.ts +477 -0
  26. package/src/rules-effects.ts +457 -0
  27. package/src/rules-forms.ts +103 -0
  28. package/src/rules-grid.ts +241 -0
  29. package/src/rules-interactivity.ts +525 -0
  30. package/src/rules-layout.ts +385 -0
  31. package/src/rules-transforms.ts +412 -0
  32. package/src/rules-typography.ts +486 -0
  33. package/src/rules.ts +805 -0
  34. package/src/scanner.ts +84 -0
  35. package/src/transformer-compile-class.ts +275 -0
  36. package/src/types.ts +197 -0
  37. package/test/advanced-features.test.ts +911 -0
  38. package/test/arbitrary.test.ts +396 -0
  39. package/test/attributify.test.ts +592 -0
  40. package/test/bracket-syntax.test.ts +1133 -0
  41. package/test/build.test.ts +99 -0
  42. package/test/colors.test.ts +934 -0
  43. package/test/flexbox.test.ts +669 -0
  44. package/test/generator.test.ts +597 -0
  45. package/test/grid.test.ts +584 -0
  46. package/test/layout.test.ts +404 -0
  47. package/test/modifiers.test.ts +417 -0
  48. package/test/parser.test.ts +564 -0
  49. package/test/performance-regression.test.ts +376 -0
  50. package/test/performance.test.ts +568 -0
  51. package/test/plugin.test.ts +160 -0
  52. package/test/scanner.test.ts +94 -0
  53. package/test/sizing.test.ts +481 -0
  54. package/test/spacing.test.ts +394 -0
  55. package/test/transformer-compile-class.test.ts +287 -0
  56. package/test/transforms.test.ts +448 -0
  57. package/test/typography.test.ts +632 -0
  58. package/test/variants-form-states.test.ts +225 -0
  59. package/test/variants-group-peer.test.ts +66 -0
  60. package/test/variants-media.test.ts +213 -0
  61. package/test/variants-positional.test.ts +58 -0
  62. package/test/variants-pseudo-elements.test.ts +47 -0
  63. package/test/variants-state.test.ts +62 -0
  64. package/tsconfig.json +18 -0
package/src/scanner.ts ADDED
@@ -0,0 +1,84 @@
1
+ import type { CompileClassTransformer } from './transformer-compile-class'
2
+ import type { ExtractClassesOptions } from './parser'
3
+ import { Glob } from 'bun'
4
+ import { extractClasses } from './parser'
5
+
6
+ export interface ScanResult {
7
+ classes: Set<string>
8
+ transformedFiles: Map<string, string>
9
+ }
10
+
11
+ /**
12
+ * Scans files for utility classes using Bun's fast Glob API
13
+ */
14
+ export class Scanner {
15
+ constructor(
16
+ private patterns: string[],
17
+ private transformer: CompileClassTransformer | null | undefined = undefined,
18
+ private extractOptions: ExtractClassesOptions | undefined = undefined,
19
+ ) {}
20
+
21
+ /**
22
+ * Scan all files matching the patterns and extract utility classes
23
+ */
24
+ async scan(): Promise<ScanResult> {
25
+ const allClasses = new Set<string>()
26
+ const transformedFiles = new Map<string, string>()
27
+
28
+ // Use Promise.all to scan all patterns concurrently for better performance
29
+ await Promise.all(
30
+ this.patterns.map(async (pattern) => {
31
+ const glob = new Glob(pattern)
32
+
33
+ // Bun's glob.scan() returns an async iterable
34
+ for await (const file of glob.scan('.')) {
35
+ try {
36
+ let content = await Bun.file(file).text()
37
+
38
+ // Apply transformer if enabled
39
+ if (this.transformer) {
40
+ const result = this.transformer.processFile(content)
41
+ if (result.hasChanges) {
42
+ transformedFiles.set(file, result.content)
43
+ content = result.content
44
+ }
45
+ }
46
+
47
+ const classes = extractClasses(content, this.extractOptions)
48
+
49
+ for (const cls of classes) {
50
+ allClasses.add(cls)
51
+ }
52
+ }
53
+ catch {
54
+ // Silently skip files that can't be read
55
+ // (e.g., binary files, permission issues)
56
+ continue
57
+ }
58
+ }
59
+ }),
60
+ )
61
+
62
+ return { classes: allClasses, transformedFiles }
63
+ }
64
+
65
+ /**
66
+ * Scan a single file for utility classes
67
+ */
68
+ async scanFile(filePath: string): Promise<Set<string>> {
69
+ try {
70
+ const content = await Bun.file(filePath).text()
71
+ return extractClasses(content, this.extractOptions)
72
+ }
73
+ catch {
74
+ return new Set<string>()
75
+ }
76
+ }
77
+
78
+ /**
79
+ * Scan content string for utility classes
80
+ */
81
+ scanContent(content: string): Set<string> {
82
+ return extractClasses(content, this.extractOptions)
83
+ }
84
+ }
@@ -0,0 +1,275 @@
1
+ import type { CrosswindConfig } from './types'
2
+
3
+ export interface CompileClassOptions {
4
+ /**
5
+ * Trigger string to mark classes for compilation
6
+ * @default ':hw:'
7
+ */
8
+ trigger?: string
9
+ /**
10
+ * Prefix for generated class names
11
+ * @default 'hw-'
12
+ */
13
+ classPrefix?: string
14
+ /**
15
+ * Hash function to generate class names
16
+ */
17
+ hashFn?: (content: string) => string
18
+ /**
19
+ * Layer name for compiled classes
20
+ * @default 'shortcuts'
21
+ */
22
+ layer?: string
23
+ }
24
+
25
+ /**
26
+ * Simple hash function for generating class names
27
+ */
28
+ function simpleHash(str: string): string {
29
+ let hash = 0
30
+ for (let i = 0; i < str.length; i++) {
31
+ const char = str.charCodeAt(i)
32
+ hash = (hash << 5) - hash + char
33
+ hash = hash & hash // Convert to 32-bit integer
34
+ }
35
+ return Math.abs(hash).toString(36)
36
+ }
37
+
38
+ /**
39
+ * Extract compile class markers from content
40
+ */
41
+ export function extractCompileClasses(
42
+ content: string,
43
+ options: CompileClassOptions = {},
44
+ ): Map<string, string[]> {
45
+ const trigger = options.trigger || ':hw:'
46
+ const compiledClasses = new Map<string, string[]>()
47
+
48
+ // Match class attributes with the trigger
49
+ // Supports: class=":uno: ..." and className=":uno: ..."
50
+ const classRegex = /(?:class|className)=["']([^"']*)["']/g
51
+ let match: RegExpExecArray | null
52
+
53
+ // eslint-disable-next-line no-cond-assign
54
+ while ((match = classRegex.exec(content)) !== null) {
55
+ const fullClass = match[1]
56
+
57
+ // Check if it starts with the trigger
58
+ if (fullClass.trim().startsWith(trigger)) {
59
+ // Remove the trigger and get the classes
60
+ const classes = fullClass
61
+ .replace(trigger, '')
62
+ .trim()
63
+ .split(/\s+/)
64
+ .filter(Boolean)
65
+
66
+ if (classes.length > 0) {
67
+ // Generate a unique identifier for this group of classes
68
+ const classKey = classes.sort().join(' ')
69
+ if (!compiledClasses.has(classKey)) {
70
+ compiledClasses.set(classKey, classes)
71
+ }
72
+ }
73
+ }
74
+ }
75
+
76
+ return compiledClasses
77
+ }
78
+
79
+ /**
80
+ * Transform content by replacing compile markers with generated class names
81
+ */
82
+ export function transformContent(
83
+ content: string,
84
+ compiledClassMap: Map<string, string>,
85
+ options: CompileClassOptions = {},
86
+ ): string {
87
+ const trigger = options.trigger || ':hw:'
88
+ let transformed = content
89
+
90
+ const classRegex = /(?:class|className)=["']([^"']*)["']/g
91
+ let match: RegExpExecArray | null
92
+
93
+ // We need to replace in reverse order to maintain string positions
94
+ const replacements: Array<{ start: number, end: number, replacement: string }> = []
95
+
96
+ // eslint-disable-next-line no-cond-assign
97
+ while ((match = classRegex.exec(content)) !== null) {
98
+ const fullClass = match[1]
99
+
100
+ if (fullClass.trim().startsWith(trigger)) {
101
+ const classes = fullClass
102
+ .replace(trigger, '')
103
+ .trim()
104
+ .split(/\s+/)
105
+ .filter(Boolean)
106
+
107
+ const classKey = classes.sort().join(' ')
108
+ const generatedClass = compiledClassMap.get(classKey)
109
+
110
+ if (generatedClass) {
111
+ const attrName = match[0].startsWith('class=') ? 'class' : 'className'
112
+ const quote = match[0].includes('"') ? '"' : '\''
113
+ const replacement = `${attrName}=${quote}${generatedClass}${quote}`
114
+
115
+ replacements.push({
116
+ start: match.index,
117
+ end: match.index + match[0].length,
118
+ replacement,
119
+ })
120
+ }
121
+ }
122
+ }
123
+
124
+ // Apply replacements in reverse order
125
+ for (let i = replacements.length - 1; i >= 0; i--) {
126
+ const { start, end, replacement } = replacements[i]
127
+ transformed = transformed.substring(0, start) + replacement + transformed.substring(end)
128
+ }
129
+
130
+ return transformed
131
+ }
132
+
133
+ /**
134
+ * Generate class names for compiled classes
135
+ */
136
+ export function generateCompiledClassNames(
137
+ compiledClasses: Map<string, string[]>,
138
+ options: CompileClassOptions = {},
139
+ ): Map<string, string> {
140
+ const classPrefix = options.classPrefix || 'hw-'
141
+ const hashFn = options.hashFn || simpleHash
142
+
143
+ const classMap = new Map<string, string>()
144
+
145
+ for (const [classKey] of compiledClasses) {
146
+ const hash = hashFn(classKey)
147
+ const generatedClassName = `${classPrefix}${hash}`
148
+ classMap.set(classKey, generatedClassName)
149
+ }
150
+
151
+ return classMap
152
+ }
153
+
154
+ /**
155
+ * Main transformer class
156
+ */
157
+ export class CompileClassTransformer {
158
+ private compiledClasses = new Map<string, string[]>()
159
+ private classNameMap = new Map<string, string>()
160
+ private options: CompileClassOptions
161
+
162
+ constructor(options: CompileClassOptions = {}) {
163
+ this.options = {
164
+ trigger: ':hw:',
165
+ classPrefix: 'hw-',
166
+ layer: 'shortcuts',
167
+ ...options,
168
+ }
169
+ }
170
+
171
+ /**
172
+ * Process a file and extract compile classes
173
+ */
174
+ processFile(content: string): { content: string, hasChanges: boolean } {
175
+ const extracted = extractCompileClasses(content, this.options)
176
+
177
+ if (extracted.size === 0) {
178
+ return { content, hasChanges: false }
179
+ }
180
+
181
+ // Merge with existing compiled classes
182
+ let hasNewClasses = false
183
+ for (const [key, classes] of extracted) {
184
+ if (!this.compiledClasses.has(key)) {
185
+ this.compiledClasses.set(key, classes)
186
+ hasNewClasses = true
187
+ }
188
+ }
189
+
190
+ // Generate class names if we have new classes
191
+ if (hasNewClasses || this.classNameMap.size !== this.compiledClasses.size) {
192
+ this.classNameMap = generateCompiledClassNames(this.compiledClasses, this.options)
193
+ }
194
+
195
+ // Transform the content
196
+ const transformed = transformContent(content, this.classNameMap, this.options)
197
+
198
+ return {
199
+ content: transformed,
200
+ hasChanges: transformed !== content,
201
+ }
202
+ }
203
+
204
+ /**
205
+ * Get all compiled classes and their generated names
206
+ */
207
+ getCompiledClasses(): Map<string, { className: string, utilities: string[] }> {
208
+ const result = new Map<string, { className: string, utilities: string[] }>()
209
+
210
+ for (const [key, utilities] of this.compiledClasses) {
211
+ const className = this.classNameMap.get(key)
212
+ if (className) {
213
+ result.set(key, { className, utilities })
214
+ }
215
+ }
216
+
217
+ return result
218
+ }
219
+
220
+ /**
221
+ * Generate CSS for compiled classes
222
+ */
223
+ generateCSS(config: CrosswindConfig, generator: any): string {
224
+ const compiledClasses = this.getCompiledClasses()
225
+ let css = ''
226
+
227
+ for (const [, { className, utilities }] of compiledClasses) {
228
+ // Generate CSS for each utility in the group
229
+ for (const utility of utilities) {
230
+ generator.generate(utility)
231
+ }
232
+
233
+ // Get the generated CSS and wrap it with the compiled class name
234
+ const _generatedCSS = generator.toCSS(false)
235
+
236
+ // We need to extract just the CSS for these utilities
237
+ // This is a simplified approach - in production you'd want to track
238
+ // which rules were generated for which utilities
239
+ css += `\n/* Compiled class: ${className} */\n`
240
+ css += `/* Original utilities: ${utilities.join(' ')} */\n`
241
+ }
242
+
243
+ return css
244
+ }
245
+
246
+ /**
247
+ * Reset the transformer state
248
+ */
249
+ reset(): void {
250
+ this.compiledClasses.clear()
251
+ this.classNameMap.clear()
252
+ }
253
+
254
+ /**
255
+ * Get statistics about compiled classes
256
+ */
257
+ getStats(): {
258
+ totalGroups: number
259
+ totalUtilities: number
260
+ averageUtilitiesPerGroup: number
261
+ } {
262
+ let totalUtilities = 0
263
+ for (const [, utilities] of this.compiledClasses) {
264
+ totalUtilities += utilities.length
265
+ }
266
+
267
+ return {
268
+ totalGroups: this.compiledClasses.size,
269
+ totalUtilities,
270
+ averageUtilitiesPerGroup: this.compiledClasses.size > 0
271
+ ? totalUtilities / this.compiledClasses.size
272
+ : 0,
273
+ }
274
+ }
275
+ }
package/src/types.ts ADDED
@@ -0,0 +1,197 @@
1
+ export interface CompileClassConfig {
2
+ /**
3
+ * Enable compile class transformer
4
+ * @default false
5
+ */
6
+ enabled?: boolean
7
+ /**
8
+ * Trigger string to mark classes for compilation
9
+ * @default ':hw:'
10
+ */
11
+ trigger?: string
12
+ /**
13
+ * Prefix for generated class names
14
+ * @default 'hw-'
15
+ */
16
+ classPrefix?: string
17
+ /**
18
+ * Layer name for compiled classes
19
+ * @default 'shortcuts'
20
+ */
21
+ layer?: string
22
+ }
23
+
24
+ export interface AttributifyConfig {
25
+ /**
26
+ * Enable attributify mode
27
+ * Allows using HTML attributes instead of class names
28
+ * e.g., <div hw-flex hw-bg="blue-500">
29
+ * @default false
30
+ */
31
+ enabled?: boolean
32
+ /**
33
+ * Prefix for attributify attributes (to avoid conflicts with HTML attributes)
34
+ * e.g., with prefix 'hw-': <div hw-flex hw-bg="blue-500">
35
+ * @default 'hw-'
36
+ */
37
+ prefix?: string
38
+ /**
39
+ * Attributes to ignore (won't be treated as utilities)
40
+ * @default ['class', 'className', 'style', 'id', ...]
41
+ */
42
+ ignoreAttributes?: string[]
43
+ }
44
+
45
+ export interface BracketSyntaxConfig {
46
+ /**
47
+ * Enable bracket/grouped syntax
48
+ * Allows grouping utilities like: flex[col jc-center ai-center] or text[white 2rem 700]
49
+ * @default false
50
+ */
51
+ enabled?: boolean
52
+ /**
53
+ * Enable colon syntax for simple values
54
+ * e.g., bg:black, w:100%, text:white
55
+ * @default false
56
+ */
57
+ colonSyntax?: boolean
58
+ /**
59
+ * Mapping of shorthand abbreviations to full utility names
60
+ * e.g., { 'jc': 'justify', 'ai': 'items', 'col': 'col' }
61
+ */
62
+ aliases?: Record<string, string>
63
+ }
64
+
65
+ export interface CrosswindConfig {
66
+ content: string[]
67
+ output: string
68
+ minify: boolean
69
+ watch: boolean
70
+ verbose?: boolean
71
+ theme: Theme
72
+ shortcuts: Record<string, string | string[]>
73
+ rules: CustomRule[]
74
+ variants: VariantConfig
75
+ safelist: string[]
76
+ blocklist: string[]
77
+ preflights: Preflight[]
78
+ presets: Preset[]
79
+ compileClass?: CompileClassConfig
80
+ attributify?: AttributifyConfig
81
+ bracketSyntax?: BracketSyntaxConfig
82
+ /** Generate :root CSS variables from theme colors (e.g., --monokai-bg: #2d2a2e) */
83
+ cssVariables?: boolean
84
+ }
85
+
86
+ export interface Theme {
87
+ colors: Record<string, string | Record<string, string>>
88
+ spacing: Record<string, string>
89
+ fontSize: Record<string, [string, { lineHeight: string }]>
90
+ fontFamily: Record<string, string[]>
91
+ screens: Record<string, string>
92
+ borderRadius: Record<string, string>
93
+ boxShadow: Record<string, string>
94
+ /** Extend theme values without replacing defaults */
95
+ extend?: Partial<Omit<Theme, 'extend'>>
96
+ }
97
+
98
+ export interface VariantConfig {
99
+ 'responsive': boolean
100
+ 'hover': boolean
101
+ 'focus': boolean
102
+ 'active': boolean
103
+ 'disabled': boolean
104
+ 'dark': boolean
105
+ // Group/Peer
106
+ 'group': boolean
107
+ 'peer': boolean
108
+ // Pseudo-elements
109
+ 'before': boolean
110
+ 'after': boolean
111
+ 'marker': boolean
112
+ // Pseudo-classes - Basic
113
+ 'first': boolean
114
+ 'last': boolean
115
+ 'odd': boolean
116
+ 'even': boolean
117
+ 'first-of-type': boolean
118
+ 'last-of-type': boolean
119
+ 'visited': boolean
120
+ 'checked': boolean
121
+ 'focus-within': boolean
122
+ 'focus-visible': boolean
123
+ // Pseudo-classes - Form states
124
+ 'placeholder': boolean
125
+ 'selection': boolean
126
+ 'file': boolean
127
+ 'required': boolean
128
+ 'valid': boolean
129
+ 'invalid': boolean
130
+ 'read-only': boolean
131
+ 'autofill': boolean
132
+ // Pseudo-classes - Additional states
133
+ 'open': boolean
134
+ 'closed': boolean
135
+ 'empty': boolean
136
+ 'enabled': boolean
137
+ 'only': boolean
138
+ 'target': boolean
139
+ 'indeterminate': boolean
140
+ 'default': boolean
141
+ 'optional': boolean
142
+ // Media
143
+ 'print': boolean
144
+ // Direction
145
+ 'rtl': boolean
146
+ 'ltr': boolean
147
+ // Motion
148
+ 'motion-safe': boolean
149
+ 'motion-reduce': boolean
150
+ // Contrast
151
+ 'contrast-more': boolean
152
+ 'contrast-less': boolean
153
+ }
154
+
155
+ export interface ParsedClass {
156
+ raw: string
157
+ variants: string[]
158
+ utility: string
159
+ value?: string
160
+ important: boolean
161
+ arbitrary: boolean
162
+ typeHint?: string // Type hint for arbitrary values, e.g., 'color' in text-[color:var(--muted)]
163
+ }
164
+
165
+ export interface CSSRule {
166
+ selector: string
167
+ properties: Record<string, string>
168
+ mediaQuery?: string
169
+ childSelector?: string // For utilities like space-x that need child selectors
170
+ }
171
+
172
+ export interface UtilityRuleResult {
173
+ properties: Record<string, string>
174
+ childSelector?: string
175
+ pseudoElement?: string // e.g., '::placeholder' — appended to selector without space
176
+ }
177
+
178
+ export type CustomRule = [RegExp, (match: RegExpMatchArray) => Record<string, string> | undefined]
179
+
180
+ export interface Preflight {
181
+ getCSS: () => string
182
+ }
183
+
184
+ export interface Preset {
185
+ name: string
186
+ theme?: Partial<Theme>
187
+ rules?: CustomRule[]
188
+ shortcuts?: Record<string, string | string[]>
189
+ variants?: Partial<VariantConfig>
190
+ preflights?: Preflight[]
191
+ }
192
+
193
+ type DeepPartial<T> = {
194
+ [P in keyof T]?: T[P] extends object ? DeepPartial<T[P]> : T[P]
195
+ }
196
+
197
+ export type CrosswindOptions = DeepPartial<CrosswindConfig>