@wix/zero-config-implementation 1.71.0 → 1.72.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.
Files changed (39) hide show
  1. package/dist/{index-DT1jTZEG.js → index-CSi4SZpQ.js} +1 -1
  2. package/dist/{index-C4cP1dwy.js → index-CdHu4-o-.js} +20459 -18722
  3. package/dist/index.d.ts +15 -8
  4. package/dist/index.js +1 -1
  5. package/package.json +4 -3
  6. package/src/__fixtures__/cjs-ref-element-entry.cjs +1 -0
  7. package/src/__fixtures__/cjs-ref-element-lazy-entry.cjs +3 -0
  8. package/src/__fixtures__/cjs-ref-element-order-entry.cjs +8 -0
  9. package/src/__fixtures__/cjs-ref-element-order-target.cjs +7 -0
  10. package/src/__fixtures__/cjs-ref-element-target.cjs +5 -0
  11. package/src/__fixtures__/esm-ref-element-entry.mjs +3 -0
  12. package/src/component-renderer.ts +12 -9
  13. package/src/converters/to-editor-component.ts +24 -6
  14. package/src/index.ts +121 -54
  15. package/src/information-extractors/react/extractors/core/tree-builder.ts +3 -3
  16. package/src/information-extractors/react/extractors/core/types.ts +4 -5
  17. package/src/information-extractors/react/extractors/css-properties.ts +2 -0
  18. package/src/information-extractors/react/extractors/prop-tracker.test.ts +251 -0
  19. package/src/information-extractors/react/extractors/prop-tracker.ts +78 -14
  20. package/src/information-extractors/react/types.ts +1 -1
  21. package/src/information-extractors/react/utils/mock-generator.test.ts +131 -0
  22. package/src/information-extractors/react/utils/mock-generator.ts +38 -13
  23. package/src/manifest-pipeline.ts +19 -14
  24. package/src/module-loader.test.ts +150 -0
  25. package/src/module-loader.ts +48 -8
  26. package/src/react-runtime-interceptor.ts +64 -0
  27. package/src/react-runtime-loader.ts +656 -0
  28. package/src/ref-elements/component-tag.ts +105 -0
  29. package/src/ref-elements/context.test.ts +179 -0
  30. package/src/ref-elements/context.ts +280 -0
  31. package/src/ref-elements/eligible-paths.ts +45 -0
  32. package/src/ref-elements/module-resolution.test.ts +50 -0
  33. package/src/ref-elements/module-resolution.ts +21 -0
  34. package/src/ref-elements/module-specifier.ts +17 -0
  35. package/src/ref-elements/path-utils.test.ts +14 -0
  36. package/src/ref-elements/path-utils.ts +55 -0
  37. package/src/ref-elements/types.ts +17 -0
  38. package/src/utils/css-class.ts +15 -0
  39. package/src/jsx-runtime-interceptor.ts +0 -245
