@wix/zero-config-implementation 1.85.0 → 1.86.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/dist/index.d.ts CHANGED
@@ -2396,6 +2396,7 @@ export declare interface PropInfo {
2396
2396
  min?: string;
2397
2397
  max?: string;
2398
2398
  isStateTrigger?: boolean;
2399
+ activeItemIndexTarget?: string;
2399
2400
  }
2400
2401
 
2401
2402
  export declare interface PropSpyMeta {
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { B as t, D as e, E as o, I as n, N as c, P as l, R as i, a as p, V as E, b as m, c as x, d as f, e as d, f as u, h as C, i as I, j as k, k as y, l as A, m as D, n as P, o as R, p as h, q as B, r as M, s as T, t as b, w } from "./index-DBSnnglR.js";
1
+ import { B as t, D as e, E as o, I as n, N as c, P as l, R as i, a as p, V as E, b as m, c as x, d as f, e as d, f as u, h as C, i as I, j as k, k as y, l as A, m as D, n as P, o as R, p as h, q as B, r as M, s as T, t as b, w } from "./index-EfIM-d53.js";
2
2
  import "react";
3
3
  export {
4
4
  t as BaseError,
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "registry": "https://registry.npmjs.org/",
5
5
  "access": "public"
6
6
  },
7
- "version": "1.85.0",
7
+ "version": "1.86.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",
@@ -82,5 +82,5 @@
82
82
  ]
83
83
  }
84
84
  },
85
- "falconPackageHash": "57c46654a2a7b434be3282e37ce2fff52e061ef822797e00a88ba528"
85
+ "falconPackageHash": "88fd007d78421c0861fdf823019f98813fd8a82582461e104c5ee2ef"
86
86
  }
