@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.
- package/dist/converters/data-item-builder.d.ts +4 -1
- package/dist/index.d.ts +2 -2
- package/dist/index.js +13316 -13291
- package/dist/information-extractors/react/extractors/prop-tracker.d.ts +3 -6
- package/dist/information-extractors/react/index.d.ts +1 -2
- package/dist/information-extractors/react/types.d.ts +4 -17
- package/dist/information-extractors/react/utils/mock-generator.d.ts +13 -2
- package/package.json +2 -2
- package/src/converters/data-item-builder.ts +120 -35
- package/src/converters/to-editor-component.ts +11 -6
- package/src/index.ts +0 -2
- package/src/information-extractors/react/extractors/core/runner.ts +2 -6
- package/src/information-extractors/react/extractors/core/tree-builder.ts +3 -1
- package/src/information-extractors/react/extractors/prop-tracker.ts +49 -28
- package/src/information-extractors/react/index.ts +0 -7
- package/src/information-extractors/react/types.ts +4 -20
- package/src/information-extractors/react/utils/mock-generator.ts +99 -31
- package/src/manifest-pipeline.ts +15 -11
- package/dist/information-extractors/react/utils/prop-spy.d.ts +0 -10
- package/src/information-extractors/react/utils/prop-spy.ts +0 -168
|
@@ -1,7 +1,5 @@
|
|
|
1
|
-
import {
|
|
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.
|
|
14
|
+
* 1. Generates spy-instrumented mock props during beforeRender
|
|
18
15
|
* 2. Detects spy markers in element props during onCreateElement
|
|
19
|
-
* 3.
|
|
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,
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
7
|
+
* Narrow interface for registering spy markers during mock generation.
|
|
8
|
+
* Satisfied structurally by PropSpyContext.
|
|
8
9
|
*/
|
|
9
|
-
export
|
|
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.
|
|
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": "
|
|
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(
|
|
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(
|
|
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
|
-
|
|
110
|
-
|
|
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(
|
|
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(
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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(
|
|
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
|
|
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
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
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(
|
|
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
|
|
55
|
+
// Create render context; extractors populate props in onBeforeRender
|
|
60
56
|
const context: RenderContext = {
|
|
61
57
|
componentInfo,
|
|
62
58
|
component,
|
|
63
|
-
props:
|
|
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
|
-
|
|
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
|
|
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.
|
|
35
|
+
* 1. Generates spy-instrumented mock props during beforeRender
|
|
41
36
|
* 2. Detects spy markers in element props during onCreateElement
|
|
42
|
-
* 3.
|
|
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
|
|
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
|
-
|
|
51
|
-
|
|
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 = (
|
|
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
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
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
|
-
|
|
83
|
-
context.props =
|
|
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
|
-
|
|
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
|
-
|
|
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 (
|
|
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
|
|
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'
|