@wix/zero-config-implementation 1.8.0 → 1.9.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.
@@ -1,7 +1,5 @@
1
- import { PropSpyMeta, TrackingStores } from '../types';
2
- import { PropSpyContext } from '../utils/prop-spy';
1
+ import { TrackingStores } from '../types';
3
2
  import { ReactExtractor } from './core/types';
4
- export type GetSpyMetadataFn = (id: string) => PropSpyMeta | null;
5
3
  export interface PropTrackerData {
6
4
  tag: string;
7
5
  role?: string;
@@ -10,13 +8,12 @@ export interface PropTrackerData {
10
8
  }
11
9
  export interface PropTrackerExtractorState {
12
10
  stores: TrackingStores;
13
- spyContext: PropSpyContext;
14
11
  }
15
12
  /**
16
13
  * Creates a prop tracker extractor that:
17
- * 1. Wraps props with spy proxies during beforeRender
14
+ * 1. Generates spy-instrumented mock props during beforeRender
18
15
  * 2. Detects spy markers in element props during onCreateElement
19
- * 3. Writes tracking data to the store namespaced by 'prop-tracker'
16
+ * 3. propUsages tracking data to the store namespaced by 'prop-tracker'
20
17
  */
21
18
  export declare function createPropTrackerExtractor(): {
22
19
  extractor: ReactExtractor;
@@ -5,5 +5,4 @@
5
5
  */
6
6
  export { ExtractorStore, runExtractors, buildElementTree, createPropTrackerExtractor, createCssPropertiesExtractor, } from './extractors';
7
7
  export type { ExtractionResult, RunExtractorsOptions, ExtractedElement, ReactExtractor, RenderContext, CreateElementEvent, RenderCompleteEvent, PropTrackerData, PropTrackerExtractorState, CssPropertiesData, } from './extractors';
8
- export type { CoupledComponentInfo, CoupledProp, TrackingStores, DOMBinding, PropReadInfo, PropWriteInfo, PropSpyMeta, } from './types';
9
- export type { PropSpyContext } from './utils/prop-spy';
8
+ export type { CoupledComponentInfo, CoupledProp, TrackingStores, DOMBinding, PropWriteInfo, PropSpyMeta, } from './types';
@@ -1,24 +1,10 @@
1
1
  import { PropInfo } from '../ts/types';
2
2
  import { ExtractedElement } from './extractors/core/tree-builder';
3
- export declare const PROP_SPY_SYMBOL: unique symbol;
4
3
  export interface PropSpyMeta {
5
4
  path: string;
6
5
  propName: string;
7
- uniqueId: string;
8
6
  originalValue: unknown;
9
7
  }
10
- export interface PropSpy<T = unknown> {
11
- [PROP_SPY_SYMBOL]: true;
12
- __meta: PropSpyMeta;
13
- valueOf: () => T;
14
- toString: () => string;
15
- toJSON: () => T;
16
- [Symbol.toPrimitive]?: (hint: string) => T | string | number;
17
- }
18
- export interface PropReadInfo {
19
- components: Set<string>;
20
- value: unknown;
21
- }
22
8
  export interface PropWriteInfo {
23
9
  elements: Map<string, {
24
10
  tag: string;
@@ -30,8 +16,7 @@ export interface PropWriteInfo {
30
16
  }>;
31
17
  }
32
18
  export interface TrackingStores {
33
- reads: Map<string, PropReadInfo>;
34
- writes: Map<string, PropWriteInfo>;
19
+ propUsages: Map<string, PropWriteInfo>;
35
20
  }
36
21
  export interface DOMBinding {
37
22
  element: string;
@@ -40,7 +25,8 @@ export interface DOMBinding {
40
25
  elementId: string;
41
26
  }
42
27
  export interface CoupledProp extends PropInfo {
43
- bindings: DOMBinding[];
28
+ /** Full stores.propUsages key for this prop, e.g. "props.linkUrl". */
29
+ propPath: string;
44
30
  logicOnly: boolean;
45
31
  }
46
32
  export interface CoupledComponentInfo {
@@ -48,4 +34,5 @@ export interface CoupledComponentInfo {
48
34
  props: Record<string, CoupledProp>;
49
35
  elements: ExtractedElement[];
50
36
  innerElementProps?: Map<string, Record<string, CoupledProp>>;
37
+ propUsages: TrackingStores['propUsages'];
51
38
  }
@@ -4,6 +4,17 @@ import { ComponentInfo } from '../../ts/types';
4
4
  */
5
5
  export declare function resetMockCounter(): void;
6
6
  /**
7
- * Generate mock props object from ComponentInfo
7
+ * Narrow interface for registering spy markers during mock generation.
8
+ * Satisfied structurally by PropSpyContext.
8
9
  */
9
- export declare function generateMockProps(componentInfo: ComponentInfo): Record<string, unknown>;
10
+ export interface PropSpyRegistrar {
11
+ registerString(path: string, propName: string, value: string): void;
12
+ registerNumber(path: string, propName: string, value: number): void;
13
+ registerFunction(path: string, propName: string, value: (...args: unknown[]) => unknown): void;
14
+ }
15
+ /**
16
+ * Generate mock props object from ComponentInfo.
17
+ * When a registrar is provided, string and number values are spy-instrumented
18
+ * for DOM binding detection.
19
+ */
20
+ export declare function generateMockProps(componentInfo: ComponentInfo, registrar?: PropSpyRegistrar): Record<string, unknown>;
package/package.json CHANGED
@@ -4,7 +4,7 @@
4
4
  "registry": "https://registry.npmjs.org/",
5
5
  "access": "public"
6
6
  },
7
- "version": "1.8.0",
7
+ "version": "1.9.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",
@@ -74,5 +74,5 @@
74
74
  ]
75
75
  }
76
76
  },
77
- "falconPackageHash": "8ca973dc7cc82c6e8d0a333de6717b866e33337ee211bd335f3fa800"
77
+ "falconPackageHash": "ce9c2a83ea75ac5b0a6b67e4fd624304cdad011d081dfdf2d8c72b4b"
78
78
  }