@@ -0,0 +1,149 @@
1
+ import { DISPLAY_GROUPS } from '@wix/react-component-schema'
2
+ import { afterEach, describe, expect, it, vi } from 'vitest'
3
+ import type { CoupledProp } from '../information-extractors/react/types'
4
+ import { buildActiveItemDisplayGroups, buildActiveItemIndexDisplayFilters } from './active-item-index-builder'
5
+
6
+ function arrayProp(name: string): CoupledProp {
7
+ return { name, type: 'Array<Item>', resolvedType: { kind: 'array' } } as unknown as CoupledProp
8
+ }
9
+
10
+ function activeIndexProp(name: string, targetArrayProp: string): CoupledProp {
11
+ return {
12
+ name,
13
+ type: 'number',
14
+ resolvedType: { kind: 'primitive', value: 'number' },
15
+ activeItemIndexTarget: targetArrayProp,
16
+ } as unknown as CoupledProp
17
+ }
18
+
19
+ function numberProp(name: string): CoupledProp {
20
+ return { name, type: 'number', resolvedType: { kind: 'primitive', value: 'number' } } as unknown as CoupledProp
21
+ }
22
+
23
+ afterEach(() => {
24
+ vi.restoreAllMocks()
25
+ })
26
+
27
+ describe('buildActiveItemDisplayGroups', () => {
28
+ it('emits displayGroups when ActiveItemIndex pairs with an array prop', () => {
29
+ const props = {
30
+ tabs: arrayProp('tabs'),
31
+ activeTab: activeIndexProp('activeTab', 'tabs'),
32
+ }
33
+
34
+ expect(buildActiveItemDisplayGroups(props)).toEqual({
35
+ tabs: {
36
+ displayName: 'Tab',
37
+ groupType: DISPLAY_GROUPS.GROUP_TYPE.arrayItems,
38
+ arrayItems: {
39
+ arrayItems: 'tabs',
40
+ selectedItemIndex: 'activeTab',
41
+ },
42
+ },
43
+ })
44
+ })
45
+
46
+ it('keeps only the first ActiveItemIndex link when multiple pairs exist', () => {
47
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
48
+ const props = {
49
+ tabs: arrayProp('tabs'),
50
+ activeTab: activeIndexProp('activeTab', 'tabs'),
51
+ slides: arrayProp('slides'),
52
+ activeSlide: activeIndexProp('activeSlide', 'slides'),
53
+ }
54
+
55
+ expect(buildActiveItemDisplayGroups(props)).toEqual({
56
+ tabs: {
57
+ displayName: 'Tab',
58
+ groupType: DISPLAY_GROUPS.GROUP_TYPE.arrayItems,
59
+ arrayItems: {
60
+ arrayItems: 'tabs',
61
+ selectedItemIndex: 'activeTab',
62
+ },
63
+ },
64
+ })
65
+ expect(warnSpy).toHaveBeenCalledWith(
66
+ 'Multiple ActiveItemIndex<> props found; only "activeTab" -> "tabs" is used. Dropping "activeSlide" -> "slides".',
67
+ )
68
+ })
69
+
70
+ it('keeps the first link and drops later indexes targeting the same array', () => {
71
+ const warnSpy = vi.spyOn(console, 'warn').mockImplementation(() => {})
72
+ const props = {
73
+ tabs: arrayProp('tabs'),
74
+ activeTab: activeIndexProp('activeTab', 'tabs'),
75
+ selectedTab: activeIndexProp('selectedTab', 'tabs'),
76
+ }
77
+
78
+ expect(buildActiveItemDisplayGroups(props)).toEqual({
79
+ tabs: {
80
+ displayName: 'Tab',
81
+ groupType: DISPLAY_GROUPS.GROUP_TYPE.arrayItems,
82
+ arrayItems: {
83
+ arrayItems: 'tabs',
84
+ selectedItemIndex: 'activeTab',
85
+ },
86
+ },
87
+ })
88
+ expect(warnSpy).toHaveBeenCalledWith(
89
+ 'ActiveItemIndex<> link "selectedTab" -> "tabs" conflicts with an existing link — dropping.',
90
+ )
91
+ })
92
+
93
+ it('returns undefined when no ActiveItemIndex props exist', () => {
94
+ const props = {
95
+ tabs: arrayProp('tabs'),
96
+ count: numberProp('count'),
97
+ }
98
+
99
+ expect(buildActiveItemDisplayGroups(props)).toBeUndefined()
100
+ })
101
+
102
+ it('returns undefined when the target array prop does not exist', () => {
103
+ const props = {
104
+ activeTab: activeIndexProp('activeTab', 'nonexistent'),
105
+ }
106
+
107
+ expect(buildActiveItemDisplayGroups(props)).toBeUndefined()
108
+ })
109
+
110
+ it('returns undefined when the target prop is not an array type', () => {
111
+ const props = {
112
+ tabs: numberProp('tabs'),
113
+ activeTab: activeIndexProp('activeTab', 'tabs'),
114
+ }
115
+
116
+ expect(buildActiveItemDisplayGroups(props)).toBeUndefined()
117
+ })
118
+ })
119
+
120
+ describe('buildActiveItemIndexDisplayFilters', () => {
121
+ it('hides active index props from Settings data panel', () => {
122
+ const props = {
123
+ tabs: arrayProp('tabs'),
124
+ activeTab: activeIndexProp('activeTab', 'tabs'),
125
+ }
126
+
127
+ expect(buildActiveItemIndexDisplayFilters(props)).toEqual({
128
+ data: { hide: ['activeTab'] },
129
+ })
130
+ })
131
+
132
+ it('returns undefined when no ActiveItemIndex props exist', () => {
133
+ const props = {
134
+ tabs: arrayProp('tabs'),
135
+ count: numberProp('count'),
136
+ }
137
+
138
+ expect(buildActiveItemIndexDisplayFilters(props)).toBeUndefined()
139
+ })
140
+
141
+ it('returns undefined when the target prop is not an array type', () => {
142
+ const props = {
143
+ tabs: numberProp('tabs'),
144
+ activeTab: activeIndexProp('activeTab', 'tabs'),
145
+ }
146
+
147
+ expect(buildActiveItemIndexDisplayFilters(props)).toBeUndefined()
148
+ })
149
+ })
@@ -0,0 +1,90 @@
1
+ /**
2
+ * Manifest emission for the `ActiveItemIndex<'<arrayProp>'>` marker type.
3
+ *
4
+ * The TS extractor sets `activeItemIndexTarget` on index props. This module is the
5
+ * single place that interprets that marker for the generated manifest:
6
+ *
7
+ * - `buildActiveItemDisplayGroups` → `displayGroups.<arrayProp>` (`arrayItems` hat)
8
+ * - `buildActiveItemIndexDisplayFilters` → `displayFilters.data.hide` (hide raw index)
9
+ *
10
+ * `displayGroups` and `displayFilters` stay separate exports because they are
11
+ * orthogonal manifest fields — they only share the same trigger here.
12
+ *
13
+ * Default value `0` for unset index props is applied in `buildData` inside
14
+ * `to-editor-component`, not here.
15
+ *
16
+ * Each index prop and array prop may participate in at most one link. Conflicts
17
+ * are logged and dropped; only the first valid link is emitted (one hat per component).
18
+ */
19
+
20
+ import type { DisplayFilters, DisplayGroupItem } from '@wix/react-component-schema'
21
+ import { DISPLAY_GROUPS } from '@wix/react-component-schema'
22
+ import type { CoupledProp } from '../information-extractors/react'
23
+ import { formatDisplayName, singularize } from './utils'
24
+
25
+ interface ActiveItemIndexLink {
26
+ indexPropName: string
27
+ arrayPropName: string
28
+ }
29
+
30
+ export function buildActiveItemDisplayGroups(
31
+ props: Record<string, CoupledProp>,
32
+ ): Record<string, DisplayGroupItem> | undefined {
33
+ const groups: Record<string, DisplayGroupItem> = {}
34
+ const activeItemIndexLinks = collectValidActiveItemIndexLinks(props)
35
+
36
+ for (const { indexPropName, arrayPropName } of activeItemIndexLinks) {
37
+ groups[arrayPropName] = {
38
+ displayName: formatDisplayName(singularize(arrayPropName)),
39
+ groupType: DISPLAY_GROUPS.GROUP_TYPE.arrayItems,
40
+ arrayItems: {
41
+ arrayItems: arrayPropName,
42
+ selectedItemIndex: indexPropName,
43
+ },
44
+ }
45
+ }
46
+
47
+ return Object.keys(groups).length > 0 ? groups : undefined
48
+ }
49
+
50
+ export function buildActiveItemIndexDisplayFilters(props: Record<string, CoupledProp>): DisplayFilters | undefined {
51
+ const hiddenIndexProps = collectValidActiveItemIndexLinks(props).map((link) => link.indexPropName)
52
+ return hiddenIndexProps.length > 0 ? { data: { hide: hiddenIndexProps } } : undefined
53
+ }
54
+
55
+ function collectValidActiveItemIndexLinks(props: Record<string, CoupledProp>): ActiveItemIndexLink[] {
56
+ const links: ActiveItemIndexLink[] = []
57
+ const registeredIndexProps = new Set<string>()
58
+ const registeredArrayProps = new Set<string>()
59
+
60
+ for (const [indexPropName, prop] of Object.entries(props)) {
61
+ const arrayPropName = prop.activeItemIndexTarget
62
+ if (!arrayPropName) continue
63
+
64
+ const arrayProp = props[arrayPropName]
65
+ if (!arrayProp || arrayProp.resolvedType.kind !== 'array') continue
66
+
67
+ const conflictsWithRegisteredLink =
68
+ registeredIndexProps.has(indexPropName) || registeredArrayProps.has(arrayPropName)
69
+ if (conflictsWithRegisteredLink) {
70
+ console.warn(
71
+ `ActiveItemIndex<> link "${indexPropName}" -> "${arrayPropName}" conflicts with an existing link — dropping.`,
72
+ )
73
+ continue
74
+ }
75
+
76
+ const hasRegisteredLink = links.length > 0
77
+ if (hasRegisteredLink) {
78
+ console.warn(
79
+ `Multiple ActiveItemIndex<> props found; only "${links[0].indexPropName}" -> "${links[0].arrayPropName}" is used. Dropping "${indexPropName}" -> "${arrayPropName}".`,
80
+ )
81
+ continue
82
+ }
83
+
84
+ links.push({ indexPropName, arrayPropName })
85
+ registeredIndexProps.add(indexPropName)
86
+ registeredArrayProps.add(arrayPropName)
87
+ }
88
+
89
+ return links
90
+ }
@@ -318,3 +318,67 @@ describe('toEditorReactComponent — @property design tokens', () => {
318
318
  expect(editorElement?.cssProperties?.['--sdf-safelight-color']).toBeUndefined()
319
319
  })
