@wix/zero-config-implementation 1.102.0 → 1.104.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.102.0",
7
+ "version": "1.104.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": "96d9e739c8567fd3813b698d6e9d84d175e3def2393dbaec37e6a9e7"
109
+ "falconPackageHash": "d233fc718503fd69a8efbad0624d506a5450fdd5e310b5b11cf42832"
110
110
  }
@@ -393,4 +393,31 @@ describe('resolveLonghand', () => {
393
393
  expect(resolveLonghand('textDecorationLine', values)).toBe('overline')
394
394
  })
395
395
  })
396
+
397
+ describe('flexDirection extraction from flex-flow shorthand', () => {
398
+ it('extracts the lone direction keyword', () => {
399
+ expect(resolveLonghand('flexDirection', new Map([['flexFlow', 'column']]))).toBe('column')
400
+ })
401
+
402
+ it('ignores token order — direction keyword can appear anywhere', () => {
403
+ expect(resolveLonghand('flexDirection', new Map([['flexFlow', 'wrap column-reverse']]))).toBe('column-reverse')
404
+ })
405
+
406
+ it('defaults to row when the shorthand carries only a wrap keyword', () => {
407
+ expect(resolveLonghand('flexDirection', new Map([['flexFlow', 'wrap']]))).toBe('row')
408
+ expect(resolveLonghand('flexDirection', new Map([['flexFlow', 'nowrap']]))).toBe('row')
409
+ })
410
+
411
+ it('returns undefined when flexFlow is absent', () => {
412
+ expect(resolveLonghand('flexDirection', new Map())).toBeUndefined()
413
+ })
414
+
415
+ it('prefers literal flexDirection over extracting from shorthand', () => {
416
+ const values = new Map([
417
+ ['flexDirection', 'row-reverse'],
418
+ ['flexFlow', 'column wrap'],
419
+ ])
420
+ expect(resolveLonghand('flexDirection', values)).toBe('row-reverse')
421
+ })
422
+ })
396
423
  })