@@ -15,6 +15,7 @@
15
15
  import type { Result } from 'neverthrow'
16
16
  import { err, ok } from 'neverthrow'
17
17
  import { ParseError } from '../errors'
18
+ import type { DOMBinding, TrackingStores } from '../information-extractors/react'
18
19
  import type { PropInfo, ResolvedType } from '../information-extractors/ts/types'
19
20
  import type { DataItem } from '../schema'
20
21
  import { DATA, MEDIA } from '../schema'
@@ -24,15 +25,42 @@ const { DATA_TYPE, WIX_TYPE_TO_DATA_TYPE } = DATA
24
25
 
25
26
  type ParseErrorInstance = InstanceType<typeof ParseError>
26
27
 
28
+ function getBindingsForPath(propUsages: TrackingStores['propUsages'], path: string): DOMBinding[] {
29
+ const writeInfo = propUsages.get(path)
30
+ if (!writeInfo) return []
31
+
32
+ const result: DOMBinding[] = []
33
+ for (const [key, attrInfo] of writeInfo.attributes) {
34
+ const elementId = key.split(':')[0]
35
+ const elementInfo = writeInfo.elements.get(elementId)
36
+ if (elementInfo) {
37
+ result.push({
38
+ element: elementInfo.tag,
39
+ attribute: attrInfo.attr,
40
+ concatenated: attrInfo.concatenated,
41
+ elementId: elementInfo.elementId,
42
+ })
43
+ }
44
+ }
45
+ return result
46
+ }
47
+
27
48
  /**
28
49
  * Converts a single PropInfo to a DataItem for the Wix Editor component schema.
29
50
  *
30
51
  * @param propInfo - The resolved TypeScript prop information to convert.
31
52
  * @param defaultValue - Optional default value for the data item.
53
+ * @param propUsages - The full stores.propUsages map for on-demand binding lookups.
54
+ * @param propPath - The full stores.propUsages key for this prop, e.g. "props.linkUrl".
32
55
  * @returns `Ok<DataItem>` on success, `Err<ParseError>` if an invariant is violated
33
56
  * (e.g. an array type missing its element type).
34
57
  */
