@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/README.md +1 -1
- package/dist/index.js +22590 -14992
- package/package.json +4 -3
- package/src/information-extractors/css/parse.ts +113 -437
- package/vite.config.ts +2 -2
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"registry": "https://registry.npmjs.org/",
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
7
|
-
"version": "1.
|
|
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
|
-
"
|
|
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": "
|
|
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 {
|
|
4
|
-
import type {
|
|
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
|
-
|
|
7
|
-
|
|
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
|
-
|
|
16
|
-
parsedSelectors
|
|
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
|
|
80
|
-
* Also builds a direct map of var() usages by
|
|
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(
|
|
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
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
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
|
-
|
|
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
|
|
202
|
-
*
|
|
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
|
|
205
|
-
|
|
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
|
-
|
|
208
|
-
if (!token || typeof token !== 'object') continue
|
|
209
|
-
const tokenObj = token as Record<string, unknown>
|
|
161
|
+
const varRefs = extractVarNames(declaration.value)
|
|
210
162
|
|
|
211
|
-
|
|
212
|
-
const
|
|
213
|
-
|
|
214
|
-
|
|
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
|
-
|
|
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
|
-
|
|
174
|
+
return null
|
|
273
175
|
}
|
|
274
|
-
|
|
275
|
-
return ''
|
|
276
176
|
}
|
|
277
177
|
|
|
278
178
|
/**
|
|
279
|
-
*
|
|
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
|
|
282
|
-
|
|
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:
|
|
205
|
+
function buildDomSelector(selector: Selector): string | null {
|
|
290
206
|
const parts: string[] = []
|
|
291
207
|
|
|
292
|
-
for (const component of selector) {
|
|
293
|
-
if (
|
|
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
|
-
*
|
|
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
|
|
511
|
-
return
|
|
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
|
|
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
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
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
|
|
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)
|
|
26
|
-
if (id.startsWith('node:') || builtinModules.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.
|