@wix/zero-config-implementation 1.60.0 → 1.62.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.60.0",
7
+ "version": "1.62.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",
@@ -18,6 +18,7 @@
18
18
  "scripts": {
19
19
  "build": "vite build",
20
20
  "build:dev": "vite build --mode development",
21
+ "typecheck": "tsc --noEmit",
21
22
  "lint": "biome check src/",
22
23
  "test": "vitest run --passWithNoTests",
23
24
  "test:watch": "vitest --passWithNoTests",
@@ -38,7 +39,7 @@
38
39
  }
39
40
  },
40
41
  "dependencies": {
41
- "@wix/react-component-schema": "1.5.0"
42
+ "@wix/react-component-schema": "1.6.0"
42
43
  },
43
44
  "devDependencies": {
44
45
  "@faker-js/faker": "^10.2.0",
@@ -79,5 +80,5 @@
79
80
  ]
80
81
  }
81
82
  },
82
- "falconPackageHash": "773d13f574bda240374a4a8e8347af42c6022e40e7d842e98c15a6cf"
83
+ "falconPackageHash": "bd7035914229aba9dd44c1b5098381d537a4c3b4e7c58e03bd0c5e84"
83
84
  }
@@ -207,7 +207,6 @@ export function renderWithExtractors(
207
207
  } catch (cause) {
208
208
  throw new Error(
209
209
  `Component bundle at "${compiledEntryPath}" resolves a different React than the host; install "react-dom" alongside "react" so SSR can run against the user's React.`,
210
- { cause: cause as Error },
211
210
  )
212
211
  }
213
212
  }