@@ -0,0 +1,251 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { createRefElementMarker } from '../../../ref-elements/path-utils'
3
+ import type { RefElementMatch } from '../../../ref-elements/types'
4
+ import { ExtractorStore } from './core/store'
5
+ import { createPropTrackerExtractor } from './prop-tracker'
6
+
7
+ function markRefElementComponent(runtimeComponent: (...args: unknown[]) => unknown, refComponentType: string): void {
8
+ Object.defineProperty(
9
+ runtimeComponent as unknown as Record<PropertyKey, unknown>,
10
+ Symbol.for('zero-config:ref-element-component-type'),
11
+ {
12
+ value: refComponentType,
13
+ },
14
+ )
15
+ }
16
+
17
+ describe('createPropTrackerExtractor', () => {
18
+ it('associates a ref-capable composite component with the rendered DOM trace id', () => {
19
+ const store = new ExtractorStore()
20
+ const runtimeRefComponent = function LocalButton(): null {
21
+ return null
22
+ }
23
+ markRefElementComponent(runtimeRefComponent, 'myPkg.LocalButton')
24
+ const { extractor } = createPropTrackerExtractor({
25
+ eligiblePaths: ['actionButton'],
26
+ })
27
+ const taggedProps = { id: createRefElementMarker('actionButton') }
28
+
29
+ extractor.onCreateElement?.({
30
+ type: runtimeRefComponent,
31
+ isDomElement: false,
32
+ props: taggedProps,
33
+ children: [],
34
+ store,
35
+ })
36
+
37
+ extractor.onCreateElement?.({
38
+ type: 'button',
39
+ isDomElement: true,
40
+ tag: 'button',
41
+ traceId: 'trace-1',
42
+ props: {
43
+ ...taggedProps,
44
+ className: 'action-button action-button--accent',
45
+ },
46
+ children: [],
47
+ store,
48
+ })
49
+
50
+ expect(store.get<RefElementMatch>('trace-1', 'ref-element')).toEqual({
51
+ elementPropsPath: 'actionButton',
52
+ refComponentType: 'myPkg.LocalButton',
53
+ selector: '.action-button',
54
+ })
55
+ })
56
+
57
+ it('uses the marker from the final id value that reaches the rendered element', () => {
58
+ const store = new ExtractorStore()
59
+ const runtimeRefComponent = function LocalButton(): null {
60
+ return null
61
+ }
62
+ markRefElementComponent(runtimeRefComponent, 'myPkg.LocalButton')
63
+ const { extractor } = createPropTrackerExtractor({
64
+ eligiblePaths: ['primaryAction', 'secondaryAction'],
65
+ })
66
+
67
+ extractor.onCreateElement?.({
68
+ type: runtimeRefComponent,
69
+ isDomElement: false,
70
+ props: { id: createRefElementMarker('secondaryAction') },
71
+ children: [],
72
+ store,
73
+ })
74
+
75
+ extractor.onCreateElement?.({
76
+ type: 'button',
77
+ isDomElement: true,
78
+ tag: 'button',
79
+ traceId: 'trace-2',
80
+ props: {
81
+ id: createRefElementMarker('secondaryAction'),
82
+ className: 'action-button',
83
+ },
84
+ children: [],
85
+ store,
86
+ })
87
+
88
+ expect(store.get<RefElementMatch>('trace-2', 'ref-element')).toEqual({
89
+ elementPropsPath: 'secondaryAction',
90
+ refComponentType: 'myPkg.LocalButton',
91
+ selector: '.action-button',
92
+ })
93
+ })
94
+
95
+ it('waits for the first semantic DOM target before consuming a pending ref element', () => {
96
+ const store = new ExtractorStore()
97
+ const runtimeRefComponent = function LocalButton(): null {
98
+ return null
99
+ }
100
+ markRefElementComponent(runtimeRefComponent, 'myPkg.LocalButton')
101
+ const { extractor } = createPropTrackerExtractor({
102
+ eligiblePaths: ['actionButton'],
103
+ })
104
+ const taggedProps = { id: createRefElementMarker('actionButton') }
105
+
106
+ extractor.onCreateElement?.({
107
+ type: runtimeRefComponent,
108
+ isDomElement: false,
109
+ props: taggedProps,
110
+ children: [],
111
+ store,
112
+ })
113
+
114
+ extractor.onCreateElement?.({
115
+ type: 'div',
116
+ isDomElement: true,
117
+ tag: 'div',
118
+ traceId: 'trace-wrapper',
119
+ props: {
120
+ ...taggedProps,
121
+ className: '_hashWrapper_123abc',
122
+ },
123
+ children: [],
124
+ store,
125
+ })
126
+
127
+ extractor.onCreateElement?.({
128
+ type: 'button',
129
+ isDomElement: true,
130
+ tag: 'button',
131
+ traceId: 'trace-button',
132
+ props: {
133
+ ...taggedProps,
134
+ className: 'action-button',
135
+ },
136
+ children: [],
137
+ store,
138
+ })
139
+
140
+ expect(store.get<RefElementMatch>('trace-wrapper', 'ref-element')).toBeUndefined()
141
+ expect(store.get<RefElementMatch>('trace-button', 'ref-element')).toEqual({
142
+ elementPropsPath: 'actionButton',
143
+ refComponentType: 'myPkg.LocalButton',
144
+ selector: '.action-button',
145
+ })
146
+ })
147
+
148
+ it('tracks repeated instances of the same ref-element marker in render order', () => {
149
+ const store = new ExtractorStore()
150
+ const runtimeRefComponent = function LocalButton(): null {
151
+ return null
152
+ }
153
+ markRefElementComponent(runtimeRefComponent, 'myPkg.LocalButton')
154
+ const { extractor } = createPropTrackerExtractor({
155
+ eligiblePaths: ['actionButton'],
156
+ })
157
+ const taggedProps = { id: createRefElementMarker('actionButton') }
158
+
159
+ extractor.onCreateElement?.({
160
+ type: runtimeRefComponent,
161
+ isDomElement: false,
162
+ props: taggedProps,
163
+ children: [],
164
+ store,
165
+ })
166
+
167
+ extractor.onCreateElement?.({
168
+ type: runtimeRefComponent,
169
+ isDomElement: false,
170
+ props: taggedProps,
171
+ children: [],
172
+ store,
173
+ })
174
+
175
+ extractor.onCreateElement?.({
176
+ type: 'button',
177
+ isDomElement: true,
178
+ tag: 'button',
179
+ traceId: 'trace-first',
180
+ props: {
181
+ ...taggedProps,
182
+ className: 'action-button',
183
+ },
184
+ children: [],
185
+ store,
186
+ })
187
+
188
+ extractor.onCreateElement?.({
189
+ type: 'button',
190
+ isDomElement: true,
191
+ tag: 'button',
192
+ traceId: 'trace-second',
193
+ props: {
194
+ ...taggedProps,
195
+ className: 'action-button action-button--secondary',
196
+ },
197
+ children: [],
198
+ store,
199
+ })
200
+
201
+ expect(store.get<RefElementMatch>('trace-first', 'ref-element')).toEqual({
202
+ elementPropsPath: 'actionButton',
203
+ refComponentType: 'myPkg.LocalButton',
204
+ selector: '.action-button',
205
+ })
206
+ expect(store.get<RefElementMatch>('trace-second', 'ref-element')).toEqual({
207
+ elementPropsPath: 'actionButton',
208
+ refComponentType: 'myPkg.LocalButton',
209
+ selector: '.action-button',
210
+ })
211
+ })
212
+
213
+ it('preserves nested elementProps paths in ref-element runtime metadata', () => {
214
+ const store = new ExtractorStore()
215
+ const runtimeRefComponent = function LocalButton(): null {
216
+ return null
217
+ }
218
+ markRefElementComponent(runtimeRefComponent, 'myPkg.LocalButton')
219
+ const { extractor } = createPropTrackerExtractor({
220
+ eligiblePaths: ['card.elementProps.cta'],
221
+ })
222
+ const taggedProps = { id: createRefElementMarker('card.elementProps.cta') }
223
+
224
+ extractor.onCreateElement?.({
225
+ type: runtimeRefComponent,
226
+ isDomElement: false,
227
+ props: taggedProps,
228
+ children: [],
229
+ store,
230
+ })
231
+
232
+ extractor.onCreateElement?.({
233
+ type: 'button',
234
+ isDomElement: true,
235
+ tag: 'button',
236
+ traceId: 'trace-nested',
237
+ props: {
238
+ ...taggedProps,
239
+ className: 'card-cta',
240
+ },
241
+ children: [],
242
+ store,
243
+ })
244
+
245
+ expect(store.get<RefElementMatch>('trace-nested', 'ref-element')).toEqual({
246
+ elementPropsPath: 'card.elementProps.cta',
247
+ refComponentType: 'myPkg.LocalButton',
248
+ selector: '.card-cta',
249
+ })
250
+ })
251
+ })
@@ -7,6 +7,10 @@
7
7
 