320
320
  })
321
+
322
+ describe('toEditorReactComponent — ActiveItemIndex display groups', () => {
323
+ it('emits displayGroups and hides the active index from Settings', () => {
324
+ const stringType = { kind: 'primitive' as const, value: 'string' as const }
325
+ const tabItemType = {
326
+ kind: 'object' as const,
327
+ properties: {
328
+ name: { name: 'name', type: 'string', required: false, resolvedType: stringType },
329
+ body: { name: 'body', type: 'string', required: false, resolvedType: stringType },
330
+ },
331
+ }
332
+
333
+ const component: ComponentInfoWithCss = {
334
+ componentName: 'SimpleTabs',
335
+ props: {
336
+ tabs: {
337
+ name: 'tabs',
338
+ type: 'Array<TabItem>',
339
+ required: false,
340
+ resolvedType: { kind: 'array', elementType: tabItemType },
341
+ propPath: 'props.tabs',
342
+ logicOnly: false,
343
+ },
344
+ activeTab: {
345
+ name: 'activeTab',
346
+ type: 'number',
347
+ required: false,
348
+ resolvedType: { kind: 'primitive', value: 'number' },
349
+ activeItemIndexTarget: 'tabs',
350
+ propPath: 'props.activeTab',
351
+ logicOnly: false,
352
+ },
353
+ },
354
+ elements: [
355
+ createSemanticElement({
356
+ traceId: 'trace-root',
357
+ name: 'root',
358
+ className: 'simple-tabs',
359
+ }),
360
+ ],
361
+ propUsages: new Map(),
362
+ css: [],
363
+ varUsedByTraceId: new Map(),
364
+ }
365
+
366
+ const editorElement = toEditorReactComponent(component).editorElement
367
+
368
+ expect(editorElement?.displayGroups).toEqual({
369
+ tabs: {
370
+ displayName: 'Tab',
371
+ groupType: 'arrayItems',
372
+ arrayItems: {
373
+ arrayItems: 'tabs',
374
+ selectedItemIndex: 'activeTab',
375
+ },
376
+ },
377
+ })
378
+ expect(editorElement?.displayFilters).toEqual({
379
+ data: { hide: ['activeTab'] },
380
+ })
381
+ expect(editorElement?.data?.activeTab?.defaultValue).toBe(0)
382
+ expect(editorElement?.data?.activeTab?.dataType).toBe('number')
383
+ })
384
+ })
@@ -25,6 +25,7 @@ import type {
25
25
  } from '../information-extractors/react'
