@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
|
@@ -9,41 +9,23 @@ import type { ExtractedElement } from './extractors/core/tree-builder'
|
|
|
9
9
|
// Prop Spy (for tracking prop access)
|
|
10
10
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
11
11
|
|
|
12
|
-
export const PROP_SPY_SYMBOL = Symbol.for('__prop_spy__')
|
|
13
|
-
|
|
14
12
|
export interface PropSpyMeta {
|
|
15
13
|
path: string
|
|
16
14
|
propName: string
|
|
17
|
-
uniqueId: string
|
|
18
15
|
originalValue: unknown
|
|
19
16
|
}
|
|
20
17
|
|
|
21
|
-
export interface PropSpy<T = unknown> {
|
|
22
|
-
[PROP_SPY_SYMBOL]: true
|
|
23
|
-
__meta: PropSpyMeta
|
|
24
|
-
valueOf: () => T
|
|
25
|
-
toString: () => string
|
|
26
|
-
toJSON: () => T
|
|
27
|
-
[Symbol.toPrimitive]?: (hint: string) => T | string | number
|
|
28
|
-
}
|
|
29
|
-
|
|
30
18
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
31
19
|
// Tracking Stores
|
|
32
20
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
33
21
|
|
|
34
|
-
export interface PropReadInfo {
|
|
35
|
-
components: Set<string>
|
|
36
|
-
value: unknown
|
|
37
|
-
}
|
|
38
|
-
|
|
39
22
|
export interface PropWriteInfo {
|
|
40
23
|
elements: Map<string, { tag: string; elementId: string }>
|
|
41
24
|
attributes: Map<string, { attr: string; concatenated: boolean }>
|
|
42
25
|
}
|
|
43
26
|
|
|
44
27
|
export interface TrackingStores {
|
|
45
|
-
|
|
46
|
-
writes: Map<string, PropWriteInfo>
|
|
28
|
+
propUsages: Map<string, PropWriteInfo>
|
|
47
29
|
}
|
|
48
30
|
|
|
49
31
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
@@ -58,7 +40,8 @@ export interface DOMBinding {
|
|
|
58
40
|
}
|
|
59
41
|
|
|
60
42
|
export interface CoupledProp extends PropInfo {
|
|
61
|
-
|
|
43
|
+
/** Full stores.propUsages key for this prop, e.g. "props.linkUrl". */
|
|
44
|
+
propPath: string
|
|
62
45
|
logicOnly: boolean
|
|
63
46
|
}
|
|
64
47
|
|
|
@@ -67,4 +50,5 @@ export interface CoupledComponentInfo {
|
|
|
67
50
|
props: Record<string, CoupledProp>
|
|
68
51
|
elements: ExtractedElement[]
|
|
69
52
|
innerElementProps?: Map<string, Record<string, CoupledProp>>
|
|
53
|
+
propUsages: TrackingStores['propUsages']
|
|
70
54
|
}
|
|
@@ -6,21 +6,49 @@
|
|
|
6
6
|
import { faker } from '@faker-js/faker'
|
|
7
7
|
import type { ComponentInfo, DefaultValue, PropInfo, ResolvedType } from '../../ts/types'
|
|
8
8
|
|
|
9
|
+
// Unique primes used as traceable number values. Each rendered number prop gets
|
|
10
|
+
// one; the value is distinct enough to be identified in DOM attributes later.
|
|
11
|
+
const TRACEABLE_PRIMES = [11, 13, 17, 19, 23, 29, 31, 37, 41, 43, 47, 53, 59, 61, 67, 71, 73, 79, 83, 89, 97]
|
|
12
|
+
const FALLBACK_NUMBER_BASE = 101
|
|
13
|
+
let primeIndex = 0
|
|
14
|
+
|
|
15
|
+
function nextTraceableNumber(): number {
|
|
16
|
+
const withinPrimeList = primeIndex < TRACEABLE_PRIMES.length
|
|
17
|
+
if (withinPrimeList) {
|
|
18
|
+
return TRACEABLE_PRIMES[primeIndex++]
|
|
19
|
+
}
|
|
20
|
+
const overflowOffset = primeIndex++ - TRACEABLE_PRIMES.length
|
|
21
|
+
return FALLBACK_NUMBER_BASE + overflowOffset
|
|
22
|
+
}
|
|
23
|
+
|
|
9
24
|
/**
|
|
10
25
|
* Reset faker's seed and internal state for reproducible results
|
|
11
26
|
*/
|
|
12
27
|
export function resetMockCounter(): void {
|
|
13
28
|
faker.seed(42)
|
|
29
|
+
primeIndex = 0
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Narrow interface for registering spy markers during mock generation.
|
|
34
|
+
* Satisfied structurally by PropSpyContext.
|
|
35
|
+
*/
|
|
36
|
+
export interface PropSpyRegistrar {
|
|
37
|
+
registerString(path: string, propName: string, value: string): void
|
|
38
|
+
registerNumber(path: string, propName: string, value: number): void
|
|
39
|
+
registerFunction(path: string, propName: string, value: (...args: unknown[]) => unknown): void
|
|
14
40
|
}
|
|
15
41
|
|
|
16
42
|
/**
|
|
17
|
-
* Generate mock props object from ComponentInfo
|
|
43
|
+
* Generate mock props object from ComponentInfo.
|
|
44
|
+
* When a registrar is provided, string and number values are spy-instrumented
|
|
45
|
+
* for DOM binding detection.
|
|
18
46
|
*/
|
|
19
|
-
export function generateMockProps(componentInfo: ComponentInfo): Record<string, unknown> {
|
|
47
|
+
export function generateMockProps(componentInfo: ComponentInfo, registrar?: PropSpyRegistrar): Record<string, unknown> {
|
|
20
48
|
const mockProps: Record<string, unknown> = {}
|
|
21
49
|
|
|
22
50
|
for (const [propName, propInfo] of Object.entries(componentInfo.props)) {
|
|
23
|
-
mockProps[propName] = generateMockValue(propInfo, propName)
|
|
51
|
+
mockProps[propName] = generateMockValue(propInfo, propName, `props.${propName}`, registrar)
|
|
24
52
|
}
|
|
25
53
|
|
|
26
54
|
return mockProps
|
|
@@ -29,13 +57,13 @@ export function generateMockProps(componentInfo: ComponentInfo): Record<string,
|
|
|
29
57
|
/**
|
|
30
58
|
* Generate a mock value based on PropInfo
|
|
31
59
|
*/
|
|
32
|
-
function generateMockValue(propInfo: PropInfo, propName: string): unknown {
|
|
33
|
-
//
|
|
34
|
-
if (propInfo.defaultValue !== undefined) {
|
|
60
|
+
function generateMockValue(propInfo: PropInfo, propName: string, path: string, registrar?: PropSpyRegistrar): unknown {
|
|
61
|
+
// In plain mode (no registrar), honour default values for realistic rendering
|
|
62
|
+
if (!registrar && propInfo.defaultValue !== undefined) {
|
|
35
63
|
return extractDefaultValueValue(propInfo.defaultValue)
|
|
36
64
|
}
|
|
37
65
|
|
|
38
|
-
return generateValueFromResolvedType(propInfo.resolvedType, propName)
|
|
66
|
+
return generateValueFromResolvedType(propInfo.resolvedType, propName, path, registrar)
|
|
39
67
|
}
|
|
40
68
|
|
|
41
69
|
/**
|
|
@@ -57,10 +85,15 @@ function extractDefaultValueValue(defaultValue: DefaultValue): unknown {
|
|
|
57
85
|
/**
|
|
58
86
|
* Generate a mock value from a ResolvedType
|
|
59
87
|
*/
|
|
60
|
-
function generateValueFromResolvedType(
|
|
88
|
+
function generateValueFromResolvedType(
|
|
89
|
+
resolvedType: ResolvedType,
|
|
90
|
+
propName: string,
|
|
91
|
+
path: string,
|
|
92
|
+
registrar?: PropSpyRegistrar,
|
|
93
|
+
): unknown {
|
|
61
94
|
const kind = resolvedType.kind
|
|
62
95
|
|
|
63
|
-
// Handle semantic types (from React or Wix packages)
|
|
96
|
+
// Handle semantic types (from React or Wix packages) — returned as plain objects
|
|
64
97
|
if (kind === 'semantic') {
|
|
65
98
|
return generateSemanticValue(resolvedType.value as string, propName)
|
|
66
99
|
}
|
|
@@ -68,28 +101,28 @@ function generateValueFromResolvedType(resolvedType: ResolvedType, propName: str
|
|
|
68
101
|
// Handle structural types
|
|
69
102
|
switch (kind) {
|
|
70
103
|
case 'primitive':
|
|
71
|
-
return generatePrimitiveValue(resolvedType.value as string, propName)
|
|
104
|
+
return generatePrimitiveValue(resolvedType.value as string, propName, path, registrar)
|
|
72
105
|
|
|
73
106
|
case 'literal':
|
|
74
107
|
return resolvedType.value
|
|
75
108
|
|
|
76
109
|
case 'union':
|
|
77
|
-
return generateUnionValue(resolvedType, propName)
|
|
110
|
+
return generateUnionValue(resolvedType, propName, path, registrar)
|
|
78
111
|
|
|
79
112
|
case 'intersection':
|
|
80
|
-
return generateIntersectionValue(resolvedType, propName)
|
|
113
|
+
return generateIntersectionValue(resolvedType, propName, path, registrar)
|
|
81
114
|
|
|
82
115
|
case 'array':
|
|
83
|
-
return generateArrayValue(resolvedType, propName)
|
|
116
|
+
return generateArrayValue(resolvedType, propName, path, registrar)
|
|
84
117
|
|
|
85
118
|
case 'object':
|
|
86
|
-
return generateObjectValue(resolvedType, propName)
|
|
119
|
+
return generateObjectValue(resolvedType, propName, path, registrar)
|
|
87
120
|
|
|
88
121
|
case 'enum':
|
|
89
122
|
return generateEnumValue(resolvedType)
|
|
90
123
|
|
|
91
124
|
case 'function':
|
|
92
|
-
return generateMockFunction(propName)
|
|
125
|
+
return generateMockFunction(propName, path, registrar)
|
|
93
126
|
|
|
94
127
|
default:
|
|
95
128
|
// Default to a string for unknown types
|
|
@@ -155,12 +188,23 @@ function generateSemanticValue(semanticType: string, propName: string): unknown
|
|
|
155
188
|
/**
|
|
156
189
|
* Generate a primitive value
|
|
157
190
|
*/
|
|
158
|
-
function generatePrimitiveValue(
|
|
191
|
+
function generatePrimitiveValue(
|
|
192
|
+
primitiveType: string,
|
|
193
|
+
propName: string,
|
|
194
|
+
path: string,
|
|
195
|
+
registrar?: PropSpyRegistrar,
|
|
196
|
+
): unknown {
|
|
159
197
|
switch (primitiveType) {
|
|
160
|
-
case 'string':
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
return
|
|
198
|
+
case 'string': {
|
|
199
|
+
const raw = `mock_${propName}_${faker.string.alphanumeric(6)}`
|
|
200
|
+
if (registrar) registrar.registerString(path, propName, raw)
|
|
201
|
+
return raw
|
|
202
|
+
}
|
|
203
|
+
case 'number': {
|
|
204
|
+
const val = nextTraceableNumber()
|
|
205
|
+
if (registrar) registrar.registerNumber(path, propName, val)
|
|
206
|
+
return val
|
|
207
|
+
}
|
|
164
208
|
case 'boolean':
|
|
165
209
|
return faker.datatype.boolean()
|
|
166
210
|
case 'null':
|
|
@@ -175,7 +219,12 @@ function generatePrimitiveValue(primitiveType: string, propName: string): unknow
|
|
|
175
219
|
/**
|
|
176
220
|
* Generate a value from a union type (pick the first non-null option)
|
|
177
221
|
*/
|
|
178
|
-
function generateUnionValue(
|
|
222
|
+
function generateUnionValue(
|
|
223
|
+
resolvedType: ResolvedType,
|
|
224
|
+
propName: string,
|
|
225
|
+
path: string,
|
|
226
|
+
registrar?: PropSpyRegistrar,
|
|
227
|
+
): unknown {
|
|
179
228
|
const types = resolvedType.types ?? []
|
|
180
229
|
|
|
181
230
|
// Prefer string literals for textEnum-like unions
|
|
@@ -186,7 +235,7 @@ function generateUnionValue(resolvedType: ResolvedType, propName: string): unkno
|
|
|
186
235
|
|
|
187
236
|
// Otherwise, use the first type
|
|
188
237
|
if (types.length > 0) {
|
|
189
|
-
return generateValueFromResolvedType(types[0], propName)
|
|
238
|
+
return generateValueFromResolvedType(types[0], propName, path, registrar)
|
|
190
239
|
}
|
|
191
240
|
|
|
192
241
|
return `mock_${propName}_${faker.string.alphanumeric(6)}`
|
|
@@ -195,14 +244,19 @@ function generateUnionValue(resolvedType: ResolvedType, propName: string): unkno
|
|
|
195
244
|
/**
|
|
196
245
|
* Generate a value from an intersection type (merge object properties)
|
|
197
246
|
*/
|
|
198
|
-
function generateIntersectionValue(
|
|
247
|
+
function generateIntersectionValue(
|
|
248
|
+
resolvedType: ResolvedType,
|
|
249
|
+
propName: string,
|
|
250
|
+
path: string,
|
|
251
|
+
registrar?: PropSpyRegistrar,
|
|
252
|
+
): unknown {
|
|
199
253
|
const types = resolvedType.types ?? []
|
|
200
254
|
const merged: Record<string, unknown> = {}
|
|
201
255
|
|
|
202
256
|
for (const type of types) {
|
|
203
257
|
if (type.kind === 'object' && type.properties) {
|
|
204
258
|
for (const [key, propInfo] of Object.entries(type.properties)) {
|
|
205
|
-
merged[key] = generateMockValue(propInfo, `${propName}.${key}
|
|
259
|
+
merged[key] = generateMockValue(propInfo, `${propName}.${key}`, `${path}.${key}`, registrar)
|
|
206
260
|
}
|
|
207
261
|
}
|
|
208
262
|
}
|
|
@@ -213,29 +267,38 @@ function generateIntersectionValue(resolvedType: ResolvedType, propName: string)
|
|
|
213
267
|
/**
|
|
214
268
|
* Generate an array value
|
|
215
269
|
*/
|
|
216
|
-
function generateArrayValue(
|
|
270
|
+
function generateArrayValue(
|
|
271
|
+
resolvedType: ResolvedType,
|
|
272
|
+
propName: string,
|
|
273
|
+
path: string,
|
|
274
|
+
registrar?: PropSpyRegistrar,
|
|
275
|
+
): unknown[] {
|
|
217
276
|
const elementType = resolvedType.elementType
|
|
218
277
|
|
|
219
278
|
if (!elementType) {
|
|
220
279
|
return [`mock_${propName}[0]_${faker.string.alphanumeric(6)}`]
|
|
221
280
|
}
|
|
222
281
|
|
|
223
|
-
// Generate 2-3 items for the array
|
|
224
282
|
return [
|
|
225
|
-
generateValueFromResolvedType(elementType, `${propName}[0]
|
|
226
|
-
generateValueFromResolvedType(elementType, `${propName}[1]
|
|
283
|
+
generateValueFromResolvedType(elementType, `${propName}[0]`, `${path}[0]`, registrar),
|
|
284
|
+
generateValueFromResolvedType(elementType, `${propName}[1]`, `${path}[1]`, registrar),
|
|
227
285
|
]
|
|
228
286
|
}
|
|
229
287
|
|
|
230
288
|
/**
|
|
231
289
|
* Generate an object value from properties
|
|
232
290
|
*/
|
|
233
|
-
function generateObjectValue(
|
|
291
|
+
function generateObjectValue(
|
|
292
|
+
resolvedType: ResolvedType,
|
|
293
|
+
propName: string,
|
|
294
|
+
path: string,
|
|
295
|
+
registrar?: PropSpyRegistrar,
|
|
296
|
+
): Record<string, unknown> {
|
|
234
297
|
const properties = resolvedType.properties ?? {}
|
|
235
298
|
const obj: Record<string, unknown> = {}
|
|
236
299
|
|
|
237
300
|
for (const [key, propInfo] of Object.entries(properties)) {
|
|
238
|
-
obj[key] = generateMockValue(propInfo, `${
|
|
301
|
+
obj[key] = generateMockValue(propInfo, key, `${path}.${key}`, registrar)
|
|
239
302
|
}
|
|
240
303
|
|
|
241
304
|
return obj
|
|
@@ -258,11 +321,16 @@ function generateEnumValue(resolvedType: ResolvedType): unknown {
|
|
|
258
321
|
/**
|
|
259
322
|
* Generate a mock function
|
|
260
323
|
*/
|
|
261
|
-
function generateMockFunction(
|
|
324
|
+
function generateMockFunction(
|
|
325
|
+
propName: string,
|
|
326
|
+
path: string,
|
|
327
|
+
registrar?: PropSpyRegistrar,
|
|
328
|
+
): (...args: unknown[]) => void {
|
|
262
329
|
const fn = function mockFn(): void {
|
|
263
330
|
// No-op mock function
|
|
264
331
|
}
|
|
265
332
|
Object.defineProperty(fn, 'name', { value: `mock_${propName}` })
|
|
333
|
+
if (registrar) registrar.registerFunction(path, propName, fn)
|
|
266
334
|
return fn
|
|
267
335
|
}
|
|
268
336
|
|
package/src/manifest-pipeline.ts
CHANGED
|
@@ -109,6 +109,7 @@ export function processComponent(
|
|
|
109
109
|
props: coupledProps,
|
|
110
110
|
elements: convertElements(extractedElements),
|
|
111
111
|
innerElementProps: innerElementProps.size > 0 ? innerElementProps : undefined,
|
|
112
|
+
propUsages: state.stores.propUsages,
|
|
112
113
|
}
|
|
113
114
|
} catch (error) {
|
|
114
115
|
warnings.push({
|
|
@@ -128,9 +129,13 @@ export function processComponent(
|
|
|
128
129
|
enhancedInfo = {
|
|
129
130
|
componentName: componentInfo.componentName,
|
|
130
131
|
props: Object.fromEntries(
|
|
131
|
-
Object.entries(componentInfo.props).map(([name, info]) => [
|
|
132
|
+
Object.entries(componentInfo.props).map(([name, info]) => [
|
|
133
|
+
name,
|
|
134
|
+
{ ...info, logicOnly: false, propPath: `props.${name}` },
|
|
135
|
+
]),
|
|
132
136
|
),
|
|
133
137
|
elements: [],
|
|
138
|
+
propUsages: new Map(),
|
|
134
139
|
}
|
|
135
140
|
}
|
|
136
141
|
|
|
@@ -175,14 +180,12 @@ function buildCoupledProps(
|
|
|
175
180
|
const result: Record<string, CoupledProp> = {}
|
|
176
181
|
|
|
177
182
|
for (const [name, info] of Object.entries(componentInfo.props)) {
|
|
178
|
-
const
|
|
179
|
-
const wasRead = stores.reads.has(path)
|
|
180
|
-
const writeInfo = stores.writes.get(path)
|
|
183
|
+
const topLevelWriteInfo = stores.propUsages.get(`props.${name}`)
|
|
181
184
|
|
|
182
185
|
result[name] = {
|
|
183
186
|
...info,
|
|
184
|
-
logicOnly:
|
|
185
|
-
|
|
187
|
+
logicOnly: !topLevelWriteInfo,
|
|
188
|
+
propPath: `props.${name}`,
|
|
186
189
|
}
|
|
187
190
|
}
|
|
188
191
|
|
|
@@ -194,7 +197,7 @@ function buildCoupledProps(
|
|
|
194
197
|
const ELEMENT_PROPS_PREFIX = 'props.elementProps.'
|
|
195
198
|
|
|
196
199
|
/**
|
|
197
|
-
* Processes stores.
|
|
200
|
+
* Processes stores.propUsages entries with paths starting with "props.elementProps."
|
|
198
201
|
* to extract inner element prop bindings grouped by elementId (traceId).
|
|
199
202
|
*/
|
|
200
203
|
function processElementPropsWrites(
|
|
@@ -206,7 +209,7 @@ function processElementPropsWrites(
|
|
|
206
209
|
const elementPropsInfo = componentInfo.props.elementProps
|
|
207
210
|
if (!elementPropsInfo) return result
|
|
208
211
|
|
|
209
|
-
for (const [path, writeInfo] of stores.
|
|
212
|
+
for (const [path, writeInfo] of stores.propUsages) {
|
|
210
213
|
if (!path.startsWith(ELEMENT_PROPS_PREFIX)) continue
|
|
211
214
|
|
|
212
215
|
// Resolve the PropInfo for this leaf prop by walking the type tree
|
|
@@ -230,10 +233,9 @@ function processElementPropsWrites(
|
|
|
230
233
|
propsForElement[leafName] = {
|
|
231
234
|
...propInfo,
|
|
232
235
|
logicOnly: false,
|
|
233
|
-
|
|
236
|
+
propPath: path,
|
|
234
237
|
}
|
|
235
238
|
}
|
|
236
|
-
propsForElement[leafName].bindings.push(binding)
|
|
237
239
|
}
|
|
238
240
|
}
|
|
239
241
|
|
|
@@ -268,7 +270,9 @@ function resolveInnerPropInfo(elementPropsPropInfo: PropInfo, relativePath: stri
|
|
|
268
270
|
return null
|
|
269
271
|
}
|
|
270
272
|
|
|
271
|
-
function extractBindings(
|
|
273
|
+
function extractBindings(
|
|
274
|
+
writeInfo: TrackingStores['propUsages'] extends Map<string, infer V> ? V : never,
|
|
275
|
+
): DOMBinding[] {
|
|
272
276
|
const bindings: DOMBinding[] = []
|
|
273
277
|
|
|
274
278
|
for (const [key, attrInfo] of writeInfo.attributes) {
|
|
@@ -1,10 +0,0 @@
|
|
|
1
|
-
import { PropSpyMeta, TrackingStores } from '../types';
|
|
2
|
-
export interface PropSpyContext {
|
|
3
|
-
createAuditedProps: <T extends object | null>(target: T, stores: TrackingStores, getComponent: () => string, basePath?: string) => T;
|
|
4
|
-
getSpyMetadataByUniqueId: (id: string) => PropSpyMeta | null;
|
|
5
|
-
}
|
|
6
|
-
/**
|
|
7
|
-
* Creates an encapsulated prop spy context.
|
|
8
|
-
* Each context has its own ID counter and metadata map, avoiding global state.
|
|
9
|
-
*/
|
|
10
|
-
export declare function createPropSpyContext(): PropSpyContext;
|
|
@@ -1,168 +0,0 @@
|
|
|
1
|
-
import type { PropSpy, PropSpyMeta, TrackingStores } from '../types'
|
|
2
|
-
import { PROP_SPY_SYMBOL } from '../types'
|
|
3
|
-
|
|
4
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
5
|
-
// Types
|
|
6
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
7
|
-
|
|
8
|
-
export interface PropSpyContext {
|
|
9
|
-
createAuditedProps: <T extends object | null>(
|
|
10
|
-
target: T,
|
|
11
|
-
stores: TrackingStores,
|
|
12
|
-
getComponent: () => string,
|
|
13
|
-
basePath?: string,
|
|
14
|
-
) => T
|
|
15
|
-
getSpyMetadataByUniqueId: (id: string) => PropSpyMeta | null
|
|
16
|
-
}
|
|
17
|
-
|
|
18
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
19
|
-
// Factory
|
|
20
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
21
|
-
|
|
22
|
-
/**
|
|
23
|
-
* Creates an encapsulated prop spy context.
|
|
24
|
-
* Each context has its own ID counter and metadata map, avoiding global state.
|
|
25
|
-
*/
|
|
26
|
-
export function createPropSpyContext(): PropSpyContext {
|
|
27
|
-
let idCounter = 0
|
|
28
|
-
const metadataMap = new Map<string, PropSpyMeta>()
|
|
29
|
-
|
|
30
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
31
|
-
// Helpers
|
|
32
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
33
|
-
|
|
34
|
-
const isPropSpy = (val: unknown): val is PropSpy => !!(val && typeof val === 'object' && PROP_SPY_SYMBOL in val)
|
|
35
|
-
|
|
36
|
-
const isReactInternal = (val: unknown): boolean => {
|
|
37
|
-
if (!val || typeof val !== 'object') return false
|
|
38
|
-
const obj = val as Record<string, unknown>
|
|
39
|
-
return !!(obj.$$typeof || obj._owner || obj._store || (obj.prototype as Record<string, unknown>)?.isReactComponent)
|
|
40
|
-
}
|
|
41
|
-
|
|
42
|
-
const logRead = (stores: TrackingStores, path: string, component: string, value: unknown) => {
|
|
43
|
-
const record = stores.reads.get(path) ?? { components: new Set(), value }
|
|
44
|
-
record.components.add(component)
|
|
45
|
-
stores.reads.set(path, record)
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const createMeta = (
|
|
49
|
-
path: string,
|
|
50
|
-
propName: string,
|
|
51
|
-
originalValue: unknown,
|
|
52
|
-
): { meta: PropSpyMeta; uniqueId: string } => {
|
|
53
|
-
const uniqueId = `__spy_${++idCounter}__`
|
|
54
|
-
const meta = { path, propName, uniqueId, originalValue }
|
|
55
|
-
return { meta, uniqueId }
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
59
|
-
// Primitive Wrapping
|
|
60
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
61
|
-
|
|
62
|
-
const wrapPrimitiveOrFunction = (
|
|
63
|
-
value: string | number | boolean | ((...args: unknown[]) => unknown),
|
|
64
|
-
path: string,
|
|
65
|
-
propName: string,
|
|
66
|
-
stores: TrackingStores,
|
|
67
|
-
getComponent: () => string,
|
|
68
|
-
) => {
|
|
69
|
-
// Functions: Wrap to log execution
|
|
70
|
-
if (typeof value === 'function') {
|
|
71
|
-
const spy = function (this: unknown, ...args: unknown[]) {
|
|
72
|
-
logRead(stores, path, getComponent(), value)
|
|
73
|
-
return value.apply(this, args)
|
|
74
|
-
}
|
|
75
|
-
Object.defineProperties(spy, {
|
|
76
|
-
name: { value: value.name || propName },
|
|
77
|
-
length: { value: value.length },
|
|
78
|
-
})
|
|
79
|
-
return spy
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
const { meta, uniqueId } = createMeta(path, propName, value)
|
|
83
|
-
|
|
84
|
-
// Strings: Embed markers
|
|
85
|
-
if (typeof value === 'string') {
|
|
86
|
-
metadataMap.set(uniqueId, meta)
|
|
87
|
-
return `${uniqueId}${value}${uniqueId}`
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Numbers/Booleans: Wrapper Objects
|
|
91
|
-
// eslint-disable-next-line @typescript-eslint/no-wrapper-object-types
|
|
92
|
-
type SpyWrapper = (number | boolean) & { [PROP_SPY_SYMBOL]?: boolean; __meta?: PropSpyMeta }
|
|
93
|
-
let spy: SpyWrapper | undefined
|
|
94
|
-
if (typeof value === 'number') spy = new Number(value) as SpyWrapper
|
|
95
|
-
if (typeof value === 'boolean') spy = new Boolean(value) as SpyWrapper
|
|
96
|
-
|
|
97
|
-
if (spy) {
|
|
98
|
-
spy[PROP_SPY_SYMBOL] = true
|
|
99
|
-
spy.__meta = meta
|
|
100
|
-
return spy
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
return value
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
107
|
-
// Core Logic
|
|
108
|
-
// ─────────────────────────────────────────────────────────────────────────────
|
|
109
|
-
|
|
110
|
-
const createAuditedProps = <T extends object | null>(
|
|
111
|
-
target: T,
|
|
112
|
-
stores: TrackingStores,
|
|
113
|
-
getComponent: () => string,
|
|
114
|
-
basePath = 'props',
|
|
115
|
-
): T => {
|
|
116
|
-
if (!target || typeof target !== 'object' || isPropSpy(target)) return target
|
|
117
|
-
|
|
118
|
-
return new Proxy(target, {
|
|
119
|
-
get(obj, prop, receiver) {
|
|
120
|
-
const value = Reflect.get(obj, prop, receiver)
|
|
121
|
-
const propKey = String(prop)
|
|
122
|
-
|
|
123
|
-
// Ignore symbols and known React internal properties
|
|
124
|
-
if (typeof prop === 'symbol' || propKey === '$$typeof' || propKey.startsWith('_')) {
|
|
125
|
-
return value
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
const path = Array.isArray(target) ? `${basePath}[${propKey}]` : `${basePath}.${propKey}`
|
|
129
|
-
const component = getComponent()
|
|
130
|
-
|
|
131
|
-
// 1. Handle React internals or nulls (Log but don't wrap)
|
|
132
|
-
if (value == null || isReactInternal(value)) {
|
|
133
|
-
logRead(stores, path, component, value)
|
|
134
|
-
return value
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// 2. Handle Primitives & Functions (Wrap & Log)
|
|
138
|
-
if (typeof value !== 'object' || typeof value === 'function') {
|
|
139
|
-
const wrapped = wrapPrimitiveOrFunction(
|
|
140
|
-
value as string | number | boolean | ((...args: unknown[]) => unknown),
|
|
141
|
-
path,
|
|
142
|
-
propKey,
|
|
143
|
-
stores,
|
|
144
|
-
getComponent,
|
|
145
|
-
)
|
|
146
|
-
logRead(stores, path, component, wrapped)
|
|
147
|
-
return wrapped
|
|
148
|
-
}
|
|
149
|
-
|
|
150
|
-
// 3. Handle Objects/Arrays (Recurse Proxy & Log)
|
|
151
|
-
const audited = createAuditedProps(value, stores, getComponent, path)
|
|
152
|
-
logRead(stores, path, component, audited)
|
|
153
|
-
return audited
|
|
154
|
-
},
|
|
155
|
-
ownKeys: (obj) => Reflect.ownKeys(obj),
|
|
156
|
-
getOwnPropertyDescriptor: (obj, prop) => Reflect.getOwnPropertyDescriptor(obj, prop),
|
|
157
|
-
})
|
|
158
|
-
}
|
|
159
|
-
|
|
160
|
-
const getSpyMetadataByUniqueId = (id: string): PropSpyMeta | null => {
|
|
161
|
-
return metadataMap.get(id) ?? null
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
return {
|
|
165
|
-
createAuditedProps,
|
|
166
|
-
getSpyMetadataByUniqueId,
|
|
167
|
-
}
|
|
168
|
-
}
|