@wix/zero-config-implementation 1.105.0 → 1.107.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.105.0",
7
+ "version": "1.107.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",
@@ -106,5 +106,5 @@
106
106
  ]
107
107
  }
108
108
  },
109
- "falconPackageHash": "0c8ba866b638c1cacaeaecfc2e82c36a40e6307f5e3896c922c1dc25"
109
+ "falconPackageHash": "06e07178bd2c8944810a496429499f8a1f4a9007f694fdeb8d896363"
110
110
  }
@@ -0,0 +1,308 @@
1
+ import { CSS_PROPERTIES } from '@wix/react-component-schema'
2
+ import { describe, expect, it } from 'vitest'
3
+ import { TRACE_ATTR } from '../component-renderer'
4
+ import { parseCss } from '../information-extractors/css/parse'
5
+ import { matchCssSelectors } from '../information-extractors/css/selector-matcher'
6
+ import type { ExtractedElement } from '../information-extractors/react'
7
+ import type { ComponentInfoWithCss, ExtractedCssInfo } from '../manifest-pipeline'
8
+ import { toEditorReactComponent } from './to-editor-component'
9
+
10
+ const { CSS_PROPERTY_TYPE } = CSS_PROPERTIES
11
+
12
+ interface ElementSpec {
13
+ name: string
14
+ tag: string
15
+ className: string
16
+ relevant: string[]
17
+ children?: ElementSpec[]
18
+ }
19
+
20
+ function toExtractedElement(spec: ElementSpec): ExtractedElement {
21
+ return {
22
+ traceId: `trace-${spec.name}`,
23
+ name: spec.name,
24
+ tag: spec.tag,
25
+ attributes: { class: spec.className },
26
+ extractorData: new Map<string, unknown>([['css-properties', { relevant: spec.relevant }]]),
27
+ children: (spec.children ?? []).map(toExtractedElement),
28
+ }
29
+ }
30
+
31
+ function toHtml(spec: ElementSpec): string {
32
+ const childrenHtml = (spec.children ?? []).map(toHtml).join('')
33
+ return `<${spec.tag} ${TRACE_ATTR}="trace-${spec.name}" class="${spec.className}">${childrenHtml}</${spec.tag}>`
34
+ }
35
+
36
+ /** Runs the CSS through the real selector matcher, then converts the matched tree. */
37
+ function convert(css: string, rootSpec: ElementSpec) {
38
+ const api = parseCss(css)
39
+ const cssInfo: ExtractedCssInfo = {
40
+ filePath: 'test.css',
41
+ api,
42
+ properties: new Map(),
43
+ customProperties: new Map(),
44
+ isCssModule: false,
45
+ }
46
+ const { elements, varUsedByTraceId } = matchCssSelectors(toHtml(rootSpec), [toExtractedElement(rootSpec)], [cssInfo])
47
+ const component: ComponentInfoWithCss = {
48
+ componentName: 'StateDefaults',
49
+ props: {},
50
+ elements,
51
+ propUsages: new Map(),
52
+ css: [cssInfo],
53
+ varUsedByTraceId,
54
+ }
55
+ const { editorElement } = toEditorReactComponent(component)
56
+ if (!editorElement) throw new Error('toEditorReactComponent produced no editorElement')
57
+ return editorElement
58
+ }
59
+
60
+ function rootButton(relevant: string[], children: ElementSpec[] = []): ElementSpec {
61
+ return { name: 'root', tag: 'button', className: 'widget', relevant, children }
62
+ }
63
+
64
+ describe('toEditorReactComponent — statesDefaultValues', () => {
65
+ it('keeps the regular default and moves a hover override into statesDefaultValues', () => {
66
+ const editorElement = convert(
67
+ `.widget { background: red; }
68
+ .widget:global(.widget--hover), .widget:hover { background: blue; }`,
69
+ rootButton([CSS_PROPERTY_TYPE.background]),
70
+ )
71
+
72
+ expect(editorElement.cssProperties?.background).toEqual({
73
+ defaultValue: 'red',
74
+ statesDefaultValues: { hover: 'blue' },
75
+ })
76
+ })
77
+
78
+ it('emits no statesDefaultValues when the hover rule does not touch the property', () => {
79
+ const editorElement = convert(
80
+ `.widget { background: red; }
81
+ .widget:hover { color: blue; }`,
82
+ rootButton([CSS_PROPERTY_TYPE.background]),
83
+ )
84
+
85
+ expect(editorElement.cssProperties?.background).toEqual({ defaultValue: 'red' })
86
+ })
87
+
88
+ it('omits a state value equal to the regular default', () => {
89
+ const editorElement = convert(
90
+ `.widget { background: red; }
91
+ .widget:hover { background: red; }`,
92
+ rootButton([CSS_PROPERTY_TYPE.background]),
93
+ )
94
+
95
+ expect(editorElement.cssProperties?.background).toEqual({ defaultValue: 'red' })
96
+ })
97
+
98
+ it('resolves a state shorthand into longhands without letting a regular longhand win', () => {
99
+ const editorElement = convert(
100
+ `.widget { padding-top: 1px; padding-bottom: 1px; }
101
+ .widget:hover { padding: 8px; }`,
102
+ rootButton([CSS_PROPERTY_TYPE.paddingTop, CSS_PROPERTY_TYPE.paddingBottom]),
103
+ )
104
+
105
+ expect(editorElement.cssProperties?.paddingTop).toEqual({
106
+ defaultValue: '1px',
107
+ statesDefaultValues: { hover: '8px' },
108
+ })
109
+ })
110
+
111
+ it('maps the custom `active` class trigger and the `:active` pseudo to the same state', () => {
112
+ const editorElement = convert(
113
+ `.widget { color: black; }
114
+ .widget:global(.widget--active), .widget:active { color: white; }`,
115
+ rootButton([CSS_PROPERTY_TYPE.color]),
116
+ )
117
+
118
+ expect(editorElement.states?.active).toEqual({ displayName: 'Active', className: 'widget--active' })
119
+ expect(editorElement.cssProperties?.color).toEqual({
120
+ defaultValue: 'black',
121
+ statesDefaultValues: { active: 'white' },
122
+ })
123
+ })
124
+
125
+ it('neither emits nor leaks a CSS state the element does not expose', () => {
126
+ // A plain <span> has no native states, so its `:hover` rule has nowhere to go.
127
+ const editorElement = convert(
128
+ `.widget__caption { color: black; }
129
+ .widget__caption:hover { color: blue; }`,
130
+ rootButton(
131
+ [],
132
+ [{ name: 'caption', tag: 'span', className: 'widget__caption', relevant: [CSS_PROPERTY_TYPE.color] }],
133
+ ),
134
+ )
135
+
136
+ expect(editorElement.elements?.caption?.inlineElement?.cssProperties?.color).toEqual({ defaultValue: 'black' })
137
+ })
138
+
139
+ it('emits statesDefaultValues on an inner element with its own native hover state', () => {
140
+ const editorElement = convert(
141
+ `.widget__action { color: black; }
142
+ .widget__action:hover { color: blue; }`,
143
+ rootButton(
144
+ [],
145
+ [{ name: 'action', tag: 'button', className: 'widget__action', relevant: [CSS_PROPERTY_TYPE.color] }],
146
+ ),
147
+ )
148
+
149
+ const actionElement = editorElement.elements?.action?.inlineElement
150
+ expect(actionElement?.states?.hover).toBeDefined()
151
+ expect(actionElement?.cssProperties?.color).toEqual({
152
+ defaultValue: 'black',
153
+ statesDefaultValues: { hover: 'blue' },
154
+ })
155
+ })
156
+
157
+ it("does not apply an ancestor's state rule to a descendant's regular default", () => {
158
+ const editorElement = convert(
159
+ `.widget__label { color: black; }
160
+ .widget:hover .widget__label { color: blue; }`,
161
+ rootButton([], [{ name: 'label', tag: 'span', className: 'widget__label', relevant: [CSS_PROPERTY_TYPE.color] }]),
162
+ )
163
+
164
+ expect(editorElement.elements?.label?.inlineElement?.cssProperties?.color).toEqual({ defaultValue: 'black' })
165
+ })
166
+
167
+ it('does not let a display declared only under hover change the regular display', () => {
168
+ const editorElement = convert(
169
+ `.widget__icon { display: inline; }
170
+ .widget:hover .widget__icon, .widget__icon:hover { display: none; }`,
171
+ rootButton([], [{ name: 'icon', tag: 'span', className: 'widget__icon', relevant: [CSS_PROPERTY_TYPE.display] }]),
172
+ )
173
+
174
+ expect(editorElement.elements?.icon?.inlineElement?.cssProperties?.display).toEqual({
175
+ display: { displayValues: ['none', 'inline'] },
176
+ })
177
+ })
178
+
179
+ it('does not let a conditional (@media) declaration beat the top-level one', () => {
180
+ const editorElement = convert(
181
+ `.widget { background: red; }
182
+ .widget:hover { background: blue; }
183
+ @media (prefers-reduced-motion: reduce) { .widget { background: gray; } .widget:hover { background: none; } }`,
184
+ rootButton([CSS_PROPERTY_TYPE.background]),
185
+ )
186
+
187
+ expect(editorElement.cssProperties?.background).toEqual({
188
+ defaultValue: 'red',
189
+ statesDefaultValues: { hover: 'blue' },
190
+ })
191
+ })
192
+
193
+ it('adds a state value to a custom property declared under that state on its own element', () => {
194
+ const editorElement = convert(
195
+ `.widget { --accent: red; color: var(--accent); }
196
+ .widget:hover { --accent: blue; }`,
197
+ rootButton([CSS_PROPERTY_TYPE.color]),
198
+ )
199
+
200
+ expect(editorElement.cssCustomProperties?.accent).toMatchObject({
201
+ defaultValue: 'red',
202
+ statesDefaultValues: { hover: 'blue' },
203
+ })
204
+ })
205
+
206
+ it("does not attach a descendant's state declaration to the ancestor a variable is placed on", () => {
207
+ // `--accent` is declared on both elements, so it is placed on their common ancestor (the root);
208
+ // the hover value belongs to the inner button's state, not the root's.
209
+ const editorElement = convert(
210
+ `.widget { --accent: red; }
211
+ .widget__action { --accent: green; }
212
+ .widget__action:hover { --accent: blue; }`,
213
+ rootButton([], [{ name: 'action', tag: 'button', className: 'widget__action', relevant: [] }]),
214
+ )
215
+
216
+ expect(editorElement.cssCustomProperties?.accent).not.toHaveProperty('statesDefaultValues')
217
+ })
218
+
219
+ it('does not let a conditional (@media) longhand beat a top-level shorthand', () => {
220
+ const editorElement = convert(
221
+ `.widget { padding: 0; }
222
+ @media (max-width: 600px) { .widget { padding-top: 10px; } }`,
223
+ rootButton([CSS_PROPERTY_TYPE.paddingTop]),
224
+ )
225
+
226
+ expect(editorElement.cssProperties?.paddingTop).toEqual({ defaultValue: '0' })
227
+ })
228
+
229
+ it('does not let a conditional (@media) custom property beat the top-level one', () => {
230
+ const editorElement = convert(
231
+ `.widget { --accent: red; color: var(--accent); }
232
+ @media (max-width: 600px) { .widget { --accent: blue; } }`,
233
+ rootButton([CSS_PROPERTY_TYPE.color]),
234
+ )
235
+
236
+ expect(editorElement.cssCustomProperties?.accent).toMatchObject({ defaultValue: 'red' })
237
+ })
238
+
239
+ it('emits no control for a variable declared only under a state the element does not expose', () => {
240
+ // A <button> has native hover/disabled states but no `active` state (no `--active` class trigger).
241
+ const editorElement = convert(
242
+ `.widget { color: var(--accent); }
243
+ .widget:active { --accent: blue; }`,
244
+ rootButton([CSS_PROPERTY_TYPE.color]),
245
+ )
246
+
247
+ expect(editorElement.states?.active).toBeUndefined()
248
+ expect(editorElement.cssCustomProperties).not.toHaveProperty('accent')
249
+ })
250
+
251
+ it('does not let a global state modifier nested in :is() leak into the regular default', () => {
252
+ const editorElement = convert(
253
+ `.widget { color: black; }
254
+ .widget:is(:global(.widget--selected)) { color: blue; }`,
255
+ rootButton([CSS_PROPERTY_TYPE.color]),
256
+ )
257
+
258
+ expect(editorElement.cssProperties?.color).toEqual({ defaultValue: 'black' })
259
+ })
260
+
261
+ it('emits a state display value while keeping the display options', () => {
262
+ const editorElement = convert(
263
+ `.widget__action { display: block; }
264
+ .widget__action:global(.widget__action--collapsed) { display: none; }
265
+ .widget__action:global(.widget__action--expanded) { display: flex; }`,
266
+ rootButton(
267
+ [],
268
+ [{ name: 'action', tag: 'button', className: 'widget__action', relevant: [CSS_PROPERTY_TYPE.display] }],
269
+ ),
270
+ )
271
+
272
+ const actionElement = editorElement.elements?.action?.inlineElement
273
+ expect(actionElement?.states?.collapsed).toBeDefined()
274
+ expect(actionElement?.cssProperties?.display).toEqual({
275
+ display: { displayValues: ['none', 'block', 'flex'] },
276
+ statesDefaultValues: { collapsed: 'none', expanded: 'flex' },
277
+ })
278
+ })
279
+
280
+ it('resolves a state display that only changes the variable the regular display reads', () => {
281
+ const editorElement = convert(
282
+ `.widget__action { display: var(--layout); --layout: block; }
283
+ .widget__action:hover { --layout: none; }`,
284
+ rootButton(
285
+ [],
286
+ [{ name: 'action', tag: 'button', className: 'widget__action', relevant: [CSS_PROPERTY_TYPE.display] }],
287
+ ),
288
+ )
289
+
290
+ expect(editorElement.elements?.action?.inlineElement?.cssProperties?.display).toEqual({
291
+ display: { displayValues: ['none', 'block'] },
292
+ statesDefaultValues: { hover: 'none' },
293
+ })
294
+ })
295
+
296
+ describe('known limitations', () => {
297
+ it('drops a state longhand that cannot be recomposed into a regular shorthand target', () => {
298
+ // `font` is composed from its longhands only when every one is declared in the same layer.
299
+ const editorElement = convert(
300
+ `.widget { font: 400 16px/1.5 Arial; }
301
+ .widget:hover { font-weight: 700; }`,
302
+ rootButton([CSS_PROPERTY_TYPE.font]),
303
+ )
304
+
305
+ expect(editorElement.cssProperties?.font).not.toHaveProperty('statesDefaultValues')
306
+ })
307
+ })
308
+ })
@@ -590,91 +590,6 @@ describe('toEditorReactComponent — @property design tokens', () => {
590
590
  })
