@wix/zero-config-implementation 1.92.0 → 1.94.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.92.0",
7
+ "version": "1.94.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",
@@ -39,7 +39,7 @@
39
39
  }
40
40
  },
41
41
  "dependencies": {
42
- "@wix/react-component-schema": "1.8.0",
42
+ "@wix/react-component-schema": "1.9.0",
43
43
  "@wix/seo-service": "^1.9.0",
44
44
  "@wix/services-manager": "^1.0.7",
45
45
  "@wix/services-manager-react": "^1.0.8",
@@ -106,5 +106,5 @@
106
106
  ]
107
107
  }
108
108
  },
109
- "falconPackageHash": "db2c3fec617a486d30e2d52ed7e239747365a40d14666a5ea04cf07d"
109
+ "falconPackageHash": "c359f3991c9148a6bb0473cbb1eef6bfef7fd8401e45720aaed8b44c"
110
110
  }
@@ -107,6 +107,103 @@ describe('toEditorReactComponent display conversion', () => {
107
107
  })
108
108
  })
109
109
 
110
+ // CSS display values whose spelling differs from the enum's. Emitting one
111
+ // unmapped makes DevCenter reject the whole components override, permanently
112
+ // blocking Checkpoint for the project (ECL-18531).
113
+ describe('display value enum mapping', () => {
114
+ const { DISPLAY_VALUE } = CSS_PROPERTIES
115
+ const tagDefaults: [tag: string, cssDisplay: string, enumValue: string][] = [
116
+ ['td', 'table-cell', DISPLAY_VALUE.tableCell],
117
+ ['th', 'table-cell', DISPLAY_VALUE.tableCell],
118
+ ['tr', 'table-row', DISPLAY_VALUE.tableRow],
119
+ ['tbody', 'table-row-group', DISPLAY_VALUE.tableRowGroup],
120
+ ['thead', 'table-header-group', DISPLAY_VALUE.tableHeaderGroup],
121
+ ['tfoot', 'table-footer-group', DISPLAY_VALUE.tableFooterGroup],
122
+ ['caption', 'table-caption', DISPLAY_VALUE.tableCaption],
123
+ ['col', 'table-column', DISPLAY_VALUE.tableColumn],
124
+ ['colgroup', 'table-column-group', DISPLAY_VALUE.tableColumnGroup],
125
+ ]
126
+
127
+ it.each(tagDefaults)('maps a <%s> tag default (%s) onto its enum name', (tag, _cssDisplay, expectedEnumValue) => {
128
+ const rootElement = createElementWithDisplayMatcher(tag, matcherDataFromCss(''))
129
+
130
+ const result = toEditorReactComponent(createComponent(rootElement))
131
+
132
+ expect(result.editorElement?.cssProperties?.display).toEqual({
133
+ display: {
134
+ displayValues: [CSS_PROPERTIES.DISPLAY_VALUE.none, expectedEnumValue],
135
+ },
136
+ })
137
+ })
138
+
139
+ it('treats an explicit `display: table-cell` declaration the same as the tag default', () => {
140
+ const fromTag = toEditorReactComponent(
141
+ createComponent(createElementWithDisplayMatcher('td', matcherDataFromCss(''))),
142
+ )
143
+ const fromCss = toEditorReactComponent(
144
+ createComponent(createElementWithDisplayMatcher('div', matcherDataFromCss('display: table-cell'))),
145
+ )
146
+
147
+ expect(fromCss.editorElement?.cssProperties?.display).toEqual(fromTag.editorElement?.cssProperties?.display)
148
+ expect(fromCss.editorElement?.cssProperties?.display).toEqual({
149
+ display: {
150
+ displayValues: [CSS_PROPERTIES.DISPLAY_VALUE.none, CSS_PROPERTIES.DISPLAY_VALUE.tableCell],
151
+ },
152
+ })
153
+ })
154
+
155
+ it('still maps kebab-case values that do have an enum representation', () => {
156
+ const rootElement = createElementWithDisplayMatcher('div', matcherDataFromCss('display: inline-block'))
157
+
158
+ const result = toEditorReactComponent(createComponent(rootElement))
159
+
160
+ expect(result.editorElement?.cssProperties?.display).toEqual({
161
+ display: {
162
+ displayValues: [CSS_PROPERTIES.DISPLAY_VALUE.none, CSS_PROPERTIES.DISPLAY_VALUE.inlineBlock],
163
+ },
164
+ })
165
+ })
166
+
167
+ it('never emits a display value outside the enum for any known tag default', () => {
168
+ const allowedValues = new Set<string>(Object.values(CSS_PROPERTIES.DISPLAY_VALUE))
169
+
170
+ for (const [tag] of tagDefaults) {
171
+ const rootElement = createElementWithDisplayMatcher(tag, matcherDataFromCss(''))
172
+ const result = toEditorReactComponent(createComponent(rootElement))
173
+ const emittedValues = result.editorElement?.cssProperties?.display?.display?.displayValues ?? []
174
+
175
+ expect(emittedValues.filter((displayValue) => !allowedValues.has(displayValue))).toEqual([])
176
+ }
177
+ })
178
+
179
+ /**
180
+ * Completeness check driven by the enum itself, so a value added in a future
181
+ * `@wix/component-protocol` bump cannot sit unmapped — which is the gap that
182
+ * made ECL-18531 possible. Values whose CSS spelling already matches the enum
183
+ * name need no entry; those that differ must be mapped.
184
+ *
185
+ * Skips names containing `_`: `UNKNOWN_DisplayValue` and the deprecated
186
+ * snake_case aliases, none of which are CSS spellings.
187
+ */
188
+ it('reaches every enum value from its CSS spelling', () => {
189
+ const toCssSpelling = (enumValue: string) => enumValue.replace(/[A-Z]/g, (char) => `-${char.toLowerCase()}`)
190
+
191
+ const unreachable = Object.values(CSS_PROPERTIES.DISPLAY_VALUE)
192
+ .filter((enumValue) => !enumValue.includes('_'))
193
+ .filter((enumValue) => {
194
+ const rootElement = createElementWithDisplayMatcher(
195
+ 'div',
196
+ matcherDataFromCss(`display: ${toCssSpelling(enumValue)}`),
197
+ )
198
+ const result = toEditorReactComponent(createComponent(rootElement))
199
+
200
+ return result.editorElement?.cssProperties?.display?.display?.displayValues?.[1] !== enumValue
201
+ })
202
+
203
+ expect(unreachable).toEqual([])
204
+ })
205
+ })
206
+
110
207
  it('omits the nearest matching ancestor class prefix from inner element display names', () => {
111
208
  const rootElement = createSemanticElement({
112
209
  traceId: 'trace-root',
@@ -541,20 +541,30 @@ function getMatchedPropertyValues(element: ExtractedElement): Map<string, string
541
541
  return values
542
542
  }
543
543
 
544
- const CSS_DISPLAY_TO_ENUM: Record<string, string> = {
545
- 'inline-block': 'inlineBlock',
546
- 'inline-flex': 'inlineFlex',
547
- 'inline-grid': 'inlineGrid',
548
- 'inline-table': 'inlineTable',
549
- 'list-item': 'listItem',
550
- 'flow-root': 'flowRoot',
551
- }
544
+ const { CSS_PROPERTY_TYPE, DISPLAY_VALUE } = CSS_PROPERTIES
552
545
 
553
- function toDisplayEnumValue(cssValue: string): NonNullable<Display['displayValues']>[number] {
554
- return (CSS_DISPLAY_TO_ENUM[cssValue] ?? cssValue) as NonNullable<Display['displayValues']>[number]
546
+ type DisplayEnumValue = NonNullable<Display['displayValues']>[number]
547
+
548
+ const CSS_DISPLAY_TO_ENUM: Record<string, DisplayEnumValue> = {
549
+ 'inline-block': DISPLAY_VALUE.inlineBlock,
550
+ 'inline-flex': DISPLAY_VALUE.inlineFlex,
551
+ 'inline-grid': DISPLAY_VALUE.inlineGrid,
552
+ 'inline-table': DISPLAY_VALUE.inlineTable,
553
+ 'list-item': DISPLAY_VALUE.listItem,
554
+ 'flow-root': DISPLAY_VALUE.flowRoot,
555
+ 'table-row-group': DISPLAY_VALUE.tableRowGroup,
556
+ 'table-header-group': DISPLAY_VALUE.tableHeaderGroup,
557
+ 'table-footer-group': DISPLAY_VALUE.tableFooterGroup,
558
+ 'table-row': DISPLAY_VALUE.tableRow,
559
+ 'table-cell': DISPLAY_VALUE.tableCell,
560
+ 'table-column-group': DISPLAY_VALUE.tableColumnGroup,
561
+ 'table-column': DISPLAY_VALUE.tableColumn,
562
+ 'table-caption': DISPLAY_VALUE.tableCaption,
555
563
  }
556
564
 
557
- const { CSS_PROPERTY_TYPE, DISPLAY_VALUE } = CSS_PROPERTIES
565
+ function toDisplayEnumValue(cssValue: string): DisplayEnumValue {
566
+ return (CSS_DISPLAY_TO_ENUM[cssValue] ?? cssValue) as DisplayEnumValue
567
+ }
558
568
 
559
569
  function buildCssProperties(element: ExtractedElement | undefined): Record<string, CssPropertyItem> {
560
570
  const result: Record<string, CssPropertyItem> = {}
@@ -38,7 +38,7 @@ function createElementWithCssProperties(
38
38
  ): ExtractedElement {
39
39
  const extractorData = new Map<string, unknown>()
40
40
  extractorData.set('css-matcher', matcherDataFromCss(declarations))
41
- extractorData.set('css-properties', { relevant: getCssPropertiesForTag(tag, attributes.role) })
41
+ extractorData.set('css-properties', { relevant: getCssPropertiesForTag(tag, attributes) })
42
42
 
43
43
  return {
44
44
  traceId: 'trace-1',
@@ -173,11 +173,38 @@ describe('getCssPropertiesForTag', () => {
173
173
  expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
174
174
  })
175
175
 
176
- it('derives box properties from default display for controls', () => {
177
- const relevantProperties = getCssPropertiesForTag('button')
176
+ it('gives interactive controls both box and text properties', () => {
177
+ for (const tag of ['button', 'select', 'textarea']) {
178
+ const relevantProperties = getCssPropertiesForTag(tag)
179
+
180
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.paddingTop)
181
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
182
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.color)
183
+ }
184
+ })
185
+
186
+ it('treats an input with no type as text-bearing', () => {
187
+ const relevantProperties = getCssPropertiesForTag('input')
178
188
 
179
189
  expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.paddingTop)
180
- expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
190
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
191
+ })
192
+
193
+ it('treats text-entry and button-like input types as text-bearing', () => {
194
+ for (const inputType of ['text', 'email', 'password', 'search', 'number', 'submit', 'button', 'reset']) {
195
+ const relevantProperties = getCssPropertiesForTag('input', { type: inputType })
196
+
197
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
198
+ }
199
+ })
200
+
201
+ it('keeps typography off input types that render no text of their own', () => {
202
+ for (const inputType of ['checkbox', 'radio', 'range', 'color', 'file', 'image', 'hidden']) {
203
+ const relevantProperties = getCssPropertiesForTag('input', { type: inputType })
204
+
205
+ expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.paddingTop)
206
+ expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
207
+ }
181
208
  })
182
209
 
183
210
  it('keeps media elements out of box properties even when their default display is non-inline', () => {
@@ -203,7 +230,7 @@ describe('getCssPropertiesForTag', () => {
203
230
  })
204
231
 
205
232
  it('keeps role-based heading text-only by default', () => {
206
- const relevantProperties = getCssPropertiesForTag('div', 'heading')
233
+ const relevantProperties = getCssPropertiesForTag('div', { role: 'heading' })
207
234
 
208
235
  expect(relevantProperties).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.font)
209
236
  expect(relevantProperties).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.paddingTop)
@@ -19,12 +19,18 @@ export interface CssPropertiesData {
19
19
  relevant: string[]
20
20
  }
21
21
 
22
+ export interface SemanticAttributes {
23
+ role?: string
24
+ type?: string
25
+ }
26
+
22
27
  // ─────────────────────────────────────────────────────────────────────────────
23
28
  // Semantic Traits
24
29
  // ─────────────────────────────────────────────────────────────────────────────
25
30
 
26
31
  /**
27
- * Text & Typography - elements where readability and hierarchy are primary
32
+ * Text & Typography - elements where readability and hierarchy are primary.
33
+ * Form controls render their own text; `input` varies by type (see below).
28
34
  */
29
35
  const TEXT_TAGS = new Set([
30
36
  'h1',
@@ -42,8 +48,14 @@ const TEXT_TAGS = new Set([
42
48
  'em',
43
49
  'b',
44
50
  'i',
51
+ 'button',
52
+ 'select',
53
+ 'textarea',
45
54
  ])
46
55
 
56
+ /** Anything else - including a missing or invalid `type` - defaults to text. */
57
+ const NON_TEXT_INPUT_TYPES = new Set(['checkbox', 'radio', 'range', 'color', 'file', 'image', 'hidden'])
58
+
47
59
  const DEFAULT_NON_BOX_TEXT_TAGS = new Set(['p', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6'])
48
60
 
49
61
  /**
@@ -107,13 +119,15 @@ const MEDIA_CSS_PROPERTIES = [
107
119
  CSS_PROPERTY_TYPE.boxShadow,
108
120
  ]
109
121
 
110
- function hasTextSemantics(tag: string, role?: string): boolean {
122
+ function hasTextSemantics(tag: string, attributes: SemanticAttributes = {}): boolean {
123
+ const normalizedRole = attributes.role?.toLowerCase()
124
+ if (normalizedRole === 'heading' || normalizedRole === 'paragraph') {
125
+ return true
126
+ }
127
+
111
128
  const normalizedTag = tag.toLowerCase()
112
- if (role) {
113
- const normalizedRole = role.toLowerCase()
114
- if (normalizedRole === 'heading' || normalizedRole === 'paragraph') {
115
- return true
116
- }
129
+ if (normalizedTag === 'input') {
130
+ return !NON_TEXT_INPUT_TYPES.has((attributes.type ?? 'text').toLowerCase())
117
131
  }
118
132
  return TEXT_TAGS.has(normalizedTag)
119
133
  }
@@ -135,11 +149,11 @@ function hasMediaSemantics(tag: string, role?: string): boolean {
135
149
  * Note: hasTextContent determination is deferred to tree building since
136
150
  * we can't know during createElement if a child will have text content.
137
151
  */
138
- export function getCssPropertiesForTag(tag: string, role?: string): string[] {
152
+ export function getCssPropertiesForTag(tag: string, attributes: SemanticAttributes = {}): string[] {
139
153
  const properties: string[] = []
140
- const hasTextProperties = hasTextSemantics(tag, role)
141
- const hasMediaProperties = hasMediaSemantics(tag, role)
142
- const hasBoxProperties = hasDefaultBoxProperties(tag, role) && !hasMediaProperties
154
+ const hasTextProperties = hasTextSemantics(tag, attributes)
155
+ const hasMediaProperties = hasMediaSemantics(tag, attributes.role)
156
+ const hasBoxProperties = hasDefaultBoxProperties(tag, attributes.role) && !hasMediaProperties
143
157
 
144
158
  if (hasTextProperties) {
145
159
  properties.push(...TEXT_CSS_PROPERTIES)
@@ -465,12 +479,11 @@ export function enrichGapProperties(elements: ExtractedElement[]): ExtractedElem
465
479
  */
466
480
  export function enrichContainerProperties(elements: ExtractedElement[]): ExtractedElement[] {
467
481
  return elements.map((element) => {
468
- const role = element.attributes.role
469
- if (hasTextSemantics(element.tag, role)) {
482
+ if (hasTextSemantics(element.tag, element.attributes)) {
470
483
  const matcherData = element.extractorData.get('css-matcher') as MatchedCssData | undefined
471
484
  const cssData = element.extractorData.get('css-properties') as CssPropertiesData | undefined
472
485
 
473
- if (cssData && hasEffectiveContainerDisplay(element.tag, role, matcherData)) {
486
+ if (cssData && hasEffectiveContainerDisplay(element.tag, element.attributes.role, matcherData)) {
474
487
  element.extractorData.set('css-properties', {
475
488
  relevant: addBoxProperties(cssData.relevant),
476
489
  })
@@ -500,9 +513,10 @@ export function createCssPropertiesExtractor(): ReactExtractor {
500
513
  if (!event.isDomElement || !event.tag || !event.traceId) return
501
514
 
502
515
  const { tag, props, traceId, store } = event
503
- const role = props.role as string | undefined
504
-
505
- const relevant = getCssPropertiesForTag(tag, role)
516
+ const relevant = getCssPropertiesForTag(tag, {
517
+ role: props.role as string | undefined,
518
+ type: props.type as string | undefined,
519
+ })
506
520
  const data: CssPropertiesData = { relevant }
507
521
 
508
522
  store.set(traceId, 'css-properties', data)