@@ -217,14 +217,20 @@ const fromTextDecorationLine: Fallback = (values) => {
217
217
 
218
218
  /**
219
219
  * `flex-flow: <flex-direction> || <flex-wrap>` — order is free and either
220
- * part may be omitted. Pick out just the direction keyword for the schema.
220
+ * part may be omitted. Pick out the direction keyword for the schema, or the
221
+ * spec's default (`row`) when the developer only wrote a wrap keyword, e.g.
222
+ * `flex-flow: wrap`.
221
223
  */
222
224
  const FLEX_DIRECTION_KEYWORDS = new Set(['row', 'row-reverse', 'column', 'column-reverse'])
225
+ const DEFAULT_FLEX_DIRECTION = 'row'
223
226
 
224
227
  const fromFlexFlowDirection: Fallback = (values) => {
225
228
  const shorthand = values.get('flexFlow')
226
229
  if (shorthand === undefined) return undefined
227
- return splitTopLevelTokens(shorthand).find((token) => FLEX_DIRECTION_KEYWORDS.has(token.toLowerCase()))
230
+ const directionToken = splitTopLevelTokens(shorthand).find((token) =>
231
+ FLEX_DIRECTION_KEYWORDS.has(token.toLowerCase()),
232
+ )
233
+ return directionToken ?? DEFAULT_FLEX_DIRECTION
228
234
  }
229
235
 
230
236
  // ─────────────────────────────────────────────────────────────────────────────
@@ -117,7 +117,7 @@ function applyResolvedTypeToDataItem(
117
117
  return handleObjectType(dataItem, resolvedType, propUsages, propPath)
118
118
 
119
119
  case 'union':
120
- return handleUnionType(dataItem, resolvedType, propInfo)
120
+ return handleUnionType({ dataItem, resolvedType, propInfo, propUsages, propPath })
121
121
 
122
122
  case 'function':
123
123
  handleFunctionType(dataItem, propInfo, bindings)
@@ -290,11 +290,19 @@ function handleObjectType(
290
290
  * Handles union types by detecting special patterns (boolean literals,
291
291
  * string literal enums) or falling back to the first valid member type.
292
292
  */
293
- function handleUnionType(
294
- dataItem: DataItem,
295
- resolvedType: ResolvedType,
296
- propInfo: PropInfo,
297
- ): Result<void, ParseErrorInstance> {
293
+ function handleUnionType({
294
+ dataItem,
295
+ resolvedType,
296
+ propInfo,
297
+ propUsages,
298
+ propPath,
299
+ }: {
300
+ dataItem: DataItem
301
+ resolvedType: ResolvedType
302
+ propInfo: PropInfo
303
+ propUsages?: TrackingStores['propUsages']
304
+ propPath?: string
305
+ }): Result<void, ParseErrorInstance> {
298
306
  if (!resolvedType.types || resolvedType.types.length === 0) {
299
307
  dataItem.dataType = DATA_TYPE.text
300
308
  dataItem.text = {}
@@ -341,7 +349,7 @@ function handleUnionType(
341
349
  // For other unions, use the first valid type
342
350
  const firstValidType = validTypes[0]
343
351
  if (firstValidType) {
344
- return applyResolvedTypeToDataItem(dataItem, firstValidType, propInfo)
352
+ return applyResolvedTypeToDataItem(dataItem, firstValidType, propInfo, propUsages, propPath)
345
353
  }
346
354
 
347
355
  dataItem.dataType = DATA_TYPE.text
@@ -6,6 +6,7 @@ import type { ExtractedElement } from './core/tree-builder'
6
6
  import {
7
7
  enrichContainerProperties,
8
8
  enrichFlexProperties,
9
+ enrichMarginProperties,
9
10
  getCssPropertiesForTag,
10
11
  getDefaultDisplayForTag,
11
12
  hasContainerLikeDisplay,
@@ -541,3 +542,71 @@ describe('enrichContainerProperties', () => {
541
542
  expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.paddingTop)
542
543
  })
543
544
  })
545
+
546
+ describe('enrichMarginProperties', () => {
547
+ it('adds the block-axis margins to a non-replaced inline element whose matched CSS changes display to block', () => {
548
+ const elements = [createElementWithCssProperties('span', 'display: block')]
549
+
550
+ const [enrichedElement] = enrichMarginProperties(elements)
551
+ const cssData = enrichedElement.extractorData.get('css-properties') as { relevant: string[] }
552
+
553
+ expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginTop)
554
+ expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginBottom)
555
+ expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginInlineStart)
556
+ expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginInlineEnd)
557
+ })
558
+
559
+ it('adds the full margin family to a non-replaced inline element whose matched CSS changes display to inline-flex', () => {
560
+ const elements = [createElementWithCssProperties('span', 'display: inline-flex')]
561
+
562
+ const [enrichedElement] = enrichMarginProperties(elements)
563
+ const cssData = enrichedElement.extractorData.get('css-properties') as { relevant: string[] }
564
+
565
+ expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginTop)
566
+ expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginBottom)
567
+ })
568
+
569
+ it('keeps only the horizontal margins when a span stays inline', () => {
570
+ const elements = [createElementWithCssProperties('span', 'display: inline')]
571
+
572
+ const [enrichedElement] = enrichMarginProperties(elements)
573
+ const cssData = enrichedElement.extractorData.get('css-properties') as { relevant: string[] }
574
+
575
+ expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginInlineStart)
576
+ expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginInlineEnd)
577
+ expect(cssData.relevant).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginTop)
578
+ expect(cssData.relevant).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginBottom)
579
+ })
580
+
581
+ it('removes all margin from an element whose matched CSS changes display to an internal table value', () => {
582
+ const elements = [createElementWithCssProperties('div', 'display: table-cell')]
583
+
584
+ const [enrichedElement] = enrichMarginProperties(elements)
585
+ const cssData = enrichedElement.extractorData.get('css-properties') as { relevant: string[] }
586
+
587
+ expect(cssData.relevant).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginTop)
588
+ expect(cssData.relevant).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginBottom)
589
+ expect(cssData.relevant).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginInlineStart)
590
+ expect(cssData.relevant).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginInlineEnd)
591
+ })
592
+
593
+ it('leaves the tag-default margin guess alone when no CSS declares display', () => {
594
+ const elements = [createElementWithCssProperties('span', '')]
595
+
596
+ const [enrichedElement] = enrichMarginProperties(elements)
597
+ const cssData = enrichedElement.extractorData.get('css-properties') as { relevant: string[] }
598
+
599
+ expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginInlineStart)
600
+ expect(cssData.relevant).not.toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginTop)
601
+ })
602
+
603
+ it('corrects margin via the --display root convention, not just a literal display declaration', () => {
604
+ const elements = [createElementWithCssProperties('span', '--display: block')]
605
+
606
+ const [enrichedElement] = enrichMarginProperties(elements)
607
+ const cssData = enrichedElement.extractorData.get('css-properties') as { relevant: string[] }
608
+
609
+ expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginTop)
610
+ expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginBottom)
611
+ })
612
+ })
@@ -135,26 +135,35 @@ const MARGIN_UNSUPPORTED_DISPLAY_VALUES = new Set([
135
135
  ])
