@momo-kits/foundation 0.169.4-beta.5 → 0.169.4-beta.6
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 +11 -5
- package/Application/BottomTab/BottomTabBar.tsx +12 -9
- package/Application/BottomTab/CustomBottomTabItem.tsx +2 -7
- package/Application/BottomTab/DuoRailTabBar.tsx +3 -3
- package/Application/BottomTab/TabBarIcon.tsx +0 -24
- package/Application/BottomTab/metrics.ts +0 -3
- package/Application/KitAdoption/AdoptionBadge.tsx +123 -0
- package/Application/KitAdoption/AdoptionProbe.tsx +147 -0
- package/Application/KitAdoption/AdoptionStore.ts +80 -0
- package/Application/KitAdoption/constants.ts +104 -0
- package/Application/KitAdoption/index.ts +5 -0
- package/Application/KitAdoption/styles.ts +55 -0
- package/Application/KitAdoption/types.ts +48 -0
- package/Application/KitAdoption/utils.ts +400 -0
- package/Application/ModalScreen.tsx +7 -1
- package/Application/NavigationContainer.tsx +2 -0
- package/Application/StackScreen.tsx +4 -1
- package/Application/__tests__/KitAdoption.integration.test.tsx +235 -0
- package/Application/__tests__/KitAdoption.test.ts +260 -0
- package/Application/__tests__/KitAdoptionStore.test.ts +110 -0
- package/Application/__tests__/modalLayout.test.ts +3 -3
- package/Application/modalLayout.ts +2 -2
- package/Application/types.tabs.ts +1 -0
- package/Popup/PopupNotify.tsx +9 -5
- package/Text/utils.ts +3 -8
- package/package.json +1 -1
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { StyleSheet } from 'react-native';
|
|
2
|
+
|
|
3
|
+
import { Colors, Radius, Spacing } from '../../Consts';
|
|
4
|
+
|
|
5
|
+
// Keeps the summary pill from stretching across a narrow screen.
|
|
6
|
+
const BADGE_MAX_WIDTH = 240;
|
|
7
|
+
// Wide enough for the three action labels without clipping.
|
|
8
|
+
const PANEL_MIN_WIDTH = 180;
|
|
9
|
+
// Floats the dev overlay above screen content.
|
|
10
|
+
const BADGE_Z_INDEX = 9999;
|
|
11
|
+
|
|
12
|
+
const styles = StyleSheet.create({
|
|
13
|
+
wrapper: {
|
|
14
|
+
position: 'absolute',
|
|
15
|
+
left: 0,
|
|
16
|
+
right: 0,
|
|
17
|
+
alignItems: 'flex-end',
|
|
18
|
+
paddingRight: Spacing.L,
|
|
19
|
+
zIndex: BADGE_Z_INDEX,
|
|
20
|
+
},
|
|
21
|
+
badge: {
|
|
22
|
+
maxWidth: BADGE_MAX_WIDTH,
|
|
23
|
+
backgroundColor: Colors.black_17,
|
|
24
|
+
borderRadius: Radius.S,
|
|
25
|
+
paddingHorizontal: Spacing.S,
|
|
26
|
+
paddingVertical: Spacing.XS,
|
|
27
|
+
},
|
|
28
|
+
panel: {
|
|
29
|
+
marginTop: Spacing.XS,
|
|
30
|
+
minWidth: PANEL_MIN_WIDTH,
|
|
31
|
+
gap: Spacing.XS,
|
|
32
|
+
padding: Spacing.S,
|
|
33
|
+
backgroundColor: Colors.black_01,
|
|
34
|
+
borderRadius: Radius.M,
|
|
35
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
36
|
+
borderColor: Colors.black_05,
|
|
37
|
+
},
|
|
38
|
+
action: {
|
|
39
|
+
alignItems: 'center',
|
|
40
|
+
paddingHorizontal: Spacing.S,
|
|
41
|
+
paddingVertical: Spacing.XS,
|
|
42
|
+
borderRadius: Radius.S,
|
|
43
|
+
backgroundColor: Colors.blue_09,
|
|
44
|
+
},
|
|
45
|
+
actionOutline: {
|
|
46
|
+
alignItems: 'center',
|
|
47
|
+
paddingHorizontal: Spacing.S,
|
|
48
|
+
paddingVertical: Spacing.XS,
|
|
49
|
+
borderRadius: Radius.S,
|
|
50
|
+
borderWidth: StyleSheet.hairlineWidth,
|
|
51
|
+
borderColor: Colors.black_07,
|
|
52
|
+
},
|
|
53
|
+
});
|
|
54
|
+
|
|
55
|
+
export { styles };
|
|
@@ -0,0 +1,48 @@
|
|
|
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
|
+
};
|
|
@@ -0,0 +1,400 @@
|
|
|
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
|
+
/** True when the mini-app context opted into kit adoption measurement. */
|
|
184
|
+
const isAdoptionEnabled = (context: unknown): boolean =>
|
|
185
|
+
isObjectLike(context) &&
|
|
186
|
+
isObjectLike(context.features) &&
|
|
187
|
+
context.features.kitAdoption === true;
|
|
188
|
+
|
|
189
|
+
/** App id from the mini-app context, or a placeholder when it is missing. */
|
|
190
|
+
const getAppId = (context: unknown): string => {
|
|
191
|
+
if (!isObjectLike(context) || context.appId == null) return UNKNOWN_APP_ID;
|
|
192
|
+
return String(context.appId);
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
/**
|
|
196
|
+
* Adds the props of React elements that the app passed into a lib unit, found
|
|
197
|
+
* among the props and in arrays up to SLOT_SCAN_DEPTH. Never looks inside an element.
|
|
198
|
+
*/
|
|
199
|
+
const collectSlotProps = (
|
|
200
|
+
props: unknown,
|
|
201
|
+
set: Set<unknown>,
|
|
202
|
+
depth: number,
|
|
203
|
+
): void => {
|
|
204
|
+
if (depth > SLOT_SCAN_DEPTH || !isObjectLike(props)) return;
|
|
205
|
+
Object.values(props).forEach(value => {
|
|
206
|
+
if (isValidElement(value)) {
|
|
207
|
+
set.add(value.props);
|
|
208
|
+
if (value.type === Fragment)
|
|
209
|
+
collectSlotProps(value.props, set, depth + 1);
|
|
210
|
+
} else if (Array.isArray(value)) {
|
|
211
|
+
collectSlotProps(value, set, depth + 1);
|
|
212
|
+
}
|
|
213
|
+
});
|
|
214
|
+
};
|
|
215
|
+
|
|
216
|
+
/** True for host string types that come from react-native's native components. */
|
|
217
|
+
const isRnHostType = (type: unknown): boolean =>
|
|
218
|
+
typeof type === 'string' &&
|
|
219
|
+
HOST_RN_PREFIXES.some(prefix => type.startsWith(prefix));
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Buckets a fiber: K/R/O for kit, RN and other components, 'unit' for lib
|
|
223
|
+
* units (counted once), or 'transparent' for app wrappers that are not counted.
|
|
224
|
+
*/
|
|
225
|
+
const classifyFiber = (
|
|
226
|
+
fiber: FiberLike,
|
|
227
|
+
registries: KitRegistries,
|
|
228
|
+
): Classification => {
|
|
229
|
+
if (fiber.tag === FIBER_TAG.HostComponent) {
|
|
230
|
+
return isRnHostType(fiber.type) ? 'R' : 'O';
|
|
231
|
+
}
|
|
232
|
+
const inner = unwrapType(fiber.type);
|
|
233
|
+
if (isInSet(registries.kit, fiber.type) || isInSet(registries.kit, inner)) {
|
|
234
|
+
return 'K';
|
|
235
|
+
}
|
|
236
|
+
if (isInSet(registries.rn, fiber.type) || isInSet(registries.rn, inner)) {
|
|
237
|
+
return 'R';
|
|
238
|
+
}
|
|
239
|
+
if (LIB_UNIT_NAMES.includes(getTypeName(fiber.type))) return 'unit';
|
|
240
|
+
return isHostDefined(fiber) ? 'O' : 'transparent';
|
|
241
|
+
};
|
|
242
|
+
|
|
243
|
+
const createTally = (): Tally => ({
|
|
244
|
+
counts: { K: 0, R: 0, O: 0 },
|
|
245
|
+
names: { K: {}, R: {}, O: {} },
|
|
246
|
+
transparent: {},
|
|
247
|
+
appAuthoredSeen: false,
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
const countComponent = (
|
|
251
|
+
tally: Tally,
|
|
252
|
+
bucket: AdoptionBucket,
|
|
253
|
+
name: string,
|
|
254
|
+
): void => {
|
|
255
|
+
tally.counts[bucket] += 1;
|
|
256
|
+
incrementName(tally.names[bucket], name);
|
|
257
|
+
};
|
|
258
|
+
|
|
259
|
+
/** Top transparent wrapper names by count, most frequent first. */
|
|
260
|
+
const topEntries = (
|
|
261
|
+
record: Record<string, number>,
|
|
262
|
+
): { name: string; count: number }[] =>
|
|
263
|
+
Object.keys(record)
|
|
264
|
+
.map(name => ({ name, count: record[name] }))
|
|
265
|
+
.sort((a, b) => b.count - a.count)
|
|
266
|
+
.slice(0, TOP_TRANSPARENT_LIMIT);
|
|
267
|
+
|
|
268
|
+
const getStatus = (
|
|
269
|
+
truncated: boolean,
|
|
270
|
+
fibers: number,
|
|
271
|
+
appAuthoredSeen: boolean,
|
|
272
|
+
): SnapshotStatus => {
|
|
273
|
+
if (truncated) return 'truncated';
|
|
274
|
+
if (fibers > 0 && !appAuthoredSeen) return 'dev_bundle';
|
|
275
|
+
return 'ok';
|
|
276
|
+
};
|
|
277
|
+
|
|
278
|
+
const buildResult = (
|
|
279
|
+
tally: Tally,
|
|
280
|
+
fibers: number,
|
|
281
|
+
truncated: boolean,
|
|
282
|
+
): WalkResult => ({
|
|
283
|
+
counts: { ...tally.counts },
|
|
284
|
+
kit: tally.names.K,
|
|
285
|
+
rn: tally.names.R,
|
|
286
|
+
other: tally.names.O,
|
|
287
|
+
top_transparent: topEntries(tally.transparent),
|
|
288
|
+
fibers,
|
|
289
|
+
truncated,
|
|
290
|
+
status: getStatus(truncated, fibers, tally.appAuthoredSeen),
|
|
291
|
+
});
|
|
292
|
+
|
|
293
|
+
/**
|
|
294
|
+
* Queues the children of a fiber (and their siblings) with the given slot set.
|
|
295
|
+
* Slot sets only pass through lib-unit internals and walk-through fibers.
|
|
296
|
+
*/
|
|
297
|
+
const pushChildren = (
|
|
298
|
+
fiber: FiberLike,
|
|
299
|
+
unitSlots: Set<unknown> | null,
|
|
300
|
+
stack: WalkEntry[],
|
|
301
|
+
): void => {
|
|
302
|
+
for (let child = fiber.child; child; child = child.sibling) {
|
|
303
|
+
stack.push({ fiber: child, unitSlots });
|
|
304
|
+
}
|
|
305
|
+
};
|
|
306
|
+
|
|
307
|
+
/**
|
|
308
|
+
* Counts one app-scope fiber. A lib unit counts as K once; its slot props are
|
|
309
|
+
* collected so that the app-passed descendants are counted too.
|
|
310
|
+
*/
|
|
311
|
+
const countFiber = (
|
|
312
|
+
fiber: FiberLike,
|
|
313
|
+
registries: KitRegistries,
|
|
314
|
+
tally: Tally,
|
|
315
|
+
stack: WalkEntry[],
|
|
316
|
+
): void => {
|
|
317
|
+
const name = getTypeName(fiber.type);
|
|
318
|
+
const kind = classifyFiber(fiber, registries);
|
|
319
|
+
if (kind === 'unit') {
|
|
320
|
+
countComponent(tally, 'K', name);
|
|
321
|
+
const slots = new Set<unknown>();
|
|
322
|
+
collectSlotProps(fiber.memoizedProps, slots, 0);
|
|
323
|
+
pushChildren(fiber, slots, stack);
|
|
324
|
+
return;
|
|
325
|
+
}
|
|
326
|
+
if (kind === 'transparent') {
|
|
327
|
+
incrementName(tally.transparent, name);
|
|
328
|
+
} else {
|
|
329
|
+
countComponent(tally, kind, name);
|
|
330
|
+
}
|
|
331
|
+
pushChildren(fiber, null, stack);
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
/**
|
|
335
|
+
* Handles one popped fiber: walk-through tags and lib-unit internals are
|
|
336
|
+
* only traversed, host-defined fibers are skipped, and app-scope fibers are counted.
|
|
337
|
+
*/
|
|
338
|
+
const visitEntry = (
|
|
339
|
+
entry: WalkEntry,
|
|
340
|
+
registries: KitRegistries,
|
|
341
|
+
tally: Tally,
|
|
342
|
+
stack: WalkEntry[],
|
|
343
|
+
): void => {
|
|
344
|
+
const { fiber, unitSlots } = entry;
|
|
345
|
+
if (!COUNTED_FIBER_TAGS.includes(fiber.tag)) {
|
|
346
|
+
pushChildren(fiber, unitSlots, stack);
|
|
347
|
+
return;
|
|
348
|
+
}
|
|
349
|
+
if (unitSlots && !unitSlots.has(fiber.memoizedProps)) {
|
|
350
|
+
pushChildren(fiber, unitSlots, stack);
|
|
351
|
+
return;
|
|
352
|
+
}
|
|
353
|
+
if (!isAppAuthored(getAuthorFiber(fiber))) {
|
|
354
|
+
pushChildren(fiber, null, stack);
|
|
355
|
+
return;
|
|
356
|
+
}
|
|
357
|
+
tally.appAuthoredSeen = true;
|
|
358
|
+
countFiber(fiber, registries, tally, stack);
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
/**
|
|
362
|
+
* Walks a screen's fiber tree from rootFiber (not its siblings) and tallies
|
|
363
|
+
* app-authored kit, RN and other components. Stops at maxFibers and marks truncated.
|
|
364
|
+
*/
|
|
365
|
+
const walkScreen = (
|
|
366
|
+
rootFiber: FiberLike,
|
|
367
|
+
registries: KitRegistries,
|
|
368
|
+
stopAt?: (fiber: FiberLike) => boolean,
|
|
369
|
+
maxFibers = MAX_FIBERS,
|
|
370
|
+
): WalkResult => {
|
|
371
|
+
const tally = createTally();
|
|
372
|
+
const stack: WalkEntry[] = [{ fiber: rootFiber, unitSlots: null }];
|
|
373
|
+
let fibers = 0;
|
|
374
|
+
let truncated = false;
|
|
375
|
+
for (let entry = stack.pop(); entry; entry = stack.pop()) {
|
|
376
|
+
if (fibers === maxFibers) {
|
|
377
|
+
truncated = true;
|
|
378
|
+
break;
|
|
379
|
+
}
|
|
380
|
+
fibers += 1;
|
|
381
|
+
if (entry.fiber !== rootFiber && stopAt?.(entry.fiber)) continue;
|
|
382
|
+
visitEntry(entry, registries, tally, stack);
|
|
383
|
+
}
|
|
384
|
+
return buildResult(tally, fibers, truncated);
|
|
385
|
+
};
|
|
386
|
+
|
|
387
|
+
export {
|
|
388
|
+
classifyFiber,
|
|
389
|
+
collectSlotProps,
|
|
390
|
+
createRegistries,
|
|
391
|
+
getAppId,
|
|
392
|
+
getAuthorFiber,
|
|
393
|
+
getCurrentFiber,
|
|
394
|
+
getTypeName,
|
|
395
|
+
isAdoptionEnabled,
|
|
396
|
+
isAppAuthored,
|
|
397
|
+
isHostDefined,
|
|
398
|
+
unwrapType,
|
|
399
|
+
walkScreen,
|
|
400
|
+
};
|
|
@@ -24,6 +24,7 @@ import Navigation from './Navigation';
|
|
|
24
24
|
import { ModalParams } from './types';
|
|
25
25
|
import BottomSheet from './BottomSheet';
|
|
26
26
|
import { isIPhoneDuo } from './utils';
|
|
27
|
+
import { AdoptionProbe } from './KitAdoption';
|
|
27
28
|
import { useModalLayout } from './modalLayout';
|
|
28
29
|
import { scheduleOnRN } from 'react-native-worklets';
|
|
29
30
|
|
|
@@ -182,7 +183,12 @@ const Modal: React.FC<ModalParams> = props => {
|
|
|
182
183
|
modalStyle,
|
|
183
184
|
]}
|
|
184
185
|
>
|
|
185
|
-
<
|
|
186
|
+
<AdoptionProbe
|
|
187
|
+
screenName={Component?.name || Component?.type?.name || 'Invalid'}
|
|
188
|
+
navigation={props.navigation}
|
|
189
|
+
>
|
|
190
|
+
<Component {...params} onRequestClose={onDismiss} />
|
|
191
|
+
</AdoptionProbe>
|
|
186
192
|
</Animated.View>
|
|
187
193
|
</KeyboardAvoidingView>
|
|
188
194
|
</Container>
|
|
@@ -30,6 +30,7 @@ import { HeaderBackground } from './Components/HeaderBackground';
|
|
|
30
30
|
import { useScreenLayout } from '../Layout';
|
|
31
31
|
import { getHeaderRowOptions } from '../Layout/utils';
|
|
32
32
|
import { ReservedRegionsProvider } from '../Layout/ReservedRegions';
|
|
33
|
+
import { AdoptionBadge } from './KitAdoption';
|
|
33
34
|
|
|
34
35
|
const Stack = createStackNavigator();
|
|
35
36
|
|
|
@@ -86,6 +87,7 @@ const NavigationContainer: React.FC<NavigationContainerProps> = ({
|
|
|
86
87
|
initialParams={initialParams}
|
|
87
88
|
localize={localize}
|
|
88
89
|
/>
|
|
90
|
+
<AdoptionBadge />
|
|
89
91
|
</MiniAppContext.Provider>
|
|
90
92
|
</ReservedRegionsProvider>
|
|
91
93
|
</SafeAreaProvider>
|
|
@@ -8,6 +8,7 @@ import { SwipeBackHaptic } from './Components/SwipeBackHaptic';
|
|
|
8
8
|
import { useAppState } from './hooks';
|
|
9
9
|
import { createScreenTracking } from './StackScreen.tracking';
|
|
10
10
|
import { TooltipPortalHost, TooltipPortalProvider } from './TooltipPortal';
|
|
11
|
+
import { AdoptionProbe } from './KitAdoption';
|
|
11
12
|
|
|
12
13
|
const runAfterInteractions = InteractionManager.runAfterInteractions;
|
|
13
14
|
|
|
@@ -283,7 +284,9 @@ const StackScreen: React.FC<any> = props => {
|
|
|
283
284
|
}}
|
|
284
285
|
>
|
|
285
286
|
<TooltipPortalProvider>
|
|
286
|
-
<
|
|
287
|
+
<AdoptionProbe screenName={screenName} navigation={props.navigation}>
|
|
288
|
+
<Component heightHeader={heightHeader} {...data} />
|
|
289
|
+
</AdoptionProbe>
|
|
287
290
|
<TooltipPortalHost />
|
|
288
291
|
<SwipeBackHaptic />
|
|
289
292
|
</TooltipPortalProvider>
|