@@ -0,0 +1,203 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { resolveLonghand, splitAxis, splitBox, splitCorners, splitTopLevelTokens } from './css-longhand-resolver'
3
+
4
+ describe('splitTopLevelTokens', () => {
5
+ it('splits a single token', () => {
6
+ expect(splitTopLevelTokens('1rem')).toEqual(['1rem'])
7
+ })
8
+
9
+ it('splits multiple plain tokens', () => {
10
+ expect(splitTopLevelTokens('1px solid red')).toEqual(['1px', 'solid', 'red'])
11
+ })
12
+
13
+ it('keeps calc() intact even with embedded spaces', () => {
14
+ expect(splitTopLevelTokens('calc(1rem + 2px) 4px')).toEqual(['calc(1rem + 2px)', '4px'])
15
+ })
16
+
17
+ it('keeps var() intact', () => {
18
+ expect(splitTopLevelTokens('var(--x) var(--y)')).toEqual(['var(--x)', 'var(--y)'])
19
+ })
20
+
21
+ it('keeps nested parens intact', () => {
22
+ expect(splitTopLevelTokens('rgb(0 0 0) calc(1px + var(--y))')).toEqual(['rgb(0 0 0)', 'calc(1px + var(--y))'])
23
+ })
24
+
25
+ it('handles leading and trailing whitespace', () => {
26
+ expect(splitTopLevelTokens(' 1px 2px ')).toEqual(['1px', '2px'])
27
+ })
28
+ })
29
+
30
+ describe('splitBox', () => {
31
+ it('one value: applies to all sides', () => {
32
+ expect(splitBox('1rem')).toEqual({ top: '1rem', right: '1rem', bottom: '1rem', left: '1rem' })
33
+ })
34
+
35
+ it('two values: block / inline', () => {
36
+ expect(splitBox('1rem 2rem')).toEqual({ top: '1rem', right: '2rem', bottom: '1rem', left: '2rem' })
37
+ })
38
+
39
+ it('three values: top / horizontal / bottom', () => {
40
+ expect(splitBox('1px 2px 3px')).toEqual({ top: '1px', right: '2px', bottom: '3px', left: '2px' })
41
+ })
42
+
43
+ it('four values: top / right / bottom / left', () => {
44
+ expect(splitBox('1px 2px 3px 4px')).toEqual({ top: '1px', right: '2px', bottom: '3px', left: '4px' })
45
+ })
46
+
47
+ it('preserves calc() as a single token', () => {
48
+ expect(splitBox('calc(10px + 1rem) 2rem')).toEqual({
49
+ top: 'calc(10px + 1rem)',
50
+ right: '2rem',
51
+ bottom: 'calc(10px + 1rem)',
52
+ left: '2rem',
53
+ })
54
+ })
55
+
56
+ it('preserves var() tokens', () => {
57
+ expect(splitBox('var(--x) var(--y)')).toEqual({
58
+ top: 'var(--x)',
59
+ right: 'var(--y)',
60
+ bottom: 'var(--x)',
61
+ left: 'var(--y)',
62
+ })
63
+ })
64
+
65
+ it('returns undefined for unsupported token counts', () => {
66
+ expect(splitBox('')).toBeUndefined()
67
+ expect(splitBox('1px 2px 3px 4px 5px')).toBeUndefined()
68
+ })
69
+ })
70
+
71
+ describe('splitAxis', () => {
72
+ it('one value: both ends', () => {
73
+ expect(splitAxis('1rem')).toEqual({ start: '1rem', end: '1rem' })
74
+ })
75
+
76
+ it('two values: start / end', () => {
77
+ expect(splitAxis('4px 8px')).toEqual({ start: '4px', end: '8px' })
78
+ })
79
+
80
+ it('returns undefined for zero or > 2 tokens', () => {
81
+ expect(splitAxis('')).toBeUndefined()
82
+ expect(splitAxis('1px 2px 3px')).toBeUndefined()
83
+ })
84
+ })
85
+
86
+ describe('splitCorners', () => {
87
+ it('one value: applies to all corners', () => {
88
+ expect(splitCorners('4px')).toEqual({ tl: '4px', tr: '4px', br: '4px', bl: '4px' })
89
+ })
90
+
91
+ it('two values: tl/br and tr/bl', () => {
92
+ expect(splitCorners('4px 8px')).toEqual({ tl: '4px', tr: '8px', br: '4px', bl: '8px' })
93
+ })
94
+
95
+ it('three values: tl / (tr & bl) / br', () => {
96
+ expect(splitCorners('4px 8px 12px')).toEqual({ tl: '4px', tr: '8px', br: '12px', bl: '8px' })
97
+ })
98
+
99
+ it('four values: tl / tr / br / bl', () => {
100
+ expect(splitCorners('4px 8px 12px 16px')).toEqual({ tl: '4px', tr: '8px', br: '12px', bl: '16px' })
101
+ })
102
+
103
+ it('drops elliptical second radii after /', () => {
104
+ expect(splitCorners('4px 8px 12px 16px / 1px 2px 3px 4px')).toEqual({
105
+ tl: '4px',
106
+ tr: '8px',
107
+ br: '12px',
108
+ bl: '16px',
109
+ })
110
+ })
111
+
112
+ it('returns undefined for unsupported token counts', () => {
113
+ expect(splitCorners('')).toBeUndefined()
114
+ })
115
+ })
116
+
117
+ describe('resolveLonghand', () => {
118
+ it('returns direct hits without consulting shorthand rules', () => {
119
+ const values = new Map([
120
+ ['paddingTop', '5px'],
121
+ ['padding', '10px'],
122
+ ])
123
+ expect(resolveLonghand('paddingTop', values)).toBe('5px')
124
+ })
125
+
126
+ it('returns undefined when target is unknown and absent', () => {
127
+ expect(resolveLonghand('marginTop', new Map())).toBeUndefined()
128
+ })
129
+
130
+ it('returns undefined when no rule produces a value', () => {
131
+ expect(resolveLonghand('paddingTop', new Map())).toBeUndefined()
132
+ })
133
+
134
+ describe('padding family', () => {
135
+ it('paddingTop falls back through paddingBlockStart → paddingBlock → padding', () => {
136
+ expect(resolveLonghand('paddingTop', new Map([['paddingBlockStart', '1px']]))).toBe('1px')
137
+ expect(resolveLonghand('paddingTop', new Map([['paddingBlock', '4px 8px']]))).toBe('4px')
138
+ expect(resolveLonghand('paddingTop', new Map([['padding', '1rem 2rem']]))).toBe('1rem')
139
+ })
140
+
141
+ it('paddingBottom falls back through paddingBlockEnd → paddingBlock → padding', () => {
142
+ expect(resolveLonghand('paddingBottom', new Map([['paddingBlockEnd', '2px']]))).toBe('2px')
143
+ expect(resolveLonghand('paddingBottom', new Map([['paddingBlock', '4px 8px']]))).toBe('8px')
144
+ expect(resolveLonghand('paddingBottom', new Map([['padding', '1rem 2rem']]))).toBe('1rem')
145
+ })
146
+
147
+ it('paddingInlineStart falls back through paddingLeft → paddingInline → padding', () => {
148
+ expect(resolveLonghand('paddingInlineStart', new Map([['paddingLeft', '7px']]))).toBe('7px')
149
+ expect(resolveLonghand('paddingInlineStart', new Map([['paddingInline', '12px']]))).toBe('12px')
150
+ expect(resolveLonghand('paddingInlineStart', new Map([['paddingInline', '12px 24px']]))).toBe('12px')
151
+ expect(resolveLonghand('paddingInlineStart', new Map([['padding', '1px 2px 3px 4px']]))).toBe('4px')
152
+ })
153
+
154
+ it('paddingInlineEnd falls back through paddingRight → paddingInline → padding', () => {
155
+ expect(resolveLonghand('paddingInlineEnd', new Map([['paddingRight', '9px']]))).toBe('9px')
156
+ expect(resolveLonghand('paddingInlineEnd', new Map([['paddingInline', '12px 24px']]))).toBe('24px')
157
+ expect(resolveLonghand('paddingInlineEnd', new Map([['padding', '1px 2px 3px 4px']]))).toBe('2px')
158
+ })
159
+ })
160
+
161
+ describe('border-radius corners', () => {
162
+ it('maps borderTopLeftRadius to borderStartStartRadius (LTR / horizontal-tb)', () => {
163
+ expect(resolveLonghand('borderStartStartRadius', new Map([['borderTopLeftRadius', '5px']]))).toBe('5px')
164
+ })
165
+
166
+ it('falls back to borderRadius corner split', () => {
167
+ const values = new Map([['borderRadius', '4px 8px 12px 16px']])
168
+ expect(resolveLonghand('borderStartStartRadius', values)).toBe('4px')
169
+ expect(resolveLonghand('borderStartEndRadius', values)).toBe('8px')
170
+ expect(resolveLonghand('borderEndEndRadius', values)).toBe('12px')
171
+ expect(resolveLonghand('borderEndStartRadius', values)).toBe('16px')
172
+ })
173
+ })
174
+
175
+ describe('border sides', () => {
176
+ it('borderTop falls back through borderBlockStart → border', () => {
177
+ expect(resolveLonghand('borderTop', new Map([['borderBlockStart', '2px dashed blue']]))).toBe('2px dashed blue')
178
+ expect(resolveLonghand('borderTop', new Map([['border', '1px solid red']]))).toBe('1px solid red')
179
+ })
180
+
181
+ it('borderInlineStart falls back through borderLeft → borderInline → border', () => {
182
+ expect(resolveLonghand('borderInlineStart', new Map([['borderLeft', '3px dotted black']]))).toBe(
183
+ '3px dotted black',
184
+ )
185
+ expect(resolveLonghand('borderInlineStart', new Map([['borderInline', '1px solid grey']]))).toBe('1px solid grey')
186
+ expect(resolveLonghand('borderInlineStart', new Map([['border', '1px solid red']]))).toBe('1px solid red')
187
+ })
188
+ })
189
+
190
+ describe('background', () => {
191
+ it('falls back to backgroundColor when background is absent', () => {
192
+ expect(resolveLonghand('background', new Map([['backgroundColor', '#abcdef']]))).toBe('#abcdef')
193
+ })
194
+
195
+ it('prefers literal background over backgroundColor', () => {
196
+ const values = new Map([
197
+ ['background', '#001'],
198
+ ['backgroundColor', '#abcdef'],
199
+ ])
200
+ expect(resolveLonghand('background', values)).toBe('#001')
201
+ })
202
+ })
203
+ })
@@ -0,0 +1,258 @@
1
+ import type { CSS_PROPERTIES } from '@wix/react-component-schema'
2
+ import { type Value, generate, parse } from 'css-tree'
3
+
4
+ /**
5
+ * Resolves a CSS longhand to its default value by reading the developer's
6
+ * parsed CSS map. Accepts whatever shorthand the developer happened to write
7
+ * (`padding`, `padding-block`, `padding-block-start`, …) and splits it into
8
+ * the longhand piece the schema asked for. Returns `undefined` when no
9
+ * shorthand can satisfy the target.
10
+ *
11
+ * Example. Developer wrote `padding: 10px 20px`. Schema asks for `paddingTop`.
12
+ * 1. Direct hit on `paddingTop` — miss.
13
+ * 2. Walk FALLBACKS['paddingTop'] in order:
14
+ * fromKey('paddingBlockStart') → miss
15
+ * fromAxis('paddingBlock', 'start') → miss
16
+ * fromBox('padding', 'top') → splitBox('10px 20px').top = '10px' ✅
17
+ *
18
+ * Writing-mode assumption: `horizontal-tb` + LTR (CSS default). The logical-
19
+ * to-physical equivalences this file relies on (e.g. `padding-block-start` ↔
20
+ * `padding-top`, `padding-left` ↔ `padding-inline-start`) only hold under
21
+ * that mode. RTL and vertical writing modes are out of scope.
22
+ */
23
+
24
+ // ─────────────────────────────────────────────────────────────────────────────
25
+ // Types
26
+ // ─────────────────────────────────────────────────────────────────────────────
27
+
28
+ /**
29
+ * Every property name the editor's schema knows about — the values of
30
+ * `CSS_PROPERTIES.CSS_PROPERTY_TYPE`. Used to type the FALLBACKS table's
31
+ * target keys so a typo or rename in the schema fails compilation here.
32
+ *
33
+ * Note: source-side names in factory calls (`paddingBlock`, `borderBlockStart`,
34
+ * etc.) stay plain strings because the developer can write any valid CSS
35
+ * property, including names the schema doesn't have controls for.
36
+ */
37
+ type CssPropertyName = (typeof CSS_PROPERTIES.CSS_PROPERTY_TYPE)[keyof typeof CSS_PROPERTIES.CSS_PROPERTY_TYPE]
38
+
39
+ type BoxValues = { top: string; right: string; bottom: string; left: string }
40
+ type AxisValues = { start: string; end: string }
41
+ type CornerValues = { tl: string; tr: string; br: string; bl: string }
42
+
43
+ // ─────────────────────────────────────────────────────────────────────────────
44
+ // Tokenizer
45
+ // ─────────────────────────────────────────────────────────────────────────────
46
+
47
+ /**
48
+ * Splits a CSS value on top-level whitespace, keeping function calls intact.
49
+ * `1px solid red` → ['1px', 'solid', 'red']
50
+ * `calc(1rem + 2px) 4px` → ['calc(1rem + 2px)', '4px']
51
+ * `rgb(0 0 0) var(--y)` → ['rgb(0 0 0)', 'var(--y)']
52
+ *
53
+ * Backed by css-tree's value AST so function calls (calc/var/rgb/…) stay as
54
+ * one opaque `Function` node — no manual paren tracking. Whitespace and
55
+ * operators are filtered out; operator-aware callers (like splitCorners,
56
+ * which stops at `/`) handle the AST themselves.
57
+ */
58
+ export function splitTopLevelTokens(value: string): string[] {
59
+ const tokens: string[] = []
60
+ try {
61
+ const valueAst = parse(value, { context: 'value' }) as Value
62
+ valueAst.children.forEach((child) => {
63
+ if (child.type === 'WhiteSpace' || child.type === 'Operator') return
64
+ tokens.push(generate(child))
65
+ })
66
+ } catch {
67
+ // Malformed value — let callers see the empty result and fall back conservatively.
68
+ }
69
+ return tokens
70
+ }
71
+
72
+ // ─────────────────────────────────────────────────────────────────────────────
73
+ // Splitters — parse a shorthand value into named parts
74
+ //
75
+ // Each splitter applies the CSS spec's 1-to-N-value expansion rule for its
76
+ // shorthand family. Unsupported token counts return `undefined` so callers
77
+ // fall back conservatively instead of guessing.
78
+ // ─────────────────────────────────────────────────────────────────────────────
79
+
80
+ /**
81
+ * `padding` / `margin` / `inset` — the four-sided box.
82
+ * 1 value: `padding: 10px` → all four sides 10px
83
+ * 2 values: `padding: 10px 20px` → block (top/bottom) 10px, inline (right/left) 20px
84
+ * 3 values: `padding: 10px 20px 30px` → top 10px, right & left 20px, bottom 30px
85
+ * 4 values: `padding: 10px 20px 30px 40px` → clockwise from top
86
+ */
87
+ export function splitBox(value: string): BoxValues | undefined {
88
+ const tokens = splitTopLevelTokens(value)
89
+ switch (tokens.length) {
90
+ case 1:
91
+ return { top: tokens[0], right: tokens[0], bottom: tokens[0], left: tokens[0] }
92
+ case 2:
93
+ return { top: tokens[0], right: tokens[1], bottom: tokens[0], left: tokens[1] }
94
+ case 3:
95
+ return { top: tokens[0], right: tokens[1], bottom: tokens[2], left: tokens[1] }
96
+ case 4:
97
+ return { top: tokens[0], right: tokens[1], bottom: tokens[2], left: tokens[3] }
98
+ default:
99
+ return undefined
100
+ }
101
+ }
102
+
103
+ /**
104
+ * `padding-block`, `padding-inline`, `margin-block`, `margin-inline` — a single axis.
105
+ * 1 value: `padding-block: 10px` → start 10px, end 10px
106
+ * 2 values: `padding-block: 4px 8px` → start 4px, end 8px
107
+ */
108
+ export function splitAxis(value: string): AxisValues | undefined {
109
+ const tokens = splitTopLevelTokens(value)
110
+ switch (tokens.length) {
111
+ case 1:
112
+ return { start: tokens[0], end: tokens[0] }
113
+ case 2:
114
+ return { start: tokens[0], end: tokens[1] }
115
+ default:
116
+ return undefined
117
+ }
118
+ }
119
+
120
+ /**
121
+ * `border-radius` — the four corners. Keys: tl, tr, br, bl.
122
+ * Anything after `/` describes elliptical second radii and is dropped (v1).
123
+ * 1 corner: `border-radius: 4px` → all four corners 4px
124
+ * 2 corners: `border-radius: 4px 8px` → tl & br 4px, tr & bl 8px
125
+ * 3 corners: `border-radius: 4px 8px 12px` → tl 4px, tr & bl 8px, br 12px
126
+ * 4 corners: `border-radius: 4px 8px 12px 16px` → clockwise from top-left
127
+ */
128
+ export function splitCorners(value: string): CornerValues | undefined {
129
+ const tokens: string[] = []
130
+ try {
131
+ const valueAst = parse(value, { context: 'value' }) as Value
132
+ for (const child of valueAst.children) {
133
+ if (child.type === 'WhiteSpace') continue
134
+ // `/` separates the corner radii from the elliptical second radii (v1: drop them).
135
+ if (child.type === 'Operator' && child.value === '/') break
136
+ tokens.push(generate(child))
137
+ }
138
+ } catch {
139
+ return undefined
140
+ }
141
+ switch (tokens.length) {
142
+ case 1:
143
+ return { tl: tokens[0], tr: tokens[0], br: tokens[0], bl: tokens[0] }
144
+ case 2:
145
+ return { tl: tokens[0], tr: tokens[1], br: tokens[0], bl: tokens[1] }
146
+ case 3:
147
+ return { tl: tokens[0], tr: tokens[1], br: tokens[2], bl: tokens[1] }
148
+ case 4:
149
+ return { tl: tokens[0], tr: tokens[1], br: tokens[2], bl: tokens[3] }
150
+ default:
151
+ return undefined
152
+ }
153
+ }
154
+
155
+ // ─────────────────────────────────────────────────────────────────────────────
156
+ // Fallback factories — used to build the FALLBACKS table declaratively
157
+ //
158
+ // A Fallback is a function: given the developer's CSS map, produce a default
159
+ // value (or `undefined` when its source isn't present). Four flavors, each
160
+ // named after what it pulls out:
161
+ //
162
+ // fromKey('paddingBlockStart') → look up that key, return its value as-is
163
+ // fromBox('padding', 'top') → look up 'padding', split it, return the top piece
164
+ // fromAxis('paddingBlock', 'start') → look up 'paddingBlock', split it, return the start piece
165
+ // fromCorner('borderRadius', 'tl') → look up 'borderRadius', split it, return the top-left corner
166
+ //
167
+ // This is the layer that lets FALLBACKS read like a sentence per row, instead
168
+ // of a wall of inline arrow functions.
169
+ // ─────────────────────────────────────────────────────────────────────────────
170
+
171
+ type Fallback = (values: Map<string, string>) => string | undefined
172
+
173
+ function fromKey(from: string): Fallback {
174
+ return (values) => values.get(from)
175
+ }
176
+
177
+ function fromBox(from: string, side: keyof BoxValues): Fallback {
178
+ return (values) => {
179
+ const value = values.get(from)
180
+ return value === undefined ? undefined : splitBox(value)?.[side]
181
+ }
182
+ }
183
+
184
+ function fromAxis(from: string, end: keyof AxisValues): Fallback {
185
+ return (values) => {
186
+ const value = values.get(from)
187
+ return value === undefined ? undefined : splitAxis(value)?.[end]
188
+ }
189
+ }
190
+
191
+ function fromCorner(from: string, corner: keyof CornerValues): Fallback {
192
+ return (values) => {
193
+ const value = values.get(from)
194
+ return value === undefined ? undefined : splitCorners(value)?.[corner]
195
+ }
196
+ }
197
+
198
+ // ─────────────────────────────────────────────────────────────────────────────
199
+ // Resolution table
200
+ //
201
+ // For each schema longhand, an ordered list of fallbacks to try when the
202
+ // developer didn't write that longhand literally. The resolver walks them
203
+ // top-to-bottom and returns the first non-undefined result.
204
+ //
205
+ // Order matters: most specific source first, broadest source last — this
206
+ // mirrors CSS's own "more specific wins" cascade behavior.
207
+ // ─────────────────────────────────────────────────────────────────────────────
208
+
209
+ const FALLBACKS: Partial<Record<CssPropertyName, Fallback[]>> = {
210
+ // Padding — block axis is physical (top/bottom), inline axis is logical (start/end).
211
+ paddingTop: [fromKey('paddingBlockStart'), fromAxis('paddingBlock', 'start'), fromBox('padding', 'top')],
212
+ paddingBottom: [fromKey('paddingBlockEnd'), fromAxis('paddingBlock', 'end'), fromBox('padding', 'bottom')],
213
+ paddingInlineStart: [fromKey('paddingLeft'), fromAxis('paddingInline', 'start'), fromBox('padding', 'left')],
214
+ paddingInlineEnd: [fromKey('paddingRight'), fromAxis('paddingInline', 'end'), fromBox('padding', 'right')],
215
+
216
+ // Border radius — schema uses all-logical corner names; physical names map under LTR/horizontal-tb.
217
+ borderStartStartRadius: [fromKey('borderTopLeftRadius'), fromCorner('borderRadius', 'tl')],
218
+ borderStartEndRadius: [fromKey('borderTopRightRadius'), fromCorner('borderRadius', 'tr')],
219
+ borderEndStartRadius: [fromKey('borderBottomLeftRadius'), fromCorner('borderRadius', 'bl')],
220
+ borderEndEndRadius: [fromKey('borderBottomRightRadius'), fromCorner('borderRadius', 'br')],
221
+
222
+ // Border sides — these are themselves shorthands (<width> || <style> || <color>).
223
+ // The schema treats them as one opaque value, so `border: 1px solid red` flows
224
+ // through verbatim to every side.
225
+ borderTop: [fromKey('borderBlockStart'), fromKey('border')],
226
+ borderBottom: [fromKey('borderBlockEnd'), fromKey('border')],
227
+ borderInlineStart: [fromKey('borderLeft'), fromKey('borderInline'), fromKey('border')],
228
+ borderInlineEnd: [fromKey('borderRight'), fromKey('borderInline'), fromKey('border')],
229
+
230
+ // Background — populate the schema's `background` from `background-color` when
231
+ // the developer only wrote the color.
232
+ background: [fromKey('backgroundColor')],
233
+ }
234
+
235
+ // ─────────────────────────────────────────────────────────────────────────────
236
+ // Public entry point
237
+ // ─────────────────────────────────────────────────────────────────────────────
238
+
239
+ /**
240
+ * Try the literal longhand first (covers the case where the developer wrote
241
+ * the exact key the schema is asking for). On a miss, walk the target's
242
+ * fallbacks and return the first non-undefined result. Returns `undefined`
243
+ * when nothing matches — the caller then emits an empty `cssProperties.<name>`
244
+ * entry, matching pre-resolver behavior for unknown cases.
245
+ */
246
+ export function resolveLonghand(target: string, values: Map<string, string>): string | undefined {
247
+ const literal = values.get(target)
248
+ if (literal !== undefined) return literal
249
+
250
+ const fallbacks = FALLBACKS[target as CssPropertyName]
251
+ if (!fallbacks) return undefined
252
+
253
+ for (const fallback of fallbacks) {
254
+ const resolved = fallback(values)
255
+ if (resolved !== undefined) return resolved
256
+ }
257
+ return undefined
258
+ }
@@ -8,6 +8,7 @@ import type {
8
8
  ElementItem,
9
9
  } from '@wix/react-component-schema'
