@wix/zero-config-implementation 1.5.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.
Files changed (78) hide show
  1. package/README.md +72 -0
  2. package/dist/component-loader.d.ts +42 -0
  3. package/dist/component-renderer.d.ts +31 -0
  4. package/dist/converters/data-item-builder.d.ts +15 -0
  5. package/dist/converters/index.d.ts +1 -0
  6. package/dist/converters/to-editor-component.d.ts +3 -0
  7. package/dist/converters/utils.d.ts +16 -0
  8. package/dist/errors.d.ts +230 -0
  9. package/dist/index.d.ts +42 -0
  10. package/dist/index.js +51978 -0
  11. package/dist/information-extractors/css/index.d.ts +3 -0
  12. package/dist/information-extractors/css/parse.d.ts +7 -0
  13. package/dist/information-extractors/css/selector-matcher.d.ts +3 -0
  14. package/dist/information-extractors/css/types.d.ts +49 -0
  15. package/dist/information-extractors/react/extractors/core/index.d.ts +6 -0
  16. package/dist/information-extractors/react/extractors/core/runner.d.ts +19 -0
  17. package/dist/information-extractors/react/extractors/core/store.d.ts +17 -0
  18. package/dist/information-extractors/react/extractors/core/tree-builder.d.ts +15 -0
  19. package/dist/information-extractors/react/extractors/core/types.d.ts +40 -0
  20. package/dist/information-extractors/react/extractors/css-properties.d.ts +20 -0
  21. package/dist/information-extractors/react/extractors/index.d.ts +11 -0
  22. package/dist/information-extractors/react/extractors/prop-tracker.d.ts +24 -0
  23. package/dist/information-extractors/react/index.d.ts +9 -0
  24. package/dist/information-extractors/react/types.d.ts +51 -0
  25. package/dist/information-extractors/react/utils/mock-generator.d.ts +9 -0
  26. package/dist/information-extractors/react/utils/prop-spy.d.ts +10 -0
  27. package/dist/information-extractors/ts/components.d.ts +9 -0
  28. package/dist/information-extractors/ts/css-imports.d.ts +2 -0
  29. package/dist/information-extractors/ts/index.d.ts +3 -0
  30. package/dist/information-extractors/ts/types.d.ts +47 -0
  31. package/dist/information-extractors/ts/utils/semantic-type-resolver.d.ts +3 -0
  32. package/dist/jsx-runtime-interceptor.d.ts +42 -0
  33. package/dist/jsx-runtime-interceptor.js +63 -0
  34. package/dist/jsx-runtime-loader.d.ts +23 -0
  35. package/dist/jsx-runtime-loader.js +7 -0
  36. package/dist/manifest-pipeline.d.ts +33 -0
  37. package/dist/schema.d.ts +167 -0
  38. package/dist/ts-compiler.d.ts +13 -0
  39. package/package.json +81 -0
  40. package/src/component-loader.test.ts +277 -0
  41. package/src/component-loader.ts +256 -0
  42. package/src/component-renderer.ts +192 -0
  43. package/src/converters/data-item-builder.ts +354 -0
  44. package/src/converters/index.ts +1 -0
  45. package/src/converters/to-editor-component.ts +167 -0
  46. package/src/converters/utils.ts +21 -0
  47. package/src/errors.ts +103 -0
  48. package/src/index.ts +223 -0
  49. package/src/information-extractors/css/README.md +3 -0
  50. package/src/information-extractors/css/index.ts +3 -0
  51. package/src/information-extractors/css/parse.ts +450 -0
  52. package/src/information-extractors/css/selector-matcher.ts +88 -0
  53. package/src/information-extractors/css/types.ts +56 -0
  54. package/src/information-extractors/react/extractors/core/index.ts +6 -0
  55. package/src/information-extractors/react/extractors/core/runner.ts +89 -0
  56. package/src/information-extractors/react/extractors/core/store.ts +36 -0
  57. package/src/information-extractors/react/extractors/core/tree-builder.ts +273 -0
  58. package/src/information-extractors/react/extractors/core/types.ts +48 -0
  59. package/src/information-extractors/react/extractors/css-properties.ts +214 -0
  60. package/src/information-extractors/react/extractors/index.ts +27 -0
  61. package/src/information-extractors/react/extractors/prop-tracker.ts +132 -0
  62. package/src/information-extractors/react/index.ts +53 -0
  63. package/src/information-extractors/react/types.ts +70 -0
  64. package/src/information-extractors/react/utils/mock-generator.ts +331 -0
  65. package/src/information-extractors/react/utils/prop-spy.ts +168 -0
  66. package/src/information-extractors/ts/components.ts +300 -0
  67. package/src/information-extractors/ts/css-imports.ts +26 -0
  68. package/src/information-extractors/ts/index.ts +3 -0
  69. package/src/information-extractors/ts/types.ts +56 -0
  70. package/src/information-extractors/ts/utils/semantic-type-resolver.ts +377 -0
  71. package/src/jsx-runtime-interceptor.ts +146 -0
  72. package/src/jsx-runtime-loader.ts +38 -0
  73. package/src/manifest-pipeline.ts +362 -0
  74. package/src/schema.ts +174 -0
  75. package/src/ts-compiler.ts +41 -0
  76. package/tsconfig.json +17 -0
  77. package/typedoc.json +18 -0
  78. package/vite.config.ts +45 -0
