@wix/zero-config-implementation 1.32.0 → 1.33.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/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "registry": "https://registry.npmjs.org/",
5
5
  "access": "public"
6
6
  },
7
- "version": "1.32.0",
7
+ "version": "1.33.0",
8
8
  "description": "Core library for extracting component manifests from JS and CSS files",
9
9
  "type": "module",
10
10
  "main": "dist/index.js",
@@ -45,10 +45,11 @@
45
45
  },
46
46
  "dependencies": {
47
47
  "@wix/zero-config-schema": "1.1.0",
48
- "lightningcss": "^1.31.1"
48
+ "css-tree": "^3.2.1"
49
49
  },
50
50
  "devDependencies": {
51
51
  "@faker-js/faker": "^10.2.0",
52
+ "@types/css-tree": "^2.3.11",
52
53
  "@types/node": "^20.0.0",
53
54
  "@types/react": "^18.3.1",
54
55
  "@types/react-dom": "^18.3.1",
@@ -83,5 +84,5 @@
83
84
  ]
84
85
  }
85
86
  },
86
- "falconPackageHash": "459bf8c730865e0703e2a8248d887164542b0044ede2b6bcdae37271"
87
+ "falconPackageHash": "f8e67fc7279e8f0ef3dcc174a79f27788a086507530da6f9c3b5f169"
87
88
  }
@@ -1,10 +1,16 @@
1
1
  import { CSS_PROPERTIES } from '@wix/zero-config-schema'
2
2
  import { camelCase } from 'case-anything'
3
- import { transform } from 'lightningcss'
4
- import type { TokenOrValue } from 'lightningcss'
3
+ import { generate, lexer, parse, walk } from 'css-tree'
4
+ import type { Atrule, CssNode, Declaration, FunctionNode, Rule, Selector } from 'css-tree'
5
5
  import type { CSSParserAPI, CSSProperty } from './types'
6
- // Store parsed selectors for later DOM selector computation
7
- const parsedSelectors = new Map<string, unknown[]>()
6
+
7
+ type WalkContext = {
8
+ atrule?: Atrule | null
9
+ }
10
+
11
+ function isInsideKeyframes(currentContext: WalkContext): boolean {
12
+ return currentContext.atrule?.name === 'keyframes'
13
+ }
8
14
 
9
15
  /**
10
16
  * Factory function that parses CSS and returns an API to query the parsed result
@@ -12,11 +18,8 @@ const parsedSelectors = new Map<string, unknown[]>()
12
18
  * @returns API object with methods to query the parsed CSS
13
19
  */
