@wix/zero-config-implementation 1.109.0 → 1.110.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.109.0",
7
+ "version": "1.110.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": "9a14eb3c7154bae6dcc34db0e6a5db751a6ed10efc0e2da43f00408c"
109
+ "falconPackageHash": "47599e898cd311d543a036e8af54090473a39ec45beb3a9f760fd806"
110
110
  }
@@ -122,3 +122,20 @@ describe('buildDataItem — container dataType', () => {
122
122
  expect(result._unsafeUnwrap().container).toEqual(expectedContainerData)
123
123
  })
124
124
  })
125
+
126
+ describe('buildDataItem — vectorArt', () => {
127
+ const vectorArtProp = (): PropInfo => ({
128
+ name: 'icon',
129
+ type: 'VectorArt',
130
+ required: false,
131
+ resolvedType: { kind: 'semantic', value: 'VectorArt', source: '@wix/editor-react-types' },
132
+ })
133
+
134
+ it('sets the vectorArt oneof member so the editor can resolve the replace panel', () => {
135
+ const dataItem = buildDataItem(vectorArtProp())._unsafeUnwrap()
136
+
137
+ expect(dataItem.dataType).toBe('vectorArt')
138
+ // Fixed to basic shapes: they are single-fill, so an icon's colour control always applies.
139
+ expect(dataItem.vectorArt).toEqual({ category: 'SHAPE_BASIC' })
140
+ })
141
+ })
@@ -534,6 +534,11 @@ function applyDataToBuilderType({
534
534
  category: MEDIA.IMAGE_CATEGORY.IMAGE,
535
535
  }
536
536
  break
537
+ case 'vectorArt':
538
+ dataItem.dataType = DATA_TYPE.vectorArt
539
+ // Required for the Replace Vector Art panel; basic shapes are single-fill, so the colour control applies.
540
+ dataItem.vectorArt = { category: MEDIA.VECTOR_ART_CATEGORY.SHAPE_BASIC }
541
+ break
537
542
  case 'a11y':
538
543
  dataItem.dataType = DATA_TYPE.a11y
539
544
  dataItem.a11y = {
@@ -7,11 +7,13 @@ import {
7
7
  enrichContainerProperties,
8
8
  enrichFlexProperties,
9
9
  enrichMarginProperties,
10
+ enrichVectorArtProperties,
10
11
  getCssPropertiesForTag,
11
12
  getDefaultDisplayForTag,
12
13
  hasContainerLikeDisplay,
13
14
  hasFlexOrGridDisplay,
14
15
  } from './css-properties'
16
+ import type { PropTrackerData } from './prop-tracker'
15
17
 
16
18
  function matcherDataFromCss(declarations: string): MatchedCssData {
17
19
  const properties = parseCss(`.test { ${declarations} }`).getPropertiesForSelector('.test')
@@ -610,3 +612,33 @@ describe('enrichMarginProperties', () => {
610
612
  expect(cssData.relevant).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.marginBottom)
611
613
  })
612
614
  })
615
+
616
+ describe('enrichVectorArtProperties', () => {
617
+ const asVectorArtHost = (element: ExtractedElement): ExtractedElement => {
618
+ element.extractorData.set('prop-tracker', { rendersVectorArt: true } as PropTrackerData)
619
+ return element
620
+ }
621
+ const relevantOf = (element: ExtractedElement) =>
622
+ (element.extractorData.get('css-properties') as { relevant: string[] }).relevant
623
+
624
+ it('narrows a vector-art host to display, dropping the box controls of its tag', () => {
625
+ const host = asVectorArtHost(createElementWithCssProperties('div', 'display: block'))
626
+ expect(relevantOf(host)).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.background)
627
+
628
+ const [enrichedHost] = enrichVectorArtProperties([host])
629
+
630
+ expect(relevantOf(enrichedHost)).toEqual([CSS_PROPERTIES.CSS_PROPERTY_TYPE.display])
631
+ })
632
+
633
+ it('narrows a nested host after the flex pass, so its alignSelf does not survive', () => {
634
+ const parent: ExtractedElement = {
635
+ ...createElementWithCssProperties('div', 'display: flex'),
636
+ children: [asVectorArtHost(createElementWithCssProperties('span', ''))],
637
+ }
638
+
639
+ const [enrichedParent] = enrichVectorArtProperties(enrichFlexProperties([parent]))
640
+
641
+ expect(relevantOf(enrichedParent.children[0])).toEqual([CSS_PROPERTIES.CSS_PROPERTY_TYPE.display])
642
+ expect(relevantOf(enrichedParent)).toContain(CSS_PROPERTIES.CSS_PROPERTY_TYPE.flexDirection)
643
+ })
644
+ })
@@ -10,6 +10,7 @@ import { resolveCssPropertyValue } from '../../css/parse'
10
10
  import type { MatchedCssData } from '../../css/types'