package/src/index.ts ADDED
@@ -0,0 +1,223 @@
1
+ import { Result, ResultAsync } from 'neverthrow'
2
+ import type { ComponentType } from 'react'
3
+ import type { EditorReactComponent } from './schema'
4
+
5
+ import { extractAllComponentInfo, extractCssImports, extractDefaultComponentInfo } from './information-extractors/ts'
6
+ import type { ComponentInfo } from './information-extractors/ts'
7
+ // TypeScript extraction
8
+ import { compileTsFile } from './ts-compiler'
9
+
10
+ // Error classes
11
+ import { type NotFoundError, ParseError } from './errors'
12
+
13
+ // Component loader helpers
14
+ import { findComponent, loadModule } from './component-loader'
15
+
16
+ // Pipeline orchestration
17
+ import { processComponent } from './manifest-pipeline'
18
+ export type {
19
+ ComponentInfoWithCss,
20
+ ExtractedCssInfo,
21
+ ExtractionWarning,
22
+ ProcessComponentResult,
23
+ } from './manifest-pipeline'
24
+
25
+ // Converter
26
+ import { toEditorReactComponent } from './converters'
27
+
28
+ // ─────────────────────────────────────────────────────────────────────────────
29
+ // Types
30
+ // ─────────────────────────────────────────────────────────────────────────────
31
+
32
+ export interface ManifestResult {
33
+ component: EditorReactComponent
34
+ errors: ExtractionError[]
35
+ }
36
+
37
+ export interface ExtractionError {
38
+ componentName: string
39
+ phase: 'render' | 'coupling' | 'css' | 'loader' | 'conversion'
40
+ error: string
41
+ }
42
+
43
+ // ─────────────────────────────────────────────────────────────────────────────
44
+ // Main API
45
+ // ─────────────────────────────────────────────────────────────────────────────
46
+
47
+ /**
48
+ * Extract component manifest from a TypeScript source file.
49
+ *
50
+ * @param componentPath - Path to the TypeScript source file
51
+ * @param compiledEntryPath - Path to the built JS entry file of the component's package (e.g. dist/index.js)
52
+ * @returns The manifest result containing components and non-fatal warnings
53
+ * @errors
54
+ * - {@link NotFoundError} — Source file does not exist (phase: `compile`)
55
+ * - {@link ParseError} — TypeScript config or component types could not be parsed (phase: `compile` | `extract`)
56
+ */
57
+ export function extractComponentManifest(
58
+ componentPath: string,
59
+ compiledEntryPath: string,
60
+ ): ResultAsync<ManifestResult, InstanceType<typeof NotFoundError> | InstanceType<typeof ParseError>> {
61
+ // Step 1: Load the compiled package module (non-fatal)
62
+ return loadModule(compiledEntryPath)
63
+ .map((moduleExports) => ({ moduleExports, loaderError: null as string | null }))
64
+ .orElse((err) =>
65
+ ResultAsync.fromSafePromise(
66
+ Promise.resolve({ moduleExports: null as Record<string, unknown> | null, loaderError: err.message }),
67
+ ),
68
+ )
69
+ .andThen(({ moduleExports, loaderError }) => {
70
+ const loadComponent: (componentName: string) => ComponentType<unknown> | null = moduleExports
71
+ ? (name) => findComponent(moduleExports, name)
72
+ : () => null
73
+
74
+ // Step 2: Compile TypeScript (fatal)
75
+ return compileTsFile(componentPath)
76
+ .andThen((program) => {
77
+ // Step 3: Extract default-exported component types (fatal)
78
+ const safeExtract = Result.fromThrowable(
79
+ (prog: typeof program) => {
80
+ const componentInfo = extractDefaultComponentInfo(prog, componentPath)
81
+ if (!componentInfo) {
82
+ throw new Error(`No default export found in "${componentPath}"`)
83
+ }
84
+ return componentInfo
85
+ },
86
+ (thrown) =>
87
+ new ParseError(
88
+ `Failed to extract component types from "${componentPath}": ${thrown instanceof Error ? thrown.message : String(thrown)}`,
89
+ {
90
+ cause: thrown as Error,
91
+ props: { phase: 'extract' },
92
+ },
93
+ ),
94
+ )
95
+ const componentInfoResult = safeExtract(program)
96
+
97
+ return componentInfoResult.map((componentInfo) => ({ program, componentInfo }))
98
+ })
99
+ .map(({ program, componentInfo }) => {
100
+ const errors: ExtractionError[] = []
101
+
102
+ // Surface loader error as a non-fatal error
103
+ if (loaderError) {
104
+ errors.push({
105
+ componentName: compiledEntryPath,
106
+ phase: 'loader',
107
+ error: loaderError,
108
+ })
109
+ }
110
+
111
+ // Step 4: Extract CSS imports (non-fatal)
112
+ let cssImportPaths: string[] = []
113
+ const safeCssImports = Result.fromThrowable(extractCssImports, (thrown) => thrown)
114
+ const cssResult = safeCssImports(program)
115
+ if (cssResult.isOk()) {
116
+ cssImportPaths = cssResult.value
117
+ } else {
118
+ const thrown = cssResult.error
119
+ errors.push({
120
+ componentName: componentPath,
121
+ phase: 'css',
122
+ error: `Failed to extract CSS imports: ${thrown instanceof Error ? (thrown as Error).message : String(thrown)}`,
123
+ })
124
+ }
125
+
126
+ // Step 5: Process the default-exported component (non-fatal)
127
+ const processResult = processComponent(componentInfo, loadComponent, cssImportPaths, !!loaderError)
128
+ errors.push(...processResult.warnings)
129
+ const component = toEditorReactComponent(processResult.component)
130
+
131
+ return { component, errors }
132
+ })
133
+ })
134
+ }
135
+
136
+ // ─────────────────────────────────────────────────────────────────────────────
137
+ // Public API Exports
138
+ //
139
+ // The API is organized into three tiers:
140
+ //
141
+ // Tier 1 — High-Level API
142
+ // One-call manifest extraction. Pass a file path, get back a manifest.
143
+ // Start here if you want the default pipeline with no customization.
144
+ //
145
+ // Tier 2 — Pipeline Building Blocks
146
+ // Individual steps of the extraction pipeline. Use these to compose a
147
+ // custom pipeline, swap out extractors, or process intermediate results.
148
+ //
149
+ // Tier 3 — Low-Level Renderer
150
+ // Direct React.createElement interception. Use this only if you need
151
+ // full control over the rendering and listener lifecycle.
152
+ //
153
+ // ─────────────────────────────────────────────────────────────────────────────
154
+
155
+ // ── Tier 1: High-Level API ──────────────────────────────────────────────────
156
+ // extractComponentManifest() is exported above as a named function declaration.
157
+
158
+ // ── Tier 2: Pipeline Building Blocks ────────────────────────────────────────
159
+
160
+ /** TypeScript compilation & static analysis */
161
+ export { compileTsFile } from './ts-compiler'
162
+ export { extractAllComponentInfo, extractDefaultComponentInfo } from './information-extractors/ts/components'
163
+ export { extractCssImports } from './information-extractors/ts/css-imports'
164
+ export type {
165
+ ComponentInfo,
166
+ PropInfo,
167
+ ResolvedType,
168
+ ResolvedKind,
169
+ DefaultValue,
170
+ } from './information-extractors/ts/types'
171
+
172
+ /** React render-time extraction */
173
+ export {
174
+ runExtractors,
175
+ ExtractorStore,
176
+ buildElementTree,
177
+ createPropTrackerExtractor,
178
+ createCssPropertiesExtractor,
179
+ } from './information-extractors/react'
180
+ export type {
181
+ ExtractionResult,
182
+ ReactExtractor,
183
+ RenderContext,
184
+ CreateElementEvent,
185
+ RenderCompleteEvent,
186
+ ExtractedElement,
187
+ PropTrackerData,
188
+ PropTrackerExtractorState,
189
+ CssPropertiesData,
190
+ PropSpyContext,
191
+ } from './information-extractors/react'
192
+ export type {
193
+ CoupledComponentInfo,
194
+ CoupledProp,
195
+ DOMBinding,
196
+ TrackingStores,
197
+ PropReadInfo,
198
+ PropWriteInfo,
199
+ PropSpyMeta,
200
+ } from './information-extractors/react'
201
+
202
+ /** CSS parsing & selector matching */
203
+ export { parseCss } from './information-extractors/css'
204
+ export type { CSSParserAPI } from './information-extractors/css'
205
+
206
+ /** Error classes */
207
+ export {
208
+ BaseError,
209
+ NotFoundError,
210
+ ParseError,
211
+ ValidationError,
212
+ IoError,
213
+ DefectError,
214
+ withDefectBoundary,
215
+ } from './errors'
216
+
217
+ /** Component loader */
218
+ export { createComponentLoader } from './component-loader'
219
+
220
+ // ── Tier 3: Low-Level Renderer ──────────────────────────────────────────────
221
+
222
+ export { renderWithExtractors } from './component-renderer'
223
+ export type { CreateElementListener } from './component-renderer'
@@ -0,0 +1,3 @@
1
+ # CSS Parser
2
+
3
+ An abstraction over a CSS file that exposes a closed API for accessing css properties in a structured way.
@@ -0,0 +1,3 @@
1
+ export { parseCss } from './parse'
2
+ export { matchCssSelectors } from './selector-matcher'
3
+ export type { CSSParserAPI } from './types'
@@ -0,0 +1,450 @@
1
+ import { transform } from 'lightningcss'
2
+ import type { CSSParserAPI, CSSProperty } from './types'
3
+ // Store parsed selectors for later DOM selector computation
4
+ const parsedSelectors = new Map<string, unknown[]>()
5
+
6
+ /**
7
+ * Factory function that parses CSS and returns an API to query the parsed result
8
+ * @param cssString - The CSS file content as a string
9
+ * @returns API object with methods to query the parsed CSS
10
+ */
11
+ export function parseCss(cssString: string): CSSParserAPI {
12
+ // Clear previous selectors for this parse
13
+ parsedSelectors.clear()
14
+
15
+ // Parse all properties eagerly at initialization using lightningcss
16
+ const allProperties = parseAllProperties(cssString)
17
+
18
+ return {
19
+ getPropertiesForSelector(selector: string): CSSProperty[] {
20
+ return allProperties.get(selector) ?? []
21
+ },
22
+
23
+ getAllProperties(): Map<string, CSSProperty[]> {
24
+ return allProperties
25
+ },
26
+
27
+ getVarUsages(varName: string): string[] {
28
+ // Normalize variable name to include --
29
+ const normalizedVarName = varName.startsWith('--') ? varName : `--${varName}`
30
+
31
+ const propertyNames = new Set<string>()
32
+
33
+ // Search through all properties for var() usage
34
+ for (const properties of allProperties.values()) {
35
+ for (const property of properties) {
36
+ // Check if the value contains var(--varName)
37
+ const varPattern = new RegExp(`var\\(\\s*${escapeRegex(normalizedVarName)}\\s*[,)]`, 'i')
38
+
39
+ if (varPattern.test(property.value)) {
40
+ // Ignore shorthand properties (values with multiple space-separated values)
41
+ if (!isShorthandValue(property.value)) {
42
+ propertyNames.add(property.name)
43
+ }
44
+ }
45
+ }
46
+ }
47
+
48
+ return Array.from(propertyNames)
49
+ },
50
+
51
+ getUniqueProperties(selectors: string[]): Map<string, string> {
52
+ const mergedProperties = new Map<string, string>()
53
+
54
+ for (const selector of selectors) {
55
+ const properties = this.getPropertiesForSelector(selector)
56
+
57
+ // Within a selector, last value wins (CSS behavior)
58
+ const selectorProperties = new Map<string, string>()
59
+ for (const prop of properties) {
60
+ selectorProperties.set(prop.name, prop.value)
61
+ }
62
+
63
+ // Merge into final map - first selector wins
64
+ for (const [name, value] of selectorProperties) {
65
+ if (!mergedProperties.has(name)) {
66
+ mergedProperties.set(name, value)
67
+ }
68
+ }
69
+ }
70
+
71
+ return mergedProperties
72
+ },
73
+
74
+ getDomSelector(selector: string): string | null {
75
+ const parsed = parsedSelectors.get(selector)
76
+ if (!parsed) return null
77
+ return buildDomSelector(parsed)
78
+ },
79
+ }
80
+ }
81
+
82
+ /**
83
+ * Parses all selectors and their properties from the CSS using lightningcss
84
+ * Supports @media rules, @keyframes, and nested selectors
85
+ * @param cssString - CSS string to parse
86
+ * @returns Map from selector to its CSS properties
87
+ */
88
+ function parseAllProperties(cssString: string): Map<string, CSSProperty[]> {
89
+ const propertiesMap = new Map<string, CSSProperty[]>()
90
+
91
+ try {
92
+ // Use lightningcss transform with visitor to collect properties
93
+ // Note: We don't return the rule from visitors - just collect data
94
+ transform({
95
+ filename: 'input.css',
96
+ code: Buffer.from(cssString),
97
+ minify: false,
98
+ visitor: {
99
+ Rule: {
100
+ style(rule) {
101
+ try {
102
+ // Extract selector string from the rule
103
+ const selectors = rule.value.selectors
104
+ if (!selectors?.length) return undefined
105
+
106
+ // Convert selector AST to string representation and store parsed selectors
107
+ const selectorStrings = selectors.map((sel: unknown[]) => {
108
+ const str = selectorToString(sel)
109
+ parsedSelectors.set(str, sel)
110
+ return str
111
+ })
112
+
113
+ // Extract properties from declarations
114
+ const properties: CSSProperty[] = []
115
+ for (const decl of rule.value.declarations?.declarations ?? []) {
116
+ const extracted = extractPropertyNameAndValue(decl)
117
+ if (extracted) {
118
+ properties.push(extracted)
119
+ }
120
+ }
121
+
122
+ // Store properties for each selector
123
+ if (properties.length > 0) {
124
+ for (const selector of selectorStrings) {
125
+ const existing = propertiesMap.get(selector) ?? []
126
+ propertiesMap.set(selector, [...existing, ...properties])
127
+ }
128
+ }
129
+ } catch {
130
+ // Skip rules that can't be processed
131
+ }
132
+
133
+ return undefined
134
+ },
135
+
136
+ keyframes(rule) {
137
+ try {
138
+ // Extract @keyframes name and its keyframe selectors
139
+ const keyframeName = rule.value.name.type === 'ident' ? rule.value.name.value : rule.value.name.value
140
+
141
+ for (const keyframe of rule.value.keyframes) {
142
+ // Each keyframe has selectors like 'from', 'to', '50%'
143
+ const keyframeSelectors = keyframe.selectors.map((s) => {
144
+ if (typeof s === 'string') return s
145
+ if ('percentage' in s) return `${s.percentage}%`
146
+ return String(s)
147
+ })
148
+
149
+ const properties: CSSProperty[] = []
150
+ for (const decl of keyframe.declarations?.declarations ?? []) {
151
+ const extracted = extractPropertyNameAndValue(decl)
152
+ if (extracted) {
153
+ properties.push(extracted)
154
+ }
155
+ }
156
+
157
+ if (properties.length > 0) {
158
+ for (const selector of keyframeSelectors) {
159
+ const fullSelector = `@keyframes ${keyframeName} ${selector}`
160
+ const existing = propertiesMap.get(fullSelector) ?? []
161
+ propertiesMap.set(fullSelector, [...existing, ...properties])
162
+ }
163
+ }
164
+ }
165
+ } catch {
166
+ // Skip keyframes that can't be processed
167
+ }
168
+
169
+ return undefined
170
+ },
171
+ },
172
+ },
173
+ })
174
+ } catch (error) {
175
+ // If parsing fails, return empty map
176
+ console.error('CSS parsing error:', error)
177
+ }
178
+
179
+ return propertiesMap
180
+ }
181
+
182
+ /**
183
+ * Converts a single selector component to a string
184
+ */
185
+ function selectorComponentToString(component: unknown): string {
186
+ if (!component || typeof component !== 'object') return ''
187
+ const comp = component as Record<string, unknown>
188
+
189
+ try {
190
+ if (comp.type === 'class') {
191
+ return `.${comp.name}`
192
+ }
193
+ if (comp.type === 'id') {
194
+ return `#${comp.name}`
195
+ }
196
+ if (comp.type === 'type') {
197
+ return comp.name as string
198
+ }
199
+ if (comp.type === 'universal') {
200
+ return '*'
201
+ }
202
+ if (comp.type === 'attribute') {
203
+ const attr = comp as { name: string; operation?: { operator: string; value: string } }
204
+ if (attr.operation) {
205
+ return `[${attr.name}${attr.operation.operator}"${attr.operation.value}"]`
206
+ }
207
+ return `[${attr.name}]`
208
+ }
209
+ if (comp.type === 'pseudo-class') {
210
+ const pseudo = comp as { kind: string; name?: string }
211
+ return `:${pseudo.kind === 'custom' ? pseudo.name : pseudo.kind}`
212
+ }
213
+ if (comp.type === 'pseudo-element') {
214
+ const pseudo = comp as { kind: string; name?: string }
215
+ return `::${pseudo.kind === 'custom' ? pseudo.name : pseudo.kind}`
216
+ }
217
+ if (comp.type === 'combinator') {
218
+ const combinator = comp.value as string
219
+ if (combinator === 'descendant') return ' '
220
+ if (combinator === 'child') return ' > '
221
+ if (combinator === 'next-sibling') return ' + '
222
+ if (combinator === 'later-sibling') return ' ~ '
223
+ }
224
+ } catch {
225
+ // Skip components that can't be processed
226
+ }
227
+
228
+ return ''
229
+ }
230
+
231
+ /**
232
+ * Converts a selector AST node to a string
233
+ */
234
+ function selectorToString(selector: unknown[]): string {
235
+ return selector.map(selectorComponentToString).join('')
236
+ }
237
+
238
+ /**
239
+ * Builds a DOM-queryable selector by filtering out pseudo-classes and pseudo-elements.
240
+ * Returns null if the selector contains pseudo-elements (unmatchable against real DOM).
241
+ */
242
+ function buildDomSelector(selector: unknown[]): string | null {
243
+ const parts: string[] = []
244
+
245
+ for (const component of selector) {
246
+ if (!component || typeof component !== 'object') continue
247
+ const comp = component as Record<string, unknown>
248
+
249
+ // Pseudo-elements cannot be matched against real DOM
250
+ if (comp.type === 'pseudo-element') {
251
+ return null
252
+ }
253
+
254
+ // Skip pseudo-classes (they apply to states, not static DOM)
255
+ if (comp.type === 'pseudo-class') {
256
+ continue
257
+ }
258
+
259
+ parts.push(selectorComponentToString(component))
260
+ }
261
+
262
+ const result = parts.join('')
263
+ return result || null
264
+ }
265
+
266
+ // Type for lightningcss declaration with custom property support
267
+ interface LightningDecl {
268
+ property: string
269
+ value: unknown
270
+ }
271
+
272
+ interface CustomPropertyValue {
273
+ name: string
274
+ value: unknown[]
275
+ }
276
+
277
+ /**
278
+ * Extracts the property name and value from a lightningcss declaration.
279
+ * Handles special cases like custom properties (CSS variables) and unparsed values.
280
+ */
281
+ function extractPropertyNameAndValue(decl: LightningDecl): CSSProperty | null {
282
+ try {
283
+ // Handle CSS custom properties (variables)
284
+ if (decl.property === 'custom') {
285
+ const customValue = decl.value as CustomPropertyValue
286
+ const name = customValue.name // e.g., "--primary-color"
287
+ const value = serializeCustomPropertyValue(customValue.value)
288
+ if (name && value) {
289
+ return { name, value }
290
+ }
291
+ return null
292
+ }
293
+
294
+ // Handle unparsed properties (e.g., properties using var())
295
+ if (decl.property === 'unparsed') {
296
+ const unparsedValue = decl.value as { propertyId: { property: string }; value: unknown[] }
297
+ const name = unparsedValue.propertyId?.property
298
+ if (name) {
299
+ const value = propertyValueToString(decl)
300
+ if (value) {
301
+ return { name, value }
302
+ }
303
+ }
304
+ return null
305
+ }
306
+
307
+ // Standard property
308
+ const name = propertyNameToString(decl.property)
309
+ const value = propertyValueToString(decl)
310
+ if (name && value) {
311
+ return { name, value }
312
+ }
313
+ return null
314
+ } catch {
315
+ return null
316
+ }
317
+ }
318
+
319
+ /**
320
+ * Serializes a custom property value array to a CSS string
321
+ */
322
+ function serializeCustomPropertyValue(valueArray: unknown[]): string {
323
+ const parts: string[] = []
324
+
325
+ for (const token of valueArray) {
326
+ if (!token || typeof token !== 'object') continue
327
+ const t = token as Record<string, unknown>
328
+
329
+ if (t.type === 'color') {
330
+ const color = t.value as Record<string, unknown>
331
+ if (color.type === 'rgb') {
332
+ const r = color.r as number
333
+ const g = color.g as number
334
+ const b = color.b as number
335
+ const a = color.alpha as number
336
+ if (a === 1) {
337
+ parts.push(`#${toHex(r)}${toHex(g)}${toHex(b)}`)
338
+ } else {
339
+ parts.push(`rgba(${r}, ${g}, ${b}, ${a})`)
340
+ }
341
+ }
342
+ } else if (t.type === 'length' || t.type === 'dimension') {
343
+ const dim = (t.value as Record<string, unknown>) ?? t
344
+ const unit = dim.unit as string
345
+ const val = dim.value as number
346
+ parts.push(`${val}${unit}`)
347
+ } else if (t.type === 'number') {
348
+ parts.push(String(t.value))
349
+ } else if (t.type === 'token') {
350
+ const tokenValue = t.value as Record<string, unknown>
351
+ if (tokenValue.type === 'ident') {
352
+ parts.push(tokenValue.value as string)
353
+ } else if (tokenValue.type === 'string') {
354
+ parts.push(`"${tokenValue.value}"`)
355
+ }
356
+ }
357
+ }
358
+
359
+ return parts.join(' ')
360
+ }
361
+
362
+ function toHex(n: number): string {
363
+ return Math.round(n).toString(16).padStart(2, '0')
364
+ }
365
+
366
+ /**
367
+ * Converts a property name enum to a CSS property string
368
+ */
369
+ function propertyNameToString(property: string): string {
370
+ // lightningcss provides property names in kebab-case format
371
+ // Handle both string names and enum values
372
+ if (typeof property === 'string') {
373
+ // Convert camelCase to kebab-case if needed
374
+ return property.replace(/[A-Z]/g, (letter) => `-${letter.toLowerCase()}`)
375
+ }
376
+ return String(property)
377
+ }
378
+
379
+ /**
380
+ * Converts a declaration AST back to a CSS string using lightningcss's own serializer.
381
+ * This approach uses lightningcss transform with a visitor to inject the AST node
382
+ * into a dummy rule, then extracts the serialized value from the output.
383
+ */
384
+ function propertyValueToString(decl: { value: unknown; property: string }): string {
385
+ try {
386
+ // Create a dummy CSS rule with a placeholder value
387
+ const dummyCss = `.x { ${decl.property}: 0; }`
388
+
389
+ const result = transform({
390
+ filename: 'input.css',
391
+ code: Buffer.from(dummyCss),
392
+ minify: false,
393
+ visitor: {
394
+ // Use property-specific visitor by creating an object with the property name as key
395
+ Declaration: {
396
+ [decl.property]: () => decl,
397
+ } as Record<string, () => typeof decl>,
398
+ },
399
+ })
400
+
401
+ // Extract just the property value from the output
402
+ const output = result.code.toString()
403
+ const match = output.match(/\.x\s*\{\s*([^}]+)\s*\}/)
404
+
405
+ if (match) {
406
+ // Parse the property: value pair and extract just the value
407
+ const declaration = match[1].trim()
408
+ const colonIndex = declaration.indexOf(':')
409
+ if (colonIndex !== -1) {
410
+ return declaration
411
+ .slice(colonIndex + 1)
412
+ .trim()
413
+ .replace(/;$/, '')
414
+ }
415
+ }
416
+
417
+ return ''
418
+ } catch {
419
+ // Fallback for any errors
420
+ return ''
421
+ }
422
+ }
423
+
424
+ /**
425
+ * Checks if a CSS property value is a shorthand (has multiple space-separated values)
426
+ * Ignores spaces inside parentheses (like in var() or calc())
427
+ */
428
+ function isShorthandValue(value: string): boolean {
429
+ let depth = 0
430
+ let result = ''
431
+
432
+ for (const char of value) {
433
+ if (char === '(') {
434
+ depth++
435
+ } else if (char === ')') {
436
+ depth--
437
+ } else if (depth === 0) {
438
+ result += char
439
+ }
440
+ }
441
+
442
+ return result.trim().includes(' ')
443
+ }
444
+
445
+ /**
446
+ * Escapes special regex characters in a string to use it as a literal pattern
447
+ */
448
+ function escapeRegex(str: string): string {
449
+ return str.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
450
+ }