@momo-kits/foundation 0.169.6 → 0.169.7-beta.10
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.hooks.ts +8 -8
- package/Application/BottomSheet.parts.tsx +34 -44
- package/Application/BottomSheet.tsx +37 -40
- package/Application/BottomTab/ActiveIndicator.tsx +14 -15
- package/Application/BottomTab/BottomTabBar.tsx +62 -49
- package/Application/BottomTab/BottomTabInset.tsx +4 -8
- package/Application/BottomTab/CurvedContainer.tsx +12 -14
- package/Application/BottomTab/CustomBottomTabItem.tsx +13 -13
- package/Application/BottomTab/DuoBarCompression.tsx +19 -13
- package/Application/BottomTab/DuoRailTabBar.tsx +24 -20
- package/Application/BottomTab/DuoTabBar.tsx +10 -8
- package/Application/BottomTab/TabBarIcon.tsx +29 -15
- package/Application/BottomTab/TabScreen.tsx +5 -1
- package/Application/BottomTab/index.tsx +30 -33
- package/Application/BottomTab/metrics.ts +23 -23
- package/Application/BottomTab/tabItems.ts +1 -1
- package/Application/BottomTab/usePressLift.ts +2 -2
- package/Application/Components/BackgroundImageView.tsx +0 -4
- package/Application/Components/DuoGlassBackground.tsx +4 -5
- package/Application/Components/HeaderAnimated.tsx +0 -4
- package/Application/Components/HeaderBackground.tsx +0 -4
- package/Application/Components/HeaderExtendHeader.tsx +10 -11
- package/Application/Components/HeaderLeft.tsx +1 -2
- package/Application/Components/HeaderRight.tsx +1 -2
- package/Application/Components/HeaderTitle.shared.ts +9 -11
- package/Application/Components/HeaderTitle.tsx +3 -4
- package/Application/Components/HeaderToolkitAction.tsx +20 -17
- package/Application/Components/NavigationButton.tsx +1 -2
- package/Application/Components/SearchHeader.tsx +10 -17
- package/Application/Components/TitleJourney.tsx +2 -2
- package/Application/Components/TitleLocation.tsx +2 -2
- package/Application/Components/TitleUser.tsx +2 -2
- package/Application/Components/useToolkitActions.tsx +32 -17
- package/Application/KitAdoption/AdoptionBadge.tsx +119 -0
- package/Application/KitAdoption/AdoptionProbe.tsx +166 -0
- package/Application/KitAdoption/AdoptionStore.ts +80 -0
- package/Application/KitAdoption/Authorship/utils.ts +137 -0
- package/Application/KitAdoption/Slots/utils.ts +68 -0
- package/Application/KitAdoption/constants.ts +120 -0
- package/Application/KitAdoption/index.ts +12 -0
- package/Application/KitAdoption/styles.ts +55 -0
- package/Application/KitAdoption/types.ts +61 -0
- package/Application/KitAdoption/utils.ts +376 -0
- package/Application/Localize.ts +0 -7
- package/Application/ModalScreen.tsx +10 -10
- package/Application/Navigation.ts +16 -14
- package/Application/NavigationContainer.tsx +18 -19
- package/Application/Navigator.ts +48 -20
- package/Application/ScaleSizeProvider.tsx +6 -13
- package/Application/StackScreen.tracking.ts +33 -11
- package/Application/StackScreen.tsx +54 -10
- package/Application/TooltipPortal.tsx +5 -12
- package/Application/WidgetContainer.tsx +4 -10
- package/Application/__tests__/KitAdoption.integration.test.tsx +253 -0
- package/Application/__tests__/KitAdoption.test.ts +283 -0
- package/Application/__tests__/KitAdoptionAuthorship.test.ts +132 -0
- package/Application/__tests__/KitAdoptionStore.test.ts +110 -0
- package/Application/__tests__/KitAdoptionStructural.test.ts +193 -0
- package/Application/__tests__/modalLayout.test.ts +128 -0
- package/Application/modalLayout.ts +14 -13
- package/Application/navigationOptions.tsx +17 -21
- package/Badge/Badge.tsx +0 -5
- package/Badge/BadgeDot.tsx +0 -1
- package/Badge/BadgeDotAnimation.tsx +7 -15
- package/Badge/BadgeRibbon.tsx +38 -46
- package/Badge/Shape.tsx +0 -2
- package/Badge/types.ts +2 -2
- package/Button/index.tsx +16 -5
- package/Button/utils.ts +15 -9
- package/CheckBox/index.tsx +0 -4
- package/Consts/theme.ts +10 -10
- package/Context/index.ts +1 -5
- package/Divider/DashDivider.tsx +0 -1
- package/Divider/index.tsx +0 -4
- package/FoundationList/index.tsx +0 -4
- package/Icon/index.tsx +6 -6
- package/Icon/types.ts +1 -1
- package/IconButton/index.tsx +12 -4
- package/Image/index.tsx +7 -9
- package/Input/Input.tsx +9 -8
- package/Input/InputDropDown.tsx +12 -10
- package/Input/InputMoney.tsx +8 -12
- package/Input/InputMoneyView.tsx +7 -14
- package/Input/InputOTP.tsx +33 -35
- package/Input/InputPhoneNumber.tsx +9 -8
- package/Input/InputSearch.tsx +6 -4
- package/Input/InputTextArea.tsx +0 -4
- package/Input/SearchTextTyping.tsx +0 -4
- package/Input/SystemTextInput.tsx +2 -7
- package/Input/TextTyping.tsx +0 -4
- package/Input/common.tsx +12 -26
- package/Input/index.tsx +20 -17
- package/Input/moneyUtils.ts +4 -10
- package/Input/styles.ts +5 -0
- package/Input/utils.ts +12 -14
- package/Layout/Card.tsx +13 -16
- package/Layout/FloatingButton.tsx +0 -4
- package/Layout/Item.tsx +2 -6
- package/Layout/ItemList.tsx +7 -9
- package/Layout/ItemSectionList.tsx +1 -2
- package/Layout/ReservedRegions.tsx +25 -25
- package/Layout/Screen.tsx +58 -56
- package/Layout/Section.tsx +13 -17
- package/Layout/TrackingScope.tsx +0 -1
- package/Layout/constants.ts +19 -0
- package/Layout/screenContent.tsx +2 -4
- package/Layout/screenFooter.tsx +18 -28
- package/Layout/screenHeader.tsx +31 -34
- package/Layout/screenQuickReturn.tsx +20 -17
- package/Layout/screenScroll.ts +7 -4
- package/Layout/screenTypes.ts +2 -3
- package/Layout/types.ts +41 -3
- package/Layout/useGridSystem.ts +49 -0
- package/Layout/utils.ts +37 -38
- package/Loader/DotLoader.tsx +0 -4
- package/Loader/Loader.tsx +0 -4
- package/Loader/ProgressBar.tsx +0 -1
- package/Loader/Spinner.tsx +0 -4
- package/Loader/utils.ts +7 -11
- package/Pagination/Dot.tsx +0 -4
- package/Pagination/PaginationDot.tsx +0 -1
- package/Pagination/PaginationNumber.tsx +3 -5
- package/Pagination/PaginationScroll.tsx +0 -5
- package/Pagination/PaginationWhiteDot.tsx +0 -1
- package/Pagination/index.tsx +0 -4
- package/Pagination/types.ts +2 -2
- package/Popup/PopupNotify.tsx +48 -44
- package/Popup/PopupPromotion.tsx +7 -4
- package/Popup/types.ts +4 -4
- package/Popup/utils.ts +0 -4
- package/Radio/index.tsx +0 -4
- package/Skeleton/index.tsx +0 -8
- package/Switch/index.tsx +0 -1
- package/Tag/index.tsx +1 -5
- package/Tag/types.ts +3 -3
- package/Text/index.tsx +3 -14
- package/Text/styles.ts +1 -1
- package/Text/types.ts +6 -1
- package/Text/utils.ts +4 -20
- package/Title/index.tsx +32 -41
- package/ViewBoundary/index.tsx +2 -4
- package/index.ts +6 -0
- package/package.json +1 -1
|
@@ -0,0 +1,166 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import { InteractionManager, Platform } from 'react-native';
|
|
3
|
+
|
|
4
|
+
import { MiniAppContext } from '../../Context';
|
|
5
|
+
import { adoptionStore } from './AdoptionStore';
|
|
6
|
+
import { SETTLE_MS } from './constants';
|
|
7
|
+
import type {
|
|
8
|
+
AdoptionMeasureRef,
|
|
9
|
+
FiberLike,
|
|
10
|
+
KitRegistries,
|
|
11
|
+
ScreenSnapshot,
|
|
12
|
+
WalkResult,
|
|
13
|
+
} from './types';
|
|
14
|
+
import {
|
|
15
|
+
createRegistries,
|
|
16
|
+
getAppId,
|
|
17
|
+
getCurrentFiber,
|
|
18
|
+
isAdoptionTrackingEnabled,
|
|
19
|
+
walkScreen,
|
|
20
|
+
} from './utils';
|
|
21
|
+
|
|
22
|
+
type NavigationLike = {
|
|
23
|
+
addListener?: (event: string, callback: () => void) => () => void;
|
|
24
|
+
};
|
|
25
|
+
|
|
26
|
+
type ProbeProps = {
|
|
27
|
+
screenName: string;
|
|
28
|
+
navigation?: NavigationLike;
|
|
29
|
+
measureRef?: AdoptionMeasureRef;
|
|
30
|
+
children: React.ReactNode;
|
|
31
|
+
};
|
|
32
|
+
|
|
33
|
+
let registriesCache: KitRegistries | undefined;
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Kit and RN identity sets, built on first use. Lazy require breaks the cycle
|
|
37
|
+
* between the foundation index and the Application screens that render this probe.
|
|
38
|
+
*/
|
|
39
|
+
const getRegistries = (): KitRegistries => {
|
|
40
|
+
if (!registriesCache) {
|
|
41
|
+
const kitModule = require('../../index') as Record<string, unknown>;
|
|
42
|
+
const rnModule = require('react-native') as Record<string, unknown>;
|
|
43
|
+
registriesCache = createRegistries([kitModule], rnModule);
|
|
44
|
+
}
|
|
45
|
+
return registriesCache;
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
/** Foundation version from its package.json, or undefined if it cannot be read. */
|
|
49
|
+
const readFoundationVersion = (): string | undefined => {
|
|
50
|
+
try {
|
|
51
|
+
const pkg = require('../../package.json') as { version?: string };
|
|
52
|
+
return pkg.version;
|
|
53
|
+
} catch {
|
|
54
|
+
return undefined;
|
|
55
|
+
}
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
const createErrorResult = (): WalkResult => ({
|
|
59
|
+
counts: { K: 0, R: 0, O: 0 },
|
|
60
|
+
kit: {},
|
|
61
|
+
rn: {},
|
|
62
|
+
other: {},
|
|
63
|
+
top_transparent: [],
|
|
64
|
+
fibers: 0,
|
|
65
|
+
truncated: false,
|
|
66
|
+
status: 'error',
|
|
67
|
+
});
|
|
68
|
+
|
|
69
|
+
/** Stops the walk at nested probes so each screen is measured on its own. */
|
|
70
|
+
const isProbeFiber = (fiber: FiberLike): boolean =>
|
|
71
|
+
fiber.type === AdoptionProbeDev;
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Wrapper around a screen. In dev it measures the fiber tree after the first
|
|
75
|
+
* interactions settle and on focus; when tracking is enabled it exposes a measure for screen-load events.
|
|
76
|
+
*/
|
|
77
|
+
class AdoptionProbeDev extends React.Component<ProbeProps> {
|
|
78
|
+
static contextType = MiniAppContext;
|
|
79
|
+
|
|
80
|
+
private timer: ReturnType<typeof setTimeout> | undefined;
|
|
81
|
+
private interaction: { cancel: () => void } | undefined;
|
|
82
|
+
private unsubscribeFocus: (() => void) | undefined;
|
|
83
|
+
|
|
84
|
+
componentDidMount(): void {
|
|
85
|
+
const { measureRef } = this.props;
|
|
86
|
+
if (measureRef && (__DEV__ || isAdoptionTrackingEnabled(this.context))) {
|
|
87
|
+
measureRef.current = this.measureForTracking;
|
|
88
|
+
}
|
|
89
|
+
if (!__DEV__) return;
|
|
90
|
+
adoptionStore.setActive(this.measure);
|
|
91
|
+
this.unsubscribeFocus = this.props.navigation?.addListener?.(
|
|
92
|
+
'focus',
|
|
93
|
+
this.handleFocus,
|
|
94
|
+
);
|
|
95
|
+
this.interaction = InteractionManager.runAfterInteractions(() => {
|
|
96
|
+
this.timer = setTimeout(this.measure, SETTLE_MS);
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
componentWillUnmount(): void {
|
|
101
|
+
const { measureRef } = this.props;
|
|
102
|
+
if (measureRef?.current === this.measureForTracking) {
|
|
103
|
+
measureRef.current = undefined;
|
|
104
|
+
}
|
|
105
|
+
if (!__DEV__) return;
|
|
106
|
+
if (this.timer !== undefined) clearTimeout(this.timer);
|
|
107
|
+
this.interaction?.cancel();
|
|
108
|
+
this.unsubscribeFocus?.();
|
|
109
|
+
adoptionStore.clearActive(this.measure);
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
render(): React.ReactNode {
|
|
113
|
+
return this.props.children;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
private handleFocus = (): void => {
|
|
117
|
+
adoptionStore.setActive(this.measure);
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
private measure = (): void => {
|
|
121
|
+
const startedAt = Date.now();
|
|
122
|
+
let result: WalkResult | undefined;
|
|
123
|
+
try {
|
|
124
|
+
result = this.walk();
|
|
125
|
+
} catch {
|
|
126
|
+
result = createErrorResult();
|
|
127
|
+
}
|
|
128
|
+
if (result) this.save(result, startedAt);
|
|
129
|
+
};
|
|
130
|
+
|
|
131
|
+
/** Measures without saving to the store; undefined on error or when there is no content. */
|
|
132
|
+
private measureForTracking = (): WalkResult | undefined => {
|
|
133
|
+
try {
|
|
134
|
+
return this.walk();
|
|
135
|
+
} catch {
|
|
136
|
+
return undefined;
|
|
137
|
+
}
|
|
138
|
+
};
|
|
139
|
+
|
|
140
|
+
private walk(): WalkResult | undefined {
|
|
141
|
+
const self = getCurrentFiber(
|
|
142
|
+
(this as unknown as { _reactInternals: FiberLike })._reactInternals,
|
|
143
|
+
);
|
|
144
|
+
const root = self.child;
|
|
145
|
+
if (!root) return undefined;
|
|
146
|
+
return walkScreen(root, getRegistries(), isProbeFiber, undefined, !__DEV__);
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
private save(result: WalkResult, startedAt: number): void {
|
|
150
|
+
const snapshot: ScreenSnapshot = {
|
|
151
|
+
...result,
|
|
152
|
+
app_id: getAppId(this.context),
|
|
153
|
+
screen_name: this.props.screenName,
|
|
154
|
+
foundation_version: readFoundationVersion(),
|
|
155
|
+
platform: Platform.OS,
|
|
156
|
+
ts: Date.now(),
|
|
157
|
+
walk_ms: Date.now() - startedAt,
|
|
158
|
+
};
|
|
159
|
+
adoptionStore.save(snapshot);
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
const AdoptionProbe = AdoptionProbeDev;
|
|
164
|
+
|
|
165
|
+
export type { NavigationLike, ProbeProps };
|
|
166
|
+
export { AdoptionProbe, AdoptionProbeDev };
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
import type { ScreenSnapshot } from './types';
|
|
2
|
+
|
|
3
|
+
type Listener = () => void;
|
|
4
|
+
type Measure = () => void;
|
|
5
|
+
|
|
6
|
+
/** Total classified nodes in a snapshot, used to keep the richer measurement. */
|
|
7
|
+
const getTotal = (snapshot: ScreenSnapshot): number =>
|
|
8
|
+
snapshot.counts.K + snapshot.counts.R + snapshot.counts.O;
|
|
9
|
+
|
|
10
|
+
const getKey = (snapshot: ScreenSnapshot): string =>
|
|
11
|
+
`${snapshot.app_id}/${snapshot.screen_name}`;
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* In-memory, dev-only store of per-screen snapshots. Keeps one snapshot per
|
|
15
|
+
* app and screen (the one with the most nodes) and holds the active measurer.
|
|
16
|
+
*/
|
|
17
|
+
class AdoptionStore {
|
|
18
|
+
private snapshots = new Map<string, ScreenSnapshot>();
|
|
19
|
+
private listeners = new Set<Listener>();
|
|
20
|
+
private latestKey: string | undefined;
|
|
21
|
+
private activeMeasure: Measure | undefined;
|
|
22
|
+
|
|
23
|
+
/** Stores a snapshot unless the stored one for the same screen is larger. */
|
|
24
|
+
save(snapshot: ScreenSnapshot): void {
|
|
25
|
+
const key = getKey(snapshot);
|
|
26
|
+
const existing = this.snapshots.get(key);
|
|
27
|
+
if (!existing || getTotal(existing) <= getTotal(snapshot)) {
|
|
28
|
+
this.snapshots.set(key, snapshot);
|
|
29
|
+
}
|
|
30
|
+
this.latestKey = key;
|
|
31
|
+
this.notify();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
getAll(): ScreenSnapshot[] {
|
|
35
|
+
return Array.from(this.snapshots.values());
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
/** The screen saved most recently, or undefined when nothing is saved. */
|
|
39
|
+
getLatest(): ScreenSnapshot | undefined {
|
|
40
|
+
return this.latestKey === undefined
|
|
41
|
+
? undefined
|
|
42
|
+
: this.snapshots.get(this.latestKey);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
clear(): void {
|
|
46
|
+
this.snapshots.clear();
|
|
47
|
+
this.latestKey = undefined;
|
|
48
|
+
this.notify();
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
/** Registers a change listener and returns its unsubscribe function. */
|
|
52
|
+
subscribe(listener: Listener): () => void {
|
|
53
|
+
this.listeners.add(listener);
|
|
54
|
+
return () => {
|
|
55
|
+
this.listeners.delete(listener);
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
setActive(measure: Measure): void {
|
|
60
|
+
this.activeMeasure = measure;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/** Clears the active measurer only if it is still the given one. */
|
|
64
|
+
clearActive(measure: Measure): void {
|
|
65
|
+
if (this.activeMeasure === measure) this.activeMeasure = undefined;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
remeasureActive(): void {
|
|
69
|
+
this.activeMeasure?.();
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
private notify(): void {
|
|
73
|
+
this.listeners.forEach(listener => listener());
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
const adoptionStore = new AdoptionStore();
|
|
78
|
+
|
|
79
|
+
export type { Listener, Measure };
|
|
80
|
+
export { AdoptionStore, adoptionStore };
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
import {
|
|
2
|
+
FIBER_TAG,
|
|
3
|
+
LOCAL_SOURCE_KEY,
|
|
4
|
+
REACT_FACTORY_FRAME_NAMES,
|
|
5
|
+
REACT_STACK_BOTTOM_FRAME,
|
|
6
|
+
} from '../constants';
|
|
7
|
+
import type { FiberLike } from '../types';
|
|
8
|
+
|
|
9
|
+
const HTTP_ORIGIN_PATTERN = /^(https?:\/\/[^/]+)/i;
|
|
10
|
+
const FILE_URL_PATTERN = /^file:\/\//i;
|
|
11
|
+
const LINE_COL_SUFFIX_PATTERN = /:\d+(?::\d+)?$/;
|
|
12
|
+
const PAREN_LOCATION_PATTERN = /\((?:address at )?(.+)\)\s*$/;
|
|
13
|
+
const BARE_LOCATION_PATTERN = /^\s*at\s+(.+?)\s*$/;
|
|
14
|
+
const FRAME_NAME_PATTERN = /^\s*at\s+([^\s(]+)/;
|
|
15
|
+
const NATIVE_FRAME_LOCATION = 'native';
|
|
16
|
+
|
|
17
|
+
const creatorSourceCache = new WeakMap<object, string | null>();
|
|
18
|
+
let hostSource: string | undefined;
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Bundle identity of a frame location: the origin for http(s), the directory
|
|
22
|
+
* for file URLs, and one shared key for bare source paths (jest).
|
|
23
|
+
*/
|
|
24
|
+
const getSourceKey = (location: string): string => {
|
|
25
|
+
const origin = HTTP_ORIGIN_PATTERN.exec(location);
|
|
26
|
+
if (origin) return origin[1];
|
|
27
|
+
if (!FILE_URL_PATTERN.test(location)) return LOCAL_SOURCE_KEY;
|
|
28
|
+
const path = location.replace(LINE_COL_SUFFIX_PATTERN, '');
|
|
29
|
+
return path.slice(0, path.lastIndexOf('/'));
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
/** Location (`url:line:col`) of a stack frame line, or undefined when it has none. */
|
|
33
|
+
const parseFrameLocation = (line: string): string | undefined => {
|
|
34
|
+
const match =
|
|
35
|
+
PAREN_LOCATION_PATTERN.exec(line) ?? BARE_LOCATION_PATTERN.exec(line);
|
|
36
|
+
return match?.[1];
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
/** Reads `.stack` of a debug stack value without throwing; undefined if it is not a string. */
|
|
40
|
+
const readStack = (debugStack: unknown): string | undefined => {
|
|
41
|
+
try {
|
|
42
|
+
const stack = (debugStack as { stack?: unknown } | null)?.stack;
|
|
43
|
+
return typeof stack === 'string' ? stack : undefined;
|
|
44
|
+
} catch {
|
|
45
|
+
return undefined;
|
|
46
|
+
}
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** Frame lines below the header, cut at React's bottom-frame marker. */
|
|
50
|
+
const getFrameLines = (stack: string): string[] => {
|
|
51
|
+
const lines = stack.split('\n').slice(1);
|
|
52
|
+
const bottom = lines.findIndex(line =>
|
|
53
|
+
line.includes(REACT_STACK_BOTTOM_FRAME),
|
|
54
|
+
);
|
|
55
|
+
return bottom === -1 ? lines : lines.slice(0, bottom);
|
|
56
|
+
};
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* True for frames that cannot be the element's creator: React's own factory
|
|
60
|
+
* wrappers and native frames (e.g. `apply (native)`), which carry no bundle.
|
|
61
|
+
*/
|
|
62
|
+
const isSkippedFrame = (line: string): boolean => {
|
|
63
|
+
const name = FRAME_NAME_PATTERN.exec(line)?.[1] ?? '';
|
|
64
|
+
const isNative = parseFrameLocation(line) === NATIVE_FRAME_LOCATION;
|
|
65
|
+
return isNative || REACT_FACTORY_FRAME_NAMES.includes(name);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
/** Source key of the frame that created the element, skipping React's factory frames. */
|
|
69
|
+
const findCreatorSource = (stack: string): string | undefined => {
|
|
70
|
+
const lines = getFrameLines(stack).slice(1);
|
|
71
|
+
const index = lines.findIndex(line => !isSkippedFrame(line));
|
|
72
|
+
const location = index === -1 ? undefined : parseFrameLocation(lines[index]);
|
|
73
|
+
return location === undefined ? undefined : getSourceKey(location);
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
/**
|
|
77
|
+
* Source key of the code that created an element, read from its debug stack.
|
|
78
|
+
* Cached per stack object since fibers and alternates share it.
|
|
79
|
+
*/
|
|
80
|
+
const getCreatorSource = (debugStack: unknown): string | undefined => {
|
|
81
|
+
const isCacheable = typeof debugStack === 'object' && debugStack !== null;
|
|
82
|
+
if (isCacheable && creatorSourceCache.has(debugStack)) {
|
|
83
|
+
return creatorSourceCache.get(debugStack) ?? undefined;
|
|
84
|
+
}
|
|
85
|
+
const stack = readStack(debugStack);
|
|
86
|
+
const source = stack === undefined ? undefined : findCreatorSource(stack);
|
|
87
|
+
if (isCacheable) creatorSourceCache.set(debugStack, source ?? null);
|
|
88
|
+
return source;
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
/** Source key of the bundle running this module (the host), cached after the first read. */
|
|
92
|
+
const getHostSource = (): string => {
|
|
93
|
+
if (hostSource !== undefined) return hostSource;
|
|
94
|
+
const line = (new Error().stack ?? '').split('\n')[1] ?? '';
|
|
95
|
+
const location = parseFrameLocation(line);
|
|
96
|
+
hostSource =
|
|
97
|
+
location === undefined ? LOCAL_SOURCE_KEY : getSourceKey(location);
|
|
98
|
+
return hostSource;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
/**
|
|
102
|
+
* The fiber whose stack decides authorship. React creates the inner fiber of a
|
|
103
|
+
* MemoComponent itself, with no stack, so it inherits its memo parent's.
|
|
104
|
+
*/
|
|
105
|
+
const getAuthorFiber = (fiber: FiberLike): FiberLike =>
|
|
106
|
+
fiber.return?.tag === FIBER_TAG.MemoComponent && fiber.return.child === fiber
|
|
107
|
+
? fiber.return
|
|
108
|
+
: fiber;
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* True when the fiber has no dev stack (prod jsx) or its creator frame lives
|
|
112
|
+
* in a bundle other than the host's. Unparseable stacks count as host.
|
|
113
|
+
*/
|
|
114
|
+
const isAppAuthored = (fiber: FiberLike): boolean => {
|
|
115
|
+
if (!fiber._debugStack) return true;
|
|
116
|
+
const source = getCreatorSource(fiber._debugStack);
|
|
117
|
+
return source !== undefined && source !== getHostSource();
|
|
118
|
+
};
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* True when the fiber's first child was created by host code and is owned by
|
|
122
|
+
* this fiber, which marks a host-internal wrapper.
|
|
123
|
+
*/
|
|
124
|
+
const isHostDefined = (fiber: FiberLike): boolean => {
|
|
125
|
+
const child = fiber.child;
|
|
126
|
+
return !!child && !isAppAuthored(child) && child._debugOwner === fiber;
|
|
127
|
+
};
|
|
128
|
+
|
|
129
|
+
export {
|
|
130
|
+
getAuthorFiber,
|
|
131
|
+
getCreatorSource,
|
|
132
|
+
getHostSource,
|
|
133
|
+
getSourceKey,
|
|
134
|
+
isAppAuthored,
|
|
135
|
+
isHostDefined,
|
|
136
|
+
parseFrameLocation,
|
|
137
|
+
};
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
import { Fragment, isValidElement } from 'react';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
CELL_WRAPPER_SCAN_DEPTH,
|
|
5
|
+
FIBER_TAG,
|
|
6
|
+
SLOT_SCAN_DEPTH,
|
|
7
|
+
} from '../constants';
|
|
8
|
+
import type { FiberLike } from '../types';
|
|
9
|
+
|
|
10
|
+
type PropBag = Record<string, unknown>;
|
|
11
|
+
|
|
12
|
+
/** True for non-null objects, the only values that can hold props. */
|
|
13
|
+
const isPropBag = (value: unknown): value is PropBag =>
|
|
14
|
+
typeof value === 'object' && value !== null;
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Adds the props of React elements that the app passed into a lib unit, found
|
|
18
|
+
* among the props and in arrays up to SLOT_SCAN_DEPTH. Never looks inside an element.
|
|
19
|
+
*/
|
|
20
|
+
const collectSlotProps = (
|
|
21
|
+
props: unknown,
|
|
22
|
+
set: Set<unknown>,
|
|
23
|
+
depth: number,
|
|
24
|
+
): void => {
|
|
25
|
+
if (depth > SLOT_SCAN_DEPTH || !isPropBag(props)) return;
|
|
26
|
+
Object.values(props).forEach(value => {
|
|
27
|
+
if (isValidElement(value)) {
|
|
28
|
+
set.add(value.props);
|
|
29
|
+
if (value.type === Fragment)
|
|
30
|
+
collectSlotProps(value.props, set, depth + 1);
|
|
31
|
+
} else if (Array.isArray(value)) {
|
|
32
|
+
collectSlotProps(value, set, depth + 1);
|
|
33
|
+
}
|
|
34
|
+
});
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
/**
|
|
38
|
+
* True for a VirtualizedList cell, detected by its props shape (`cellKey` and a
|
|
39
|
+
* numeric `index`) because release bundles may minify component names.
|
|
40
|
+
*/
|
|
41
|
+
const isListCell = (fiber: FiberLike): boolean => {
|
|
42
|
+
const props = fiber.memoizedProps;
|
|
43
|
+
if (fiber.tag !== FIBER_TAG.ClassComponent || !isPropBag(props)) return false;
|
|
44
|
+
return 'cellKey' in props && typeof props.index === 'number';
|
|
45
|
+
};
|
|
46
|
+
|
|
47
|
+
/** True for the context provider and fragment fibers that sit above a cell's wrapper view. */
|
|
48
|
+
const isCellPassThrough = (fiber: FiberLike): boolean =>
|
|
49
|
+
fiber.tag === FIBER_TAG.ContextProvider || fiber.tag === FIBER_TAG.Fragment;
|
|
50
|
+
|
|
51
|
+
/**
|
|
52
|
+
* Adds the rendered item and separator of a list cell to the unit's slots. They
|
|
53
|
+
* come from `renderItem`, so no app-passed props hold them; they are the
|
|
54
|
+
* children of the wrapper under the cell's providers.
|
|
55
|
+
*/
|
|
56
|
+
const collectCellSlots = (cell: FiberLike, unitSlots: Set<unknown>): void => {
|
|
57
|
+
let wrapper = cell.child;
|
|
58
|
+
for (
|
|
59
|
+
let step = 0;
|
|
60
|
+
wrapper && isCellPassThrough(wrapper) && step < CELL_WRAPPER_SCAN_DEPTH;
|
|
61
|
+
step += 1
|
|
62
|
+
) {
|
|
63
|
+
wrapper = wrapper.child;
|
|
64
|
+
}
|
|
65
|
+
if (wrapper) collectSlotProps(wrapper.memoizedProps, unitSlots, 0);
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
export { collectCellSlots, collectSlotProps, isListCell };
|
|
@@ -0,0 +1,120 @@
|
|
|
1
|
+
const SETTLE_MS = 3000;
|
|
2
|
+
const MAX_FIBERS = 20000;
|
|
3
|
+
const SLOT_SCAN_DEPTH = 4;
|
|
4
|
+
const CELL_WRAPPER_SCAN_DEPTH = 3;
|
|
5
|
+
const TOP_TRANSPARENT_LIMIT = 10;
|
|
6
|
+
|
|
7
|
+
// Generic lib names (Badge, Title, Logo, ...) are left out on purpose so app components with the same name are not counted as kit.
|
|
8
|
+
const LIB_UNIT_NAMES: string[] = [
|
|
9
|
+
'AutoComplete',
|
|
10
|
+
'Avatar',
|
|
11
|
+
'Calendar',
|
|
12
|
+
'Carousel',
|
|
13
|
+
'Chip',
|
|
14
|
+
'Collapse',
|
|
15
|
+
'DateTimePicker',
|
|
16
|
+
'ProgressInfo',
|
|
17
|
+
'Rating',
|
|
18
|
+
'Slider',
|
|
19
|
+
'Stepper',
|
|
20
|
+
'Steps',
|
|
21
|
+
'SuggestAction',
|
|
22
|
+
'Swipe',
|
|
23
|
+
'TabView',
|
|
24
|
+
'TabBar',
|
|
25
|
+
'CardTabBar',
|
|
26
|
+
'ScrollableTabBar',
|
|
27
|
+
'HeaderSliderBanner',
|
|
28
|
+
'TrustBanner',
|
|
29
|
+
'Uploader',
|
|
30
|
+
];
|
|
31
|
+
|
|
32
|
+
const RN_PRIMITIVE_NAMES: string[] = [
|
|
33
|
+
'View',
|
|
34
|
+
'Text',
|
|
35
|
+
'Image',
|
|
36
|
+
'ImageBackground',
|
|
37
|
+
'TextInput',
|
|
38
|
+
'ScrollView',
|
|
39
|
+
'FlatList',
|
|
40
|
+
'SectionList',
|
|
41
|
+
'VirtualizedList',
|
|
42
|
+
'TouchableOpacity',
|
|
43
|
+
'TouchableHighlight',
|
|
44
|
+
'TouchableWithoutFeedback',
|
|
45
|
+
'Pressable',
|
|
46
|
+
'Switch',
|
|
47
|
+
'ActivityIndicator',
|
|
48
|
+
'Modal',
|
|
49
|
+
'KeyboardAvoidingView',
|
|
50
|
+
'RefreshControl',
|
|
51
|
+
];
|
|
52
|
+
|
|
53
|
+
/** Foundation exports that come from third-party or RN and must not count as kit. */
|
|
54
|
+
const FOUNDATION_REEXPORT_NAMES: string[] = [
|
|
55
|
+
'TouchableOpacity',
|
|
56
|
+
'LinearGradient',
|
|
57
|
+
'SafeAreaView',
|
|
58
|
+
'SafeAreaProvider',
|
|
59
|
+
'SafeAreaConsumer',
|
|
60
|
+
'SafeAreaInsetsContext',
|
|
61
|
+
'SafeAreaFrameContext',
|
|
62
|
+
'withSafeAreaInsets',
|
|
63
|
+
];
|
|
64
|
+
|
|
65
|
+
const FIBER_TAG = {
|
|
66
|
+
FunctionComponent: 0,
|
|
67
|
+
ClassComponent: 1,
|
|
68
|
+
HostRoot: 3,
|
|
69
|
+
HostComponent: 5,
|
|
70
|
+
HostText: 6,
|
|
71
|
+
Fragment: 7,
|
|
72
|
+
ContextProvider: 10,
|
|
73
|
+
ForwardRef: 11,
|
|
74
|
+
MemoComponent: 14,
|
|
75
|
+
SimpleMemoComponent: 15,
|
|
76
|
+
} as const;
|
|
77
|
+
|
|
78
|
+
/** Fiber tags that are component fibers; every other tag is walked through without counting. */
|
|
79
|
+
const COUNTED_FIBER_TAGS: number[] = [
|
|
80
|
+
FIBER_TAG.FunctionComponent,
|
|
81
|
+
FIBER_TAG.ClassComponent,
|
|
82
|
+
FIBER_TAG.HostComponent,
|
|
83
|
+
FIBER_TAG.ForwardRef,
|
|
84
|
+
FIBER_TAG.SimpleMemoComponent,
|
|
85
|
+
];
|
|
86
|
+
|
|
87
|
+
const HOST_RN_PREFIXES: string[] = ['RCT', 'Android'];
|
|
88
|
+
|
|
89
|
+
const LOCAL_SOURCE_KEY = 'local';
|
|
90
|
+
const REACT_STACK_BOTTOM_FRAME = 'react-stack-bottom-frame';
|
|
91
|
+
const REACT_FACTORY_FRAME_NAMES: string[] = [
|
|
92
|
+
'jsx',
|
|
93
|
+
'jsxs',
|
|
94
|
+
'jsxDEV',
|
|
95
|
+
'jsxDEVImpl',
|
|
96
|
+
'createElement',
|
|
97
|
+
'jsxProd',
|
|
98
|
+
];
|
|
99
|
+
|
|
100
|
+
const REACT_MEMO_TYPE = Symbol.for('react.memo');
|
|
101
|
+
const REACT_FORWARD_REF_TYPE = Symbol.for('react.forward_ref');
|
|
102
|
+
|
|
103
|
+
export {
|
|
104
|
+
CELL_WRAPPER_SCAN_DEPTH,
|
|
105
|
+
COUNTED_FIBER_TAGS,
|
|
106
|
+
FIBER_TAG,
|
|
107
|
+
FOUNDATION_REEXPORT_NAMES,
|
|
108
|
+
HOST_RN_PREFIXES,
|
|
109
|
+
LIB_UNIT_NAMES,
|
|
110
|
+
LOCAL_SOURCE_KEY,
|
|
111
|
+
MAX_FIBERS,
|
|
112
|
+
REACT_FACTORY_FRAME_NAMES,
|
|
113
|
+
REACT_FORWARD_REF_TYPE,
|
|
114
|
+
REACT_MEMO_TYPE,
|
|
115
|
+
REACT_STACK_BOTTOM_FRAME,
|
|
116
|
+
RN_PRIMITIVE_NAMES,
|
|
117
|
+
SETTLE_MS,
|
|
118
|
+
SLOT_SCAN_DEPTH,
|
|
119
|
+
TOP_TRANSPARENT_LIMIT,
|
|
120
|
+
};
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
import { AdoptionBadge } from './AdoptionBadge';
|
|
2
|
+
import { AdoptionProbe } from './AdoptionProbe';
|
|
3
|
+
import { adoptionStore } from './AdoptionStore';
|
|
4
|
+
import { getAdoptionTrackingParams, isAdoptionTrackingEnabled } from './utils';
|
|
5
|
+
|
|
6
|
+
export {
|
|
7
|
+
AdoptionBadge,
|
|
8
|
+
AdoptionProbe,
|
|
9
|
+
adoptionStore,
|
|
10
|
+
getAdoptionTrackingParams,
|
|
11
|
+
isAdoptionTrackingEnabled,
|
|
12
|
+
};
|
|
@@ -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,61 @@
|
|
|
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 AdoptionTrackingParams = {
|
|
17
|
+
num_design_system: number;
|
|
18
|
+
num_rn: number;
|
|
19
|
+
num_other: number;
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
/** Ref the probe fills with a function that measures its screen for tracking. */
|
|
23
|
+
type AdoptionMeasureRef = {
|
|
24
|
+
current: (() => WalkResult | undefined) | undefined;
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
type ScreenSnapshot = WalkResult & {
|
|
28
|
+
app_id: string;
|
|
29
|
+
screen_name: string;
|
|
30
|
+
foundation_version?: string;
|
|
31
|
+
platform: string;
|
|
32
|
+
ts: number;
|
|
33
|
+
walk_ms: number;
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
type FiberLike = {
|
|
37
|
+
tag: number;
|
|
38
|
+
type: unknown;
|
|
39
|
+
child?: FiberLike | null;
|
|
40
|
+
sibling?: FiberLike | null;
|
|
41
|
+
return?: FiberLike | null;
|
|
42
|
+
alternate?: FiberLike | null;
|
|
43
|
+
memoizedProps?: unknown;
|
|
44
|
+
_debugStack?: unknown;
|
|
45
|
+
_debugOwner?: unknown;
|
|
46
|
+
stateNode?: unknown;
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
/** Identity sets of component objects: kit exports and RN primitives. */
|
|
50
|
+
type KitRegistries = { kit: WeakSet<object>; rn: WeakSet<object> };
|
|
51
|
+
|
|
52
|
+
export type {
|
|
53
|
+
AdoptionBucket,
|
|
54
|
+
AdoptionMeasureRef,
|
|
55
|
+
AdoptionTrackingParams,
|
|
56
|
+
FiberLike,
|
|
57
|
+
KitRegistries,
|
|
58
|
+
ScreenSnapshot,
|
|
59
|
+
SnapshotStatus,
|
|
60
|
+
WalkResult,
|
|
61
|
+
};
|