11
11
  import type { ExtractedElement } from './core/tree-builder'
12
12
  import type { CreateElementEvent, ReactExtractor } from './core/types'
13
+ import type { PropTrackerData } from './prop-tracker'
13
14
 
14
15
  // ─────────────────────────────────────────────────────────────────────────────
15
16
  // Types
@@ -188,6 +189,11 @@ const TEXT_CSS_PROPERTIES = [
188
189
  CSS_PROPERTY_TYPE.color,
189
190
  ]
190
191
 
192
+ /**
193
+ * CSS properties for vector-art hosts. Size, colour and rotation come from CSS custom properties.
194
+ */
195
+ const VECTOR_ART_CSS_PROPERTIES: string[] = [CSS_PROPERTY_TYPE.display]
196
+
191
197
  /**
192
198
  * CSS properties for Media elements (excluding svg)
193
199
  */
@@ -706,6 +712,28 @@ export function enrichMarginProperties(elements: ExtractedElement[]): ExtractedE
706
712
  })
707
713
  }
708
714
 
715
+ /**
716
+ * Narrows vector-art hosts to `VECTOR_ART_CSS_PROPERTIES`.
717
+ * Must run after every other enrichment pass, so none of them can add box controls back.
718
+ */
719
+ export function enrichVectorArtProperties(elements: ExtractedElement[]): ExtractedElement[] {
720
+ return elements.map((element) => {
721
+ const propTrackerData = element.extractorData.get('prop-tracker') as PropTrackerData | undefined
722
+ const cssData = element.extractorData.get('css-properties') as CssPropertiesData | undefined
723
+
724
+ if (propTrackerData?.rendersVectorArt && cssData) {
725
+ element.extractorData.set('css-properties', {
726
+ relevant: cssData.relevant.filter((property) => VECTOR_ART_CSS_PROPERTIES.includes(property)),
727
+ })
728
+ }
729
+
730
+ return {
731
+ ...element,
732
+ children: enrichVectorArtProperties(element.children),
733
+ }
734
+ })
735
+ }
736
+
709
737
  // ─────────────────────────────────────────────────────────────────────────────
710
738
  // Factory
711
739
  // ─────────────────────────────────────────────────────────────────────────────
@@ -30,6 +30,7 @@ export {
30
30
  enrichContainerProperties,
31
31
  enrichFlexProperties,
32
32
  enrichMarginProperties,
33
+ enrichVectorArtProperties,
33
34
  resolveDisplayValue,
34
35
  getDefaultDisplayForTag,
35
36
  ROOT_DISPLAY_CUSTOM_PROPERTY,
@@ -1,6 +1,7 @@
1
1
  import { describe, expect, it } from 'vitest'
2
2
  import { createRefElementMarker } from '../../../extensions/ref-elements/path-utils'
3
3
  import type { RefElementMatch } from '../../../extensions/ref-elements/types'
4
+ import type { ResolvedType } from '../../ts/types'
4
5
  import { ExtractorStore } from './core/store'
5
6
  import { type PropTrackerData, createPropTrackerExtractor } from './prop-tracker'
6
7
 
@@ -267,3 +268,91 @@ describe('createPropTrackerExtractor', () => {
267
268
  expect(store.get<PropTrackerData>('trace-1', 'prop-tracker')?.hasEmptySlot).toBe(true)
268
269
  })
269
270
  })