26
26
  import { getDefaultDisplayForTag, resolveDisplayValue } from '../information-extractors/react'
27
27
  import { findPreferredSemanticClass, normalizeClassNames } from '../utils/css-class'
28
+ import { buildActiveItemDisplayGroups, buildActiveItemIndexDisplayFilters } from './active-item-index-builder'
28
29
  import { resolveLonghand } from './css-longhand-resolver'
29
30
  import { type CustomClassTrigger, buildCustomStatesBlock } from './custom-states-builder'
30
31
  import { buildDataItem } from './data-item-builder'
@@ -94,6 +95,9 @@ function buildEditorElement(
94
95
  )
95
96
  : undefined
96
97
 
98
+ const displayGroups = buildActiveItemDisplayGroups(component.props)
99
+ const displayFilters = buildActiveItemIndexDisplayFilters(component.props)
100
+
97
101
  return {
98
102
  selector: buildSelector(rootElement),
99
103
  displayName: formatDisplayName(component.componentName),
@@ -109,6 +113,8 @@ function buildEditorElement(
109
113
  cssProperties: buildCssProperties(rootElement),
110
114
  cssCustomProperties: rootCustomProps,
111
115
  ...(rootStates && { states: rootStates }),
116
+ ...(displayGroups && { displayGroups }),
117
+ ...(displayFilters && { displayFilters }),
112
118
  }
113
119
  }