8
8
  import type { HTMLAttributes } from 'react'
9
9
  import { TRACE_ATTR } from '../../../component-renderer'
10
+ import { readRefElementComponentType } from '../../../ref-elements/component-tag'
11
+ import { parseRefElementMarker } from '../../../ref-elements/path-utils'
12
+ import type { RefElementMatch } from '../../../ref-elements/types'
13
+ import { findPreferredSemanticClass, normalizeClassNames } from '../../../utils/css-class'
10
14
  import type { PropSpyMeta, TrackingStores } from '../types'
11
15
  import { type PropSpyRegistrar, generateMockProps, resetMockCounter } from '../utils/mock-generator'
12
16
  import type { CreateElementEvent, ReactExtractor, RenderContext } from './core/types'
@@ -28,6 +32,15 @@ export interface PropTrackerExtractorState {
28
32
  stores: TrackingStores
29
33
  }
30
34
 
35
+ interface CreatePropTrackerExtractorOptions {
36
+ eligiblePaths?: string[]
37
+ }
38
+
39
+ interface PendingRefElement {
40
+ elementPropsPath: string
41
+ refComponentType: string
42
+ }
43
+
31
44
  // ─────────────────────────────────────────────────────────────────────────────
32
45
  // Factory
33
46
  // ─────────────────────────────────────────────────────────────────────────────