271
+
272
+ describe('createPropTrackerExtractor — vector-art hosts', () => {
273
+ const vectorArtType: ResolvedType = { kind: 'semantic', value: 'VectorArt', source: '@wix/editor-react-types' }
274
+
275
+ /** Mocks an `icon` prop of `resolvedType`, renders one element with `renderProps`, returns its tracker data. */
276
+ function renderIcon<MockedIcon = Record<string, string>>(
277
+ resolvedType: ResolvedType,
278
+ renderProps: (icon: MockedIcon) => Record<string, unknown>,
279
+ ): PropTrackerData | undefined {
280
+ const store = new ExtractorStore()
281
+ const { extractor } = createPropTrackerExtractor()
282
+ const context = {
283
+ componentInfo: {
284
+ componentName: 'Icon',
285
+ props: { icon: { name: 'icon', type: 'VectorArt', required: false, resolvedType } },
286
+ },
287
+ component: () => null,
288
+ props: {} as Record<string, unknown>,
289
+ store,
290
+ }
291
+ extractor.onBeforeRender?.(context)
292
+
293
+ extractor.onCreateElement?.({
294
+ type: 'div',
295
+ isDomElement: true,
296
+ tag: 'div',
297
+ traceId: 'trace-1',
298
+ props: renderProps(context.props.icon as MockedIcon),
299
+ children: [],
300
+ store,
301
+ })
302
+ return store.get<PropTrackerData>('trace-1', 'prop-tracker')
303
+ }
304
+
305
+ it('flags an element that injects svgContent as raw markup', () => {
306
+ const data = renderIcon(vectorArtType, (icon) => ({ injected: { __html: icon.svgContent } }))
307
+
308
+ expect(data?.rendersVectorArt).toBe(true)
309
+ })
310
+
311
+ it('does not flag a vector-art field bound to an ordinary attribute', () => {
312
+ const data = renderIcon(vectorArtType, (icon) => ({ 'data-src': icon.uri }))
313
+
314
+ expect(data?.rendersVectorArt).toBe(false)
315
+ })
316
+
317
+ it('does not flag raw markup that does not come from vector art', () => {
318
+ const data = renderIcon({ kind: 'primitive', value: 'string' }, (markup) => ({
319
+ injected: { __html: markup },
320
+ }))
321
+
322
+ expect(data?.rendersVectorArt).toBe(false)
323
+ })
324
+
325
+ it('flags a host when vector art is the union member the data item describes', () => {
326
+ const data = renderIcon({ kind: 'union', types: [vectorArtType, { kind: 'literal', value: 'none' }] }, (icon) => ({
327
+ injected: { __html: icon.svgContent },
328
+ }))
329
+
330
+ expect(data?.rendersVectorArt).toBe(true)
331
+ })
332
+
333
+ /** `{ name: string; badge: VectorArt }` */
334
+ const badgeHolderType: ResolvedType = {
335
+ kind: 'object',
336
+ properties: {
337
+ name: { name: 'name', type: 'string', required: true, resolvedType: { kind: 'primitive', value: 'string' } },
338
+ badge: { name: 'badge', type: 'VectorArt', required: true, resolvedType: vectorArtType },
339
+ },
340
+ }
341
+ type BadgeHolder = { name: string; badge: Record<string, string> }
342
+
343
+ it('flags a host of vector art nested in an object prop', () => {
344
+ const data = renderIcon<BadgeHolder>(badgeHolderType, (avatar) => ({
345
+ injected: { __html: avatar.badge.svgContent },
346
+ }))
347
+
348
+ expect(data?.rendersVectorArt).toBe(true)
349
+ })
350
+
351
+ it('flags a host of vector art nested in array items', () => {
352
+ const data = renderIcon<BadgeHolder[]>({ kind: 'array', elementType: badgeHolderType }, (items) => ({
353
+ injected: { __html: items[0].badge.svgContent },
354
+ }))
355
+
356
+ expect(data?.rendersVectorArt).toBe(true)
357
+ })
358
+ })
@@ -28,6 +28,15 @@ export interface PropTrackerData {
28
28
  eventHandlers: string[]
29
29
  /** True when a child slot rendered nothing, so the content is unknown. */
30
30
  hasEmptySlot: boolean
31
+ /** True when the element injects a `VectorArt` value's `svgContent` as raw markup. */
32
+ rendersVectorArt?: boolean
33
+ }
34
+
35
+ interface SpyMatch {
36
+ propName: string
37
+ path: string
38
+ embedded: boolean
39
+ semantic?: string
31
40
  }
32
41
 