136
136
 
137
137
  /**
138
- * Margin properties relevant to a tag's default display. Internal table
138
+ * Margin properties relevant to a given `display` value. Internal table
139
139
  * display values (`tr`, `thead`/`tbody`/`tfoot`, `col`/`colgroup`, `td`/`th`)
140
140
  * don't honor margin at all. Non-replaced inline elements (`span`, `a`, …)
141
141
  * only honor the horizontal (inline) margins — `margin-top`/`margin-bottom`
142
142
  * have no effect on them. Replaced inline elements (`img`, `video`, …) honor
143
143
  * margin on every side, same as a block box.
144
144
  */
145
- function getMarginPropertiesForTag(tag: string): string[] {
146
- const normalizedTag = tag.toLowerCase()
147
- const defaultDisplay = getDefaultDisplayForTag(normalizedTag)
148
-
149
- if (MARGIN_UNSUPPORTED_DISPLAY_VALUES.has(defaultDisplay)) {
145
+ function getMarginPropertiesForDisplay(displayValue: string, isReplacedElement: boolean): string[] {
146
+ const normalizedDisplayValue = normalizeDisplayValue(displayValue)
147
+ if (MARGIN_UNSUPPORTED_DISPLAY_VALUES.has(normalizedDisplayValue)) {
150
148
  return []
151
149
  }
152
- if (defaultDisplay === 'inline' && !MEDIA_TAGS.has(normalizedTag)) {
150
+ if (normalizedDisplayValue === 'inline' && !isReplacedElement) {
153
151
  return HORIZONTAL_MARGIN_CSS_PROPERTIES
154
152
  }
155
153
  return MARGIN_CSS_PROPERTIES
156
154
  }
157
155
 
156
+ /**
157
+ * Margin properties relevant to a tag's default display, used as the
158
+ * best-effort guess at element creation time, before CSS matching has run.
159
+ * `enrichMarginProperties` corrects this once the effective display —
160
+ * matched CSS, including the `--display` root convention — is known.
161
+ */
162
+ function getMarginPropertiesForTag(tag: string): string[] {
163
+ const normalizedTag = tag.toLowerCase()
164
+ return getMarginPropertiesForDisplay(getDefaultDisplayForTag(normalizedTag), MEDIA_TAGS.has(normalizedTag))
165
+ }
166
+
158
167
  /**
159
168
  * Child-layout controls for flex/grid containers, added post-hoc by
160
169
  * `enrichFlexProperties` once the element's resolved `display` is known.
@@ -654,6 +663,49 @@ export function enrichContainerProperties(elements: ExtractedElement[]): Extract
654
663
  })
655
664
  }
656
665
 
666
+ /**
667
+ * Replaces whatever margin properties are currently in `existing` with the
668
+ * ones relevant to `marginProperties` — used by `enrichMarginProperties` to
669
+ * correct the tag-default guess once the effective display is known.
670
+ */
671
+ function withMarginProperties(existing: string[], marginProperties: string[]): string[] {
672
+ const withoutMargin = existing.filter((property) => !(MARGIN_CSS_PROPERTIES as string[]).includes(property))
673
+ return [...withoutMargin, ...marginProperties]
674
+ }
675
+
676
+ /**
677
+ * Walks the element tree and corrects margin properties from the tag-default
678
+ * guess made at creation time to the element's effective display — matched
679
+ * CSS, including the `--display` root convention. A non-replaced inline
680
+ * element (e.g. `<span>`) whose matched CSS changes display to `block` or
681
+ * `flex` gains the block-axis margins it was missing; conversely, an element
682
+ * whose effective display becomes an internal table value loses margin
683
+ * entirely. Must be called after CSS selector matching (requires
684
+ * css-matcher data).
685
+ */
686
+ export function enrichMarginProperties(elements: ExtractedElement[]): ExtractedElement[] {
687
+ return elements.map((element) => {
688
+ const matcherData = element.extractorData.get('css-matcher') as MatchedCssData | undefined
689
+ const cssData = element.extractorData.get('css-properties') as CssPropertiesData | undefined
690
+
691
+ if (cssData && matcherData && hasEffectiveDisplayDeclaration(matcherData)) {
692
+ const effectiveDisplay = resolveEffectiveDisplayValue(matcherData)
693
+ if (effectiveDisplay !== undefined) {
694
+ const isReplacedElement = MEDIA_TAGS.has(element.tag.toLowerCase())
695
+ const marginProperties = getMarginPropertiesForDisplay(effectiveDisplay, isReplacedElement)
696
+ element.extractorData.set('css-properties', {
697
+ relevant: withMarginProperties(cssData.relevant, marginProperties),
698
+ })
699
+ }
700
+ }
701
+
702
+ return {
703
+ ...element,
704
+ children: enrichMarginProperties(element.children),
705
+ }
706
+ })
707
+ }
708
+
657
709
  // ─────────────────────────────────────────────────────────────────────────────
658
710
  // Factory
659
711
  // ─────────────────────────────────────────────────────────────────────────────
@@ -29,6 +29,7 @@ export {
29
29
  enrichGapProperties,
30
30
  enrichContainerProperties,
31
31
  enrichFlexProperties,
32
+ enrichMarginProperties,
32
33
  resolveDisplayValue,
33
34
  getDefaultDisplayForTag,
34
35
  ROOT_DISPLAY_CUSTOM_PROPERTY,
@@ -19,6 +19,7 @@ export {
19
19
  enrichGapProperties,
20
20
  enrichContainerProperties,
21
21
  enrichFlexProperties,
22
+ enrichMarginProperties,
22
23
  resolveDisplayValue,
23
24
  getDefaultDisplayForTag,
24
25
  ROOT_DISPLAY_CUSTOM_PROPERTY,
@@ -1,7 +1,7 @@
1
1
  import fs from 'node:fs'
2
2
 
3
3
  import type { ComponentInfo } from './information-extractors/ts'
4
- import type { PropInfo } from './information-extractors/ts/types'
4
+ import type { PropInfo, ResolvedType } from './information-extractors/ts/types'
5
5
 
6
6
  import {
7
7
  type ExtractedElement,
@@ -12,6 +12,7 @@ import {
12
12
  enrichContainerProperties,
13
13
  enrichFlexProperties,
14
14
  enrichGapProperties,
15
+ enrichMarginProperties,
15
16
  runExtractors,
16
17
  } from './information-extractors/react'
17
18
  import type { CoupledComponentInfo, CoupledProp, DOMBinding, TrackingStores } from './information-extractors/react'
@@ -150,7 +151,8 @@ export function processComponent(
150
151
  if (html && extractedElements.length > 0 && css.length > 0) {
151
152
  try {
152
153
  const matchResult = matchCssSelectors(html, extractedElements, css)
153
- const containerEnrichedElements = enrichContainerProperties(matchResult.elements)
154
+ const marginEnrichedElements = enrichMarginProperties(matchResult.elements)
155
+ const containerEnrichedElements = enrichContainerProperties(marginEnrichedElements)
154
156
  const gapEnrichedElements = enrichGapProperties(containerEnrichedElements)
155
157
  const flexEnrichedElements = enrichFlexProperties(gapEnrichedElements)
156
158
  enhancedInfo = {
@@ -225,10 +227,11 @@ function processElementPropsWrites(
225
227
  if (!relativePath) continue
226
228
 
227
229
  // Resolve the PropInfo for this leaf prop by walking the type tree
228
- const propInfo = resolveInnerPropInfo(elementPropsInfo, relativePath)
229
- if (!propInfo) continue
230
+ const resolvedProp = resolveInnerPropInfo(elementPropsInfo, relativePath)
231
+ if (!resolvedProp) continue
232
+ const { propInfo, relativePropPath } = resolvedProp
230
233
 
231
- const elementPropsPath = extractParentElementPropsPath(relativePath)
234
+ const elementPropsPath = extractParentElementPropsPath(relativePropPath)
232
235
  if (!elementPropsPath) continue
233
236
  const bindings = extractBindings(writeInfo)
234
237
 
@@ -246,7 +249,7 @@ function processElementPropsWrites(
246
249
  entry.props[leafName] = {
247
250
  ...propInfo,
248
251
  logicOnly: false,
249
- propPath: path,
252
+ propPath: `props.elementProps.${relativePropPath}`,
250
253
  }
251
254
  }
252
255
  }
@@ -260,29 +263,44 @@ function processElementPropsWrites(
260
263
  * e.g., path "navbar.items" -> walk elementProps type -> navbar property -> items property
261
264
  * Handles recursive nesting: "hamburgerMenu.elementProps.hamburgerCloseButton.label"
262
265
  */
263
- function resolveInnerPropInfo(elementPropsPropInfo: PropInfo, relativePath: string): PropInfo | null {
266
+ function resolveInnerPropInfo(
267
+ elementPropsPropInfo: PropInfo,
268
+ relativePath: string,
269
+ ): { propInfo: PropInfo; relativePropPath: string } | null {
264
270
  const segments = relativePath.split('.')
265
- let currentType = elementPropsPropInfo.resolvedType
271
+ let currentType = unwrapOptionalType(elementPropsPropInfo.resolvedType)
266
272
 
267
- for (let i = 0; i < segments.length; i++) {
268
- const segment = segments[i]
273
+ for (let segmentIndex = 0; segmentIndex < segments.length; segmentIndex++) {
274
+ const segment = segments[segmentIndex]
269
275
  if (!currentType.properties) return null
270
276
 
271
277
  const propInfo = currentType.properties[segment]
272
278
  if (!propInfo) return null
273
279
 
274
- // If this is the last segment, return the PropInfo
275
- if (i === segments.length - 1) {
276
- return propInfo
280
+ // A11y is one semantic data item. Its tracked DOM writes point at fields
281
+ // such as next.a11y.ariaLabel, but the type has no property tree to descend.
282
+ // Keep the parent path so the builder can collect only the used attributes.
283
+ const propType = unwrapOptionalType(propInfo.resolvedType)
284
+ const isA11y = propType.kind === 'semantic' && propType.value === 'A11y'
285
+ if (segmentIndex === segments.length - 1 || isA11y) {
286
+ return { propInfo, relativePropPath: segments.slice(0, segmentIndex + 1).join('.') }
277
287
  }
278
288
 
279
289
  // Otherwise, descend into this property's resolved type
280
- currentType = propInfo.resolvedType
290
+ currentType = propType
281
291
  }
282
292
 
283
293
  return null
284
294
  }
285
295
 
296
+ function unwrapOptionalType(resolvedType: ResolvedType): ResolvedType {
297
+ if (resolvedType.kind !== 'union') return resolvedType
298
+ const definedTypes = resolvedType.types?.filter(
299
+ (member) => !(member.kind === 'primitive' && (member.value === 'undefined' || member.value === 'null')),
300
+ )
301
+ return definedTypes?.length === 1 ? definedTypes[0] : resolvedType
302
+ }
303
+
286
304
  function extractBindings(
287
305
  writeInfo: TrackingStores['propUsages'] extends Map<string, infer V> ? V : never,
288
306
  ): DOMBinding[] {