@@ -36,9 +49,10 @@ export interface PropTrackerExtractorState {
36
49
  * Creates a prop tracker extractor that:
37
50
  * 1. Generates spy-instrumented mock props during beforeRender
38
51
  * 2. Detects spy markers in element props during onCreateElement
39
- * 3. propUsages tracking data to the store namespaced by 'prop-tracker'
52
+ * 3. Tracks runtime refElement markers from composite components to rendered DOM
53
+ * 4. Writes prop usage data to the store namespaced by 'prop-tracker'
40
54
  */
41
- export function createPropTrackerExtractor(): {
55
+ export function createPropTrackerExtractor(options?: CreatePropTrackerExtractorOptions): {
42
56
  extractor: ReactExtractor
43
57
  state: PropTrackerExtractorState
44
58
  } {
@@ -48,6 +62,8 @@ export function createPropTrackerExtractor(): {
48
62
  const stores: TrackingStores = {
49
63
  propUsages: new Map(),
50
64
  }
65
+ const eligiblePaths = new Set(options?.eligiblePaths ?? [])
66
+ const pendingRefElementsByPath = new Map<string, PendingRefElement[]>()
51
67
 
52
68
  const registrar: PropSpyRegistrar = {
53
69
  registerString(path, propName, value) {
@@ -84,16 +100,14 @@ export function createPropTrackerExtractor(): {
84
100
  return meta ? [{ propName: meta.propName, path: meta.path, embedded: false }] : []
85
101
  }
86
102
  if (value && typeof value === 'object') {
87
- // Prevent infinite recursion from circular references
88
103
  if (seen.has(value as object)) {
89
104
  return []
90
105
  }
91
106
  seen.add(value as object)
92
- // Skip React elements (have $$typeof symbol) - they have circular refs
93
107
  if ('$$typeof' in value) {
94
108
  return []
95
109
  }
96
- return Object.values(value).flatMap((v) => extractSpies(v, seen))
110
+ return Object.values(value).flatMap((nestedValue) => extractSpies(nestedValue, seen))
97
111
  }
98
112
  return []
99
113
  }
@@ -103,10 +117,19 @@ export function createPropTrackerExtractor(): {
103
117
 
104
118
  onBeforeRender(context: RenderContext): void {
105
119
  resetMockCounter()
106
- context.props = generateMockProps(context.componentInfo, registrar)
120
+ context.props = generateMockProps(context.componentInfo, registrar, {
121
+ eligibleRefElementPaths: eligiblePaths,
122
+ })
107
123
  },
108
124
 
109
125
  onCreateElement(event: CreateElementEvent): void {
126
+ if (!event.isDomElement) {
127
+ queuePendingRefElement(event.props.id, readRefElementComponentType(event.type))
128
+ return
129
+ }
130
+
131
+ if (!event.tag || !event.traceId) return
132
+
110
133
  const { tag, props, traceId, store } = event
111
134
  const htmlProps = props as HTMLAttributes<unknown>
112
135
 
@@ -132,25 +155,24 @@ export function createPropTrackerExtractor(): {
132
155
  const entry = stores.propUsages.get(path)!
133
156
 
134
157
  entry.elements.set(traceId, { tag, elementId: traceId })
135
- const isConcat = spies.length > 1 || spy.embedded
136
- entry.attributes.set(`${traceId}:${key}`, { attr: key, concatenated: isConcat })
158
+ const isConcatenatedAttribute = spies.length > 1 || spy.embedded
159
+ entry.attributes.set(`${traceId}:${key}`, { attr: key, concatenated: isConcatenatedAttribute })
137
160
 
138
- // Track concatenated attributes
139
- if (isConcat) {
161
+ if (isConcatenatedAttribute) {
140
162
  concatenatedAttrs.set(key, spy.propName)
141
163
  }
142
164
  })
143
165
  })
144
166
 
145
- // Write to store
146
- const data: PropTrackerData = {
167
+ store.set(traceId, 'prop-tracker', {
147
168
  tag,
148
169
  role: htmlProps.role,
149
170
  boundProps: [...boundProps],
150
171
  concatenatedAttrs,
151
172
  eventHandlers,
152
- }
153
- store.set(traceId, 'prop-tracker', data)
173
+ } satisfies PropTrackerData)
174
+
175
+ attachMatchedRefElement(traceId, props, store)
154
176
  },
155
177
  }
156
178
 
@@ -158,4 +180,46 @@ export function createPropTrackerExtractor(): {
158
180
  extractor,
159
181
  state: { stores },
160
182
  }
183
+
184
+ function queuePendingRefElement(idValue: unknown, refComponentType: string | undefined): void {
185
+ const elementPropsPath = parseRefElementMarker(idValue)
186
+ if (!elementPropsPath || !refComponentType) return
187
+ if (eligiblePaths.size > 0 && !eligiblePaths.has(elementPropsPath)) return
188
+
189
+ const pendingRefElements = pendingRefElementsByPath.get(elementPropsPath) ?? []
190
+ pendingRefElements.push({ elementPropsPath, refComponentType })
191
+ pendingRefElementsByPath.set(elementPropsPath, pendingRefElements)
192
+ }
193
+
194
+ function attachMatchedRefElement(
195
+ traceId: string,
196
+ props: Record<string, unknown>,
197
+ store: CreateElementEvent['store'],
198
+ ): void {
199
+ const elementPropsPath = parseRefElementMarker(props.id)
200
+ if (!elementPropsPath) return
201
+
202
+ const selector = getRefElementSelector(props.className)
203
+ if (!selector) return
204
+
205
+ const pendingRefElements = pendingRefElementsByPath.get(elementPropsPath)
206
+ const pendingRefElement = pendingRefElements?.shift()
207
+ if (!pendingRefElement) return
208
+ if (pendingRefElements?.length === 0) {
209
+ pendingRefElementsByPath.delete(elementPropsPath)
210
+ }
211
+
212
+ const refElementMatch: RefElementMatch = {
213
+ elementPropsPath: pendingRefElement.elementPropsPath,
214
+ refComponentType: pendingRefElement.refComponentType,
215
+ selector,
216
+ }
217
+
218
+ store.set(traceId, 'ref-element', refElementMatch)
219
+ }
220
+ }
221
+
222
+ function getRefElementSelector(classNameValue: unknown): string | undefined {
223
+ const semanticClassName = findPreferredSemanticClass(normalizeClassNames(classNameValue))
224
+ return semanticClassName ? `.${semanticClassName}` : undefined
161
225
  }
@@ -49,6 +49,6 @@ export interface CoupledComponentInfo {
49
49
  componentName: string
50
50
  props: Record<string, CoupledProp>
51
51
  elements: ExtractedElement[]
52
- innerElementProps?: Map<string, Record<string, CoupledProp>>
52
+ innerElementProps?: Map<string, { elementPropsPath: string; props: Record<string, CoupledProp> }>
53
53
  propUsages: TrackingStores['propUsages']
54
54
  }
@@ -0,0 +1,131 @@
1
+ import { describe, expect, it } from 'vitest'
2
+ import { createRefElementMarker, parseRefElementMarker } from '../../../ref-elements/path-utils'
3
+ import type { ComponentInfo } from '../../ts/types'
4
+ import { generateMockProps, resetMockCounter } from './mock-generator'
5
+
6
+ describe('generateMockProps', () => {
7
+ it('injects a dedicated ref-element id marker into each eligible elementProps branch', () => {
8
+ resetMockCounter()
9
+
10
+ const componentInfo: ComponentInfo = {
11
+ componentName: 'TestComponent',
12
+ props: {
13
+ elementProps: {
14
+ name: 'elementProps',
15
+ type: 'ElementProps',
16
+ required: true,
17
+ resolvedType: {
18
+ kind: 'object',
19
+ properties: {
20
+ actionButton: {
21
+ name: 'actionButton',
22
+ type: '{ className?: string; label?: string }',
23
+ required: true,
24
+ resolvedType: {
25
+ kind: 'object',
26
+ properties: {
27
+ className: {
28
+ name: 'className',
29
+ type: 'string',
30
+ required: false,
31
+ resolvedType: { kind: 'primitive', value: 'string' },
32
+ },
33
+ label: {
34
+ name: 'label',
35
+ type: 'string',
36
+ required: false,
37
+ resolvedType: { kind: 'primitive', value: 'string' },
38
+ },
39
+ },
40
+ },
41
+ },
42
+ },
43
+ },
44
+ },
45
+ },
46
+ }
47
+
48
+ const mockProps = generateMockProps(componentInfo, undefined, {
49
+ eligibleRefElementPaths: new Set(['actionButton']),
50
+ })
51
+ expect(mockProps).toMatchObject({
52
+ elementProps: {
53
+ actionButton: {
54
+ id: createRefElementMarker('actionButton'),
55
+ },
56
+ },
57
+ })
58
+
59
+ const actionButtonProps = (mockProps.elementProps as Record<string, unknown>).actionButton as Record<
60
+ string,
61
+ unknown
62
+ >
63
+ expect(parseRefElementMarker(actionButtonProps.id)).toBe('actionButton')
64
+ })
65
+
66
+ it('injects nested ref-element markers using the full elementProps path', () => {
67
+ resetMockCounter()
68
+
69
+ const componentInfo: ComponentInfo = {
70
+ componentName: 'NestedTestComponent',
71
+ props: {
72
+ elementProps: {
73
+ name: 'elementProps',
74
+ type: 'ElementProps',
75
+ required: true,
76
+ resolvedType: {
77
+ kind: 'object',
78
+ properties: {
79
+ card: {
80
+ name: 'card',
81
+ type: '{ elementProps: { cta: { className?: string } } }',
82
+ required: true,
83
+ resolvedType: {
84
+ kind: 'object',
85
+ properties: {
86
+ elementProps: {
87
+ name: 'elementProps',
88
+ type: '{ cta: { className?: string } }',
89
+ required: true,
90
+ resolvedType: {
91
+ kind: 'object',
92
+ properties: {
93
+ cta: {
94
+ name: 'cta',
95
+ type: '{ className?: string }',
96
+ required: true,
97
+ resolvedType: {
98
+ kind: 'object',
99
+ properties: {
100
+ className: {
101
+ name: 'className',
102
+ type: 'string',
103
+ required: false,
104
+ resolvedType: { kind: 'primitive', value: 'string' },
105
+ },
106
+ },
107
+ },
108
+ },
109
+ },
110
+ },
111
+ },
112
+ },
113
+ },
114
+ },
115
+ },
116
+ },
117
+ },
118
+ },
119
+ }
120
+
121
+ const mockProps = generateMockProps(componentInfo, undefined, {
122
+ eligibleRefElementPaths: new Set(['card.elementProps.cta']),
123
+ })
124
+
125
+ const cardProps = (mockProps.elementProps as Record<string, unknown>).card as Record<string, unknown>
126
+ const nestedElementProps = cardProps.elementProps as Record<string, unknown>
127
+ const ctaProps = nestedElementProps.cta as Record<string, unknown>
128
+
129
+ expect(parseRefElementMarker(ctaProps.id)).toBe('card.elementProps.cta')
130
+ })
131
+ })