33
42
  export interface PropTrackerExtractorState {
@@ -68,8 +77,8 @@ export function createPropTrackerExtractor(options?: CreatePropTrackerExtractorO
68
77
  const pendingRefElementsByPath = new Map<string, PendingRefElement[]>()
69
78
 
70
79
  const registrar: PropSpyRegistrar = {
71
- registerString(path, propName, value) {
72
- stringMeta.set(value, { path, propName, originalValue: value })
80
+ registerString(path, propName, value, semantic) {
81
+ stringMeta.set(value, { path, propName, originalValue: value, semantic })
73
82
  },
74
83
  registerNumber(path, propName, value) {
75
84
  numberMeta.set(value, { path, propName, originalValue: value })
@@ -79,16 +88,13 @@ export function createPropTrackerExtractor(options?: CreatePropTrackerExtractorO
79
88
  },
80
89
  }
81
90
 
82
- const extractSpies = (
83
- value: unknown,
84
- seen = new WeakSet<object>(),
85
- ): { propName: string; path: string; embedded: boolean }[] => {
91
+ const extractSpies = (value: unknown, seen = new WeakSet<object>()): SpyMatch[] => {
86
92
  if (typeof value === 'string') {
87
- const results: { propName: string; path: string; embedded: boolean }[] = []
93
+ const results: SpyMatch[] = []
88
94
  for (const [key, meta] of stringMeta) {
89
95
  if (value.includes(key)) {
90
96
  // embedded = spy marker was part of a longer string (e.g. `mailto:${email}`)
91
- results.push({ propName: meta.propName, path: meta.path, embedded: value !== key })
97
+ results.push({ propName: meta.propName, path: meta.path, embedded: value !== key, semantic: meta.semantic })
92
98
  }
93
99
  }
94
100
  return results
@@ -138,6 +144,7 @@ export function createPropTrackerExtractor(options?: CreatePropTrackerExtractorO
138
144
  const boundProps = new Set<string>()
139
145
  const concatenatedAttrs = new Map<string, string>()
140
146
  const eventHandlers: string[] = []
147
+ let rendersVectorArt = false
141
148
 
142
149
  Object.entries(props).forEach(([key, value]) => {
143
150
  if (key.startsWith('__') || key === TRACE_ATTR) return
@@ -146,7 +153,14 @@ export function createPropTrackerExtractor(options?: CreatePropTrackerExtractorO
146
153
  eventHandlers.push(key)
147
154
  }
148
155
 
156
+ // Injected markup is identified by React's `{ __html }` wrapper, not by the prop's name.
157
+ const valueKeys = typeof value === 'object' && value !== null ? Object.keys(value) : []
158
+ const isRawHtmlValue = valueKeys.length === 1 && valueKeys[0] === '__html'
159
+
149
160
  const spies = extractSpies(value)
161
+ if (isRawHtmlValue && spies.some((spy) => spy.semantic === 'VectorArt')) {
162
+ rendersVectorArt = true
163
+ }
150
164
 
151
165
  spies.forEach((spy) => {
152
166
  boundProps.add(spy.propName)
@@ -173,6 +187,7 @@ export function createPropTrackerExtractor(options?: CreatePropTrackerExtractorO
173
187
  concatenatedAttrs,
174
188
  eventHandlers,
175
189
  hasEmptySlot: event.hasEmptySlot ?? false,
190
+ rendersVectorArt,
176
191
  } satisfies PropTrackerData)
177
192
 
178
193
  attachMatchedRefElement(traceId, props, store)
@@ -20,6 +20,7 @@ export {
20
20
  enrichContainerProperties,
21
21
  enrichFlexProperties,
22
22
  enrichMarginProperties,
23
+ enrichVectorArtProperties,
23
24
  resolveDisplayValue,
24
25
  getDefaultDisplayForTag,
25
26
  ROOT_DISPLAY_CUSTOM_PROPERTY,
@@ -13,6 +13,8 @@ export interface PropSpyMeta {
13
13
  path: string
14
14
  propName: string
15
15
  originalValue: unknown
16
+ /** Semantic type the value was mocked for (e.g. `VectorArt`). */
17
+ semantic?: string
16
18
  }
17
19
 
18
20
  // ─────────────────────────────────────────────────────────────────────────────
@@ -41,7 +41,7 @@ export function resetMockCounter(): void {
41
41
  * Satisfied structurally by PropSpyContext.
42
42
  */
43
43
  export interface PropSpyRegistrar {
44
- registerString(path: string, propName: string, value: string): void
44
+ registerString(path: string, propName: string, value: string, semantic?: string): void
45
45
  registerNumber(path: string, propName: string, value: number): void
46
46
  registerFunction(path: string, propName: string, value: (...args: unknown[]) => unknown): void
47
47
  }
@@ -168,7 +168,7 @@ function generateSemanticValue(
168
168
  case 'A11y':
169
169
  return generateMockA11y(path, registrar)
170
170
  case 'VectorArt':
171
- return generateMockVectorArt()
171
+ return generateMockVectorArt(path, registrar)
172
172
  case 'Audio':
173
173
  case 'AudioUri':
174
174
  return generateMockAudio()
@@ -249,19 +249,20 @@ function generateUnionValue(
249
249
  ): unknown {
250
250
  const types = resolvedType.types ?? []
251
251
 
252
- // Prefer string literals for textEnum-like unions
253
- const stringLiteral = types.find(
254
- (candidateType) => candidateType.kind === 'literal' && typeof candidateType.value === 'string',
255
- )
256
- if (stringLiteral) {
257
- return stringLiteral.value
258
- }
259
-
260
252
  // Skip undefined/null when picking a representative type — Partial<T> produces T | undefined
261
253
  // unions where TypeScript may place undefined first, which would generate useless mock values
262
254
  const isNullish = (candidateType: ResolvedType) =>
263
255
  candidateType.kind === 'primitive' && (candidateType.value === 'undefined' || candidateType.value === 'null')
264
- const representative = types.find((candidateType) => !isNullish(candidateType)) ?? types[0]
256
+ const definedTypes = types.filter((candidateType) => !isNullish(candidateType))
257
+
258
+ // Mirror the data-item builder: textEnum-like unions mock a literal, others their first defined member.
259
+ const isStringLiteral = (candidateType: ResolvedType) =>
260
+ candidateType.kind === 'literal' && typeof candidateType.value === 'string'
261
+ if (definedTypes.length > 0 && definedTypes.every(isStringLiteral)) {
262
+ return definedTypes[0].value
263
+ }
264
+
265
+ const representative = definedTypes[0] ?? types[0]
265
266
  if (representative) {
266
267
  return generateValueFromResolvedType(representative, propName, path, registrar, options)
267
268
  }
@@ -465,12 +466,19 @@ function generateMockA11y(path: string, registrar?: PropSpyRegistrar): Record<st
465
466
  return a11y
466
467
  }
467
468
 
468
- function generateMockVectorArt(): Record<string, unknown> {
469
- return {
470
- svgId: faker.string.uuid(),
471
- url: `${faker.internet.url()}/icon.svg`,
472
- svgContent: '<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><circle cx="12" cy="12" r="10"/></svg>',
473
- }
469
+ /**
470
+ * Mocks `VectorArt`. `svgContent` is registered as `VectorArt` so the prop tracker can detect its host element.
471
+ */
472
+ function generateMockVectorArt(path: string, registrar?: PropSpyRegistrar): Record<string, unknown> {
473
+ const viewBox = '0 0 24 24'
474
+ // Spies match by substring, so each registered value must be unique across props.
475
+ const token = faker.string.alphanumeric(8)
476
+ const uri = `mock_vector_${token}.svg`
477
+ const svgContent = `<svg xmlns="http://www.w3.org/2000/svg" viewBox="${viewBox}" data-mock="${token}"><circle cx="12" cy="12" r="10"/></svg>`
478
+ registrar?.registerString(`${path}.uri`, 'uri', uri)
479
+ registrar?.registerString(`${path}.svgContent`, 'svgContent', svgContent, 'VectorArt')
480
+
481
+ return { viewBox, contentBox: viewBox, contentType: 'shape', colors: {}, uri, svgContent }
474
482
  }
475
483
 
476
484
  function generateMockAudio(): Record<string, unknown> {
@@ -13,6 +13,7 @@ import {
13
13
  enrichFlexProperties,
14
14
  enrichGapProperties,
15
15
  enrichMarginProperties,
16
+ enrichVectorArtProperties,
16
17
  runExtractors,
17
18
  } from './information-extractors/react'
18
19
  import type { CoupledComponentInfo, CoupledProp, DOMBinding, TrackingStores } from './information-extractors/react'
@@ -171,6 +172,9 @@ export function processComponent(
171
172
  }
172
173
  }
173
174
 
175
+ // Runs last so no earlier pass can add box controls back to a vector-art host.
176
+ enhancedInfo = { ...enhancedInfo, elements: enrichVectorArtProperties(enhancedInfo.elements) }
177
+
174
178
  return {
175
179
  ok: true,
176
180
  component: {