591
591
  })
592
592
 
593
- it('carries the --min / --max / --step descriptors of a number @property into number limits', () => {
594
- const component = createComponentWithCss(`
595
- @property --speed { syntax: "<number>"; inherits: true; initial-value: 4; --min: 1; --max: 10; --step: 0.5; }
596
- `)
597
-
598
- const customProps = toEditorReactComponent(component).editorElement?.cssCustomProperties
599
-
600
- expect(customProps?.speed).toEqual({
601
- displayName: 'Speed',
602
- defaultValue: '4',
603
- cssPropertyType: 'number',
604
- number: { min: '1', max: '10', multiplier: '0.5' },
605
- })
606
- })
607
-
608
- it('emits only the usable half of a partial range and omits the rest', () => {
609
- const component = createComponentWithCss(`
610
- @property --weight { syntax: "<number>"; inherits: true; initial-value: 2; --min: 0; }
611
- @property --tempo { syntax: "<number>"; inherits: true; initial-value: 3; --max: 9; --step: nonsense; }
612
- @property --offset { syntax: "<number>"; inherits: true; initial-value: 0; --min: ; --max: 12; }
613
- `)
614
-
615
- const customProps = toEditorReactComponent(component).editorElement?.cssCustomProperties
616
-
617
- expect(customProps?.weight).toEqual({
618
- displayName: 'Weight',
619
- defaultValue: '2',
620
- cssPropertyType: 'number',
621
- number: { min: '0' },
622
- })
623
- expect(customProps?.tempo).toEqual({
624
- displayName: 'Tempo',
625
- defaultValue: '3',
626
- cssPropertyType: 'number',
627
- number: { max: '9' },
628
- })
629
- expect(customProps?.offset).toEqual({
630
- displayName: 'Offset',
631
- defaultValue: '0',
632
- cssPropertyType: 'number',
633
- number: { max: '12' },
634
- })
635
- })
636
-
637
- it('drops an inverted range and a non-positive step, omitting `number` when nothing is usable', () => {
638
- const component = createComponentWithCss(`
639
- @property --span { syntax: "<number>"; inherits: true; initial-value: 5; --min: 10; --max: 2; --step: 0; }
640
- @property --frozen { syntax: "<number>"; inherits: true; initial-value: 5; --min: 5; --max: 5; --step: 1; }
641
- `)
642
-
643
- const customProps = toEditorReactComponent(component).editorElement?.cssCustomProperties
644
-
645
- expect(customProps?.span).toEqual({
646
- displayName: 'Span',
647
- defaultValue: '5',
648
- cssPropertyType: 'number',
649
- })
650
- expect(customProps?.frozen).toEqual({
651
- displayName: 'Frozen',
652
- defaultValue: '5',
653
- cssPropertyType: 'number',
654
- number: { multiplier: '1' },
655
- })
656
- })
657
-
658
- it('ignores range descriptors on a non-number @property', () => {
659
- const component = createComponentWithCss(`
660
- @property --sdf-safelight-color { syntax: "<color>"; inherits: true; initial-value: #F02011; --min: 1; --max: 10; }
661
- @property --gap { syntax: "<length>"; inherits: true; initial-value: 4px; --min: 1; --max: 10; --step: 1; }
662
- `)
663
-
664
- const customProps = toEditorReactComponent(component).editorElement?.cssCustomProperties
665
-
666
- expect(customProps?.['sdf-safelight-color']).toEqual({
667
- displayName: 'Sdf Safelight Color',
668
- defaultValue: '#F02011',
669
- cssPropertyType: 'color',
670
- })
671
- expect(customProps?.gap).toEqual({
672
- displayName: 'Gap',
673
- defaultValue: '4px',
674
- cssPropertyType: 'length',
675
- })
676
- })
677
-
678
593
  it('emits a customEnum dropdown for a pipe-separated ident-list @property', () => {
679
594
  const component = createComponentWithCss(`
680
595
  @property --sdf-palette { syntax: "warm | cool | neutral"; inherits: false; initial-value: warm; }