114
120
 
@@ -157,6 +163,10 @@ function buildData(
157
163
 
158
164
  const result = buildDataItem(prop, defaultValue, propUsages, prop.propPath)
159
165
  if (result.isErr()) throw result.error
166
+ // ActiveItemIndex props default to 0 when no default is specified
167
+ if (prop.activeItemIndexTarget && result.value.defaultValue === undefined) {
168
+ result.value.defaultValue = 0
169
+ }
160
170
  data[name] = result.value
161
171
  }
162
172
 
@@ -1,5 +1,5 @@
1
1
  import { describe, expect, it } from 'vitest'
2
- import { formatDisplayName } from './utils'
2
+ import { formatDisplayName, singularize } from './utils'
3
3
 
4
4
  describe('formatDisplayName', () => {
5
5
  it('maps a11y to Accessibility', () => {
@@ -47,3 +47,16 @@ describe('formatDisplayName', () => {
47
47
  expect(formatDisplayName(longSingleWord).length).toBe(50)
48
48
  })
49
49
  })
50
+
51
+ describe('singularize', () => {
52
+ it.each([
53
+ ['slides', 'slide'],
54
+ ['entries', 'entry'],
55
+ ['boxes', 'box'],
56
+ ['tabs', 'tab'],
57
+ ['glass', 'glass'],
58
+ ['item', 'item'],
59
+ ])('singularizes "%s" to "%s"', (input, expected) => {
60
+ expect(singularize(input)).toBe(expected)
61
+ })
62
+ })
@@ -41,3 +41,14 @@ export function formatDisplayName(input: string): string {
41
41
  const result = words.join(' ')
42
42
  return result.slice(0, DISPLAY_NAME_MAX_LENGTH)
43
43
  }
44
+
45
+ /**
46
+ * Naive singularization for display names.
47
+ * Falls back to the original if no rule matches.
48
+ */
49
+ export function singularize(name: string): string {
50
+ if (name.endsWith('ies')) return `${name.slice(0, -3)}y`
51
+ if (name.endsWith('ses') || name.endsWith('xes') || name.endsWith('zes')) return name.slice(0, -2)
52
+ if (name.endsWith('s') && !name.endsWith('ss')) return name.slice(0, -1)
53
+ return name
54
+ }
@@ -18,6 +18,11 @@ const inheritedChildrenFixture = path.resolve(
18
18
  '../../../../example-components/src/components/InheritedChildren/component.tsx',
19
19
  )
20
20
 
21
+ const activeItemIndexFixture = path.resolve(
22
+ __dirname,
23
+ '../../../../example-components/src/components/ActiveItemIndex/SimpleTabs/component.tsx',
24
+ )
25
+
21
26
  describe('TS extractor — ElementState<> trigger detection', () => {
22
27
  it('flags an ElementState<>-wrapped prop and unwraps its inner type', async () => {
23
28
  const programResult = await compileTsFile(propTriggeredFixture)
@@ -79,3 +84,35 @@ describe('TS extractor — container children props', () => {
79
84
  expect(component!.props.label).toBeDefined()
80
85
  })
81
86
  })
87
+
88
+ describe('TS extractor — ActiveItemIndex<> detection', () => {
89
+ it('flags an ActiveItemIndex<>-wrapped prop with the target array prop name and unwraps to number', async () => {
90
+ const programResult = await compileTsFile(activeItemIndexFixture)
91
+ expect(programResult.isOk()).toBe(true)
92
+ const program = programResult._unsafeUnwrap()
93
+
94
+ const components = extractAllComponentInfo(program, activeItemIndexFixture)
95
+ const component = components.find((candidate) => candidate.componentName === 'SimpleTabs')
96
+ expect(component).toBeDefined()
97
+
98
+ const activeTab = component!.props.activeTab
99
+ expect(activeTab.activeItemIndexTarget).toBe('tabs')
100
+ // The `ActiveItemIndex<'tabs'>` wrapper is unwrapped — the prop behaves as a plain number.
101
+ expect(activeTab.type).toBe('number')
102
+ // It should not be flagged as a state trigger.
103
+ expect(activeTab.isStateTrigger).toBeUndefined()
104
+ })
105
+
106
+ it('does not flag a plain number prop as an active item index', async () => {
107
+ const programResult = await compileTsFile(activeItemIndexFixture)
108
+ expect(programResult.isOk()).toBe(true)
109
+ const program = programResult._unsafeUnwrap()
110
+
111
+ const components = extractAllComponentInfo(program, activeItemIndexFixture)
112
+ const component = components.find((candidate) => candidate.componentName === 'SimpleTabs')
113
+ expect(component).toBeDefined()
114
+
115
+ // `tabs` is an array prop, not an active item index.
116
+ expect(component!.props.tabs.activeItemIndexTarget).toBeUndefined()
117
+ })
118
+ })
@@ -211,25 +211,32 @@ function convertComponentDoc(doc: ComponentDoc, program: ts.Program, checker: ts
211
211
  // Resolve the type deeply
212
212
  let declaredTypeInfo: DeclaredTypeInfo | undefined
213
213
  let stateTrigger: StateTriggerInfo | undefined
214
+ let activeItemIndex: ActiveItemIndexInfo | undefined
214
215
  if (propSymbol) {
215
216
  const decl = propSymbol.getDeclarations()?.[0]
216
217
  if (decl) {
217
218
  const declSourceFile = decl.getSourceFile()
218
219
  declaredTypeInfo = getDeclaredTypeInfo(propSymbol, declSourceFile, checker)
219
220
  stateTrigger = detectStateTrigger(propSymbol, declSourceFile, checker)
221
+ activeItemIndex = detectActiveItemIndex(propSymbol, declSourceFile, checker)
220
222
  }
221
223
  }
222
224
 
223
225
  // An `ElementState<Inner>` prop should behave as its inner type everywhere except
224
226
  // for the `isStateTrigger` flag, so unwrap the inner type for `type` and resolution
225
227
  // (and drop the `ElementState` declared symbol so it isn't mistaken for a semantic type).
226
- const typeString = stateTrigger?.innerTypeName ?? declaredTypeInfo?.name ?? propItem.type.name
228
+ // Similarly, `ActiveItemIndex<'arrayPropName'>` unwraps to `number`.
229
+ const typeString =
230
+ stateTrigger?.innerTypeName ??
231
+ (activeItemIndex ? 'number' : undefined) ??
232
+ declaredTypeInfo?.name ??
233
+ propItem.type.name
227
234
  const resolvedType = propType
228
235
  ? resolveType({
229
236
  type: propType,
230
237
  checker,
231
238
  typeString,
232
- declaredSymbol: stateTrigger ? undefined : declaredTypeInfo?.symbol,
239
+ declaredSymbol: stateTrigger || activeItemIndex ? undefined : declaredTypeInfo?.symbol,
233
240
  })
234
241
  : { kind: 'primitive' as const, value: propItem.type.name }
235
242
 
@@ -255,6 +262,7 @@ function convertComponentDoc(doc: ComponentDoc, program: ts.Program, checker: ts
255
262
  min: parseNumericTag(tags?.min, propName, 'min'),
256
263
  max: parseNumericTag(tags?.max, propName, 'max'),
257
264
  isStateTrigger: stateTrigger ? true : undefined,
265
+ activeItemIndexTarget: activeItemIndex?.targetArrayProp,
258
266
  }
259
267
  }
260
268
 
@@ -506,6 +514,11 @@ interface StateTriggerInfo {
506
514
  innerTypeName: string
507
515
  }
508
516
 
517
+ interface ActiveItemIndexInfo {
518
+ /** The string literal from the type argument, naming the target array prop. */
519
+ targetArrayProp: string
520
+ }
521
+
509
522
  /**
510
523
  * Detects when a prop is declared with the `ElementState<>` marker type from
511
524
  * `@wix/react-component-utils` (e.g. `isLoading?: ElementState<boolean>`), which opts
@@ -558,6 +571,65 @@ function isElementStateAliasFromUtils(identifier: ts.Identifier, checker: ts.Typ
558
571
  return declaration.getSourceFile().fileName.includes('react-component-utils')
559
572
  }
560
573
 
574
+ /**
575
+ * Detects when a prop is declared with the `ActiveItemIndex<>` marker type from
576
+ * `@wix/react-component-utils` (e.g. `activeTab?: ActiveItemIndex<'tabs'>`), which
577
+ * links the prop to an array data prop for the `displayGroups.arrayItems` manifest
578
+ * block. Returns the target array prop name from the string literal type argument.
579
+ * Returns `undefined` for any other declaration, or when the type argument is missing
580
+ * or not a string literal.
581
+ */
582
+ function detectActiveItemIndex(
583
+ prop: ts.Symbol,
584
+ sourceFile: ts.SourceFile,
585
+ checker: ts.TypeChecker,
586
+ ): ActiveItemIndexInfo | undefined {
587
+ const declaration = prop.getDeclarations()?.[0]
588
+ if (!declaration) return undefined
589
+
590
+ if (
591
+ !(ts.isPropertySignature(declaration) || ts.isPropertyDeclaration(declaration) || ts.isParameter(declaration)) ||
592
+ !declaration.type ||
593
+ !ts.isTypeReferenceNode(declaration.type)
594
+ ) {
595
+ return undefined
596
+ }
597
+
598
+ const typeName = declaration.type.typeName
599
+ const identifier = ts.isIdentifier(typeName) ? typeName : typeName.right
600
+ if (identifier.text !== 'ActiveItemIndex') return undefined
601
+ if (!isActiveItemIndexAliasFromUtils(identifier, checker)) return undefined
602
+
603
+ // Require an explicit string literal type argument — bare ActiveItemIndex (no arg)
604
+ // compiles but gives ZeroConfig no array name to link to.
605
+ const typeArgs = declaration.type.typeArguments
606
+ if (!typeArgs || typeArgs.length !== 1) return undefined
607
+
608
+ const typeArgNode = typeArgs[0]
609
+ if (!ts.isLiteralTypeNode(typeArgNode) || !ts.isStringLiteral(typeArgNode.literal)) return undefined
610
+
611
+ return { targetArrayProp: typeArgNode.literal.text }
612
+ }
613
+
614
+ /**
615
+ * Confirms an identifier referencing `ActiveItemIndex` resolves to the marker type
616
+ * alias exported by `@wix/react-component-utils`, rather than an unrelated local
617
+ * type that happens to be named `ActiveItemIndex`.
618
+ */
619
+ function isActiveItemIndexAliasFromUtils(identifier: ts.Identifier, checker: ts.TypeChecker): boolean {
620
+ let symbol = checker.getSymbolAtLocation(identifier)
621
+ if (!symbol) return false
622
+ if ((symbol.getFlags() & ts.SymbolFlags.Alias) !== 0) {
623
+ symbol = checker.getAliasedSymbol(symbol)
624
+ }
625
+
626
+ const declaration = symbol.getDeclarations()?.[0]
627
+ if (!declaration || !ts.isTypeAliasDeclaration(declaration)) return false
628
+ if (declaration.name.text !== 'ActiveItemIndex') return false
629
+
630
+ return declaration.getSourceFile().fileName.includes('react-component-utils')
631
+ }
632
+
561
633
  function getDeclaredTypeInfo(
562
634
  prop: ts.Symbol,
563
635
  sourceFile: ts.SourceFile,
@@ -59,6 +59,11 @@ export interface PropInfo {
59
59
  // state. `type` and `resolvedType` reflect the unwrapped inner type, so the prop
60
60
  // still behaves as a normal data prop everywhere else.
61
61
  isStateTrigger?: boolean
62
+ // When the prop is wrapped in `ActiveItemIndex<'arrayPropName'>` from
63
+ // @wix/react-component-utils, carries the target array prop name. `type` and
64
+ // `resolvedType` reflect the unwrapped `number` type. Used by the display-groups
65
+ // builder to auto-generate the `displayGroups.arrayItems` manifest block.
66
+ activeItemIndexTarget?: string
62
67
  }
63
68
 
64
69
  export interface ComponentInfo {