@momo-kits/foundation 0.169.4-beta.8 → 0.169.4
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/Application/BottomSheet.tsx +5 -11
- package/Application/ModalScreen.tsx +1 -7
- package/Application/NavigationContainer.tsx +0 -2
- package/Application/StackScreen.tsx +1 -4
- package/Application/__tests__/modalLayout.test.ts +3 -3
- package/Application/modalLayout.ts +2 -2
- package/Popup/PopupNotify.tsx +5 -9
- package/package.json +1 -1
- package/Application/KitAdoption/AdoptionBadge.tsx +0 -119
- package/Application/KitAdoption/AdoptionProbe.tsx +0 -145
- package/Application/KitAdoption/AdoptionStore.ts +0 -80
- package/Application/KitAdoption/constants.ts +0 -104
- package/Application/KitAdoption/index.ts +0 -5
- package/Application/KitAdoption/styles.ts +0 -55
- package/Application/KitAdoption/types.ts +0 -48
- package/Application/KitAdoption/utils.ts +0 -393
- package/Application/__tests__/KitAdoption.integration.test.tsx +0 -235
- package/Application/__tests__/KitAdoption.test.ts +0 -260
- package/Application/__tests__/KitAdoptionStore.test.ts +0 -110
|
@@ -1,48 +0,0 @@
|
|
|
1
|
-
type AdoptionBucket = 'K' | 'R' | 'O';
|
|
2
|
-
|
|
3
|
-
type SnapshotStatus = 'ok' | 'dev_bundle' | 'truncated' | 'error';
|
|
4
|
-
|
|
5
|
-
type WalkResult = {
|
|
6
|
-
counts: { K: number; R: number; O: number };
|
|
7
|
-
kit: Record<string, number>;
|
|
8
|
-
rn: Record<string, number>;
|
|
9
|
-
other: Record<string, number>;
|
|
10
|
-
top_transparent: { name: string; count: number }[];
|
|
11
|
-
fibers: number;
|
|
12
|
-
truncated: boolean;
|
|
13
|
-
status: SnapshotStatus;
|
|
14
|
-
};
|
|
15
|
-
|
|
16
|
-
type ScreenSnapshot = WalkResult & {
|
|
17
|
-
app_id: string;
|
|
18
|
-
screen_name: string;
|
|
19
|
-
foundation_version?: string;
|
|
20
|
-
platform: string;
|
|
21
|
-
ts: number;
|
|
22
|
-
walk_ms: number;
|
|
23
|
-
};
|
|
24
|
-
|
|
25
|
-
type FiberLike = {
|
|
26
|
-
tag: number;
|
|
27
|
-
type: unknown;
|
|
28
|
-
child?: FiberLike | null;
|
|
29
|
-
sibling?: FiberLike | null;
|
|
30
|
-
return?: FiberLike | null;
|
|
31
|
-
alternate?: FiberLike | null;
|
|
32
|
-
memoizedProps?: unknown;
|
|
33
|
-
_debugStack?: unknown;
|
|
34
|
-
_debugOwner?: unknown;
|
|
35
|
-
stateNode?: unknown;
|
|
36
|
-
};
|
|
37
|
-
|
|
38
|
-
/** Identity sets of component objects: kit exports and RN primitives. */
|
|
39
|
-
type KitRegistries = { kit: WeakSet<object>; rn: WeakSet<object> };
|
|
40
|
-
|
|
41
|
-
export type {
|
|
42
|
-
AdoptionBucket,
|
|
43
|
-
FiberLike,
|
|
44
|
-
KitRegistries,
|
|
45
|
-
ScreenSnapshot,
|
|
46
|
-
SnapshotStatus,
|
|
47
|
-
WalkResult,
|
|
48
|
-
};
|
|
@@ -1,393 +0,0 @@
|
|
|
1
|
-
import { Fragment, isValidElement } from 'react';
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
COUNTED_FIBER_TAGS,
|
|
5
|
-
FIBER_TAG,
|
|
6
|
-
FOUNDATION_REEXPORT_NAMES,
|
|
7
|
-
HOST_RN_PREFIXES,
|
|
8
|
-
LIB_UNIT_NAMES,
|
|
9
|
-
MAX_FIBERS,
|
|
10
|
-
REACT_FORWARD_REF_TYPE,
|
|
11
|
-
REACT_MEMO_TYPE,
|
|
12
|
-
RN_PRIMITIVE_NAMES,
|
|
13
|
-
SLOT_SCAN_DEPTH,
|
|
14
|
-
TOP_TRANSPARENT_LIMIT,
|
|
15
|
-
} from './constants';
|
|
16
|
-
import type {
|
|
17
|
-
AdoptionBucket,
|
|
18
|
-
FiberLike,
|
|
19
|
-
KitRegistries,
|
|
20
|
-
SnapshotStatus,
|
|
21
|
-
WalkResult,
|
|
22
|
-
} from './types';
|
|
23
|
-
|
|
24
|
-
const ANONYMOUS_NAME = 'Anonymous';
|
|
25
|
-
const UNKNOWN_APP_ID = 'unknown';
|
|
26
|
-
|
|
27
|
-
type LooseObject = Record<string, unknown>;
|
|
28
|
-
type Classification = AdoptionBucket | 'unit' | 'transparent';
|
|
29
|
-
type WalkEntry = { fiber: FiberLike; unitSlots: Set<unknown> | null };
|
|
30
|
-
type Tally = {
|
|
31
|
-
counts: Record<AdoptionBucket, number>;
|
|
32
|
-
names: Record<AdoptionBucket, Record<string, number>>;
|
|
33
|
-
transparent: Record<string, number>;
|
|
34
|
-
appAuthoredSeen: boolean;
|
|
35
|
-
};
|
|
36
|
-
|
|
37
|
-
/**
|
|
38
|
-
* True for non-null objects and functions, the only values React fibers and
|
|
39
|
-
* component types can hold.
|
|
40
|
-
*/
|
|
41
|
-
const isObjectLike = (value: unknown): value is LooseObject =>
|
|
42
|
-
(typeof value === 'object' && value !== null) || typeof value === 'function';
|
|
43
|
-
|
|
44
|
-
/** Non-empty string name, or undefined. */
|
|
45
|
-
const readName = (value: unknown): string | undefined =>
|
|
46
|
-
typeof value === 'string' && value !== '' ? value : undefined;
|
|
47
|
-
|
|
48
|
-
const readNameOf = (value: unknown): string | undefined =>
|
|
49
|
-
isObjectLike(value) ? readName(value.name) : undefined;
|
|
50
|
-
|
|
51
|
-
/** Runs a read that may throw (RN exports use throwing getters); returns undefined on failure. */
|
|
52
|
-
const readSafely = (read: () => unknown): unknown => {
|
|
53
|
-
try {
|
|
54
|
-
return read();
|
|
55
|
-
} catch {
|
|
56
|
-
return undefined;
|
|
57
|
-
}
|
|
58
|
-
};
|
|
59
|
-
|
|
60
|
-
/** Functions and React exotic objects ($$typeof) are component candidates. */
|
|
61
|
-
const isComponentLike = (value: unknown): value is object =>
|
|
62
|
-
typeof value === 'function' ||
|
|
63
|
-
(isObjectLike(value) && value.$$typeof !== undefined);
|
|
64
|
-
|
|
65
|
-
const isInSet = (set: WeakSet<object>, value: unknown): boolean =>
|
|
66
|
-
isObjectLike(value) && set.has(value);
|
|
67
|
-
|
|
68
|
-
/** Adds a non-null object-typed count, keeping prototype keys (e.g. "constructor") out of the lookup. */
|
|
69
|
-
const incrementName = (record: Record<string, number>, name: string): void => {
|
|
70
|
-
const current = Object.prototype.hasOwnProperty.call(record, name)
|
|
71
|
-
? record[name]
|
|
72
|
-
: 0;
|
|
73
|
-
record[name] = current + 1;
|
|
74
|
-
};
|
|
75
|
-
|
|
76
|
-
/**
|
|
77
|
-
* Returns the live copy of a fiber: the one under the committed HostRoot,
|
|
78
|
-
* otherwise its alternate. Walking a stale copy would count the old tree.
|
|
79
|
-
*/
|
|
80
|
-
const getCurrentFiber = (fiber: FiberLike): FiberLike => {
|
|
81
|
-
let hostRoot: FiberLike | null | undefined = fiber;
|
|
82
|
-
while (hostRoot && hostRoot.tag !== FIBER_TAG.HostRoot) {
|
|
83
|
-
hostRoot = hostRoot.return;
|
|
84
|
-
}
|
|
85
|
-
if (!hostRoot) return fiber;
|
|
86
|
-
const root = hostRoot.stateNode as { current?: unknown } | null | undefined;
|
|
87
|
-
return root?.current === hostRoot ? fiber : fiber.alternate ?? fiber;
|
|
88
|
-
};
|
|
89
|
-
|
|
90
|
-
/** Inner component of a React.memo or forwardRef wrapper, or undefined for other types. */
|
|
91
|
-
const getWrappedType = (type: LooseObject): unknown => {
|
|
92
|
-
if (type.$$typeof === REACT_MEMO_TYPE) return type.type;
|
|
93
|
-
if (type.$$typeof === REACT_FORWARD_REF_TYPE) return type.render;
|
|
94
|
-
return undefined;
|
|
95
|
-
};
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Strips memo and forwardRef wrappers (recursively) to reach the underlying
|
|
99
|
-
* component, since a fiber's type is often the wrapper.
|
|
100
|
-
*/
|
|
101
|
-
const unwrapType = (type: unknown): unknown => {
|
|
102
|
-
if (!isObjectLike(type)) return type;
|
|
103
|
-
const inner = getWrappedType(type);
|
|
104
|
-
return inner === undefined ? type : unwrapType(inner);
|
|
105
|
-
};
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Display name for a fiber type, following wrappers when the type has no name
|
|
109
|
-
* of its own. Used for lib-unit matching and the report.
|
|
110
|
-
*/
|
|
111
|
-
const getTypeName = (type: unknown): string => {
|
|
112
|
-
if (typeof type === 'string') return type;
|
|
113
|
-
if (!isObjectLike(type)) return ANONYMOUS_NAME;
|
|
114
|
-
return (
|
|
115
|
-
readName(type.displayName) ??
|
|
116
|
-
readName(type.name) ??
|
|
117
|
-
readNameOf(type.render) ??
|
|
118
|
-
readNameOf(type.type) ??
|
|
119
|
-
ANONYMOUS_NAME
|
|
120
|
-
);
|
|
121
|
-
};
|
|
122
|
-
|
|
123
|
-
/** Adds a component and its unwrapped form to the set. */
|
|
124
|
-
const addRegistryEntry = (set: WeakSet<object>, value: unknown): void => {
|
|
125
|
-
if (!isComponentLike(value)) return;
|
|
126
|
-
set.add(value);
|
|
127
|
-
const inner = unwrapType(value);
|
|
128
|
-
if (isObjectLike(inner)) set.add(inner);
|
|
129
|
-
};
|
|
130
|
-
|
|
131
|
-
/**
|
|
132
|
-
* Builds the identity sets used to classify fibers. RN is read by name only,
|
|
133
|
-
* because enumerating react-native exports hits throwing getters.
|
|
134
|
-
*/
|
|
135
|
-
const createRegistries = (
|
|
136
|
-
kitModules: Record<string, unknown>[],
|
|
137
|
-
rnModule: Record<string, unknown>,
|
|
138
|
-
): KitRegistries => {
|
|
139
|
-
const kit = new WeakSet<object>();
|
|
140
|
-
const rn = new WeakSet<object>();
|
|
141
|
-
kitModules.forEach(mod => {
|
|
142
|
-
Object.keys(mod).forEach(name => {
|
|
143
|
-
if (FOUNDATION_REEXPORT_NAMES.includes(name)) return;
|
|
144
|
-
addRegistryEntry(
|
|
145
|
-
kit,
|
|
146
|
-
readSafely(() => mod[name]),
|
|
147
|
-
);
|
|
148
|
-
});
|
|
149
|
-
});
|
|
150
|
-
RN_PRIMITIVE_NAMES.forEach(name => {
|
|
151
|
-
addRegistryEntry(
|
|
152
|
-
rn,
|
|
153
|
-
readSafely(() => rnModule[name]),
|
|
154
|
-
);
|
|
155
|
-
});
|
|
156
|
-
return { kit, rn };
|
|
157
|
-
};
|
|
158
|
-
|
|
159
|
-
/**
|
|
160
|
-
* The fiber whose stack decides authorship. React creates the inner fiber of a
|
|
161
|
-
* MemoComponent itself, with no stack, so it inherits its memo parent's.
|
|
162
|
-
*/
|
|
163
|
-
const getAuthorFiber = (fiber: FiberLike): FiberLike =>
|
|
164
|
-
fiber.return?.tag === FIBER_TAG.MemoComponent && fiber.return.child === fiber
|
|
165
|
-
? fiber.return
|
|
166
|
-
: fiber;
|
|
167
|
-
|
|
168
|
-
/**
|
|
169
|
-
* True when the fiber has no dev stack, i.e. it was created by mini-app code
|
|
170
|
-
* (prod bundle) rather than by the host's dev build.
|
|
171
|
-
*/
|
|
172
|
-
const isAppAuthored = (fiber: FiberLike): boolean => !fiber._debugStack;
|
|
173
|
-
|
|
174
|
-
/**
|
|
175
|
-
* True when the fiber's first child was created by host code and is owned by
|
|
176
|
-
* this fiber, which marks a host-internal wrapper.
|
|
177
|
-
*/
|
|
178
|
-
const isHostDefined = (fiber: FiberLike): boolean => {
|
|
179
|
-
const child = fiber.child;
|
|
180
|
-
return !!child && !isAppAuthored(child) && child._debugOwner === fiber;
|
|
181
|
-
};
|
|
182
|
-
|
|
183
|
-
/** App id from the mini-app context, or a placeholder when it is missing. */
|
|
184
|
-
const getAppId = (context: unknown): string => {
|
|
185
|
-
if (!isObjectLike(context) || context.appId == null) return UNKNOWN_APP_ID;
|
|
186
|
-
return String(context.appId);
|
|
187
|
-
};
|
|
188
|
-
|
|
189
|
-
/**
|
|
190
|
-
* Adds the props of React elements that the app passed into a lib unit, found
|
|
191
|
-
* among the props and in arrays up to SLOT_SCAN_DEPTH. Never looks inside an element.
|
|
192
|
-
*/
|
|
193
|
-
const collectSlotProps = (
|
|
194
|
-
props: unknown,
|
|
195
|
-
set: Set<unknown>,
|
|
196
|
-
depth: number,
|
|
197
|
-
): void => {
|
|
198
|
-
if (depth > SLOT_SCAN_DEPTH || !isObjectLike(props)) return;
|
|
199
|
-
Object.values(props).forEach(value => {
|
|
200
|
-
if (isValidElement(value)) {
|
|
201
|
-
set.add(value.props);
|
|
202
|
-
if (value.type === Fragment)
|
|
203
|
-
collectSlotProps(value.props, set, depth + 1);
|
|
204
|
-
} else if (Array.isArray(value)) {
|
|
205
|
-
collectSlotProps(value, set, depth + 1);
|
|
206
|
-
}
|
|
207
|
-
});
|
|
208
|
-
};
|
|
209
|
-
|
|
210
|
-
/** True for host string types that come from react-native's native components. */
|
|
211
|
-
const isRnHostType = (type: unknown): boolean =>
|
|
212
|
-
typeof type === 'string' &&
|
|
213
|
-
HOST_RN_PREFIXES.some(prefix => type.startsWith(prefix));
|
|
214
|
-
|
|
215
|
-
/**
|
|
216
|
-
* Buckets a fiber: K/R/O for kit, RN and other components, 'unit' for lib
|
|
217
|
-
* units (counted once), or 'transparent' for app wrappers that are not counted.
|
|
218
|
-
*/
|
|
219
|
-
const classifyFiber = (
|
|
220
|
-
fiber: FiberLike,
|
|
221
|
-
registries: KitRegistries,
|
|
222
|
-
): Classification => {
|
|
223
|
-
if (fiber.tag === FIBER_TAG.HostComponent) {
|
|
224
|
-
return isRnHostType(fiber.type) ? 'R' : 'O';
|
|
225
|
-
}
|
|
226
|
-
const inner = unwrapType(fiber.type);
|
|
227
|
-
if (isInSet(registries.kit, fiber.type) || isInSet(registries.kit, inner)) {
|
|
228
|
-
return 'K';
|
|
229
|
-
}
|
|
230
|
-
if (isInSet(registries.rn, fiber.type) || isInSet(registries.rn, inner)) {
|
|
231
|
-
return 'R';
|
|
232
|
-
}
|
|
233
|
-
if (LIB_UNIT_NAMES.includes(getTypeName(fiber.type))) return 'unit';
|
|
234
|
-
return isHostDefined(fiber) ? 'O' : 'transparent';
|
|
235
|
-
};
|
|
236
|
-
|
|
237
|
-
const createTally = (): Tally => ({
|
|
238
|
-
counts: { K: 0, R: 0, O: 0 },
|
|
239
|
-
names: { K: {}, R: {}, O: {} },
|
|
240
|
-
transparent: {},
|
|
241
|
-
appAuthoredSeen: false,
|
|
242
|
-
});
|
|
243
|
-
|
|
244
|
-
const countComponent = (
|
|
245
|
-
tally: Tally,
|
|
246
|
-
bucket: AdoptionBucket,
|
|
247
|
-
name: string,
|
|
248
|
-
): void => {
|
|
249
|
-
tally.counts[bucket] += 1;
|
|
250
|
-
incrementName(tally.names[bucket], name);
|
|
251
|
-
};
|
|
252
|
-
|
|
253
|
-
/** Top transparent wrapper names by count, most frequent first. */
|
|
254
|
-
const topEntries = (
|
|
255
|
-
record: Record<string, number>,
|
|
256
|
-
): { name: string; count: number }[] =>
|
|
257
|
-
Object.keys(record)
|
|
258
|
-
.map(name => ({ name, count: record[name] }))
|
|
259
|
-
.sort((a, b) => b.count - a.count)
|
|
260
|
-
.slice(0, TOP_TRANSPARENT_LIMIT);
|
|
261
|
-
|
|
262
|
-
const getStatus = (
|
|
263
|
-
truncated: boolean,
|
|
264
|
-
fibers: number,
|
|
265
|
-
appAuthoredSeen: boolean,
|
|
266
|
-
): SnapshotStatus => {
|
|
267
|
-
if (truncated) return 'truncated';
|
|
268
|
-
if (fibers > 0 && !appAuthoredSeen) return 'dev_bundle';
|
|
269
|
-
return 'ok';
|
|
270
|
-
};
|
|
271
|
-
|
|
272
|
-
const buildResult = (
|
|
273
|
-
tally: Tally,
|
|
274
|
-
fibers: number,
|
|
275
|
-
truncated: boolean,
|
|
276
|
-
): WalkResult => ({
|
|
277
|
-
counts: { ...tally.counts },
|
|
278
|
-
kit: tally.names.K,
|
|
279
|
-
rn: tally.names.R,
|
|
280
|
-
other: tally.names.O,
|
|
281
|
-
top_transparent: topEntries(tally.transparent),
|
|
282
|
-
fibers,
|
|
283
|
-
truncated,
|
|
284
|
-
status: getStatus(truncated, fibers, tally.appAuthoredSeen),
|
|
285
|
-
});
|
|
286
|
-
|
|
287
|
-
/**
|
|
288
|
-
* Queues the children of a fiber (and their siblings) with the given slot set.
|
|
289
|
-
* Slot sets only pass through lib-unit internals and walk-through fibers.
|
|
290
|
-
*/
|
|
291
|
-
const pushChildren = (
|
|
292
|
-
fiber: FiberLike,
|
|
293
|
-
unitSlots: Set<unknown> | null,
|
|
294
|
-
stack: WalkEntry[],
|
|
295
|
-
): void => {
|
|
296
|
-
for (let child = fiber.child; child; child = child.sibling) {
|
|
297
|
-
stack.push({ fiber: child, unitSlots });
|
|
298
|
-
}
|
|
299
|
-
};
|
|
300
|
-
|
|
301
|
-
/**
|
|
302
|
-
* Counts one app-scope fiber. A lib unit counts as K once; its slot props are
|
|
303
|
-
* collected so that the app-passed descendants are counted too.
|
|
304
|
-
*/
|
|
305
|
-
const countFiber = (
|
|
306
|
-
fiber: FiberLike,
|
|
307
|
-
registries: KitRegistries,
|
|
308
|
-
tally: Tally,
|
|
309
|
-
stack: WalkEntry[],
|
|
310
|
-
): void => {
|
|
311
|
-
const name = getTypeName(fiber.type);
|
|
312
|
-
const kind = classifyFiber(fiber, registries);
|
|
313
|
-
if (kind === 'unit') {
|
|
314
|
-
countComponent(tally, 'K', name);
|
|
315
|
-
const slots = new Set<unknown>();
|
|
316
|
-
collectSlotProps(fiber.memoizedProps, slots, 0);
|
|
317
|
-
pushChildren(fiber, slots, stack);
|
|
318
|
-
return;
|
|
319
|
-
}
|
|
320
|
-
if (kind === 'transparent') {
|
|
321
|
-
incrementName(tally.transparent, name);
|
|
322
|
-
} else {
|
|
323
|
-
countComponent(tally, kind, name);
|
|
324
|
-
}
|
|
325
|
-
pushChildren(fiber, null, stack);
|
|
326
|
-
};
|
|
327
|
-
|
|
328
|
-
/**
|
|
329
|
-
* Handles one popped fiber: walk-through tags and lib-unit internals are
|
|
330
|
-
* only traversed, host-defined fibers are skipped, and app-scope fibers are counted.
|
|
331
|
-
*/
|
|
332
|
-
const visitEntry = (
|
|
333
|
-
entry: WalkEntry,
|
|
334
|
-
registries: KitRegistries,
|
|
335
|
-
tally: Tally,
|
|
336
|
-
stack: WalkEntry[],
|
|
337
|
-
): void => {
|
|
338
|
-
const { fiber, unitSlots } = entry;
|
|
339
|
-
if (!COUNTED_FIBER_TAGS.includes(fiber.tag)) {
|
|
340
|
-
pushChildren(fiber, unitSlots, stack);
|
|
341
|
-
return;
|
|
342
|
-
}
|
|
343
|
-
if (unitSlots && !unitSlots.has(fiber.memoizedProps)) {
|
|
344
|
-
pushChildren(fiber, unitSlots, stack);
|
|
345
|
-
return;
|
|
346
|
-
}
|
|
347
|
-
if (!isAppAuthored(getAuthorFiber(fiber))) {
|
|
348
|
-
pushChildren(fiber, null, stack);
|
|
349
|
-
return;
|
|
350
|
-
}
|
|
351
|
-
tally.appAuthoredSeen = true;
|
|
352
|
-
countFiber(fiber, registries, tally, stack);
|
|
353
|
-
};
|
|
354
|
-
|
|
355
|
-
/**
|
|
356
|
-
* Walks a screen's fiber tree from rootFiber (not its siblings) and tallies
|
|
357
|
-
* app-authored kit, RN and other components. Stops at maxFibers and marks truncated.
|
|
358
|
-
*/
|
|
359
|
-
const walkScreen = (
|
|
360
|
-
rootFiber: FiberLike,
|
|
361
|
-
registries: KitRegistries,
|
|
362
|
-
stopAt?: (fiber: FiberLike) => boolean,
|
|
363
|
-
maxFibers = MAX_FIBERS,
|
|
364
|
-
): WalkResult => {
|
|
365
|
-
const tally = createTally();
|
|
366
|
-
const stack: WalkEntry[] = [{ fiber: rootFiber, unitSlots: null }];
|
|
367
|
-
let fibers = 0;
|
|
368
|
-
let truncated = false;
|
|
369
|
-
for (let entry = stack.pop(); entry; entry = stack.pop()) {
|
|
370
|
-
if (fibers === maxFibers) {
|
|
371
|
-
truncated = true;
|
|
372
|
-
break;
|
|
373
|
-
}
|
|
374
|
-
fibers += 1;
|
|
375
|
-
if (entry.fiber !== rootFiber && stopAt?.(entry.fiber)) continue;
|
|
376
|
-
visitEntry(entry, registries, tally, stack);
|
|
377
|
-
}
|
|
378
|
-
return buildResult(tally, fibers, truncated);
|
|
379
|
-
};
|
|
380
|
-
|
|
381
|
-
export {
|
|
382
|
-
classifyFiber,
|
|
383
|
-
collectSlotProps,
|
|
384
|
-
createRegistries,
|
|
385
|
-
getAppId,
|
|
386
|
-
getAuthorFiber,
|
|
387
|
-
getCurrentFiber,
|
|
388
|
-
getTypeName,
|
|
389
|
-
isAppAuthored,
|
|
390
|
-
isHostDefined,
|
|
391
|
-
unwrapType,
|
|
392
|
-
walkScreen,
|
|
393
|
-
};
|
|
@@ -1,235 +0,0 @@
|
|
|
1
|
-
jest.unmock('react-native');
|
|
2
|
-
|
|
3
|
-
/* Native packages are stubbed: the real modules are ESM or need native code under jest. */
|
|
4
|
-
jest.mock('react-native-reanimated', () => {
|
|
5
|
-
const { View, Text, Image } = require('react-native');
|
|
6
|
-
const Animated = {
|
|
7
|
-
View,
|
|
8
|
-
Text,
|
|
9
|
-
Image,
|
|
10
|
-
createAnimatedComponent: (component: unknown) => component,
|
|
11
|
-
};
|
|
12
|
-
return {
|
|
13
|
-
__esModule: true,
|
|
14
|
-
default: Animated,
|
|
15
|
-
...Animated,
|
|
16
|
-
useSharedValue: (value: unknown) => ({ value }),
|
|
17
|
-
useAnimatedStyle: () => ({}),
|
|
18
|
-
useDerivedValue: (fn: () => unknown) => ({ value: fn() }),
|
|
19
|
-
withTiming: (value: unknown) => value,
|
|
20
|
-
withSpring: (value: unknown) => value,
|
|
21
|
-
runOnJS: (fn: unknown) => fn,
|
|
22
|
-
interpolate: () => 0,
|
|
23
|
-
};
|
|
24
|
-
});
|
|
25
|
-
jest.mock('react-native-fast-image', () => {
|
|
26
|
-
const mockReact = require('react');
|
|
27
|
-
const FastImageStub = (props: Record<string, unknown>) =>
|
|
28
|
-
mockReact.createElement('FastImage', props);
|
|
29
|
-
return { __esModule: true, default: FastImageStub };
|
|
30
|
-
});
|
|
31
|
-
jest.mock('react-native-linear-gradient', () => {
|
|
32
|
-
const mockReact = require('react');
|
|
33
|
-
const LinearGradientStub = (props: Record<string, unknown>) =>
|
|
34
|
-
mockReact.createElement('LinearGradient', props);
|
|
35
|
-
return { __esModule: true, default: LinearGradientStub };
|
|
36
|
-
});
|
|
37
|
-
jest.mock('lottie-react-native', () => {
|
|
38
|
-
const mockReact = require('react');
|
|
39
|
-
const LottieStub = () => null;
|
|
40
|
-
return { __esModule: true, default: mockReact.forwardRef(LottieStub) };
|
|
41
|
-
});
|
|
42
|
-
jest.mock('../../Application', () => ({
|
|
43
|
-
useComponentId: () => ({ componentId: 'test-id' }),
|
|
44
|
-
}));
|
|
45
|
-
|
|
46
|
-
import React, { Fragment, type ElementType, type ReactNode } from 'react';
|
|
47
|
-
import {
|
|
48
|
-
FlatList,
|
|
49
|
-
Image,
|
|
50
|
-
Text as RNText,
|
|
51
|
-
TouchableOpacity,
|
|
52
|
-
View,
|
|
53
|
-
} from 'react-native';
|
|
54
|
-
import { act, create, type ReactTestRenderer } from 'react-test-renderer';
|
|
55
|
-
|
|
56
|
-
import { Button as FoundationButton } from '../../Button';
|
|
57
|
-
import { Text as FoundationText } from '../../Text';
|
|
58
|
-
import {
|
|
59
|
-
createRegistries,
|
|
60
|
-
getCurrentFiber,
|
|
61
|
-
walkScreen,
|
|
62
|
-
} from '../KitAdoption/utils';
|
|
63
|
-
import type { FiberLike, WalkResult } from '../KitAdoption/types';
|
|
64
|
-
|
|
65
|
-
/**
|
|
66
|
-
* Production jsx of the React build. Elements made with it carry no
|
|
67
|
-
* `_debugStack`, the same as mini-app code shipped as a prod bundle. Loaded by
|
|
68
|
-
* resolved path: the react `exports` map blocks cjs/ under jest.
|
|
69
|
-
*/
|
|
70
|
-
const reactEntry = (
|
|
71
|
-
require as unknown as { resolve: (id: string) => string }
|
|
72
|
-
).resolve('react');
|
|
73
|
-
const prodJsxRuntime = require(reactEntry.replace(
|
|
74
|
-
/index\.js$/,
|
|
75
|
-
'cjs/react-jsx-runtime.production.js',
|
|
76
|
-
)) as {
|
|
77
|
-
jsx: (
|
|
78
|
-
type: ElementType,
|
|
79
|
-
props: Record<string, unknown>,
|
|
80
|
-
) => React.ReactElement;
|
|
81
|
-
};
|
|
82
|
-
|
|
83
|
-
/** Builds an element with the prod jsx runtime, the way a mini-app prod bundle does. */
|
|
84
|
-
const prodJsx = (
|
|
85
|
-
type: ElementType,
|
|
86
|
-
props: Record<string, unknown>,
|
|
87
|
-
...children: ReactNode[]
|
|
88
|
-
): React.ReactElement => {
|
|
89
|
-
if (children.length === 0) return prodJsxRuntime.jsx(type, props);
|
|
90
|
-
return prodJsxRuntime.jsx(type, {
|
|
91
|
-
...props,
|
|
92
|
-
children: children.length > 1 ? children : children[0],
|
|
93
|
-
});
|
|
94
|
-
};
|
|
95
|
-
|
|
96
|
-
/** Test-only probe: exposes its fiber so the measurement starts from the real tree. */
|
|
97
|
-
class Probe extends React.Component<{ children?: ReactNode }> {
|
|
98
|
-
render(): ReactNode {
|
|
99
|
-
return this.props.children;
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
/**
|
|
104
|
-
* Kit registry built from the components this test renders. The full foundation
|
|
105
|
-
* index cannot load under jest (navigation and native-only packages), so it is
|
|
106
|
-
* not used. TouchableOpacity is listed as the foundation re-export, which must not count as kit.
|
|
107
|
-
*/
|
|
108
|
-
const registries = createRegistries(
|
|
109
|
-
[{ Button: FoundationButton, Text: FoundationText }, { TouchableOpacity }],
|
|
110
|
-
require('react-native') as Record<string, unknown>,
|
|
111
|
-
);
|
|
112
|
-
|
|
113
|
-
/** Renders the tree under a Probe and walks the probe's child, as AdoptionProbe does. */
|
|
114
|
-
const measure = (tree: ReactNode): WalkResult => {
|
|
115
|
-
let instance: Probe | null = null;
|
|
116
|
-
let renderer: ReactTestRenderer | undefined;
|
|
117
|
-
act(() => {
|
|
118
|
-
renderer = create(
|
|
119
|
-
<Probe
|
|
120
|
-
ref={(value: Probe | null) => {
|
|
121
|
-
instance = value;
|
|
122
|
-
}}
|
|
123
|
-
>
|
|
124
|
-
{tree}
|
|
125
|
-
</Probe>,
|
|
126
|
-
);
|
|
127
|
-
});
|
|
128
|
-
const fiber = (instance as unknown as { _reactInternals: FiberLike })
|
|
129
|
-
._reactInternals;
|
|
130
|
-
const root = getCurrentFiber(fiber).child;
|
|
131
|
-
if (!root) throw new Error('probe rendered no child fiber');
|
|
132
|
-
const result = walkScreen(root, registries);
|
|
133
|
-
act(() => {
|
|
134
|
-
renderer?.unmount();
|
|
135
|
-
});
|
|
136
|
-
return result;
|
|
137
|
-
};
|
|
138
|
-
|
|
139
|
-
/** Shorthand for the K/R/O triple the demo screen prints. */
|
|
140
|
-
const kro = (result: WalkResult): string =>
|
|
141
|
-
`K${result.counts.K} R${result.counts.R} O${result.counts.O}`;
|
|
142
|
-
|
|
143
|
-
describe('KitAdoption integration (prod-built app tree over dev host)', () => {
|
|
144
|
-
it('a. kit-only tree: kit components counted, their internals not', () => {
|
|
145
|
-
const result = measure(
|
|
146
|
-
prodJsx(
|
|
147
|
-
View,
|
|
148
|
-
{},
|
|
149
|
-
prodJsx(FoundationText, {}, 'a'),
|
|
150
|
-
prodJsx(FoundationButton, { title: 'b' }),
|
|
151
|
-
),
|
|
152
|
-
);
|
|
153
|
-
|
|
154
|
-
expect(kro(result)).toBe('K2 R1 O0');
|
|
155
|
-
expect(result.status).toBe('ok');
|
|
156
|
-
expect(result.kit).toEqual({ Text: 1, Button: 1 });
|
|
157
|
-
});
|
|
158
|
-
|
|
159
|
-
it('b. app wrapper is transparent, each use of the kit inside it is counted', () => {
|
|
160
|
-
// Two buttons are siblings, so they sit under one dev root, like a real screen.
|
|
161
|
-
// The probe walks a single root and not its siblings (see walkScreen).
|
|
162
|
-
function MyButton(props: { title: string }): React.ReactElement {
|
|
163
|
-
return prodJsx(FoundationButton, props);
|
|
164
|
-
}
|
|
165
|
-
function Screen(): React.ReactElement {
|
|
166
|
-
return prodJsx(
|
|
167
|
-
Fragment,
|
|
168
|
-
{},
|
|
169
|
-
prodJsx(MyButton, { title: '1' }),
|
|
170
|
-
prodJsx(MyButton, { title: '2' }),
|
|
171
|
-
);
|
|
172
|
-
}
|
|
173
|
-
const result = measure(<Screen />);
|
|
174
|
-
|
|
175
|
-
expect(result.counts.K).toBe(2);
|
|
176
|
-
expect(kro(result)).toBe('K2 R0 O0');
|
|
177
|
-
expect(result.top_transparent).toEqual([{ name: 'MyButton', count: 2 }]);
|
|
178
|
-
});
|
|
179
|
-
|
|
180
|
-
it('c. raw react-native primitives written by the app count as R', () => {
|
|
181
|
-
const result = measure(
|
|
182
|
-
prodJsx(
|
|
183
|
-
View,
|
|
184
|
-
{},
|
|
185
|
-
prodJsx(RNText, {}, 'x'),
|
|
186
|
-
prodJsx(Image, { source: { uri: 'https://example.com/a.png' } }),
|
|
187
|
-
prodJsx(TouchableOpacity, {}),
|
|
188
|
-
),
|
|
189
|
-
);
|
|
190
|
-
|
|
191
|
-
expect(kro(result)).toBe('K0 R4 O0');
|
|
192
|
-
});
|
|
193
|
-
|
|
194
|
-
it('d. app child in a kit container counts K, the container internals do not', () => {
|
|
195
|
-
const result = measure(
|
|
196
|
-
prodJsx(FoundationText, {}, prodJsx(FoundationText, {}, 'child')),
|
|
197
|
-
);
|
|
198
|
-
|
|
199
|
-
expect(kro(result)).toBe('K2 R0 O0');
|
|
200
|
-
expect(result.kit).toEqual({ Text: 2 });
|
|
201
|
-
});
|
|
202
|
-
|
|
203
|
-
it('e. FlatList counts R once, each renderItem element counts K', () => {
|
|
204
|
-
const items = ['one', 'two', 'three'];
|
|
205
|
-
const result = measure(
|
|
206
|
-
prodJsx(FlatList, {
|
|
207
|
-
data: items,
|
|
208
|
-
keyExtractor: (item: string) => item,
|
|
209
|
-
renderItem: ({ item }: { item: string }) =>
|
|
210
|
-
prodJsx(FoundationText, {}, item),
|
|
211
|
-
}),
|
|
212
|
-
);
|
|
213
|
-
|
|
214
|
-
expect(kro(result)).toBe('K3 R1 O0');
|
|
215
|
-
expect(result.rn).toEqual({ FlatList: 1 });
|
|
216
|
-
});
|
|
217
|
-
|
|
218
|
-
it('f. foundation re-export of TouchableOpacity counts as R, not K', () => {
|
|
219
|
-
const result = measure(prodJsx(TouchableOpacity, {}, 'x'));
|
|
220
|
-
|
|
221
|
-
expect(kro(result)).toBe('K0 R1 O0');
|
|
222
|
-
});
|
|
223
|
-
|
|
224
|
-
it('g. an all-dev tree is reported as dev_bundle', () => {
|
|
225
|
-
const result = measure(
|
|
226
|
-
<View>
|
|
227
|
-
<RNText>x</RNText>
|
|
228
|
-
</View>,
|
|
229
|
-
);
|
|
230
|
-
|
|
231
|
-
expect(result.status).toBe('dev_bundle');
|
|
232
|
-
expect(result.counts).toEqual({ K: 0, R: 0, O: 0 });
|
|
233
|
-
expect(result.fibers).toBeGreaterThan(0);
|
|
234
|
-
});
|
|
235
|
-
});
|