10
10
  import { CSS_PROPERTIES, ELEMENTS } from '@wix/react-component-schema'
11
+ import { camelCase } from 'case-anything'
11
12
  import type { ComponentInfoWithCss } from '../index'
12
13
  import type { MatchedCssData } from '../information-extractors/css/types'
13
14
  import type {
@@ -19,6 +20,7 @@ import type {
19
20
  } from '../information-extractors/react'
20
21
  import { getDefaultDisplayForTag, resolveDisplayValue } from '../information-extractors/react'
21
22
  import { findPreferredSemanticClass } from '../utils/css-class'
23
+ import { resolveLonghand } from './css-longhand-resolver'
22
24
  import { buildDataItem } from './data-item-builder'
23
25
  import { formatDisplayName } from './utils'
24
26
 
@@ -305,7 +307,7 @@ function getMatchedPropertyValues(element: ExtractedElement): Map<string, string
305
307
  const values = new Map<string, string>()
306
308
  for (const match of matches) {
307
309
  for (const prop of match.properties) {
308
- values.set(prop.name, prop.value)
310
+ values.set(camelCase(prop.name), prop.value)
309
311
  }
310
312
  }
311
313
  return values
@@ -340,7 +342,7 @@ function buildCssProperties(element: ExtractedElement | undefined): Record<strin
340
342
  result[propName] = buildDisplayProperty(element)
341
343
  continue
342
344
  }
343
- const defaultValue = cssPropertyValues.get(propName)
345
+ const defaultValue = resolveLonghand(propName, cssPropertyValues)
344
346
  result[propName] = {
345
347
  ...(defaultValue !== undefined && { defaultValue }),
346
348
  }