@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-CQq7GOf9.js → index-B27P3e9W.js} +1 -1
- package/dist/{index-DBSnnglR.js → index-EfIM-d53.js} +7316 -7249
- package/dist/index.d.ts +1 -0
- package/dist/index.js +1 -1
- package/package.json +2 -2
- package/src/converters/active-item-index-builder.test.ts +149 -0
- package/src/converters/active-item-index-builder.ts +90 -0
- package/src/converters/to-editor-component.test.ts +64 -0
- package/src/converters/to-editor-component.ts +10 -0
- package/src/converters/utils.test.ts +14 -1
- package/src/converters/utils.ts +11 -0
- package/src/information-extractors/ts/components.test.ts +37 -0
- package/src/information-extractors/ts/components.ts +74 -2
- package/src/information-extractors/ts/types.ts +5 -0
package/dist/index.d.ts
CHANGED
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-
|
|
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.
|
|
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": "
|
|
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
|
+
})
|
package/src/converters/utils.ts
CHANGED
|
@@ -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
|
-
|
|
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 {
|