14
20
  export function parseCss(cssString: string): CSSParserAPI {
15
- // Clear previous selectors for this parse
16
- parsedSelectors.clear()
17
-
18
- // Parse all properties eagerly at initialization using lightningcss
19
- const { propertiesMap: allProperties, varUsagesByProperty } = parseAllProperties(cssString)
21
+ const parsedSelectors = new Map<string, Selector>()
22
+ const { propertiesMap: allProperties, varUsagesByProperty } = parseAllProperties(cssString, parsedSelectors)
20
23
 
21
24
  return {
22
25
  getPropertiesForSelector(selector: string): CSSProperty[] {
@@ -76,14 +79,17 @@ export function parseCss(cssString: string): CSSParserAPI {
76
79
  }
77
80
 
78
81
  /**
79
- * Parses all selectors and their properties from the CSS using lightningcss.
80
- * Also builds a direct map of var() usages by scanning unparsed declaration tokens,
81
- * since serialized property values lose var() references.
82
+ * Parses all selectors and their properties from the CSS using css-tree.
83
+ * Also builds a direct map of var() usages by walking declaration value ASTs.
82
84
  * Supports @media rules, @keyframes, and nested selectors
83
85
  * @param cssString - CSS string to parse
86
+ * @param parsedSelectors - Map to store extracted Selectors for DOM matching
84
87
  * @returns propertiesMap (selector → properties) and varUsagesByProperty (varName → CSS property names)
85
88
  */
86
- function parseAllProperties(cssString: string): {
89
+ function parseAllProperties(
90
+ cssString: string,
91
+ parsedSelectors: Map<string, Selector>,
92
+ ): {
87
93
  propertiesMap: Map<string, CSSProperty[]>
88
94
  varUsagesByProperty: Map<string, Set<string>>
89
95
  } {
@@ -91,106 +97,51 @@ function parseAllProperties(cssString: string): {
91
97
  const varUsagesByProperty = new Map<string, Set<string>>()
92
98
 
93
99
  try {
94
- // Use lightningcss transform with visitor to collect properties
95
- // Note: We don't return the rule from visitors - just collect data
96
- transform({
97
- filename: 'input.css',
98
- code: Buffer.from(cssString),
99
- minify: false,
100
- visitor: {
101
- Rule: {
102
- style(rule) {
103
- try {
104
- // Extract selector string from the rule
105
- const selectors = rule.value.selectors
106
- if (!selectors?.length) return undefined
107
-
108
- // Convert selector AST to string representation and store parsed selectors
109
- const selectorStrings = selectors.map((sel: unknown[]) => {
110
- const str = selectorToString(sel)
111
- parsedSelectors.set(str, sel)
112
- return str
113
- })
114
-
115
- // Extract properties from declarations
116
- const properties: CSSProperty[] = []
117
- for (const decl of rule.value.declarations?.declarations ?? []) {
118
- const declTyped = decl as LightningDecl
119
- const extracted = extractPropertyNameAndValue(declTyped)
120
- if (extracted) {
121
- properties.push(extracted)
122
- }
123
-
124
- // Track var() usages by scanning unparsed declaration tokens directly.
125
- // propertyValueToString cannot recover var() from unparsed values, so
126
- // we inspect the token array instead.
127
- if (declTyped.property === 'unparsed') {
128
- const unparsed = declTyped.value as { propertyId?: { property?: string }; value?: unknown[] }
129
- const cssPropertyName = unparsed.propertyId?.property
130
- if (cssPropertyName) {
131
- for (const varName of extractVarNamesFromTokens(unparsed.value ?? [])) {
132
- const usageSet = varUsagesByProperty.get(varName) ?? new Set<string>()
133
- usageSet.add(cssPropertyName)
134
- varUsagesByProperty.set(varName, usageSet)
135
- }
136
- }
137
- }
138
- }
139
-
140
- // Store properties for each selector
141
- if (properties.length > 0) {
142
- for (const selector of selectorStrings) {
143
- const existing = propertiesMap.get(selector) ?? []
144
- propertiesMap.set(selector, [...existing, ...properties])
145
- }
146
- }
147
- } catch {
148
- // Skip rules that can't be processed
100
+ const ast = parse(cssString, { parseCustomProperty: true })
101
+
102
+ walk(ast, {
103
+ visit: 'Rule',
104
+ enter(this: WalkContext, rule: Rule) {
105
+ try {
106
+ const properties: CSSProperty[] = []
107
+ for (const child of rule.block.children) {
108
+ if (child.type !== 'Declaration') continue
109
+ const extracted = extractProperty(child, varUsagesByProperty)
110
+ if (extracted) {
111
+ properties.push(extracted)
149
112
  }
113
+ }
114
+
115
+ if (properties.length === 0) return
150
116
 
151
- return undefined
152
- },
153
-
154
- keyframes(rule) {
155
- try {
156
- // Extract @keyframes name and its keyframe selectors
157
- const keyframeName = rule.value.name.type === 'ident' ? rule.value.name.value : rule.value.name.value
158
-
159
- for (const keyframe of rule.value.keyframes) {
160
- // Each keyframe has selectors like 'from', 'to', '50%'
161
- const keyframeSelectors = keyframe.selectors.map((selector) => {
162
- if (typeof selector === 'string') return selector
163
- if ('percentage' in selector) return `${selector.percentage}%`
164
- return String(selector)
165
- })
166
-
167
- const properties: CSSProperty[] = []
168
- for (const decl of keyframe.declarations?.declarations ?? []) {
169
- const extracted = extractPropertyNameAndValue(decl)
170
- if (extracted) {
171
- properties.push(extracted)
172
- }
173
- }
174
-
175
- if (properties.length > 0) {
176
- for (const selector of keyframeSelectors) {
177
- const fullSelector = `@keyframes ${keyframeName} ${selector}`
178
- const existing = propertiesMap.get(fullSelector) ?? []
179
- propertiesMap.set(fullSelector, [...existing, ...properties])
180
- }
181
- }
182
- }
183
- } catch {
184
- // Skip keyframes that can't be processed
117
+ const selectorStrings: string[] = []
118
+
119
+ if (isInsideKeyframes(this)) {
120
+ const keyframeName = this.atrule?.prelude ? generate(this.atrule.prelude).trim() : ''
121
+ if (!keyframeName) return
122
+
123
+ const keyframeSelector = generate(rule.prelude).trim()
124
+ selectorStrings.push(`@keyframes ${keyframeName} ${keyframeSelector}`)
125
+ } else {
126
+ if (rule.prelude.type !== 'SelectorList') return
127
+ for (const selectorNode of rule.prelude.children) {
128
+ if (selectorNode.type !== 'Selector') continue
129
+ const selectorString = generate(selectorNode)
130
+ parsedSelectors.set(selectorString, selectorNode)
131
+ selectorStrings.push(selectorString)
185
132
  }
133
+ }
186
134
 
187
- return undefined
188
- },
189
- },
135
+ for (const selectorString of selectorStrings) {
136
+ const existing = propertiesMap.get(selectorString) ?? []
137
+ propertiesMap.set(selectorString, [...existing, ...properties])
138
+ }
139
+ } catch {
140
+ // Skip rules that can't be processed
141
+ }
190
142
  },
191
143
  })
192
144
  } catch (error) {
193
- // If parsing fails, return empty map
194
145
  console.error('CSS parsing error:', error)
195
146
  }
196
147
 
@@ -198,381 +149,106 @@ function parseAllProperties(cssString: string): {
198
149
  }
199
150
 
200
151
  /**
201
- * Extracts all CSS custom property names referenced via var() from a lightningcss token array.
202
- * Handles nested token arrays recursively (e.g. tokens inside function arguments).
152
+ * Extracts property name, value, and var() references from a css-tree Declaration node.
153
+ * Also records var() usages in the provided tracking map.
203
154
  */
204
- function extractVarNamesFromTokens(tokens: unknown[]): string[] {
205
- const varNames: string[] = []
155
+ function extractProperty(declaration: Declaration, varUsagesByProperty: Map<string, Set<string>>): CSSProperty | null {
156
+ try {
157
+ const name = declaration.property
158
+ const value = generate(declaration.value)
159
+ if (!name || !value) return null
206
160
 
207
- for (const token of tokens) {
208
- if (!token || typeof token !== 'object') continue
209
- const tokenObj = token as Record<string, unknown>
161
+ const varRefs = extractVarNames(declaration.value)
210
162
 
211
- if (tokenObj.type === 'var') {
212
- const varValue = tokenObj.value as Record<string, unknown> | undefined
213
- const nameObj = varValue?.name as Record<string, unknown> | undefined
214
- const identName = nameObj?.ident as string | undefined
215
- if (identName) {
216
- varNames.push(identName.startsWith('--') ? identName : `--${identName}`)
217
- }
218
- } else if (tokenObj.type === 'function' || Array.isArray(tokenObj.value)) {
219
- const nestedTokens = tokenObj.value as unknown[]
220
- if (Array.isArray(nestedTokens)) {
221
- varNames.push(...extractVarNamesFromTokens(nestedTokens))
222
- }
163
+ for (const varName of varRefs) {
164
+ const usageSet = varUsagesByProperty.get(varName) ?? new Set<string>()
165
+ usageSet.add(name)
166
+ varUsagesByProperty.set(varName, usageSet)
223
167
  }
224
- }
225
-
226
- return varNames
227
- }
228
168
 
229
- /**
230
- * Converts a single selector component to a string
231
- */
232
- function selectorComponentToString(component: unknown): string {
233
- if (!component || typeof component !== 'object') return ''
234
- const comp = component as Record<string, unknown>
235
-
236
- try {
237
- if (comp.type === 'class') {
238
- return `.${comp.name}`
239
- }
240
- if (comp.type === 'id') {
241
- return `#${comp.name}`
242
- }
243
- if (comp.type === 'type') {
244
- return comp.name as string
245
- }
246
- if (comp.type === 'universal') {
247
- return '*'
248
- }
249
- if (comp.type === 'attribute') {
250
- const attr = comp as { name: string; operation?: { operator: string; value: string } }
251
- if (attr.operation) {
252
- return `[${attr.name}${attr.operation.operator}"${attr.operation.value}"]`
253
- }
254
- return `[${attr.name}]`
255
- }
256
- if (comp.type === 'pseudo-class') {
257
- const pseudo = comp as { kind: string; name?: string }
258
- return `:${pseudo.kind === 'custom' ? pseudo.name : pseudo.kind}`
259
- }
260
- if (comp.type === 'pseudo-element') {
261
- const pseudo = comp as { kind: string; name?: string }
262
- return `::${pseudo.kind === 'custom' ? pseudo.name : pseudo.kind}`
263
- }
264
- if (comp.type === 'combinator') {
265
- const combinator = comp.value as string
266
- if (combinator === 'descendant') return ' '
267
- if (combinator === 'child') return ' > '
268
- if (combinator === 'next-sibling') return ' + '
269
- if (combinator === 'later-sibling') return ' ~ '
169
+ if (varRefs.length > 0) {
170
+ return { name, value, varRefs }
270
171
  }
172
+ return { name, value }
271
173
  } catch {
272
- // Skip components that can't be processed
174
+ return null
273
175
  }
274
-
275
- return ''
276
176
  }
277
177
 
278
178
  /**
279
- * Converts a selector AST node to a string
179
+ * Extracts all CSS custom property names referenced via var() by walking
180
+ * the declaration value AST for Function nodes named "var".
280
181
  */
281
- function selectorToString(selector: unknown[]): string {
282
- return selector.map(selectorComponentToString).join('')
182
+ function extractVarNames(valueNode: CssNode): string[] {
183
+ const varNames: string[] = []
184
+
185
+ walk(valueNode, {
186
+ visit: 'Function',
187
+ enter(functionNode: FunctionNode) {
188
+ if (functionNode.name !== 'var') return
189
+
190
+ const firstChild = functionNode.children.first
191
+ if (firstChild && firstChild.type === 'Identifier') {
192
+ const identName = firstChild.name
193
+ varNames.push(identName.startsWith('--') ? identName : `--${identName}`)
194
+ }
195
+ },
196
+ })
197
+
198
+ return varNames
283
199
  }
284
200
 
285
201
  /**
286
202
  * Builds a DOM-queryable selector by filtering out pseudo-classes and pseudo-elements.
287
203
  * Returns null if the selector contains pseudo-elements (unmatchable against real DOM).
288
204
  */
289
- function buildDomSelector(selector: unknown[]): string | null {
205
+ function buildDomSelector(selector: Selector): string | null {
290
206
  const parts: string[] = []
291
207
 
292
- for (const component of selector) {
293
- if (!component || typeof component !== 'object') continue
294
- const comp = component as Record<string, unknown>
295
-
296
- // Pseudo-elements cannot be matched against real DOM
297
- if (comp.type === 'pseudo-element') {
208
+ for (const component of selector.children) {
209
+ if (component.type === 'PseudoElementSelector') {
298
210
  return null
299
211
  }
300
-
301
- // Skip pseudo-classes (they apply to states, not static DOM)
302
- if (comp.type === 'pseudo-class') {
212
+ if (component.type === 'PseudoClassSelector') {
303
213
  continue
304
214
  }
305
-
306
- parts.push(selectorComponentToString(component))
215
+ parts.push(generate(component))
307
216
  }
308
217
 
309
218
  const result = parts.join('')
310
219
  return result || null
311
220
  }
312
221
 
313
- // Type for lightningcss declaration with custom property support
314
- interface LightningDecl {
315
- property: string
316
- value: unknown
317
- }
318
-
319
- interface CustomPropertyValue {
320
- name: string
321
- value: TokenOrValue[]
322
- }
323
-
324
- /**
325
- * Extracts the property name and value from a lightningcss declaration.
326
- * Handles special cases like custom properties (CSS variables) and unparsed values.
327
- */
328
- function extractPropertyNameAndValue(decl: LightningDecl): CSSProperty | null {
329
- try {
330
- // Handle CSS custom properties (variables)
331
- if (decl.property === 'custom') {
332
- const customValue = decl.value as CustomPropertyValue
333
- const name = customValue.name // e.g., "--primary-color"
334
- const value = serializeCustomPropertyValue(customValue.value)
335
- if (name && value) {
336
- return { name, value }
337
- }
338
- return null
339
- }
340
-
341
- // Handle unparsed properties (e.g., properties using var())
342
- if (decl.property === 'unparsed') {
343
- const unparsedValue = decl.value as { propertyId: { property: string }; value: unknown[] }
344
- const name = unparsedValue.propertyId?.property
345
- if (name) {
346
- const value = propertyValueToString(decl)
347
- if (value) {
348
- const varRefs = extractVarNamesFromTokens(unparsedValue.value ?? [])
349
- return { name, value, ...(varRefs.length > 0 && { varRefs }) }
350
- }
351
- }
352
- return null
353
- }
354
-
355
- // Standard property
356
- const name = propertyNameToString(decl.property)
357
- const value = propertyValueToString(decl)
358
- if (name && value) {
359
- return { name, value }
360
- }
361
- return null
362
- } catch {
363
- return null
364
- }
365
- }
366
-
367
- /**
368
- * Serializes a custom property value array to a CSS string
369
- */
370
- function serializeCustomPropertyValue(valueArray: unknown[]): string {
371
- const parts: string[] = []
372
-
373
- for (const token of valueArray) {
374
- if (!token || typeof token !== 'object') continue
375
- const t = token as Record<string, unknown>
376
-
377
- if (t.type === 'color') {
378
- const color = t.value as Record<string, unknown>
379
- if (color.type === 'rgb') {
380
- const r = color.r as number
381
- const g = color.g as number
382
- const b = color.b as number
383
- const a = color.alpha as number
384
- if (a === 1) {
385
- parts.push(`#${toHex(r)}${toHex(g)}${toHex(b)}`)
386
- } else {
387
- parts.push(`rgba(${r}, ${g}, ${b}, ${a})`)
388
- }
389
- }
390
- } else if (t.type === 'length' || t.type === 'resolution') {
391
- // length/resolution: { unit: string, value: number }
392
- const dim = t.value as Record<string, unknown>
393
- parts.push(`${dim.value}${dim.unit}`)
394
- } else if (t.type === 'angle') {
395
- // angle: { type: 'deg' | 'rad' | 'grad' | 'turn', value: number }
396
- const dim = t.value as Record<string, unknown>
397
- parts.push(`${dim.value}${dim.type}`)
398
- } else if (t.type === 'time') {
399
- // time: { type: 'seconds' | 'milliseconds', value: number }
400
- const dim = t.value as Record<string, unknown>
401
- const unit = dim.type === 'milliseconds' ? 'ms' : 's'
402
- parts.push(`${dim.value}${unit}`)
403
- } else if (t.type === 'token') {
404
- const tokenValue = t.value as Record<string, unknown>
405
- if (tokenValue.type === 'ident') {
406
- parts.push(tokenValue.value as string)
407
- } else if (tokenValue.type === 'string') {
408
- parts.push(`"${tokenValue.value}"`)
409
- } else if (tokenValue.type === 'number') {
410
- parts.push(String(tokenValue.value))
411
- } else if (tokenValue.type === 'percentage') {
412
- parts.push(`${(tokenValue.value as number) * 100}%`)
413
- } else if (tokenValue.type === 'dimension') {
414
- parts.push(`${tokenValue.value}${tokenValue.unit}`)
415
- } else if (tokenValue.type === 'white-space') {
416
- // skip — parts.join(' ') already handles separation
417
- }
418
- }
419
- }
420
-
421
- return parts.join(' ')
422
- }
423
-
424
- function toHex(n: number): string {
425
- return Math.round(n).toString(16).padStart(2, '0')
426
- }
427
-
428
- /**
429
- * Converts a property name enum to a CSS property string
430
- */
431
- function propertyNameToString(property: string): string {
432
- // lightningcss provides property names in kebab-case format
433
- // Handle both string names and enum values
434
- if (typeof property === 'string') {
435
- // Convert camelCase to kebab-case if needed
436
- return property.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)
437
- }
438
- return String(property)
439
- }
440
-
441
- /**
442
- * Converts a declaration AST back to a CSS string using lightningcss's own serializer.
443
- * This approach uses lightningcss transform with a visitor to inject the AST node
444
- * into a dummy rule, then extracts the serialized value from the output.
445
- */
446
- function propertyValueToString(decl: { value: unknown; property: string }): string {
447
- try {
448
- // Create a dummy CSS rule with a placeholder value
449
- const dummyCss = `.x { ${decl.property}: 0; }`
450
-
451
- const result = transform({
452
- filename: 'input.css',
453
- code: Buffer.from(dummyCss),
454
- minify: false,
455
- visitor: {
456
- // Use property-specific visitor by creating an object with the property name as key
457
- Declaration: {
458
- [decl.property]: () => decl,
459
- } as Record<string, () => typeof decl>,
460
- },
461
- })
462
-
463
- // Extract just the property value from the output
464
- const output = result.code.toString()
465
- const match = output.match(/\.x\s*\{\s*([^}]+)\s*\}/)
466
-
467
- if (match) {
468
- // Parse the property: value pair and extract just the value
469
- const declaration = match[1].trim()
470
- const colonIndex = declaration.indexOf(':')
471
- if (colonIndex !== -1) {
472
- return declaration
473
- .slice(colonIndex + 1)
474
- .trim()
475
- .replace(/;$/, '')
476
- }
477
- }
478
-
479
- return ''
480
- } catch {
481
- // Fallback for any errors
482
- return ''
483
- }
484
- }
485
-
486
- /**
487
- * Checks if a CSS property value is a shorthand (has multiple space-separated values)
488
- * Ignores spaces inside parentheses (like in var() or calc())
489
- */
490
- function isShorthandValue(value: string): boolean {
491
- let depth = 0
492
- let result = ''
493
-
494
- for (const char of value) {
495
- if (char === '(') {
496
- depth++
497
- } else if (char === ')') {
498
- depth--
499
- } else if (depth === 0) {
500
- result += char
501
- }
502
- }
503
-
504
- return result.trim().includes(' ')
505
- }
506
-
507
222
  /**
508
- * Escapes special regex characters in a string to use it as a literal pattern
223
+ * Checks whether a CSS value matches a given CSS type (e.g. 'color', 'length').
224
+ * Uses css-tree's lexer; returns true when the value conforms to the type grammar.
225
+ * The `error` field is null on a successful match (the typed `matched` field is not
226
+ * exposed by @types/css-tree, so we check `error` instead).
509
227
  */
510
- function escapeRegex(str: string): string {
511
- return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
228
+ function matchesCssType(typeName: string, value: CssNode): boolean {
229
+ return !lexer.matchType(typeName, value).error
512
230
  }
513
231
 
514
- const ANGLE_UNITS = new Set(['deg', 'rad', 'grad', 'turn'])
515
- const TIME_UNITS = new Set(['s', 'ms'])
516
-
517
232
  /**
518
233
  * Infers the CSS data type category from a custom property's initial value string.
519
- * Uses lightningcss to parse the value and inspect the first token's type.
234
+ * Uses css-tree's lexer to match the value against known CSS types.
520
235
  * Returns a value from CSS_PROPERTIES.CSS_DATA_TYPE.
521
236
  */
522
237
  function inferCssDataType(defaultValue: string): string {
523
238
  const { CSS_DATA_TYPE } = CSS_PROPERTIES
524
- let inferredType: string = CSS_DATA_TYPE.string
525
239
 
526
240
  try {
527
- transform({
528
- filename: 'input.css',
529
- code: Buffer.from(`.x { --foo: ${defaultValue}; }`),
530
- minify: false,
531
- visitor: {
532
- Rule: {
533
- style(rule) {
534
- for (const declaration of rule.value.declarations?.declarations ?? []) {
535
- const decl = declaration as LightningDecl
536
- if (decl.property === 'custom') {
537
- const tokens = (decl.value as CustomPropertyValue).value
538
- if (tokens?.length > 0) {
539
- const firstToken = tokens[0]
540
- if (firstToken.type === 'color') {
541
- inferredType = CSS_DATA_TYPE.color
542
- } else if (firstToken.type === 'length') {
543
- inferredType = CSS_DATA_TYPE.length
544
- } else if (firstToken.type === 'angle') {
545
- inferredType = CSS_DATA_TYPE.angle
546
- } else if (firstToken.type === 'time') {
547
- inferredType = CSS_DATA_TYPE.time
548
- } else if (firstToken.type === 'token') {
549
- const raw = firstToken.value
550
- if (raw.type === 'number') {
551
- inferredType = CSS_DATA_TYPE.number
552
- } else if (raw.type === 'percentage') {
553
- inferredType = CSS_DATA_TYPE.percentage
554
- } else if (raw.type === 'dimension') {
555
- const unit = raw.unit.toString()
556
- if (ANGLE_UNITS.has(unit)) {
557
- inferredType = CSS_DATA_TYPE.angle
558
- } else if (TIME_UNITS.has(unit)) {
559
- inferredType = CSS_DATA_TYPE.time
560
- } else {
561
- inferredType = CSS_DATA_TYPE.length
562
- }
563
- }
564
- }
565
- }
566
- }
567
- }
568
- return undefined
569
- },
570
- },
571
- },
572
- })
241
+ const valueAst = parse(defaultValue, { context: 'value' })
242
+
243
+ if (matchesCssType('color', valueAst)) return CSS_DATA_TYPE.color
244
+ if (matchesCssType('length', valueAst)) return CSS_DATA_TYPE.length
245
+ if (matchesCssType('angle', valueAst)) return CSS_DATA_TYPE.angle
246
+ if (matchesCssType('time', valueAst)) return CSS_DATA_TYPE.time
247
+ if (matchesCssType('number', valueAst)) return CSS_DATA_TYPE.number
248
+ if (matchesCssType('percentage', valueAst)) return CSS_DATA_TYPE.percentage
573
249
  } catch {
574
250
  // Fall back to 'string' if parsing fails
575
251
  }
576
252
 
577
- return inferredType
253
+ return CSS_DATA_TYPE.string
578
254
  }
package/vite.config.ts CHANGED
@@ -22,8 +22,8 @@ export default defineConfig(({ mode }) => ({
22
22
  },
23
23
  rollupOptions: {
24
24
  external: (id) => {
25
- // Externalize Node.js built-ins (both node: prefixed and bare), typescript (peer dep), and lightningcss (native bindings)
26
- if (id.startsWith('node:') || builtinModules.includes(id) || ['typescript', 'lightningcss'].includes(id)) {
25
+ // Externalize Node.js built-ins (both node: prefixed and bare) and typescript (peer dep)
26
+ if (id.startsWith('node:') || builtinModules.includes(id) || id === 'typescript') {
27
27
  return true
28
28
  }
29
29
  // Externalize React so the host project's React instance is used at runtime.