35
- export function buildDataItem(propInfo: PropInfo, defaultValue?: unknown): Result<DataItem, ParseErrorInstance> {
58
+ export function buildDataItem(
59
+ propInfo: PropInfo,
60
+ defaultValue?: unknown,
61
+ propUsages?: TrackingStores['propUsages'],
62
+ propPath?: string,
63
+ ): Result<DataItem, ParseErrorInstance> {
36
64
  const dataItem: DataItem = {
37
65
  displayName: formatDisplayName(propInfo.name),
38
66
  }
@@ -41,7 +69,7 @@ export function buildDataItem(propInfo: PropInfo, defaultValue?: unknown): Resul
41
69
  dataItem.defaultValue = defaultValue
42
70
  }
43
71
 
44
- const result = applyResolvedTypeToDataItem(dataItem, propInfo.resolvedType, propInfo)
72
+ const result = applyResolvedTypeToDataItem(dataItem, propInfo.resolvedType, propInfo, propUsages, propPath)
45
73
  if (result.isErr()) {
46
74
  return err(result.error)
47
75
  }
@@ -59,10 +87,14 @@ function applyResolvedTypeToDataItem(
59
87
  dataItem: DataItem,
60
88
  resolvedType: ResolvedType,
61
89
  propInfo: PropInfo,
90
+ propUsages?: TrackingStores['propUsages'],
91
+ propPath?: string,
62
92
  ): Result<void, ParseErrorInstance> {
93
+ const bindings = propUsages && propPath ? getBindingsForPath(propUsages, propPath) : undefined
94
+
63
95
  switch (resolvedType.kind) {
64
96
  case 'primitive':
65
- handlePrimitiveType(dataItem, resolvedType, propInfo)
97
+ handlePrimitiveType(dataItem, resolvedType, propInfo, bindings)
66
98
  return ok(undefined)
67
99
 
68
100
  case 'literal':
@@ -74,16 +106,16 @@ function applyResolvedTypeToDataItem(
74
106
  return ok(undefined)
75
107
 
76
108
  case 'array':
77
- return handleArrayType(dataItem, resolvedType)
109
+ return handleArrayType(dataItem, resolvedType, propUsages, propPath)
78
110
 
79
111
  case 'object':
80
- return handleObjectType(dataItem, resolvedType)
112
+ return handleObjectType(dataItem, resolvedType, propUsages, propPath)
81
113
 
82
114
  case 'union':
83
115
  return handleUnionType(dataItem, resolvedType, propInfo)
84
116
 
85
117
  case 'function':
86
- handleFunctionType(dataItem, propInfo)
118
+ handleFunctionType(dataItem, propInfo, bindings)
87
119
  return ok(undefined)
88
120
 
89
121
  case 'semantic':
@@ -100,14 +132,34 @@ function applyResolvedTypeToDataItem(
100
132
 
101
133
  /**
102
134
  * Handles primitive types (string, number, boolean).
135
+ * When bindings are provided, uses bound DOM attributes to infer more specific types:
136
+ * - string bound to `dir` → direction
137
+ * - string bound to `href` on an anchor element (non-concatenated) → webUrl
138
+ * - string bound to `id` → guid
139
+ * - string bound to `pattern` → regex
103
140
  * Unrecognized primitives silently fall back to text.
104
141
  */
105
- function handlePrimitiveType(dataItem: DataItem, resolvedType: ResolvedType, propInfo: PropInfo): void {
142
+ function handlePrimitiveType(
143
+ dataItem: DataItem,
144
+ resolvedType: ResolvedType,
145
+ propInfo: PropInfo,
146
+ bindings?: DOMBinding[],
147
+ ): void {
106
148
  const typeValue = (resolvedType.value as string | undefined)?.toLowerCase() || propInfo.type.toLowerCase()
107
149
 
108
150
  if (typeValue.includes('string')) {
109
- dataItem.dataType = DATA_TYPE.text
110
- dataItem.text = {}
151
+ if (bindings?.some((b) => b.attribute === 'dir')) {
152
+ dataItem.dataType = DATA_TYPE.direction
153
+ } else if (bindings?.some((b) => b.attribute === 'href' && b.element === 'a' && !b.concatenated)) {
154
+ dataItem.dataType = DATA_TYPE.webUrl
155
+ } else if (bindings?.some((b) => b.attribute === 'id')) {
156
+ dataItem.dataType = DATA_TYPE.guid
157
+ } else if (bindings?.some((b) => b.attribute === 'pattern')) {
158
+ dataItem.dataType = DATA_TYPE.regex
159
+ } else {
160
+ dataItem.dataType = DATA_TYPE.text
161
+ dataItem.text = {}
162
+ }
111
163
  } else if (typeValue.includes('number')) {
112
164
  dataItem.dataType = DATA_TYPE.number
113
165
  dataItem.number = {}
@@ -141,8 +193,15 @@ function handleEnumType(dataItem: DataItem): void {
141
193
  /**
142
194
  * Handles array types. Fails with a `ParseError` if the resolved type
143
195
  * has kind 'array' but is missing its `elementType` (invariant violation).
196
+ * propPath is forwarded unchanged — getBindingsForPath strips numeric indices
197
+ * so `props.items.url` matches propUsages like `props.items[0].url`.
144
198
  */
145
- function handleArrayType(dataItem: DataItem, resolvedType: ResolvedType): Result<void, ParseErrorInstance> {
199
+ function handleArrayType(
200
+ dataItem: DataItem,
201
+ resolvedType: ResolvedType,
202
+ propUsages?: TrackingStores['propUsages'],
203
+ propPath?: string,
204
+ ): Result<void, ParseErrorInstance> {
146
205
  if (resolvedType.kind !== 'array' || resolvedType.elementType === undefined) {
147
206
  return err(
148
207
  new ParseError('Invalid array type: resolved type has kind "array" but is missing elementType', {
@@ -153,12 +212,18 @@ function handleArrayType(dataItem: DataItem, resolvedType: ResolvedType): Result
153
212
 
154
213
  dataItem.dataType = DATA_TYPE.arrayItems
155
214
  const elementDataItem: DataItem = {}
156
- const result = applyResolvedTypeToDataItem(elementDataItem, resolvedType.elementType, {
157
- name: 'element',
158
- required: false,
159
- type: resolvedType.elementType.kind,
160
- resolvedType: resolvedType.elementType,
161
- } satisfies PropInfo)
215
+ const result = applyResolvedTypeToDataItem(
216
+ elementDataItem,
217
+ resolvedType.elementType,
218
+ {
219
+ name: 'element',
220
+ required: false,
221
+ type: resolvedType.elementType.kind,
222
+ resolvedType: resolvedType.elementType,
223
+ } satisfies PropInfo,
224
+ propUsages,
225
+ propPath,
226
+ )
162
227
 
163
228
  if (result.isErr()) {
164
229
  return result
@@ -173,16 +238,23 @@ function handleArrayType(dataItem: DataItem, resolvedType: ResolvedType): Result
173
238
 
174
239
  /**
175
240
  * Handles object types with nested properties.
176
- * Recursively calls `buildDataItem` for each property.
241
+ * Recursively calls `buildDataItem` for each property, appending the property name
242
+ * to propPath so nested bindings can be looked up on-demand.
177
243
  */
178
- function handleObjectType(dataItem: DataItem, resolvedType: ResolvedType): Result<void, ParseErrorInstance> {
244
+ function handleObjectType(
245
+ dataItem: DataItem,
246
+ resolvedType: ResolvedType,
247
+ propUsages?: TrackingStores['propUsages'],
248
+ propPath?: string,
249
+ ): Result<void, ParseErrorInstance> {
179
250
  dataItem.dataType = DATA_TYPE.data
180
251
 
181
252
  if (resolvedType.properties) {
182
253
  const nestedItems: Record<string, DataItem> = {}
183
254
 
184
255
  for (const [propName, propInfo] of Object.entries(resolvedType.properties)) {
185
- const result = buildDataItem(propInfo, undefined)
256
+ const childPath = propPath ? `${propPath}.${propName}` : propName
257
+ const result = buildDataItem(propInfo, undefined, propUsages, childPath)
186
258
  if (result.isErr()) {
187
259
  return err(result.error)
188
260
  }
@@ -297,26 +369,39 @@ function handleSemanticType(dataItem: DataItem, resolvedType: ResolvedType): voi
297
369
  dataItem.text = {}
298
370
  }
299
371
 
372
+ const EVENT_HANDLER_ATTR_TO_DATA_TYPE: Record<string, string> = {
373
+ onclick: DATA_TYPE.onClick,
374
+ onchange: DATA_TYPE.onChange,
375
+ onkeypress: DATA_TYPE.onKeyPress,
376
+ onkeyup: DATA_TYPE.onKeyUp,
377
+ onsubmit: DATA_TYPE.onSubmit,
378
+ }
379
+
300
380
  /**
301
381
  * Handles function types — maps to event handlers.
382
+ * Checks bound DOM attributes first (e.g. `onChange` → onChange), then falls back to prop name.
302
383
  */
303
- function handleFunctionType(dataItem: DataItem, propInfo: PropInfo): void {
304
- const propName = propInfo.name.toLowerCase()
305
-
306
- if (propName === 'onclick') {
307
- dataItem.dataType = DATA_TYPE.onClick
308
- } else if (propName === 'onchange') {
309
- dataItem.dataType = DATA_TYPE.onChange
310
- } else if (propName === 'onkeypress') {
311
- dataItem.dataType = DATA_TYPE.onKeyPress
312
- } else if (propName === 'onkeyup') {
313
- dataItem.dataType = DATA_TYPE.onKeyUp
314
- } else if (propName === 'onsubmit') {
315
- dataItem.dataType = DATA_TYPE.onSubmit
316
- } else {
317
- dataItem.dataType = DATA_TYPE.function
318
- dataItem.function = {}
384
+ function handleFunctionType(dataItem: DataItem, propInfo: PropInfo, bindings?: DOMBinding[]): void {
385
+ // Check bound attributes first — they reflect actual DOM wiring regardless of prop name
386
+ if (bindings) {
387
+ for (const binding of bindings) {
388
+ const dataType = EVENT_HANDLER_ATTR_TO_DATA_TYPE[binding.attribute.toLowerCase()]
389
+ if (dataType) {
390
+ dataItem.dataType = dataType
391
+ return
392
+ }
393
+ }
319
394
  }
395
+
396
+ // Fall back to prop name matching
397
+ const dataType = EVENT_HANDLER_ATTR_TO_DATA_TYPE[propInfo.name.toLowerCase()]
398
+ if (dataType) {
399
+ dataItem.dataType = dataType
400
+ return
401
+ }
402
+
403
+ dataItem.dataType = DATA_TYPE.function
404
+ dataItem.function = {}
320
405
  }
321
406
 
322
407
  /**
@@ -5,6 +5,7 @@ import type {
5
5
  CoupledProp,
6
6
  CssPropertiesData,
7
7
  ExtractedElement,
8
+ TrackingStores,
8
9
  } from '../information-extractors/react'
9
10
  import type {
10
11
  CssCustomPropertyItem,
@@ -34,8 +35,8 @@ function buildEditorElement(component: ComponentInfoWithCss): EditorElement {
34
35
  return {
35
36
  selector: buildSelector(rootElement),
36
37
  displayName: formatDisplayName(component.componentName),
37
- data: buildData(component.props),
38
- elements: buildElements(childElements, component.innerElementProps),
38
+ data: buildData(component.props, component.propUsages),
39
+ elements: buildElements(childElements, component.innerElementProps, component.propUsages),
39
40
  cssProperties: buildCssProperties(rootElement),
40
41
  cssCustomProperties: buildCssCustomPropertiesForElement(rootElement),
41
42
  }
@@ -48,7 +49,10 @@ function buildSelector(rootElement?: ExtractedElement): string {
48
49
  return rootElement?.tag ?? ''
49
50
  }
50
51
 
51
- function buildData(props: Record<string, CoupledProp>): Record<string, DataItem> {
52
+ function buildData(
53
+ props: Record<string, CoupledProp>,
54
+ propUsages: TrackingStores['propUsages'],
55
+ ): Record<string, DataItem> {
52
56
  const data: Record<string, DataItem> = {}
53
57
 
54
58
  for (const [name, prop] of Object.entries(props)) {
@@ -58,7 +62,7 @@ function buildData(props: Record<string, CoupledProp>): Record<string, DataItem>
58
62
  // Extract default value for buildDataItem
59
63
  const defaultValue = prop.defaultValue?.kind !== 'unresolved' ? prop.defaultValue?.value : undefined
60
64
 
61
- const result = buildDataItem(prop, defaultValue)
65
+ const result = buildDataItem(prop, defaultValue, propUsages, prop.propPath)
62
66
  if (result.isOk()) {
63
67
  data[name] = result.value
64
68
  }
@@ -70,12 +74,13 @@ function buildData(props: Record<string, CoupledProp>): Record<string, DataItem>
70
74
  function buildElements(
71
75
  elements: ExtractedElement[],
72
76
  innerElementProps?: CoupledComponentInfo['innerElementProps'],
77
+ propUsages?: TrackingStores['propUsages'],
73
78
  ): Record<string, ElementItem> {
74
79
  const result: Record<string, ElementItem> = {}
75
80
 
76
81
  for (const el of elements) {
77
82
  const elementData = innerElementProps?.get(el.traceId)
78
- const data = elementData ? buildData(elementData) : undefined
83
+ const data = elementData && propUsages ? buildData(elementData, propUsages) : undefined
79
84
  const cssProps = buildCssProperties(el)
80
85
  const cssCustomProps = buildCssCustomPropertiesForElement(el)
81
86
 
@@ -91,7 +96,7 @@ function buildElements(
91
96
  // CSS custom properties from matched rules for this element
92
97
  ...(Object.keys(cssCustomProps).length > 0 && { cssCustomProperties: cssCustomProps }),
93
98
  // Recursively build nested elements
94
- elements: el.children.length > 0 ? buildElements(el.children, innerElementProps) : undefined,
99
+ elements: el.children.length > 0 ? buildElements(el.children, innerElementProps, propUsages) : undefined,
95
100
  },
96
101
  }
97
102
  }
package/src/index.ts CHANGED
@@ -193,14 +193,12 @@ export type {
193
193
  PropTrackerData,
194
194
  PropTrackerExtractorState,
195
195
  CssPropertiesData,
196
- PropSpyContext,
197
196
  } from './information-extractors/react'
198
197
  export type {
199
198
  CoupledComponentInfo,
200
199
  CoupledProp,
201
200
  DOMBinding,
202
201
  TrackingStores,
203
- PropReadInfo,
204
202
  PropWriteInfo,
205
203
  PropSpyMeta,
206
204
  } from './information-extractors/react'
@@ -12,7 +12,6 @@
12
12
  import type { ComponentType } from 'react'
13
13
  import { type CreateElementListener, renderWithExtractors } from '../../../../component-renderer'
14
14
  import type { ComponentInfo } from '../../../ts/types'
15
- import { generateMockProps, resetMockCounter } from '../../utils/mock-generator'
16
15
  import { ExtractorStore } from './store'
17
16
  import { type ExtractedElement, buildElementTree } from './tree-builder'
18
17
  import type { CreateElementEvent, ReactExtractor, RenderContext } from './types'
@@ -50,17 +49,14 @@ export function runExtractors(
50
49
  extractors: ReactExtractor[],
51
50
  options?: RunExtractorsOptions,
52
51
  ): ExtractionResult {
53
- // Reset mock counter for reproducible results
54
- resetMockCounter()
55
-
56
52
  // Create shared store
57
53
  const store = new ExtractorStore()
58
54
 
59
- // Create render context with mutable props
55
+ // Create render context; extractors populate props in onBeforeRender
60
56
  const context: RenderContext = {
61
57
  componentInfo,
62
58
  component,
63
- props: generateMockProps(componentInfo),
59
+ props: {},
64
60
  store,
65
61
  }
66
62
 
@@ -146,7 +146,9 @@ function getTextContent(element: Element): string {
146
146
  */
147
147
  function getElementNamePart(element: Element, getElementById: (id: string) => Element | undefined): string {
148
148
  const id = getAttribute(element, 'id')
149
- if (id) {
149
+ // Skip spy-instrumented ids (mock_propName_XXXXXX) — they are runtime mock values,
150
+ // not semantic identifiers, and would produce garbage element names.
151
+ if (id && !id.startsWith('mock_')) {
150
152
  return pascalCase(id)
151
153
  }
152
154
 
@@ -8,17 +8,13 @@
8
8
  import type { HTMLAttributes } from 'react'
9
9
  import { TRACE_ATTR } from '../../../component-renderer'
10
10
  import type { PropSpyMeta, TrackingStores } from '../types'
11
- import { type PropSpyContext, createPropSpyContext } from '../utils/prop-spy'
11
+ import { type PropSpyRegistrar, generateMockProps, resetMockCounter } from '../utils/mock-generator'
12
12
  import type { CreateElementEvent, ReactExtractor, RenderContext } from './core/types'
13
13
 
14
14
  // ─────────────────────────────────────────────────────────────────────────────
15
15
  // Types
16
16
  // ─────────────────────────────────────────────────────────────────────────────
17
17
 
18
- const SPY_REGEX = /__spy_\d+__/g
19
-
20
- export type GetSpyMetadataFn = (id: string) => PropSpyMeta | null
21
-
22
18
  export interface PropTrackerData {
23
19
  tag: string
24
20
  role?: string
@@ -28,7 +24,6 @@ export interface PropTrackerData {
28
24
 
29
25
  export interface PropTrackerExtractorState {
30
26
  stores: TrackingStores
31
- spyContext: PropSpyContext
32
27
  }
33
28
 
34
29
  // ─────────────────────────────────────────────────────────────────────────────
@@ -37,28 +32,54 @@ export interface PropTrackerExtractorState {
37
32
 
38
33
  /**
39
34
  * Creates a prop tracker extractor that:
40
- * 1. Wraps props with spy proxies during beforeRender
35
+ * 1. Generates spy-instrumented mock props during beforeRender
41
36
  * 2. Detects spy markers in element props during onCreateElement
42
- * 3. Writes tracking data to the store namespaced by 'prop-tracker'
37
+ * 3. propUsages tracking data to the store namespaced by 'prop-tracker'
43
38
  */
44
39
  export function createPropTrackerExtractor(): {
45
40
  extractor: ReactExtractor
46
41
  state: PropTrackerExtractorState
47
42
  } {
48
- const spyContext = createPropSpyContext()
43
+ const stringMeta = new Map<string, PropSpyMeta>()
44
+ const numberMeta = new Map<number, PropSpyMeta>()
45
+ const fnMeta = new WeakMap<(...args: unknown[]) => unknown, PropSpyMeta>()
49
46
  const stores: TrackingStores = {
50
- reads: new Map(),
51
- writes: new Map(),
47
+ propUsages: new Map(),
48
+ }
49
+
50
+ const registrar: PropSpyRegistrar = {
51
+ registerString(path, propName, value) {
52
+ stringMeta.set(value, { path, propName, originalValue: value })
53
+ },
54
+ registerNumber(path, propName, value) {
55
+ numberMeta.set(value, { path, propName, originalValue: value })
56
+ },
57
+ registerFunction(path, propName, value) {
58
+ fnMeta.set(value, { path, propName, originalValue: value })
59
+ },
52
60
  }
53
61
 
54
- const extractSpies = (value: unknown, seen = new WeakSet<object>()): { propName: string; path: string }[] => {
62
+ const extractSpies = (
63
+ value: unknown,
64
+ seen = new WeakSet<object>(),
65
+ ): { propName: string; path: string; embedded: boolean }[] => {
55
66
  if (typeof value === 'string') {
56
- return (value.match(SPY_REGEX) ?? [])
57
- .map((id) => {
58
- const meta = spyContext.getSpyMetadataByUniqueId(id)
59
- return meta ? { propName: meta.propName, path: meta.path } : null
60
- })
61
- .filter(Boolean) as { propName: string; path: string }[]
67
+ const results: { propName: string; path: string; embedded: boolean }[] = []
68
+ for (const [key, meta] of stringMeta) {
69
+ if (value.includes(key)) {
70
+ // embedded = spy marker was part of a longer string (e.g. `mailto:${email}`)
71
+ results.push({ propName: meta.propName, path: meta.path, embedded: value !== key })
72
+ }
73
+ }
74
+ return results
75
+ }
76
+ if (typeof value === 'number') {
77
+ const meta = numberMeta.get(value)
78
+ return meta ? [{ propName: meta.propName, path: meta.path, embedded: false }] : []
79
+ }
80
+ if (typeof value === 'function') {
81
+ const meta = fnMeta.get(value as (...args: unknown[]) => unknown)
82
+ return meta ? [{ propName: meta.propName, path: meta.path, embedded: false }] : []
62
83
  }
63
84
  if (value && typeof value === 'object') {
64
85
  // Prevent infinite recursion from circular references
@@ -79,8 +100,8 @@ export function createPropTrackerExtractor(): {
79
100
  name: 'prop-tracker',
80
101
 
81
102
  onBeforeRender(context: RenderContext): void {
82
- // Wrap props with spy proxies to track access
83
- context.props = spyContext.createAuditedProps(context.props, stores, () => context.componentInfo.componentName)
103
+ resetMockCounter()
104
+ context.props = generateMockProps(context.componentInfo, registrar)
84
105
  },
85
106
 
86
107
  onCreateElement(event: CreateElementEvent): void {
@@ -97,18 +118,18 @@ export function createPropTrackerExtractor(): {
97
118
 
98
119
  spies.forEach((spy) => {
99
120
  boundProps.add(spy.propName)
100
- const path = spy.path
101
- const entry = stores.writes.get(path) ?? { elements: new Map(), attributes: new Map() }
102
-
103
- if (!stores.writes.has(path)) {
104
- stores.writes.set(path, entry)
121
+ const path = spy.path.replace(/\[\d+\]/g, '')
122
+ if (!stores.propUsages.has(path)) {
123
+ stores.propUsages.set(path, { elements: new Map(), attributes: new Map() })
105
124
  }
125
+ const entry = stores.propUsages.get(path)!
106
126
 
107
127
  entry.elements.set(traceId, { tag, elementId: traceId })
108
- entry.attributes.set(`${traceId}:${key}`, { attr: key, concatenated: spies.length > 1 })
128
+ const isConcat = spies.length > 1 || spy.embedded
129
+ entry.attributes.set(`${traceId}:${key}`, { attr: key, concatenated: isConcat })
109
130
 
110
131
  // Track concatenated attributes
111
- if (spies.length > 1) {
132
+ if (isConcat) {
112
133
  concatenatedAttrs.set(key, spy.propName)
113
134
  }
114
135
  })
@@ -127,6 +148,6 @@ export function createPropTrackerExtractor(): {
127
148
 
128
149
  return {
129
150
  extractor,
130
- state: { stores, spyContext },
151
+ state: { stores },
131
152
  }
132
153
  }
@@ -42,13 +42,6 @@ export type {
42
42
  CoupledProp,
43
43
  TrackingStores,
44
44
  DOMBinding,
45
- PropReadInfo,
46
45
  PropWriteInfo,
47
46
  PropSpyMeta,
48
47
  } from './types'
49
-
50
- // ─────────────────────────────────────────────────────────────────────────────
51
- // Utility Exports
52
- // ─────────────────────────────────────────────────────────────────────────────
53
-
54
- export type { PropSpyContext } from './utils/prop-spy'