@atlassian/chippy 0.0.0-stage → 0.0.1-beta.2
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/LICENSE +201 -0
- package/README.md +276 -2
- package/bin/chippy.mjs +92 -0
- package/bin/pack.mjs +204 -0
- package/bin/skill-dir.mjs +15 -0
- package/lib/avalanche.js +2601 -0
- package/lib/avalanche.js.map +7 -0
- package/lib/bootstrap.d.ts +1 -0
- package/lib/bootstrap.js +70 -0
- package/lib/bootstrap.js.map +7 -0
- package/lib/browser.js +15 -0
- package/lib/browser.js.map +7 -0
- package/lib/extension/avalanche.es.js +5535 -0
- package/lib/extension/avalanche.es.js.map +1 -0
- package/lib/extension/chippy-page-bridge.js +1953 -0
- package/lib/extension/inspect-react.js +216 -0
- package/lib/extension/layout-lab.js +1453 -0
- package/lib/extension/panel-dom.js +90 -0
- package/lib/extension/resolve-probes.js +342 -0
- package/lib/mcp-skill/LICENSE +201 -0
- package/lib/mcp-skill/README.md +148 -0
- package/lib/mcp-skill/SKILL.md +335 -0
- package/lib/mcp-skill/browser-scripts/playwright-probes.js +368 -0
- package/lib/mcp-skill/browser-scripts/playwright-session.js +114 -0
- package/lib/mcp-skill/extension/armed-reload-startup.js +83 -0
- package/lib/mcp-skill/extension/avalanche.es.js +5535 -0
- package/lib/mcp-skill/extension/avalanche.es.js.map +1 -0
- package/lib/mcp-skill/extension/avalanche.iife.js +5536 -0
- package/lib/mcp-skill/extension/avalanche.iife.js.map +1 -0
- package/lib/mcp-skill/extension/background.js +325 -0
- package/lib/mcp-skill/extension/chippy-page-bridge.js +1953 -0
- package/lib/mcp-skill/extension/content-script.js +190 -0
- package/lib/mcp-skill/extension/devtools-hook-shim.js +85 -0
- package/lib/mcp-skill/extension/early-bootstrap.js +49 -0
- package/lib/mcp-skill/extension/icon-128.png +0 -0
- package/lib/mcp-skill/extension/icon-16.png +0 -0
- package/lib/mcp-skill/extension/icon-32.png +0 -0
- package/lib/mcp-skill/extension/icon-48.png +0 -0
- package/lib/mcp-skill/extension/inspect-react.js +216 -0
- package/lib/mcp-skill/extension/layout-lab.js +1453 -0
- package/lib/mcp-skill/extension/logo.svg +14 -0
- package/lib/mcp-skill/extension/manifest.json +69 -0
- package/lib/mcp-skill/extension/panel-dom.js +90 -0
- package/lib/mcp-skill/extension/panel-settings.js +173 -0
- package/lib/mcp-skill/extension/popup.html +41 -0
- package/lib/mcp-skill/extension/popup.js +80 -0
- package/lib/mcp-skill/extension/probe-descriptor.js +241 -0
- package/lib/mcp-skill/extension/resolve-probes.js +342 -0
- package/lib/mcp-skill/extension/startup-handoff.js +30 -0
- package/lib/mcp-skill/package.json +1 -0
- package/lib/mcp-skill/references/README.md +56 -0
- package/lib/mcp-skill/references/active-stimulus.md +85 -0
- package/lib/mcp-skill/references/avalanche-detector.md +731 -0
- package/lib/mcp-skill/references/bdd-router.md +310 -0
- package/lib/mcp-skill/references/browser-runtime-api.md +193 -0
- package/lib/mcp-skill/references/command-output-formats.md +1085 -0
- package/lib/mcp-skill/references/command-surface.md +183 -0
- package/lib/mcp-skill/references/cynefin.md +99 -0
- package/lib/mcp-skill/references/debug-hook.md +161 -0
- package/lib/mcp-skill/references/fixing-performance.md +490 -0
- package/lib/mcp-skill/references/hook-metrics.md +199 -0
- package/lib/mcp-skill/references/hooks-inspection.md +61 -0
- package/lib/mcp-skill/references/jira-taxonomy.md +81 -0
- package/lib/mcp-skill/references/mcp.md +122 -0
- package/lib/mcp-skill/references/mode-a-injection.md +102 -0
- package/lib/mcp-skill/references/operating-model.md +128 -0
- package/lib/mcp-skill/references/playwright-cli-setup.md +105 -0
- package/lib/mcp-skill/references/playwright-probes.md +205 -0
- package/lib/mcp-skill/references/procedures.md +545 -0
- package/lib/mcp-skill/references/recipes.txt +114 -0
- package/lib/mcp-skill/references/stability-render.md +428 -0
- package/lib/mcp-skill/references/test-mode-playbook.md +194 -0
- package/lib/mcp-skill/references/troubleshooting.md +59 -0
- package/lib/mcp-skill/scripts/avalanche-analyze.cjs +495 -0
- package/lib/mcp-skill/scripts/build-avalanche-command.js +34 -0
- package/lib/mcp-skill/scripts/build-fiber-state-command.js +19 -0
- package/lib/mcp-skill/scripts/chippy-helper-server.js +1768 -0
- package/lib/mcp-skill/scripts/chippy-mcp.cjs +273 -0
- package/lib/mcp-skill/scripts/chippy.sh +1252 -0
- package/lib/mcp-skill/scripts/format-probe-result.js +34 -0
- package/lib/mcp-skill/scripts/helper-access.js +158 -0
- package/lib/mcp-skill/scripts/helper-proxy.js +87 -0
- package/lib/mcp-skill/scripts/page-socket-trust.js +81 -0
- package/lib/mcp-skill/scripts/probe-annotation-channel.js +116 -0
- package/lib/mcp-skill/scripts/search-recipes.js +248 -0
- package/lib/mcp-skill/snippets/README.md +89 -0
- package/lib/mcp-skill/snippets/count-context-consumers.js +40 -0
- package/lib/mcp-skill/snippets/find-context-providers.js +38 -0
- package/lib/mcp-skill/snippets/find-legacy-context.js +47 -0
- package/lib/mcp-skill/snippets/find-prop.js +83 -0
- package/lib/mcp-skill/snippets/hook-metrics-sample.js +69 -0
- package/lib/mcp-skill/snippets/hook-metrics.js +60 -0
- package/lib/mcp-skill/snippets/hook-tracker.js +198 -0
- package/lib/mcp-skill/snippets/stability-nudge.js +101 -0
- package/lib/package.json +1 -0
- package/lib/plugin.js +119 -0
- package/lib/plugin.js.map +7 -0
- package/lib/storybook/avalanche-panel-model.js +79 -0
- package/lib/storybook/brand.jsx +6 -0
- package/lib/storybook/connection-fallback.jsx +47 -0
- package/lib/storybook/constants.js +15 -0
- package/lib/storybook/context-card.jsx +171 -0
- package/lib/storybook/manager-owner.js +42 -0
- package/lib/storybook/manager.jsx +425 -0
- package/lib/storybook/preset.js +7 -0
- package/lib/storybook/preview.js +298 -0
- package/lib/storybook/probe-annotation-editor.jsx +52 -0
- package/lib/storybook/probe-intent.js +23 -0
- package/lib/storybook/setup.js +104 -0
- package/lib/types/avalanche/commit-metrics-adapter.d.ts +76 -0
- package/lib/types/avalanche/commit-stats-indices.d.ts +41 -0
- package/lib/types/avalanche/commit-surface-ownership.d.ts +9 -0
- package/lib/types/avalanche/detector.d.ts +183 -0
- package/lib/types/avalanche/interaction-capture.d.ts +25 -0
- package/lib/types/avalanche/internals.d.ts +189 -0
- package/lib/types/avalanche/report-serializer.d.ts +283 -0
- package/lib/types/avalanche/reporter.d.ts +610 -0
- package/lib/types/chippy-bridge/fiber-source.d.ts +48 -0
- package/lib/types/chippy-bridge/react-renderer.d.ts +74 -0
- package/lib/types/chippy-bridge/recorded-source.d.ts +44 -0
- package/lib/types/chippy-bridge/source-map.d.ts +30 -0
- package/lib/types/debug-render/core.d.ts +78 -0
- package/lib/types/layout-lab/fiber-boundaries.d.ts +51 -0
- package/lib/types/layout-lab/fiber-walk.d.ts +131 -0
- package/lib/types/layout-lab/marker-context.d.ts +94 -0
- package/lib/types/package/avalanche.d.ts +6 -0
- package/lib/types/package/browser.d.ts +10 -0
- package/lib/types/package/plugin.d.ts +15 -0
- package/lib/types/plugins/atl/commit-metrics-adapter.d.ts +65 -0
- package/lib/types/plugins/atl/metrics-slot.d.ts +37 -0
- package/lib/types/plugins/index.d.ts +49 -0
- package/lib/types/plugins/jira/context-provider.d.ts +36 -0
- package/package.json +106 -4
|
@@ -0,0 +1,1953 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
// The page bridge is the transport/orchestration shell, not the owner of
|
|
3
|
+
// React-internals algorithms. It resolves page events and helper commands,
|
|
4
|
+
// delegates capability work to __AVALANCHE__.bridge, and publishes snapshots.
|
|
5
|
+
// Keeping that boundary thin is what lets the TypeScript bundle and the
|
|
6
|
+
// extension/import/Storybook integrations share behavior without drift.
|
|
7
|
+
(function installChippyPageBridge(global) {
|
|
8
|
+
/** @type {ChippyPageRuntime} */
|
|
9
|
+
const chippy = global.__CHIPPY = global.__CHIPPY || {};
|
|
10
|
+
const BRIDGE_VERSION = '0.2.5';
|
|
11
|
+
// Returned by every command that needs __AVALANCHE__.bridge when the Avalanche
|
|
12
|
+
// runtime is absent. The helper reports it as an unavailable runtime rather
|
|
13
|
+
// than an empty result, so callers never read `count: 0` as "nothing found".
|
|
14
|
+
const BRIDGE_MODULE_MISSING = Object.freeze({
|
|
15
|
+
ok: false,
|
|
16
|
+
code: 'bridge-module-missing',
|
|
17
|
+
error: 'bridge module not loaded',
|
|
18
|
+
});
|
|
19
|
+
// Read and removed by startup-handoff.js on the next document.
|
|
20
|
+
const STARTUP_HANDOFF_KEY = '__chippyStartupHandoff';
|
|
21
|
+
if (chippy.pageBridge) {
|
|
22
|
+
// Always tear down and reinstall — version-equality skip caused stale code to win.
|
|
23
|
+
try { chippy.pageBridge.destroy?.(); } catch (e) {}
|
|
24
|
+
}
|
|
25
|
+
const DEFAULTS = {
|
|
26
|
+
helperBaseUrl: 'http://127.0.0.1:7823',
|
|
27
|
+
// activated = false: bridge connects but does NOT auto-show probes.
|
|
28
|
+
// User must explicitly call ensureProbes() or send 'ensure-probes' command.
|
|
29
|
+
activated: false,
|
|
30
|
+
// Marker context (declarative boundaries plus context-provider plugins) is
|
|
31
|
+
// visible by default and can be toggled at runtime by the helper command.
|
|
32
|
+
contextUiEnabled: true,
|
|
33
|
+
reconnectDelayMs: 1500,
|
|
34
|
+
publishDebounceMs: 180,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function normalizeBaseUrl(value) {
|
|
38
|
+
return String(value || DEFAULTS.helperBaseUrl).replace(/\/+$/, '');
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function deriveWsUrl(baseUrl) {
|
|
42
|
+
// http → ws, https → wss. Respects whatever the user configured.
|
|
43
|
+
return normalizeBaseUrl(baseUrl).replace(/^http/i, 'ws') + '/__chippy/ws';
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function getIntegrationIdentity() {
|
|
47
|
+
return config?.integration || global.__CHIPPY_OWNER__?.integration || null;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
// Same-origin frames share sessionStorage, so each frame keys its id by its
|
|
51
|
+
// position in the frame tree; the Storybook manager and its preview iframe
|
|
52
|
+
// must not share one client id. The top frame keeps the original key.
|
|
53
|
+
function frameKeySuffix() {
|
|
54
|
+
const path = [];
|
|
55
|
+
try {
|
|
56
|
+
/** @type {Window} */
|
|
57
|
+
let current = global;
|
|
58
|
+
while (current !== current.parent && path.length < 16) {
|
|
59
|
+
const siblings = current.parent.frames;
|
|
60
|
+
let index = -1;
|
|
61
|
+
for (let i = 0; i < siblings.length; i += 1) {
|
|
62
|
+
if (siblings[i] === current) { index = i; break; }
|
|
63
|
+
}
|
|
64
|
+
path.unshift(index);
|
|
65
|
+
current = current.parent;
|
|
66
|
+
}
|
|
67
|
+
} catch {
|
|
68
|
+
return path.length ? ':frame' : '';
|
|
69
|
+
}
|
|
70
|
+
return path.length ? ':' + path.join('.') : '';
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
function getStoredClientId() {
|
|
74
|
+
const key = '__chippyClientId' + frameKeySuffix();
|
|
75
|
+
try {
|
|
76
|
+
const existing = global.sessionStorage?.getItem(key);
|
|
77
|
+
if (existing) return existing;
|
|
78
|
+
const next = global.crypto?.randomUUID?.() || `chippy-${Date.now()}`;
|
|
79
|
+
global.sessionStorage?.setItem(key, next);
|
|
80
|
+
return next;
|
|
81
|
+
} catch {
|
|
82
|
+
return `chippy-${Date.now()}`;
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// The extension is the fallback integration. A live owner of another kind,
|
|
87
|
+
// such as the Storybook addon in its manager window, outranks it.
|
|
88
|
+
function isLiveForeignOwner(owner) {
|
|
89
|
+
if (!owner || !owner.integration || owner.integration.kind === 'extension') return false;
|
|
90
|
+
return !owner.staleAfterMs || Date.now() - (owner.heartbeatAt || 0) <= owner.staleAfterMs;
|
|
91
|
+
}
|
|
92
|
+
|
|
93
|
+
function isExtensionBridge() {
|
|
94
|
+
return config?.integration?.kind === 'extension';
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
function getStoryId() {
|
|
98
|
+
const params = new URLSearchParams(global.location.search || '');
|
|
99
|
+
return params.get('id') || null;
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
// ─── Build mode detection ───────────────────────────────────────────────
|
|
103
|
+
|
|
104
|
+
function detectBuildMode() {
|
|
105
|
+
// Walk ALL elements with React fibers — cast a wider net than a single node.
|
|
106
|
+
function getFiberKey(node) {
|
|
107
|
+
return Object.getOwnPropertyNames(node).find((k) => k.startsWith('__reactFiber$'));
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
// Use body as root — head/meta/script nodes don't carry React fibers.
|
|
111
|
+
const root = global.document.body || global.document.documentElement || global.document;
|
|
112
|
+
const candidates = Array.from(root.querySelectorAll('*'));
|
|
113
|
+
for (const el of candidates) {
|
|
114
|
+
const key = getFiberKey(el);
|
|
115
|
+
if (!key) continue;
|
|
116
|
+
let fiber = el[key];
|
|
117
|
+
let steps = 0;
|
|
118
|
+
while (fiber && steps < 30) {
|
|
119
|
+
// React <=18 exposes _debugSource; React 19 captures the JSX call site
|
|
120
|
+
// in _debugStack instead. Both are development evidence.
|
|
121
|
+
if (fiber._debugSource?.fileName || typeof fiber._debugStack?.stack === 'string') return 'dev';
|
|
122
|
+
fiber = fiber.return;
|
|
123
|
+
steps++;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// No development source metadata found anywhere — check if we at least see named component functions.
|
|
128
|
+
// That means production (names intact) vs fully stripped (unknown).
|
|
129
|
+
for (const el of candidates) {
|
|
130
|
+
const key = getFiberKey(el);
|
|
131
|
+
if (!key) continue;
|
|
132
|
+
let fiber = el[key];
|
|
133
|
+
let steps = 0;
|
|
134
|
+
while (fiber && steps < 30) {
|
|
135
|
+
if (typeof fiber.type === 'function' && (fiber.type.name || fiber.type.displayName)) {
|
|
136
|
+
return 'production';
|
|
137
|
+
}
|
|
138
|
+
fiber = fiber.return;
|
|
139
|
+
steps++;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
return 'unknown';
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// ─── Marker summaries ───────────────────────────────────────────────────
|
|
147
|
+
|
|
148
|
+
function summarizeMarkerContext(id) {
|
|
149
|
+
if (config.contextUiEnabled === false) return null;
|
|
150
|
+
const fiber = chippy.layoutLab?.getMarkerProbe?.(id)?.probe?._bestFiber || null;
|
|
151
|
+
const lab = global.__AVALANCHE__?.layoutLab;
|
|
152
|
+
const describe = lab?.describeContext;
|
|
153
|
+
if (!fiber || typeof describe !== 'function') return null;
|
|
154
|
+
try {
|
|
155
|
+
return describe(fiber) || null;
|
|
156
|
+
} catch {
|
|
157
|
+
return null;
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
function summarizeMarkers() {
|
|
162
|
+
const states = chippy.layoutLab?.getAllMarkerStates?.() || {};
|
|
163
|
+
const activeIds = Object.entries(states)
|
|
164
|
+
.filter(([, s]) => !s.docked)
|
|
165
|
+
.map(([id]) => id);
|
|
166
|
+
const summaries = activeIds.length > 0
|
|
167
|
+
? (chippy.resolveProbesCompact?.(...activeIds) || {})
|
|
168
|
+
: {};
|
|
169
|
+
const result = {};
|
|
170
|
+
for (const id of ['1', '2']) {
|
|
171
|
+
const context = summaries[id] ? summarizeMarkerContext(id) : null;
|
|
172
|
+
result[id] = {
|
|
173
|
+
position: states[id]?.position || null,
|
|
174
|
+
docked: states[id]?.docked ?? true,
|
|
175
|
+
intent: states[id]?.intent ?? null,
|
|
176
|
+
comment: states[id]?.comment || '',
|
|
177
|
+
resolved: summaries[id] || null,
|
|
178
|
+
context,
|
|
179
|
+
};
|
|
180
|
+
}
|
|
181
|
+
const commonInspection = global.__CHIPPY?.layoutLab?.getCommonParentInspection?.();
|
|
182
|
+
if (commonInspection) {
|
|
183
|
+
result['3'] = {
|
|
184
|
+
position: null,
|
|
185
|
+
docked: false,
|
|
186
|
+
intent: null,
|
|
187
|
+
comment: '',
|
|
188
|
+
context: null,
|
|
189
|
+
resolved: {
|
|
190
|
+
confidence: commonInspection.component?.debugSource?.fileName || commonInspection.owners?.some((o) => o?.debugSource?.fileName) ? 'dev' : 'production',
|
|
191
|
+
dom: {
|
|
192
|
+
tag: commonInspection.dom?.tagName || null,
|
|
193
|
+
text: (commonInspection.dom?.text || '').slice(0, 80),
|
|
194
|
+
fiberType: commonInspection.fiber?.type || commonInspection.fiber?.elementType || null,
|
|
195
|
+
source: commonInspection.fiber?.debugSource || null,
|
|
196
|
+
parents: [],
|
|
197
|
+
matchedStyles: [],
|
|
198
|
+
},
|
|
199
|
+
component: {
|
|
200
|
+
name: commonInspection.component?.type || commonInspection.component?.elementType || null,
|
|
201
|
+
key: commonInspection.component?.key ?? null,
|
|
202
|
+
propNames: commonInspection.component?.memoizedPropsKeys?.filter((k) => k !== 'children') || [],
|
|
203
|
+
props: {},
|
|
204
|
+
hookCount: null,
|
|
205
|
+
hookCountTruncated: false,
|
|
206
|
+
hookCountMax: null,
|
|
207
|
+
complexityNote: null,
|
|
208
|
+
source: commonInspection.component?.debugSource || null,
|
|
209
|
+
owners: (commonInspection.owners || []).map((o) => ({
|
|
210
|
+
name: o?.type || o?.elementType || null,
|
|
211
|
+
key: o?.key ?? null,
|
|
212
|
+
source: o?.debugSource || null,
|
|
213
|
+
propNames: o?.memoizedPropsKeys?.filter((k) => k !== 'children') || [],
|
|
214
|
+
hookCount: null,
|
|
215
|
+
hookCountTruncated: false,
|
|
216
|
+
hookCountMax: null,
|
|
217
|
+
complexityNote: null,
|
|
218
|
+
})),
|
|
219
|
+
componentStack: null,
|
|
220
|
+
parents: (commonInspection.returns || []).map((o) => ({
|
|
221
|
+
name: o?.type || o?.elementType || null,
|
|
222
|
+
key: o?.key ?? null,
|
|
223
|
+
source: o?.debugSource || null,
|
|
224
|
+
propNames: o?.memoizedPropsKeys?.filter((k) => k !== 'children') || [],
|
|
225
|
+
hookCount: null,
|
|
226
|
+
hookCountTruncated: false,
|
|
227
|
+
hookCountMax: null,
|
|
228
|
+
complexityNote: null,
|
|
229
|
+
})),
|
|
230
|
+
},
|
|
231
|
+
nearestTestId: commonInspection.nearestTestIdParent?.testId || null,
|
|
232
|
+
note: 'Nearest common parent for probes 1, 2.',
|
|
233
|
+
},
|
|
234
|
+
};
|
|
235
|
+
}
|
|
236
|
+
return result;
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
function getCommonParentSourceFibers(lab) {
|
|
240
|
+
const first = chippy.layoutLab?.getMarkerProbe?.('1')?.probe?._bestFiber || null;
|
|
241
|
+
const second = chippy.layoutLab?.getMarkerProbe?.('2')?.probe?._bestFiber || null;
|
|
242
|
+
if (!first || !second) return null;
|
|
243
|
+
|
|
244
|
+
const firstElements = new Set();
|
|
245
|
+
let current = first.return || null;
|
|
246
|
+
let steps = 0;
|
|
247
|
+
while (current && steps < 400) {
|
|
248
|
+
if (current.stateNode?.nodeType === 1) firstElements.add(current.stateNode);
|
|
249
|
+
current = current.return || null;
|
|
250
|
+
steps += 1;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
current = second.return || null;
|
|
254
|
+
steps = 0;
|
|
255
|
+
while (current && steps < 400) {
|
|
256
|
+
const element = current.stateNode?.nodeType === 1 ? current.stateNode : null;
|
|
257
|
+
if (element && firstElements.has(element)) {
|
|
258
|
+
const exactFiber = typeof lab.getNearestFiber === 'function' ? lab.getNearestFiber(element) : current;
|
|
259
|
+
const componentFiber = typeof lab.getNearestComponent === 'function'
|
|
260
|
+
? lab.getNearestComponent(element)
|
|
261
|
+
: chippy.inspectReact?.getNearestCompositeFiber?.(exactFiber) || null;
|
|
262
|
+
return { exactFiber, componentFiber };
|
|
263
|
+
}
|
|
264
|
+
current = current.return || null;
|
|
265
|
+
steps += 1;
|
|
266
|
+
}
|
|
267
|
+
return null;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
async function enrichOneMarkerSource(marker, exactFiber, componentFiber, lab) {
|
|
271
|
+
if (!marker?.resolved || !componentFiber) return;
|
|
272
|
+
const ownerFibers = typeof lab.getOwnerFibers === 'function' ? lab.getOwnerFibers(componentFiber).slice(0, 10) : [];
|
|
273
|
+
const parentFibers = typeof lab.getReturnComponentFibers === 'function' ? lab.getReturnComponentFibers(componentFiber).slice(0, 20) : [];
|
|
274
|
+
const [domSource, componentSource, ownerSources, parentSources] = await Promise.all([
|
|
275
|
+
exactFiber ? lab.resolveFiberSource(exactFiber) : null,
|
|
276
|
+
lab.resolveFiberSource(componentFiber),
|
|
277
|
+
Promise.all(ownerFibers.map((fiber) => lab.resolveFiberSource(fiber))),
|
|
278
|
+
Promise.all(parentFibers.map((fiber) => lab.resolveFiberSource(fiber))),
|
|
279
|
+
]);
|
|
280
|
+
|
|
281
|
+
if (domSource && marker.resolved.dom) marker.resolved.dom.source = domSource;
|
|
282
|
+
if (componentSource && marker.resolved.component) marker.resolved.component.source = componentSource;
|
|
283
|
+
for (let index = 0; index < ownerSources.length; index++) {
|
|
284
|
+
if (ownerSources[index] && marker.resolved.component?.owners?.[index]) {
|
|
285
|
+
marker.resolved.component.owners[index].source = ownerSources[index];
|
|
286
|
+
}
|
|
287
|
+
}
|
|
288
|
+
for (let index = 0; index < parentSources.length; index++) {
|
|
289
|
+
if (parentSources[index] && marker.resolved.component?.parents?.[index]) {
|
|
290
|
+
marker.resolved.component.parents[index].source = parentSources[index];
|
|
291
|
+
}
|
|
292
|
+
}
|
|
293
|
+
if (domSource || componentSource || ownerSources.some(Boolean) || parentSources.some(Boolean)) {
|
|
294
|
+
marker.resolved.confidence = 'dev';
|
|
295
|
+
marker.resolved.note = null;
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
async function enrichMarkerSources(markers) {
|
|
300
|
+
const lab = global.__AVALANCHE__?.layoutLab;
|
|
301
|
+
if (typeof lab?.resolveFiberSource !== 'function') return markers;
|
|
302
|
+
|
|
303
|
+
const jobs = ['1', '2'].map((id) => {
|
|
304
|
+
const probe = chippy.layoutLab?.getMarkerProbe?.(id)?.probe || null;
|
|
305
|
+
const componentFiber = probe?._bestFiber || null;
|
|
306
|
+
const targetElement = probe?._targetEl || null;
|
|
307
|
+
const exactFiber = targetElement && typeof lab.getNearestFiber === 'function'
|
|
308
|
+
? lab.getNearestFiber(targetElement)
|
|
309
|
+
: null;
|
|
310
|
+
return enrichOneMarkerSource(markers[id], exactFiber, componentFiber, lab);
|
|
311
|
+
});
|
|
312
|
+
|
|
313
|
+
const common = getCommonParentSourceFibers(lab);
|
|
314
|
+
if (common) jobs.push(enrichOneMarkerSource(markers['3'], common.exactFiber, common.componentFiber, lab));
|
|
315
|
+
await Promise.all(jobs);
|
|
316
|
+
return markers;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
function probesActive() {
|
|
320
|
+
const states = chippy.layoutLab?.getAllMarkerStates?.() || {};
|
|
321
|
+
// At least one probe must be placed (not docked) to count as active
|
|
322
|
+
return Object.values(states).some((s) => !s.docked);
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function probesInstalled() {
|
|
326
|
+
const states = chippy.layoutLab?.getAllMarkerStates?.() || {};
|
|
327
|
+
return ['1', '2'].every((id) => Boolean(states[id]));
|
|
328
|
+
}
|
|
329
|
+
|
|
330
|
+
function getPageState() {
|
|
331
|
+
return {
|
|
332
|
+
url: global.location.href,
|
|
333
|
+
title: global.document.title,
|
|
334
|
+
storyId: getStoryId(),
|
|
335
|
+
readyState: global.document.readyState,
|
|
336
|
+
};
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// ─── Status overlay ─────────────────────────────────────────────────────
|
|
340
|
+
|
|
341
|
+
function ensureStatus() {
|
|
342
|
+
/** @type {HTMLElement | null} */
|
|
343
|
+
let node = global.document.querySelector('[data-chippy-status]');
|
|
344
|
+
if (!node) {
|
|
345
|
+
node = global.document.createElement('div');
|
|
346
|
+
node.setAttribute('data-chippy-status', 'true');
|
|
347
|
+
node.style.cssText = [
|
|
348
|
+
'position:fixed', 'top:16px', 'right:16px', 'max-width:320px',
|
|
349
|
+
'padding:10px 12px', 'border-radius:12px',
|
|
350
|
+
'background:rgba(15,23,42,0.94)', 'color:#e2e8f0',
|
|
351
|
+
'font:12px/1.4 Inter,system-ui,sans-serif',
|
|
352
|
+
'box-shadow:0 12px 28px rgba(0,0,0,0.28)',
|
|
353
|
+
'z-index:2147483647', 'pointer-events:none', 'display:none',
|
|
354
|
+
].join(';');
|
|
355
|
+
global.document.body.appendChild(node);
|
|
356
|
+
}
|
|
357
|
+
return node;
|
|
358
|
+
}
|
|
359
|
+
|
|
360
|
+
function showStatus(_message) {
|
|
361
|
+
// Banner suppressed — connection state lives in chippy panel via setConnState().
|
|
362
|
+
// Kept as no-op so existing call sites and command handlers don't break.
|
|
363
|
+
}
|
|
364
|
+
|
|
365
|
+
function hideStatus() {
|
|
366
|
+
const node = global.document.querySelector('[data-chippy-status]');
|
|
367
|
+
if (node) node.remove();
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
// Connection state is mirrored to chippy.connState and dispatched as event for layout-lab panel.
|
|
371
|
+
function setConnState(state) {
|
|
372
|
+
chippy.connState = state;
|
|
373
|
+
try {
|
|
374
|
+
global.dispatchEvent(new CustomEvent('chippy-conn-state', { detail: { state } }));
|
|
375
|
+
} catch {}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// ─── State ──────────────────────────────────────────────────────────────
|
|
379
|
+
|
|
380
|
+
// Config is stored in window.__CHIPPY.config by the extension or proxy bootstrap.
|
|
381
|
+
function readExtensionConfig() {
|
|
382
|
+
return chippy.config || {};
|
|
383
|
+
}
|
|
384
|
+
const extensionConfig = readExtensionConfig();
|
|
385
|
+
|
|
386
|
+
/** @type {Record<string, any>} */
|
|
387
|
+
let config = {
|
|
388
|
+
...DEFAULTS,
|
|
389
|
+
...extensionConfig,
|
|
390
|
+
helperBaseUrl: normalizeBaseUrl(
|
|
391
|
+
extensionConfig.helperBaseUrl || DEFAULTS.helperBaseUrl,
|
|
392
|
+
),
|
|
393
|
+
activated: extensionConfig.activated ?? DEFAULTS.activated,
|
|
394
|
+
};
|
|
395
|
+
chippy.config = { ...config };
|
|
396
|
+
|
|
397
|
+
let socket = null;
|
|
398
|
+
// WebSocket state is disposable transport state. Probes and their marker
|
|
399
|
+
// coordinates belong to the page integration and are re-published after a
|
|
400
|
+
// reconnect; the helper must never become the source of truth for them.
|
|
401
|
+
let reconnectTimer = null;
|
|
402
|
+
let locationInterval = null;
|
|
403
|
+
let heartbeatInterval = null;
|
|
404
|
+
let lastPointerActivityAt = 0;
|
|
405
|
+
let lastFocusActivityAt = 0;
|
|
406
|
+
let lastVisibilityState = global.document.visibilityState || 'unknown';
|
|
407
|
+
let destroyed = false;
|
|
408
|
+
let onLayoutLabUpdate = null;
|
|
409
|
+
let onWindowMessage = null;
|
|
410
|
+
let onExtCommand = null;
|
|
411
|
+
let onOwnerClaimed = null;
|
|
412
|
+
let publishTimer = null;
|
|
413
|
+
let snapshotSequence = 0;
|
|
414
|
+
let lastLocationHref = global.location.href;
|
|
415
|
+
const clientId = getStoredClientId();
|
|
416
|
+
// Build mode is detected lazily — React fibers are not available at injection
|
|
417
|
+
// time if the script runs before the first render. We resolve it on first
|
|
418
|
+
// snapshot and cache the result.
|
|
419
|
+
let buildMode = 'unknown';
|
|
420
|
+
|
|
421
|
+
function resolveBuildMode() {
|
|
422
|
+
if (buildMode !== 'unknown') return buildMode;
|
|
423
|
+
buildMode = detectBuildMode();
|
|
424
|
+
return buildMode;
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
// ─── Messaging ──────────────────────────────────────────────────────────
|
|
428
|
+
|
|
429
|
+
function send(message) {
|
|
430
|
+
if (!socket || socket.readyState !== WebSocket.OPEN) return false;
|
|
431
|
+
socket.send(JSON.stringify(message));
|
|
432
|
+
return true;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
function sendActivity(reason, detail) {
|
|
436
|
+
return send({
|
|
437
|
+
type: 'activity',
|
|
438
|
+
clientId,
|
|
439
|
+
bridgeVersion: BRIDGE_VERSION,
|
|
440
|
+
buildMode: resolveBuildMode(),
|
|
441
|
+
page: getPageState(),
|
|
442
|
+
integration: getIntegrationIdentity(),
|
|
443
|
+
activated: config.activated,
|
|
444
|
+
probesActive: probesActive(),
|
|
445
|
+
activity: {
|
|
446
|
+
reason,
|
|
447
|
+
detail: detail || null,
|
|
448
|
+
at: new Date().toISOString(),
|
|
449
|
+
},
|
|
450
|
+
});
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
function publishSnapshot(reason) {
|
|
454
|
+
const sequence = ++snapshotSequence;
|
|
455
|
+
const mode = resolveBuildMode();
|
|
456
|
+
const markers = summarizeMarkers();
|
|
457
|
+
const payload = {
|
|
458
|
+
type: 'snapshot',
|
|
459
|
+
clientId,
|
|
460
|
+
bridgeVersion: BRIDGE_VERSION,
|
|
461
|
+
buildMode: mode,
|
|
462
|
+
page: getPageState(),
|
|
463
|
+
integration: getIntegrationIdentity(),
|
|
464
|
+
activated: config.activated,
|
|
465
|
+
probesActive: probesActive(),
|
|
466
|
+
reason,
|
|
467
|
+
snapshot: {
|
|
468
|
+
page: getPageState(),
|
|
469
|
+
integration: getIntegrationIdentity(),
|
|
470
|
+
buildMode: mode,
|
|
471
|
+
activated: config.activated,
|
|
472
|
+
probesActive: probesActive(),
|
|
473
|
+
markers,
|
|
474
|
+
},
|
|
475
|
+
};
|
|
476
|
+
const beforeResolution = JSON.stringify(markers);
|
|
477
|
+
const sent = send(payload);
|
|
478
|
+
if (!sent) return false;
|
|
479
|
+
|
|
480
|
+
// React 19 locations require fetching the served module and decoding its
|
|
481
|
+
// source map. Publish the immediate stack-frame position first, then replace
|
|
482
|
+
// it with the original source position once the shared resolver finishes.
|
|
483
|
+
Promise.resolve(enrichMarkerSources(markers)).then((resolvedMarkers) => {
|
|
484
|
+
if (sequence !== snapshotSequence) return;
|
|
485
|
+
if (JSON.stringify(resolvedMarkers) === beforeResolution) return;
|
|
486
|
+
send({
|
|
487
|
+
...payload,
|
|
488
|
+
reason: reason + '-source-resolved',
|
|
489
|
+
snapshot: { ...payload.snapshot, markers: resolvedMarkers },
|
|
490
|
+
});
|
|
491
|
+
}).catch(() => {});
|
|
492
|
+
|
|
493
|
+
return sent;
|
|
494
|
+
}
|
|
495
|
+
|
|
496
|
+
function setProbeAnnotation(probeId, patch) {
|
|
497
|
+
return chippy.layoutLab?.setMarkerAnnotation?.(String(probeId), patch || {}) || null;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
// Snapshot publication is debounced because marker placement, activation,
|
|
501
|
+
// and probe resolution can change in the same browser task. Consumers need
|
|
502
|
+
// the settled state, not a sequence of transient intermediate snapshots.
|
|
503
|
+
function scheduleSnapshot(reason) {
|
|
504
|
+
global.clearTimeout(publishTimer);
|
|
505
|
+
publishTimer = global.setTimeout(() => publishSnapshot(reason), config.publishDebounceMs);
|
|
506
|
+
}
|
|
507
|
+
|
|
508
|
+
function sendHello(reason) {
|
|
509
|
+
send({
|
|
510
|
+
type: 'hello',
|
|
511
|
+
reason,
|
|
512
|
+
clientId,
|
|
513
|
+
bridgeVersion: BRIDGE_VERSION,
|
|
514
|
+
buildMode: resolveBuildMode(),
|
|
515
|
+
page: getPageState(),
|
|
516
|
+
integration: getIntegrationIdentity(),
|
|
517
|
+
activated: config.activated,
|
|
518
|
+
probesActive: probesActive(),
|
|
519
|
+
userAgent: global.navigator.userAgent,
|
|
520
|
+
});
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
// ─── Probe control ───────────────────────────────────────────────────────
|
|
524
|
+
|
|
525
|
+
function activate() {
|
|
526
|
+
config.activated = true;
|
|
527
|
+
if (!probesInstalled()) chippy.layoutLab?.exposeDefaultMarkers?.();
|
|
528
|
+
else chippy.layoutLab?.updatePanel?.();
|
|
529
|
+
hideStatus();
|
|
530
|
+
// Defer build-mode detection until after first render settles
|
|
531
|
+
global.setTimeout(() => {
|
|
532
|
+
buildMode = detectBuildMode();
|
|
533
|
+
scheduleSnapshot('activate');
|
|
534
|
+
}, 300);
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function deactivate() {
|
|
538
|
+
config.activated = false;
|
|
539
|
+
chippy.layoutLab?.clearAll?.();
|
|
540
|
+
scheduleSnapshot('deactivate');
|
|
541
|
+
}
|
|
542
|
+
|
|
543
|
+
function ensureProbes() {
|
|
544
|
+
if (!config.activated) activate();
|
|
545
|
+
else if (!probesInstalled()) chippy.layoutLab?.exposeDefaultMarkers?.();
|
|
546
|
+
else chippy.layoutLab?.updatePanel?.();
|
|
547
|
+
scheduleSnapshot('ensure-probes');
|
|
548
|
+
}
|
|
549
|
+
|
|
550
|
+
function clearProbes() {
|
|
551
|
+
chippy.layoutLab?.clearAll?.();
|
|
552
|
+
scheduleSnapshot('clear-probes');
|
|
553
|
+
}
|
|
554
|
+
|
|
555
|
+
function setContextUiEnabled(enabled, reason) {
|
|
556
|
+
applyConfig({ contextUiEnabled: Boolean(enabled) });
|
|
557
|
+
chippy.layoutLab?.updatePanel?.();
|
|
558
|
+
scheduleSnapshot(reason);
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
// ─── Avalanche bridge (upstream-native) ──────────────────────────────────
|
|
562
|
+
//
|
|
563
|
+
// Reads the Avalanche IIFE published as globalThis.__AVALANCHE__. The page
|
|
564
|
+
// bridge exposes lifecycle, status, export, metrics, help, and debug-hook
|
|
565
|
+
// conveniences while request commands are translated to the bundled API.
|
|
566
|
+
|
|
567
|
+
function getAvalancheApi() {
|
|
568
|
+
const root = global.__AVALANCHE__ || null;
|
|
569
|
+
if (!root) return null;
|
|
570
|
+
if (typeof root.status === 'function' || typeof root.report === 'function' || typeof root.install === 'function') return root;
|
|
571
|
+
const d = root.default || null;
|
|
572
|
+
if (d && (typeof d.status === 'function' || typeof d.report === 'function' || typeof d.install === 'function')) return d;
|
|
573
|
+
return root;
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
function getAvalancheModule() {
|
|
577
|
+
return global.__AVALANCHE__ || null;
|
|
578
|
+
}
|
|
579
|
+
|
|
580
|
+
// Upstream commitStats tuple index → field name.
|
|
581
|
+
// Source: src/avalanche/detector.tsx ~line 952 (commitStats[N] = [...])
|
|
582
|
+
// ─── Avalanche command surface — delegated to __AVALANCHE__.bridge ────────
|
|
583
|
+
// Implementation lives in src/chippy-bridge/avalanche-commands.ts and is
|
|
584
|
+
// bundled into __AVALANCHE__.bridge. The bridge supplies the live api/mod
|
|
585
|
+
// refs at call time; the bundle owns all action handling, getReport,
|
|
586
|
+
// tupleToStats, and the UPSTREAM_COMMIT_STAT_KEYS constant.
|
|
587
|
+
function readAvalancheStatus() {
|
|
588
|
+
const b = getBridge();
|
|
589
|
+
return b
|
|
590
|
+
? b.readAvalancheStatus(getAvalancheApi(), getAvalancheModule())
|
|
591
|
+
: { ok: false, available: false, ...BRIDGE_MODULE_MISSING };
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function evalAvalanche(command) {
|
|
595
|
+
const b = getBridge();
|
|
596
|
+
if (!b) {
|
|
597
|
+
const action = String(command?.action || 'status');
|
|
598
|
+
return { ok: false, available: false, action, ...BRIDGE_MODULE_MISSING };
|
|
599
|
+
}
|
|
600
|
+
return b.evalAvalanche(command, getAvalancheApi(), getAvalancheModule());
|
|
601
|
+
}
|
|
602
|
+
|
|
603
|
+
// An armed reload must install Avalanche before the next document's React
|
|
604
|
+
// commits. The arm config travels in sessionStorage, which the next
|
|
605
|
+
// document's startup script reads synchronously; the extension registers that
|
|
606
|
+
// script for one navigation. Without the extension the wait times out and the
|
|
607
|
+
// page reloads anyway. Returns how early startup was prepared, for the ack.
|
|
608
|
+
function prepareReload(avalanche) {
|
|
609
|
+
if (!avalanche || !avalanche.autoStart) return Promise.resolve('not-armed');
|
|
610
|
+
try {
|
|
611
|
+
const startup = /** @type {any} */ (global).__CHIPPY_STARTUP_CONFIG || {};
|
|
612
|
+
global.sessionStorage.setItem(STARTUP_HANDOFF_KEY, JSON.stringify({
|
|
613
|
+
at: Date.now(),
|
|
614
|
+
helperBaseUrl: config.helperBaseUrl,
|
|
615
|
+
plugins: Array.isArray(startup.plugins) ? startup.plugins : undefined,
|
|
616
|
+
avalanche: avalanche,
|
|
617
|
+
}));
|
|
618
|
+
} catch (e) {
|
|
619
|
+
return Promise.resolve('storage-unavailable');
|
|
620
|
+
}
|
|
621
|
+
return new Promise(function(done) {
|
|
622
|
+
let settled = false;
|
|
623
|
+
function finish(state) {
|
|
624
|
+
if (settled) return;
|
|
625
|
+
settled = true;
|
|
626
|
+
global.document.removeEventListener('chippy-armed-reload-ready', onReady);
|
|
627
|
+
done(state);
|
|
628
|
+
}
|
|
629
|
+
function onReady(event) {
|
|
630
|
+
let detail = {};
|
|
631
|
+
try { detail = JSON.parse(String(event.detail || '{}')); } catch (e) {}
|
|
632
|
+
finish(detail.ok ? 'registered' : 'handoff-only');
|
|
633
|
+
}
|
|
634
|
+
global.document.addEventListener('chippy-armed-reload-ready', onReady);
|
|
635
|
+
setTimeout(function() { finish('handoff-only'); }, 500);
|
|
636
|
+
global.document.dispatchEvent(new CustomEvent('chippy-armed-reload-prepare'));
|
|
637
|
+
});
|
|
638
|
+
}
|
|
639
|
+
|
|
640
|
+
function handleCommand(command) {
|
|
641
|
+
switch (command?.type) {
|
|
642
|
+
case 'activate':
|
|
643
|
+
activate();
|
|
644
|
+
break;
|
|
645
|
+
case 'deactivate':
|
|
646
|
+
deactivate();
|
|
647
|
+
break;
|
|
648
|
+
case 'ensure-probes':
|
|
649
|
+
ensureProbes();
|
|
650
|
+
break;
|
|
651
|
+
case 'clear-probes':
|
|
652
|
+
clearProbes();
|
|
653
|
+
break;
|
|
654
|
+
case 'request-snapshot':
|
|
655
|
+
publishSnapshot('request-snapshot');
|
|
656
|
+
break;
|
|
657
|
+
case 'show-status':
|
|
658
|
+
showStatus(command.text || 'Chippy is connected.');
|
|
659
|
+
break;
|
|
660
|
+
case 'hide-status':
|
|
661
|
+
hideStatus();
|
|
662
|
+
break;
|
|
663
|
+
case 'set-context-ui':
|
|
664
|
+
setContextUiEnabled(command.enabled !== false, 'set-context-ui');
|
|
665
|
+
break;
|
|
666
|
+
case 'reload':
|
|
667
|
+
prepareReload(command.avalanche).then(function(earlyStartup) {
|
|
668
|
+
try { send({ type: 'reload-ack', clientId, requestId: command.requestId || null, earlyStartup: earlyStartup }); } catch (e) {}
|
|
669
|
+
setTimeout(function() { try { window.location.reload(); } catch (e) {} }, 50);
|
|
670
|
+
});
|
|
671
|
+
break;
|
|
672
|
+
case 'storybook-force-remount': {
|
|
673
|
+
// The Storybook addon owns the host API. The bridge only relays the
|
|
674
|
+
// request so a Storybook SUT remount does not navigate its preview.
|
|
675
|
+
// The arm, when present, lets the addon install Avalanche before the
|
|
676
|
+
// remount commits.
|
|
677
|
+
const remountEvent = new CustomEvent('chippy-storybook-force-remount', {
|
|
678
|
+
cancelable: true,
|
|
679
|
+
detail: { requestId: command.requestId || null, avalanche: command.avalanche || null },
|
|
680
|
+
});
|
|
681
|
+
global.dispatchEvent(remountEvent);
|
|
682
|
+
try {
|
|
683
|
+
send({
|
|
684
|
+
type: 'reload-ack',
|
|
685
|
+
clientId,
|
|
686
|
+
requestId: command.requestId || null,
|
|
687
|
+
action: 'storybook-force-remount',
|
|
688
|
+
ok: remountEvent.defaultPrevented,
|
|
689
|
+
error: remountEvent.defaultPrevented ? undefined : 'The Storybook preview did not remount: no rendered story matched the current selection',
|
|
690
|
+
});
|
|
691
|
+
} catch (e) {}
|
|
692
|
+
break;
|
|
693
|
+
}
|
|
694
|
+
case 'eval-computed-style': {
|
|
695
|
+
const requestId = command.requestId || null;
|
|
696
|
+
const result = chippy.getComputedStyleAt
|
|
697
|
+
? chippy.getComputedStyleAt({ probeId: command.probeId, parentLevel: command.parentLevel || 0, selector: command.selector || undefined })
|
|
698
|
+
: { error: 'getComputedStyleAt not available' };
|
|
699
|
+
send({ type: 'computed-style-result', clientId, requestId, result });
|
|
700
|
+
break;
|
|
701
|
+
}
|
|
702
|
+
case 'eval-probe-dom': {
|
|
703
|
+
const requestId = command.requestId || null;
|
|
704
|
+
const result = evalProbeDom(command.probeId, command.parentLevel || 0);
|
|
705
|
+
send({ type: 'eval-probe-result', clientId, requestId, result });
|
|
706
|
+
break;
|
|
707
|
+
}
|
|
708
|
+
case 'eval-probe-props': {
|
|
709
|
+
const requestId = command.requestId || null;
|
|
710
|
+
const result = evalProbeProps(command.probeId, command.ownerIndex);
|
|
711
|
+
send({ type: 'eval-probe-result', clientId, requestId, result });
|
|
712
|
+
break;
|
|
713
|
+
}
|
|
714
|
+
case 'eval-probe-owners': {
|
|
715
|
+
const requestId = command.requestId || null;
|
|
716
|
+
const result = evalProbeOwners(command.probeId);
|
|
717
|
+
send({ type: 'eval-probe-result', clientId, requestId, result });
|
|
718
|
+
break;
|
|
719
|
+
}
|
|
720
|
+
case 'eval-probe-parents': {
|
|
721
|
+
const requestId = command.requestId || null;
|
|
722
|
+
const result = evalProbeParents(command.probeId, command.max);
|
|
723
|
+
send({ type: 'eval-probe-result', clientId, requestId, result });
|
|
724
|
+
break;
|
|
725
|
+
}
|
|
726
|
+
case 'eval-probe-boundaries': {
|
|
727
|
+
const requestId = command.requestId || null;
|
|
728
|
+
const result = evalProbeBoundaries(command.probeId, command.max);
|
|
729
|
+
send({ type: 'eval-probe-result', clientId, requestId, result });
|
|
730
|
+
break;
|
|
731
|
+
}
|
|
732
|
+
case 'eval-probe-hooks': {
|
|
733
|
+
const requestId = command.requestId || null;
|
|
734
|
+
const result = evalProbeHooks(command.probeId);
|
|
735
|
+
send({ type: 'eval-probe-result', clientId, requestId, result });
|
|
736
|
+
break;
|
|
737
|
+
}
|
|
738
|
+
case 'eval-probe-hooks-summary': {
|
|
739
|
+
const requestId = command.requestId || null;
|
|
740
|
+
const result = evalProbeHooksSummary(command.probeId);
|
|
741
|
+
send({ type: 'eval-probe-result', clientId, requestId, result });
|
|
742
|
+
break;
|
|
743
|
+
}
|
|
744
|
+
case 'find-fibers': {
|
|
745
|
+
const requestId = command.requestId || null;
|
|
746
|
+
Promise.resolve(evalFindFibers(command.filters || {}))
|
|
747
|
+
.then((result) => send({ type: 'eval-probe-result', clientId, requestId, result }))
|
|
748
|
+
.catch((e) => send({ type: 'eval-probe-result', clientId, requestId, result: { count: 0, items: [], error: e?.message || String(e) } }));
|
|
749
|
+
break;
|
|
750
|
+
}
|
|
751
|
+
case 'capture-fiber-state': {
|
|
752
|
+
const requestId = command.requestId || null;
|
|
753
|
+
const result = evalCaptureFiberState(command.options || {});
|
|
754
|
+
send({ type: 'eval-probe-result', clientId, requestId, result });
|
|
755
|
+
break;
|
|
756
|
+
}
|
|
757
|
+
case 'compare-fiber-states': {
|
|
758
|
+
const requestId = command.requestId || null;
|
|
759
|
+
const result = evalCompareFiberStates(command.before, command.after, command.options || {});
|
|
760
|
+
send({ type: 'eval-probe-result', clientId, requestId, result });
|
|
761
|
+
break;
|
|
762
|
+
}
|
|
763
|
+
case 'page-boundaries': {
|
|
764
|
+
const requestId = command.requestId || null;
|
|
765
|
+
const result = evalPageBoundaries(command.max);
|
|
766
|
+
send({ type: 'eval-probe-result', clientId, requestId, result });
|
|
767
|
+
break;
|
|
768
|
+
}
|
|
769
|
+
case 'inspect-fiber-hooks': {
|
|
770
|
+
const requestId = command.requestId || null;
|
|
771
|
+
const result = evalInspectFiberHooks(command.fiberId);
|
|
772
|
+
send({ type: 'eval-probe-result', clientId, requestId, result });
|
|
773
|
+
break;
|
|
774
|
+
}
|
|
775
|
+
case 'inspect-fiber-hooks-summary': {
|
|
776
|
+
const requestId = command.requestId || null;
|
|
777
|
+
const result = evalInspectFiberHooksSummary(command.fiberId);
|
|
778
|
+
send({ type: 'eval-probe-result', clientId, requestId, result });
|
|
779
|
+
break;
|
|
780
|
+
}
|
|
781
|
+
case 'inspect-fiber-props': {
|
|
782
|
+
const requestId = command.requestId || null;
|
|
783
|
+
const result = evalInspectFiberProps(command.fiberId);
|
|
784
|
+
send({ type: 'eval-probe-result', clientId, requestId, result });
|
|
785
|
+
break;
|
|
786
|
+
}
|
|
787
|
+
case 'inspect-fiber-owners': {
|
|
788
|
+
const requestId = command.requestId || null;
|
|
789
|
+
const result = evalInspectFiberOwners(command.fiberId);
|
|
790
|
+
send({ type: 'eval-probe-result', clientId, requestId, result });
|
|
791
|
+
break;
|
|
792
|
+
}
|
|
793
|
+
case 'inspect-fiber-parents': {
|
|
794
|
+
const requestId = command.requestId || null;
|
|
795
|
+
const result = evalInspectFiberParents(command.fiberId, command.max);
|
|
796
|
+
send({ type: 'eval-probe-result', clientId, requestId, result });
|
|
797
|
+
break;
|
|
798
|
+
}
|
|
799
|
+
case 'inspect-fiber-boundaries': {
|
|
800
|
+
const requestId = command.requestId || null;
|
|
801
|
+
const result = evalInspectFiberBoundaries(command.fiberId, command.max);
|
|
802
|
+
send({ type: 'eval-probe-result', clientId, requestId, result });
|
|
803
|
+
break;
|
|
804
|
+
}
|
|
805
|
+
case 'inspect-fiber-structure': {
|
|
806
|
+
const requestId = command.requestId || null;
|
|
807
|
+
const result = evalInspectFiberStructure(command.fiberId, command.hookIndex, command.windowSize);
|
|
808
|
+
send({ type: 'eval-probe-result', clientId, requestId, result });
|
|
809
|
+
break;
|
|
810
|
+
}
|
|
811
|
+
case 'describe-component': {
|
|
812
|
+
const requestId = command.requestId || null;
|
|
813
|
+
Promise.resolve(evalDescribeComponent(command.fiberId, command.name))
|
|
814
|
+
.then((result) => send({ type: 'eval-probe-result', clientId, requestId, result }))
|
|
815
|
+
.catch((error) => send({
|
|
816
|
+
type: 'eval-probe-result',
|
|
817
|
+
clientId,
|
|
818
|
+
requestId,
|
|
819
|
+
result: { ok: false, error: error?.message || String(error), stack: error?.stack },
|
|
820
|
+
}));
|
|
821
|
+
break;
|
|
822
|
+
}
|
|
823
|
+
case 'explain-component': {
|
|
824
|
+
const requestId = command.requestId || null;
|
|
825
|
+
Promise.resolve(evalExplainComponent(command.probeId, command.fiberId, command.name))
|
|
826
|
+
.then((result) => send({ type: 'eval-probe-result', clientId, requestId, result }))
|
|
827
|
+
.catch((error) => send({
|
|
828
|
+
type: 'eval-probe-result',
|
|
829
|
+
clientId,
|
|
830
|
+
requestId,
|
|
831
|
+
result: { ok: false, error: error?.message || String(error), stack: error?.stack },
|
|
832
|
+
}));
|
|
833
|
+
break;
|
|
834
|
+
}
|
|
835
|
+
case 'page-click': {
|
|
836
|
+
const requestId = command.requestId || null;
|
|
837
|
+
const result = evalPageClick(command.target || {});
|
|
838
|
+
send({ type: 'eval-probe-result', clientId, requestId, result });
|
|
839
|
+
break;
|
|
840
|
+
}
|
|
841
|
+
case 'fiber-invoke': {
|
|
842
|
+
const requestId = command.requestId || null;
|
|
843
|
+
Promise.resolve(evalFiberInvoke(command.fiberId, command.propName, command.args))
|
|
844
|
+
.then((result) => {
|
|
845
|
+
send({ type: 'eval-probe-result', clientId, requestId, result });
|
|
846
|
+
})
|
|
847
|
+
.catch((error) => {
|
|
848
|
+
send({
|
|
849
|
+
type: 'eval-probe-result',
|
|
850
|
+
clientId,
|
|
851
|
+
requestId,
|
|
852
|
+
result: {
|
|
853
|
+
ok: false,
|
|
854
|
+
error: error && error.message ? error.message : String(error),
|
|
855
|
+
stack: error && error.stack ? error.stack : undefined,
|
|
856
|
+
},
|
|
857
|
+
});
|
|
858
|
+
});
|
|
859
|
+
break;
|
|
860
|
+
}
|
|
861
|
+
case 'fiber-prop-contract': {
|
|
862
|
+
const requestId = command.requestId || null;
|
|
863
|
+
const result = evalFiberPropContract(command.fiberId, command.propName);
|
|
864
|
+
send({ type: 'eval-probe-result', clientId, requestId, result });
|
|
865
|
+
break;
|
|
866
|
+
}
|
|
867
|
+
case 'fiber-schedule-update': {
|
|
868
|
+
const requestId = command.requestId || null;
|
|
869
|
+
const result = evalFiberScheduleUpdate(command.fiberId, command.ownerIndex);
|
|
870
|
+
send({ type: 'eval-probe-result', clientId, requestId, result });
|
|
871
|
+
break;
|
|
872
|
+
}
|
|
873
|
+
case 'eval-prop-value': {
|
|
874
|
+
const requestId = command.requestId || null;
|
|
875
|
+
const result = evalPropValue(command.probeId, command.prop, command.ownerIndex);
|
|
876
|
+
send({ type: 'eval-probe-result', clientId, requestId, result });
|
|
877
|
+
break;
|
|
878
|
+
}
|
|
879
|
+
case 'eval-avalanche': {
|
|
880
|
+
const requestId = command.requestId || null;
|
|
881
|
+
const result = evalAvalanche(command);
|
|
882
|
+
send({ type: 'eval-avalanche-result', clientId, requestId, result });
|
|
883
|
+
break;
|
|
884
|
+
}
|
|
885
|
+
case 'eval-script': {
|
|
886
|
+
const requestId = command.requestId || null;
|
|
887
|
+
// async — must be handled with a promise. send result when ready.
|
|
888
|
+
evalScript(command).then((result) => {
|
|
889
|
+
send({ type: 'eval-script-result', clientId, requestId, result });
|
|
890
|
+
});
|
|
891
|
+
break;
|
|
892
|
+
}
|
|
893
|
+
default:
|
|
894
|
+
break;
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
// ─── Generic in-page script execution ────────────────────────────────────
|
|
899
|
+
// Snippet receives ctx = { args, helpers, log[] }. Must return a JSON-serializable value.
|
|
900
|
+
// Helpers walk React fibers from all roots discovered via DevTools renderer interface.
|
|
901
|
+
|
|
902
|
+
async function evalScript(command) {
|
|
903
|
+
const startedAt = Date.now();
|
|
904
|
+
const source = String(command.source || '');
|
|
905
|
+
const args = command.args && typeof command.args === 'object' ? command.args : {};
|
|
906
|
+
const timeoutMs = Math.min(30000, Math.max(50, Number(command.timeoutMs) || 5000));
|
|
907
|
+
if (!source.trim()) return { ok: false, error: 'empty script source' };
|
|
908
|
+
const log = [];
|
|
909
|
+
const helpers = buildScriptHelpers(log);
|
|
910
|
+
const ctx = { args, helpers, log };
|
|
911
|
+
try {
|
|
912
|
+
// Wrap as async IIFE; allow `return` and `await`. Indirect Function() to keep page-scope, not extension scope.
|
|
913
|
+
const factory = new Function(
|
|
914
|
+
'ctx',
|
|
915
|
+
'"use strict"; return (async function chippyScript(ctx){\n' + source + '\n}).call(globalThis, ctx);',
|
|
916
|
+
);
|
|
917
|
+
const resultPromise = Promise.resolve(factory(ctx));
|
|
918
|
+
const timeout = new Promise((_, reject) => global.setTimeout(() => reject(new Error('script timeout after ' + timeoutMs + 'ms')), timeoutMs));
|
|
919
|
+
const value = await Promise.race([resultPromise, timeout]);
|
|
920
|
+
const safe = safeJson(value);
|
|
921
|
+
return { ok: true, durationMs: Date.now() - startedAt, log, result: safe };
|
|
922
|
+
} catch (e) {
|
|
923
|
+
return { ok: false, durationMs: Date.now() - startedAt, log, error: String(e?.stack || e?.message || e) };
|
|
924
|
+
}
|
|
925
|
+
}
|
|
926
|
+
|
|
927
|
+
// Delegated to avalanche.iife.js — see src/chippy-bridge/fiber-introspect.ts
|
|
928
|
+
function safeJson(value, depth, seen) {
|
|
929
|
+
const b = getBridge();
|
|
930
|
+
if (b) return b.safeJson(value, depth, seen);
|
|
931
|
+
// Minimal fallback: pass primitives through, stringify the rest.
|
|
932
|
+
if (value == null) return value;
|
|
933
|
+
const t = typeof value;
|
|
934
|
+
if (t === 'string' || t === 'number' || t === 'boolean') return value;
|
|
935
|
+
return String(value);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
const fiberRegistry = new Map();
|
|
939
|
+
let fiberRegistrySeq = 1;
|
|
940
|
+
|
|
941
|
+
function rememberFiber(fiber) {
|
|
942
|
+
const id = 'fiber:' + (fiberRegistrySeq++);
|
|
943
|
+
fiberRegistry.set(id, fiber);
|
|
944
|
+
return id;
|
|
945
|
+
}
|
|
946
|
+
|
|
947
|
+
function normalizeRememberedFiberId(id) {
|
|
948
|
+
const raw = String(id || '').trim();
|
|
949
|
+
if (!raw) return '';
|
|
950
|
+
return /^\d+$/.test(raw) ? `fiber:${raw}` : raw;
|
|
951
|
+
}
|
|
952
|
+
|
|
953
|
+
function getRememberedFiber(id) {
|
|
954
|
+
const key = normalizeRememberedFiberId(id);
|
|
955
|
+
return key ? (fiberRegistry.get(key) || null) : null;
|
|
956
|
+
}
|
|
957
|
+
|
|
958
|
+
function buildScriptHelpers(log) {
|
|
959
|
+
function devtoolsHook() { return global.__REACT_DEVTOOLS_GLOBAL_HOOK__ || null; }
|
|
960
|
+
function getDisplayName(fiber) {
|
|
961
|
+
const t = fiber?.type;
|
|
962
|
+
if (!t) return null;
|
|
963
|
+
return t.displayName || t.name || (typeof t === 'string' ? t : null);
|
|
964
|
+
}
|
|
965
|
+
// Return the live HostRoot fiber of every React root. A FiberRoot (from
|
|
966
|
+
// `getFiberRoots`) exposes it as `.current`; a HostRoot fiber reached from a
|
|
967
|
+
// DOM container or by climbing `.return` may be the stale alternate, so it
|
|
968
|
+
// is re-resolved through its FiberRoot (`stateNode.current`).
|
|
969
|
+
function getRootFibers() {
|
|
970
|
+
const hook = devtoolsHook();
|
|
971
|
+
const out = [];
|
|
972
|
+
const seen = new Set();
|
|
973
|
+
function add(candidate) {
|
|
974
|
+
if (!candidate || typeof candidate !== 'object') return;
|
|
975
|
+
const fiberRoot = candidate.current ? candidate
|
|
976
|
+
: (candidate.tag === 3 && candidate.stateNode && candidate.stateNode.current ? candidate.stateNode : null);
|
|
977
|
+
const live = fiberRoot ? fiberRoot.current : candidate;
|
|
978
|
+
const key = fiberRoot || live;
|
|
979
|
+
if (!live || seen.has(key)) return;
|
|
980
|
+
seen.add(key);
|
|
981
|
+
out.push(live);
|
|
982
|
+
}
|
|
983
|
+
if (hook?.renderers && typeof hook.getFiberRoots === 'function') {
|
|
984
|
+
for (const [rendererId] of hook.renderers) {
|
|
985
|
+
try {
|
|
986
|
+
const roots = hook.getFiberRoots(rendererId);
|
|
987
|
+
if (roots && typeof roots.forEach === 'function') roots.forEach(add);
|
|
988
|
+
} catch (_) {}
|
|
989
|
+
}
|
|
990
|
+
}
|
|
991
|
+
// Fallback for hooks without getFiberRoots: walk root containers in the document.
|
|
992
|
+
const containers = document.querySelectorAll('[id], body div, body main');
|
|
993
|
+
containers.forEach((el) => {
|
|
994
|
+
const key = Object.keys(el).find((k) => k.startsWith('__reactContainer$') || k.startsWith('__reactFiber$'));
|
|
995
|
+
if (!key) return;
|
|
996
|
+
let fiber = el[key];
|
|
997
|
+
while (fiber && fiber.return) fiber = fiber.return;
|
|
998
|
+
add(fiber);
|
|
999
|
+
});
|
|
1000
|
+
return out;
|
|
1001
|
+
}
|
|
1002
|
+
function walkAll(visit, options) {
|
|
1003
|
+
const limit = options?.limit ?? 200000;
|
|
1004
|
+
let count = 0;
|
|
1005
|
+
const roots = getRootFibers();
|
|
1006
|
+
for (const root of roots) {
|
|
1007
|
+
const stack = [root];
|
|
1008
|
+
while (stack.length) {
|
|
1009
|
+
const f = stack.pop();
|
|
1010
|
+
if (!f) continue;
|
|
1011
|
+
if (count++ > limit) { log.push('walkAll: hit limit ' + limit); return; }
|
|
1012
|
+
try { visit(f); } catch (e) { log.push('visit error: ' + (e?.message || e)); }
|
|
1013
|
+
if (f.child) stack.push(f.child);
|
|
1014
|
+
if (f.sibling) stack.push(f.sibling);
|
|
1015
|
+
}
|
|
1016
|
+
}
|
|
1017
|
+
}
|
|
1018
|
+
function findFibers(predicate, options) {
|
|
1019
|
+
const out = [];
|
|
1020
|
+
const max = options?.max ?? 5000;
|
|
1021
|
+
walkAll((f) => { if (out.length < max && predicate(f)) out.push(f); }, options);
|
|
1022
|
+
return out;
|
|
1023
|
+
}
|
|
1024
|
+
function fiberInfo(f) {
|
|
1025
|
+
const dom = f?.stateNode && f.stateNode.nodeType === 1 ? f.stateNode : null;
|
|
1026
|
+
return {
|
|
1027
|
+
name: getDisplayName(f),
|
|
1028
|
+
tag: typeof f?.tag === 'number' ? f.tag : null,
|
|
1029
|
+
elementType: typeof f?.elementType === 'object' ? null : (f?.elementType?.displayName || f?.elementType?.name || null),
|
|
1030
|
+
source: getFiberSource(f),
|
|
1031
|
+
owner: f?._debugOwner ? (getDisplayName(f._debugOwner) || null) : null,
|
|
1032
|
+
domTag: dom?.tagName?.toLowerCase() || null,
|
|
1033
|
+
domText: dom?.textContent ? String(dom.textContent).trim().slice(0, 120) : null,
|
|
1034
|
+
domTestId: dom?.getAttribute?.('data-testid') || null,
|
|
1035
|
+
};
|
|
1036
|
+
}
|
|
1037
|
+
function ancestorFibers(fiber, max = 30) {
|
|
1038
|
+
const out = [];
|
|
1039
|
+
let cur = fiber?.return || null;
|
|
1040
|
+
while (cur && out.length < max) { out.push(fiber.return ? fiberInfo(cur) : null); cur = cur.return; }
|
|
1041
|
+
return out.filter(Boolean);
|
|
1042
|
+
}
|
|
1043
|
+
return { getDisplayName, getRootFibers, walkAll, findFibers, fiberInfo, ancestorFibers, devtoolsHook };
|
|
1044
|
+
}
|
|
1045
|
+
|
|
1046
|
+
// ─── DOM node inspection ──────────────────────────────────────────────────
|
|
1047
|
+
|
|
1048
|
+
function getProbeData(probeId) {
|
|
1049
|
+
const probeData = chippy.layoutLab?.getMarkerProbe?.(String(probeId));
|
|
1050
|
+
if (!probeData?.probe) return { error: 'No probe data for ' + probeId };
|
|
1051
|
+
return probeData;
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
function getProbeElement(probeId) {
|
|
1055
|
+
const probeData = getProbeData(probeId);
|
|
1056
|
+
if (probeData?.error) return probeData;
|
|
1057
|
+
const element = probeData.probe?._targetEl || null;
|
|
1058
|
+
if (!element) return { error: 'No target element for probe ' + probeId + ' (probe not placed on an element)' };
|
|
1059
|
+
return { probeData, element };
|
|
1060
|
+
}
|
|
1061
|
+
|
|
1062
|
+
function getProbeFiber(probeId) {
|
|
1063
|
+
const probeData = getProbeData(probeId);
|
|
1064
|
+
if (probeData?.error) return probeData;
|
|
1065
|
+
const fiber = probeData.probe?._bestFiber || null;
|
|
1066
|
+
if (!fiber) return { error: 'No fiber for probe ' + probeId };
|
|
1067
|
+
return { probeData, fiber };
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
function getFiberDomNode(fiber) {
|
|
1071
|
+
if (!fiber) return null;
|
|
1072
|
+
let current = fiber;
|
|
1073
|
+
while (current) {
|
|
1074
|
+
const node = current.stateNode;
|
|
1075
|
+
if (node && node.nodeType === 1) return node;
|
|
1076
|
+
current = current.child || null;
|
|
1077
|
+
}
|
|
1078
|
+
return null;
|
|
1079
|
+
}
|
|
1080
|
+
|
|
1081
|
+
function normalizeTargetText(value) {
|
|
1082
|
+
if (typeof value !== 'string') return null;
|
|
1083
|
+
const normalized = value.replace(/\s+/g, ' ').trim();
|
|
1084
|
+
return normalized || null;
|
|
1085
|
+
}
|
|
1086
|
+
|
|
1087
|
+
function describeClickedElement(element) {
|
|
1088
|
+
if (!element || element.nodeType !== 1) return null;
|
|
1089
|
+
return {
|
|
1090
|
+
tag: String(element.tagName || '').toLowerCase() || null,
|
|
1091
|
+
testId: element.getAttribute?.('data-testid') || null,
|
|
1092
|
+
text: normalizeTargetText(element.textContent || ''),
|
|
1093
|
+
};
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
/** @param {{ selector?: string, probeId?: string, fiberId?: string, [key: string]: any }} [target] */
|
|
1097
|
+
function resolvePageClickTarget(target = {}) {
|
|
1098
|
+
if (target == null || typeof target !== 'object') {
|
|
1099
|
+
return { ok: false, error: 'target object is required' };
|
|
1100
|
+
}
|
|
1101
|
+
if (target.selector != null) {
|
|
1102
|
+
const selector = String(target.selector || '').trim();
|
|
1103
|
+
if (!selector) return { ok: false, error: 'selector is required' };
|
|
1104
|
+
let elements;
|
|
1105
|
+
try {
|
|
1106
|
+
elements = Array.from(global.document.querySelectorAll(selector));
|
|
1107
|
+
} catch (e) {
|
|
1108
|
+
return { ok: false, error: `invalid selector: ${e.message}` };
|
|
1109
|
+
}
|
|
1110
|
+
if (elements.length === 0) return { ok: false, error: `selector matched no elements: ${selector}` };
|
|
1111
|
+
if (elements.length > 1) {
|
|
1112
|
+
return { ok: false, error: `selector matched ${elements.length} elements; refine the selector to one target`, matched: elements.length };
|
|
1113
|
+
}
|
|
1114
|
+
return { ok: true, mode: 'selector', selector, element: elements[0] };
|
|
1115
|
+
}
|
|
1116
|
+
if (target.probeId != null) {
|
|
1117
|
+
const probeId = String(target.probeId || '').trim();
|
|
1118
|
+
if (!probeId) return { ok: false, error: 'probeId is required' };
|
|
1119
|
+
const resolved = getProbeElement(probeId);
|
|
1120
|
+
if (resolved?.error) return { ok: false, error: resolved.error };
|
|
1121
|
+
return { ok: true, mode: 'probe', probeId, element: resolved.element };
|
|
1122
|
+
}
|
|
1123
|
+
if (target.fiberId != null) {
|
|
1124
|
+
const fiberId = normalizeRememberedFiberId(target.fiberId);
|
|
1125
|
+
if (!fiberId) return { ok: false, error: 'fiberId is required' };
|
|
1126
|
+
const fiber = getRememberedFiber(fiberId);
|
|
1127
|
+
if (!fiber) return { ok: false, error: `fiber ${fiberId} not found` };
|
|
1128
|
+
const element = getFiberDomNode(fiber);
|
|
1129
|
+
if (!element) return { ok: false, error: `fiber ${fiberId} has no DOM node` };
|
|
1130
|
+
return { ok: true, mode: 'fiber', fiberId, element };
|
|
1131
|
+
}
|
|
1132
|
+
return { ok: false, error: 'one of selector, probeId, or fiberId is required' };
|
|
1133
|
+
}
|
|
1134
|
+
|
|
1135
|
+
function evalPageClick(target) {
|
|
1136
|
+
const resolved = resolvePageClickTarget(target);
|
|
1137
|
+
if (!resolved.ok) return resolved;
|
|
1138
|
+
const element = resolved.element;
|
|
1139
|
+
if (!element || typeof element.click !== 'function') {
|
|
1140
|
+
return { ok: false, error: 'target element is not clickable', target: describeClickedElement(element) };
|
|
1141
|
+
}
|
|
1142
|
+
element.click();
|
|
1143
|
+
return {
|
|
1144
|
+
ok: true,
|
|
1145
|
+
mode: resolved.mode,
|
|
1146
|
+
target: describeClickedElement(element),
|
|
1147
|
+
selector: resolved.selector || null,
|
|
1148
|
+
probeId: resolved.probeId || null,
|
|
1149
|
+
fiberId: resolved.fiberId ?? null,
|
|
1150
|
+
};
|
|
1151
|
+
}
|
|
1152
|
+
|
|
1153
|
+
function readFiberProp(fiberId, propName) {
|
|
1154
|
+
const rememberedFiberId = normalizeRememberedFiberId(fiberId);
|
|
1155
|
+
if (!rememberedFiberId) return { ok: false, error: 'fiberId is required' };
|
|
1156
|
+
const name = String(propName || '').trim();
|
|
1157
|
+
if (!name) return { ok: false, error: 'propName is required' };
|
|
1158
|
+
const fiber = getRememberedFiber(rememberedFiberId);
|
|
1159
|
+
if (!fiber) return { ok: false, error: `fiber ${rememberedFiberId} not found`, fiberId: rememberedFiberId };
|
|
1160
|
+
const props = fiber.memoizedProps;
|
|
1161
|
+
if (!props || typeof props !== 'object') {
|
|
1162
|
+
return { ok: false, error: `fiber ${rememberedFiberId} has no props`, fiberId: rememberedFiberId, fiber };
|
|
1163
|
+
}
|
|
1164
|
+
if (!(name in props)) {
|
|
1165
|
+
return { ok: false, error: `prop ${name} not found on fiber ${rememberedFiberId}`, fiberId: rememberedFiberId, fiber, props };
|
|
1166
|
+
}
|
|
1167
|
+
return { ok: true, fiberId: rememberedFiberId, fiber, props, propName: name, value: props[name] };
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
function getFiberDisplayName(fiber) {
|
|
1171
|
+
if (!fiber) return 'Unknown';
|
|
1172
|
+
if (typeof fiber.type === 'string') return fiber.type;
|
|
1173
|
+
if (fiber.type && (fiber.type.displayName || fiber.type.name)) return fiber.type.displayName || fiber.type.name;
|
|
1174
|
+
if (typeof fiber.elementType === 'string') return fiber.elementType;
|
|
1175
|
+
if (fiber.elementType && (fiber.elementType.displayName || fiber.elementType.name)) return fiber.elementType.displayName || fiber.elementType.name;
|
|
1176
|
+
return 'Unknown';
|
|
1177
|
+
}
|
|
1178
|
+
|
|
1179
|
+
function functionSourcePreview(fn, max = 800) {
|
|
1180
|
+
try {
|
|
1181
|
+
const source = Function.prototype.toString.call(fn);
|
|
1182
|
+
return source.length > max ? source.slice(0, max) + '…' : source;
|
|
1183
|
+
} catch (e) {
|
|
1184
|
+
return null;
|
|
1185
|
+
}
|
|
1186
|
+
}
|
|
1187
|
+
|
|
1188
|
+
function getFiberSource(fiber) {
|
|
1189
|
+
const b = getBridge();
|
|
1190
|
+
if (b?.describeFiberSource) return b.describeFiberSource(fiber);
|
|
1191
|
+
const source = fiber?._debugSource || fiber?.type?._debugSource || null;
|
|
1192
|
+
if (!source) return null;
|
|
1193
|
+
return {
|
|
1194
|
+
fileName: source.fileName || null,
|
|
1195
|
+
lineNumber: source.lineNumber || null,
|
|
1196
|
+
columnNumber: source.columnNumber || null,
|
|
1197
|
+
};
|
|
1198
|
+
}
|
|
1199
|
+
|
|
1200
|
+
function summarizeFunctionContract(value) {
|
|
1201
|
+
if (typeof value !== 'function') {
|
|
1202
|
+
return { propType: typeof value, isFunction: false, fnLength: null, fnName: null, fnSourcePreview: null };
|
|
1203
|
+
}
|
|
1204
|
+
return {
|
|
1205
|
+
propType: 'function',
|
|
1206
|
+
isFunction: true,
|
|
1207
|
+
fnLength: Number.isFinite(value.length) ? value.length : null,
|
|
1208
|
+
fnName: value.name || null,
|
|
1209
|
+
fnSourcePreview: functionSourcePreview(value),
|
|
1210
|
+
};
|
|
1211
|
+
}
|
|
1212
|
+
|
|
1213
|
+
// Delegated to avalanche.iife.js — see src/chippy-bridge/fiber-invoke.ts
|
|
1214
|
+
function evalFiberPropContract(fiberId, propName) {
|
|
1215
|
+
const b = getBridge();
|
|
1216
|
+
if (!b || !b.evalFiberPropContract) return { ok: false, ...BRIDGE_MODULE_MISSING };
|
|
1217
|
+
return b.evalFiberPropContract(fiberId, propName, {
|
|
1218
|
+
normalizeRememberedFiberId,
|
|
1219
|
+
getRememberedFiber,
|
|
1220
|
+
});
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
// Delegated to avalanche.iife.js — see src/chippy-bridge/fiber-invoke.ts
|
|
1224
|
+
// Returns a Promise. The handler routes async responses through `send` once resolved.
|
|
1225
|
+
function evalFiberInvoke(fiberId, propName, args) {
|
|
1226
|
+
const b = getBridge();
|
|
1227
|
+
if (!b || !b.evalFiberInvoke) return Promise.resolve({ ok: false, ...BRIDGE_MODULE_MISSING });
|
|
1228
|
+
return Promise.resolve(b.evalFiberInvoke(fiberId, propName, args, {
|
|
1229
|
+
normalizeRememberedFiberId,
|
|
1230
|
+
getRememberedFiber,
|
|
1231
|
+
safeSerialize,
|
|
1232
|
+
}));
|
|
1233
|
+
}
|
|
1234
|
+
|
|
1235
|
+
// Delegated to avalanche.iife.js — see src/chippy-bridge/fiber-schedule-update.ts
|
|
1236
|
+
// Forces one re-render on a fiber via the DevTools renderer's scheduleUpdate.
|
|
1237
|
+
function evalFiberScheduleUpdate(fiberId, ownerIndex) {
|
|
1238
|
+
const b = getBridge();
|
|
1239
|
+
if (!b || !b.evalFiberScheduleUpdate) {
|
|
1240
|
+
return { ok: false, reason: 'no-scheduleUpdate', ...BRIDGE_MODULE_MISSING };
|
|
1241
|
+
}
|
|
1242
|
+
return b.evalFiberScheduleUpdate(fiberId, ownerIndex, {
|
|
1243
|
+
normalizeRememberedFiberId,
|
|
1244
|
+
getRememberedFiber,
|
|
1245
|
+
});
|
|
1246
|
+
}
|
|
1247
|
+
|
|
1248
|
+
// Delegated to avalanche.iife.js — see src/chippy-bridge/eval-helpers.ts
|
|
1249
|
+
function evalElementDom(element, parentLevel) {
|
|
1250
|
+
const b = getBridge();
|
|
1251
|
+
if (!b) return { ...BRIDGE_MODULE_MISSING };
|
|
1252
|
+
return b.evalElementDom(element, parentLevel, window.getComputedStyle.bind(window));
|
|
1253
|
+
}
|
|
1254
|
+
|
|
1255
|
+
function evalProbeDom(probeId, parentLevel) {
|
|
1256
|
+
const resolved = getProbeElement(probeId);
|
|
1257
|
+
if (resolved?.error) return resolved;
|
|
1258
|
+
return evalElementDom(resolved.element, parentLevel);
|
|
1259
|
+
}
|
|
1260
|
+
|
|
1261
|
+
// Delegated to avalanche.iife.js — see src/chippy-bridge/eval-helpers.ts
|
|
1262
|
+
function evalFiberProps(baseFiber, ownerIndex) {
|
|
1263
|
+
const b = getBridge();
|
|
1264
|
+
if (!b) return { ...BRIDGE_MODULE_MISSING };
|
|
1265
|
+
return b.evalFiberProps(baseFiber, ownerIndex, chippy.layoutLab?.getHookSummary, resolveOwnerFiber);
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
// Delegated to avalanche.iife.js — see src/chippy-bridge/eval-helpers.ts
|
|
1269
|
+
function evalFiberPropValue(baseFiber, propName, ownerIndex) {
|
|
1270
|
+
const b = getBridge();
|
|
1271
|
+
if (!b) return { ...BRIDGE_MODULE_MISSING };
|
|
1272
|
+
return b.evalFiberPropValue(baseFiber, propName, ownerIndex, chippy.layoutLab?.getHookSummary, resolveOwnerFiber, safeSerialize);
|
|
1273
|
+
}
|
|
1274
|
+
|
|
1275
|
+
// Delegated to avalanche.iife.js — see src/debug-render/replay.ts
|
|
1276
|
+
function evalFiberHooks(fiber) {
|
|
1277
|
+
const r = getReplay();
|
|
1278
|
+
if (r) return r.evalFiberHooks(fiber);
|
|
1279
|
+
if (!fiber) return { name: 'Unknown', hookCount: 0, truncated: false, maxHooks: 500, hooks: [], error: 'No fiber available' };
|
|
1280
|
+
return { name: (fiber.type && (fiber.type.displayName || fiber.type.name)) || 'Unknown', hookCount: 0, truncated: false, maxHooks: 500, hooks: [] };
|
|
1281
|
+
}
|
|
1282
|
+
|
|
1283
|
+
function summarizeHookList(raw) {
|
|
1284
|
+
const b = getBridge();
|
|
1285
|
+
return b ? b.summarizeHookList(raw) : (raw || { ...BRIDGE_MODULE_MISSING });
|
|
1286
|
+
}
|
|
1287
|
+
|
|
1288
|
+
function formatCounts(counts) {
|
|
1289
|
+
const b = getBridge();
|
|
1290
|
+
return b ? b.formatCounts(counts) : '';
|
|
1291
|
+
}
|
|
1292
|
+
|
|
1293
|
+
// ─── Replay engine — delegated to avalanche.iife.js → __AVALANCHE__.replay ─
|
|
1294
|
+
// The full implementation lives in src/debug-render/replay.ts and is bundled
|
|
1295
|
+
// into dist/avalanche.iife.js (single Chippy IIFE; no extra globals).
|
|
1296
|
+
// These shims delegate to that module, with inline fallbacks so the bridge
|
|
1297
|
+
// does not hard-crash when avalanche.iife.js has not yet loaded.
|
|
1298
|
+
|
|
1299
|
+
function getReplay() {
|
|
1300
|
+
const av = getAvalancheApi();
|
|
1301
|
+
return (av && av.replay) || null;
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
function getBridge() {
|
|
1305
|
+
const av = getAvalancheApi();
|
|
1306
|
+
return (av && av.bridge) || null;
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
function replayErrorPayload(e) {
|
|
1310
|
+
const r = getReplay();
|
|
1311
|
+
if (r) return r.replayErrorPayload(e);
|
|
1312
|
+
if (!e) return null;
|
|
1313
|
+
return { name: e.name || 'Error', message: e.message || String(e), stack: e.stack || '' };
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
function logReplayError(label, componentName, err) {
|
|
1317
|
+
const r = getReplay();
|
|
1318
|
+
if (r) { r.logReplayError(label, componentName, err); return; }
|
|
1319
|
+
try { console.error('[Chippy replay-error] ' + label + ' ' + componentName, err); } catch (_) {}
|
|
1320
|
+
}
|
|
1321
|
+
|
|
1322
|
+
// ─── Safe serialization ───────────────────────────────────────────────────
|
|
1323
|
+
|
|
1324
|
+
function safeSerialize(value, depth, seen) {
|
|
1325
|
+
const r = getReplay();
|
|
1326
|
+
if (r) return r.safeSerialize(value, depth === undefined ? 0 : depth, seen === undefined ? new Set() : seen);
|
|
1327
|
+
// Minimal inline fallback (used only before avalanche.iife.js loads).
|
|
1328
|
+
if (value === null || value === undefined) return value;
|
|
1329
|
+
if (typeof value === 'string') return value.length > 120 ? value.slice(0, 120) + '…' : value;
|
|
1330
|
+
if (typeof value === 'number' || typeof value === 'boolean') return value;
|
|
1331
|
+
if (typeof value === 'function') return '[function' + (value.name ? ':' + value.name : '') + ']';
|
|
1332
|
+
if (typeof value === 'symbol') return '[symbol:' + value.toString() + ']';
|
|
1333
|
+
if (Array.isArray(value)) return '[array:' + value.length + ']';
|
|
1334
|
+
return '[object]';
|
|
1335
|
+
}
|
|
1336
|
+
|
|
1337
|
+
// ─── Probe props eval ─────────────────────────────────────────────────────
|
|
1338
|
+
|
|
1339
|
+
// Delegated to avalanche.iife.js — see src/chippy-bridge/fiber-introspect.ts
|
|
1340
|
+
// Walks _debugOwner chain for component fibers; ownerIndex is deterministic.
|
|
1341
|
+
function resolveOwnerFiber(baseFiber, ownerIndex) {
|
|
1342
|
+
const b = getBridge();
|
|
1343
|
+
if (b) return b.resolveOwnerFiber(baseFiber, ownerIndex);
|
|
1344
|
+
return null; // safe fallback — caller treats null as "not found"
|
|
1345
|
+
}
|
|
1346
|
+
|
|
1347
|
+
function getFiberDomSummary(fiber) {
|
|
1348
|
+
const dom = getFiberDomNode(fiber);
|
|
1349
|
+
if (!dom) return null;
|
|
1350
|
+
return {
|
|
1351
|
+
tag: dom.tagName ? dom.tagName.toLowerCase() : null,
|
|
1352
|
+
testId: dom.getAttribute?.('data-testid') || null,
|
|
1353
|
+
text: dom.textContent ? String(dom.textContent).trim().slice(0, 120) : null,
|
|
1354
|
+
};
|
|
1355
|
+
}
|
|
1356
|
+
|
|
1357
|
+
function summarizePathFiber(fiber, index, relation) {
|
|
1358
|
+
if (!fiber) return null;
|
|
1359
|
+
const props = fiber.memoizedProps || {};
|
|
1360
|
+
const propNames = props && typeof props === 'object'
|
|
1361
|
+
? Object.keys(props).filter((k) => k !== 'children')
|
|
1362
|
+
: [];
|
|
1363
|
+
const hooks = evalFiberHooks(fiber);
|
|
1364
|
+
return {
|
|
1365
|
+
index,
|
|
1366
|
+
relation,
|
|
1367
|
+
name: getFiberDisplayName(fiber),
|
|
1368
|
+
tag: typeof fiber.tag === 'number' ? fiber.tag : null,
|
|
1369
|
+
key: fiber.key ?? null,
|
|
1370
|
+
propNames,
|
|
1371
|
+
hookCount: hooks?.hookCount ?? null,
|
|
1372
|
+
hookCountTruncated: hooks?.truncated ?? false,
|
|
1373
|
+
hookCountMax: hooks?.maxHooks ?? null,
|
|
1374
|
+
source: getFiberSource(fiber),
|
|
1375
|
+
dom: getFiberDomSummary(fiber),
|
|
1376
|
+
};
|
|
1377
|
+
}
|
|
1378
|
+
|
|
1379
|
+
function summarizeOwnerFiber(fiber, ownerIndex) {
|
|
1380
|
+
const summary = summarizePathFiber(fiber, ownerIndex, 'owner');
|
|
1381
|
+
if (!summary) return null;
|
|
1382
|
+
const { index, relation, tag, dom, ...owner } = summary;
|
|
1383
|
+
return { ownerIndex: index, ...owner };
|
|
1384
|
+
}
|
|
1385
|
+
|
|
1386
|
+
function collectParentFibers(fiber, max) {
|
|
1387
|
+
const limit = Math.max(0, Math.min(200, Number(max) || 50));
|
|
1388
|
+
const parents = [];
|
|
1389
|
+
let cur = fiber?.return || null;
|
|
1390
|
+
while (cur && parents.length < limit) {
|
|
1391
|
+
parents.push(summarizePathFiber(cur, parents.length, 'return'));
|
|
1392
|
+
cur = cur.return || null;
|
|
1393
|
+
}
|
|
1394
|
+
return parents.filter(Boolean);
|
|
1395
|
+
}
|
|
1396
|
+
|
|
1397
|
+
function evalFiberParents(fiber, fiberId, max) {
|
|
1398
|
+
if (!fiber) return { ok: false, error: 'No fiber available' };
|
|
1399
|
+
return {
|
|
1400
|
+
ok: true,
|
|
1401
|
+
fiberId: fiberId || null,
|
|
1402
|
+
component: summarizePathFiber(fiber, -1, 'self'),
|
|
1403
|
+
parents: collectParentFibers(fiber, max),
|
|
1404
|
+
};
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
function evalProbeParents(probeId, max) {
|
|
1408
|
+
const resolved = getProbeFiber(probeId);
|
|
1409
|
+
if (resolved?.error) return { ok: false, ...resolved };
|
|
1410
|
+
return { probeId: String(probeId), ...evalFiberParents(resolved.fiber, null, max) };
|
|
1411
|
+
}
|
|
1412
|
+
|
|
1413
|
+
function evalProbeOwners(probeId) {
|
|
1414
|
+
const resolved = getProbeFiber(probeId);
|
|
1415
|
+
if (resolved?.error) return { ok: false, ...resolved };
|
|
1416
|
+
const componentHooks = evalFiberHooks(resolved.fiber);
|
|
1417
|
+
const props = resolved.fiber?.memoizedProps || {};
|
|
1418
|
+
const propNames = props && typeof props === 'object'
|
|
1419
|
+
? Object.keys(props).filter((k) => k !== 'children')
|
|
1420
|
+
: [];
|
|
1421
|
+
const owners = [];
|
|
1422
|
+
for (let i = 0; i < 50; i++) {
|
|
1423
|
+
const owner = resolveOwnerFiber(resolved.fiber, i);
|
|
1424
|
+
if (!owner) break;
|
|
1425
|
+
owners.push(summarizeOwnerFiber(owner, i));
|
|
1426
|
+
}
|
|
1427
|
+
return {
|
|
1428
|
+
ok: true,
|
|
1429
|
+
probeId: String(probeId),
|
|
1430
|
+
component: {
|
|
1431
|
+
name: getFiberDisplayName(resolved.fiber),
|
|
1432
|
+
propNames,
|
|
1433
|
+
hookCount: componentHooks?.hookCount ?? null,
|
|
1434
|
+
hookCountTruncated: componentHooks?.truncated ?? false,
|
|
1435
|
+
hookCountMax: componentHooks?.maxHooks ?? null,
|
|
1436
|
+
},
|
|
1437
|
+
owners,
|
|
1438
|
+
};
|
|
1439
|
+
}
|
|
1440
|
+
|
|
1441
|
+
function evalProbeProps(probeId, ownerIndex) {
|
|
1442
|
+
const resolved = getProbeFiber(probeId);
|
|
1443
|
+
if (resolved?.error) return resolved;
|
|
1444
|
+
return evalFiberProps(resolved.fiber, ownerIndex);
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
// ─── Single prop value eval ───────────────────────────────────────────────
|
|
1448
|
+
|
|
1449
|
+
function evalPropValue(probeId, propName, ownerIndex) {
|
|
1450
|
+
const resolved = getProbeFiber(probeId);
|
|
1451
|
+
if (resolved?.error) return resolved;
|
|
1452
|
+
return evalFiberPropValue(resolved.fiber, propName, ownerIndex);
|
|
1453
|
+
}
|
|
1454
|
+
|
|
1455
|
+
// ─── Probe hooks eval ─────────────────────────────────────────────────────
|
|
1456
|
+
|
|
1457
|
+
function evalProbeHooks(probeId) {
|
|
1458
|
+
const resolved = getProbeFiber(probeId);
|
|
1459
|
+
if (resolved?.error) return resolved;
|
|
1460
|
+
return evalFiberHooks(resolved.fiber);
|
|
1461
|
+
}
|
|
1462
|
+
|
|
1463
|
+
function evalProbeHooksSummary(probeId) {
|
|
1464
|
+
const raw = evalProbeHooks(probeId);
|
|
1465
|
+
return summarizeHookList(raw);
|
|
1466
|
+
}
|
|
1467
|
+
|
|
1468
|
+
// Delegated to avalanche.iife.js — see src/chippy-bridge/eval-helpers.ts
|
|
1469
|
+
async function evalFindFibers(filters) {
|
|
1470
|
+
const b = getBridge();
|
|
1471
|
+
if (!b) return { count: 0, items: [], ...BRIDGE_MODULE_MISSING };
|
|
1472
|
+
const result = b.evalFindFibers(filters, buildScriptHelpers([]), rememberFiber);
|
|
1473
|
+
// Map each item's source through the served module, as `explain` does.
|
|
1474
|
+
if (b.resolveFindFiberSources && b.resolveFiberSource) {
|
|
1475
|
+
await b.resolveFindFiberSources(result.items, getRememberedFiber, b.resolveFiberSource);
|
|
1476
|
+
}
|
|
1477
|
+
return result;
|
|
1478
|
+
}
|
|
1479
|
+
|
|
1480
|
+
function evalCaptureFiberState(options) {
|
|
1481
|
+
const b = getBridge();
|
|
1482
|
+
if (!b?.captureFiberState) return { ok: false, ...BRIDGE_MODULE_MISSING };
|
|
1483
|
+
const helpers = buildScriptHelpers([]);
|
|
1484
|
+
return b.captureFiberState({
|
|
1485
|
+
...options,
|
|
1486
|
+
page: { url: global.location?.href || null, title: global.document?.title || null },
|
|
1487
|
+
}, {
|
|
1488
|
+
getRootFibers: helpers.getRootFibers,
|
|
1489
|
+
getDisplayName: helpers.getDisplayName,
|
|
1490
|
+
});
|
|
1491
|
+
}
|
|
1492
|
+
|
|
1493
|
+
function evalCompareFiberStates(before, after, options) {
|
|
1494
|
+
const b = getBridge();
|
|
1495
|
+
if (!b?.compareFiberStates) return { ok: false, ...BRIDGE_MODULE_MISSING };
|
|
1496
|
+
return b.compareFiberStates(before, after, options);
|
|
1497
|
+
}
|
|
1498
|
+
|
|
1499
|
+
// Declarative Error/Suspense inspection is bundled with Avalanche so the
|
|
1500
|
+
// extension shim stays version-coupled but implementation-free.
|
|
1501
|
+
function evalBoundaryChain(fiber, max) {
|
|
1502
|
+
const b = getBridge();
|
|
1503
|
+
if (!b) return { ok: false, ...BRIDGE_MODULE_MISSING };
|
|
1504
|
+
return b.inspectDeclarativeBoundaryChain(fiber, max, {
|
|
1505
|
+
rememberFiber,
|
|
1506
|
+
getFiberDomNode: getAvalancheApi()?.layoutLab?.getFiberDomNode || getFiberDomNode,
|
|
1507
|
+
});
|
|
1508
|
+
}
|
|
1509
|
+
|
|
1510
|
+
function evalProbeBoundaries(probeId, max) {
|
|
1511
|
+
const resolved = getProbeFiber(probeId);
|
|
1512
|
+
if (resolved?.error) return { ok: false, ...resolved };
|
|
1513
|
+
return { probeId: String(probeId), ...evalBoundaryChain(resolved.fiber, max) };
|
|
1514
|
+
}
|
|
1515
|
+
|
|
1516
|
+
function evalInspectFiberBoundaries(fiberId, max) {
|
|
1517
|
+
const fiber = getRememberedFiber(fiberId);
|
|
1518
|
+
if (!fiber) return { ok: false, error: 'Unknown fiberId: ' + fiberId };
|
|
1519
|
+
return { fiberId: String(fiberId), ...evalBoundaryChain(fiber, max) };
|
|
1520
|
+
}
|
|
1521
|
+
|
|
1522
|
+
function evalPageBoundaries(max) {
|
|
1523
|
+
const b = getBridge();
|
|
1524
|
+
if (!b) return { ok: false, ...BRIDGE_MODULE_MISSING };
|
|
1525
|
+
const helpers = buildScriptHelpers([]);
|
|
1526
|
+
return b.inspectPageDeclarativeBoundaries(max, {
|
|
1527
|
+
rememberFiber,
|
|
1528
|
+
getFiberDomNode: getAvalancheApi()?.layoutLab?.getFiberDomNode || getFiberDomNode,
|
|
1529
|
+
getRootFibers: helpers.getRootFibers,
|
|
1530
|
+
});
|
|
1531
|
+
}
|
|
1532
|
+
|
|
1533
|
+
function evalInspectFiberHooks(fiberId) {
|
|
1534
|
+
const fiber = getRememberedFiber(fiberId);
|
|
1535
|
+
if (!fiber) return { error: 'Unknown fiberId: ' + fiberId };
|
|
1536
|
+
return { fiberId: String(fiberId), ...evalFiberHooks(fiber) };
|
|
1537
|
+
}
|
|
1538
|
+
|
|
1539
|
+
function evalInspectFiberHooksSummary(fiberId) {
|
|
1540
|
+
return summarizeHookList(evalInspectFiberHooks(fiberId));
|
|
1541
|
+
}
|
|
1542
|
+
|
|
1543
|
+
// Delegated to avalanche.iife.js — see src/debug-render/replay.ts
|
|
1544
|
+
function noop() {}
|
|
1545
|
+
function getDispatcherRef() { const r = getReplay(); return r ? r.getDispatcherRef() : null; }
|
|
1546
|
+
function readHookValueFromNode(hookNode) { const r = getReplay(); return r ? r.readHookValueFromNode(hookNode) : (hookNode ? hookNode.memoizedState : undefined); }
|
|
1547
|
+
function fiberOnlyHookRecords(fiber) { const r = getReplay(); return r ? r.fiberOnlyHookRecords(fiber) : []; }
|
|
1548
|
+
function readContextValueFromFiber(fiber, contextArg) { const r = getReplay(); return r ? r.readContextValueFromFiber(fiber, contextArg) : undefined; }
|
|
1549
|
+
function isDispatcherIdle(ref) { const r = getReplay(); return r ? r.isDispatcherIdle(ref) : false; }
|
|
1550
|
+
|
|
1551
|
+
// Delegated to avalanche.iife.js — see src/debug-render/replay.ts
|
|
1552
|
+
function replayComponentHooks(fiber) {
|
|
1553
|
+
const r = getReplay();
|
|
1554
|
+
if (r) return r.replayComponentHooks(fiber);
|
|
1555
|
+
// Minimal fallback: fiber-only records, no replay attempted.
|
|
1556
|
+
const componentName = (fiber && fiber.type && (fiber.type.displayName || fiber.type.name)) || 'Unknown';
|
|
1557
|
+
return {
|
|
1558
|
+
componentName,
|
|
1559
|
+
records: fiberOnlyHookRecords(fiber),
|
|
1560
|
+
replayAttempted: false,
|
|
1561
|
+
replayError: 'avalanche.iife.js not loaded',
|
|
1562
|
+
replayErrorDetails: null,
|
|
1563
|
+
replayStatus: 'fiber-only-fallback',
|
|
1564
|
+
};
|
|
1565
|
+
}
|
|
1566
|
+
|
|
1567
|
+
// Delegated to avalanche.iife.js — see src/chippy-bridge/fiber-introspect.ts
|
|
1568
|
+
function evalInspectFiberStructure(fiberId, hookIndex, windowSize) {
|
|
1569
|
+
const fiber = getRememberedFiber(fiberId);
|
|
1570
|
+
if (!fiber) return { error: 'Unknown fiberId: ' + fiberId };
|
|
1571
|
+
const b = getBridge();
|
|
1572
|
+
if (!b) return { ...BRIDGE_MODULE_MISSING };
|
|
1573
|
+
return b.inspectFiberStructure(fiber, fiberId, hookIndex, windowSize, replayComponentHooks, evalFiberHooks);
|
|
1574
|
+
}
|
|
1575
|
+
|
|
1576
|
+
function evalInspectFiberProps(fiberId) {
|
|
1577
|
+
const fiber = getRememberedFiber(fiberId);
|
|
1578
|
+
if (!fiber) return { error: 'Unknown fiberId: ' + fiberId };
|
|
1579
|
+
return { fiberId: String(fiberId), ...evalFiberProps(fiber, null) };
|
|
1580
|
+
}
|
|
1581
|
+
|
|
1582
|
+
// Delegated to avalanche.iife.js — see src/chippy-bridge/fiber-introspect.ts
|
|
1583
|
+
function findFiberByName(name) {
|
|
1584
|
+
const b = getBridge();
|
|
1585
|
+
if (!b) return null;
|
|
1586
|
+
return b.findFiberByName(name, buildScriptHelpers([]));
|
|
1587
|
+
}
|
|
1588
|
+
|
|
1589
|
+
function renderTreeText(componentName, source, groups) {
|
|
1590
|
+
const b = getBridge();
|
|
1591
|
+
return b ? b.renderTreeText(componentName, source, groups) : componentName;
|
|
1592
|
+
}
|
|
1593
|
+
|
|
1594
|
+
function resolveFiberTarget(probeIdRaw, fiberIdRaw, nameRaw) {
|
|
1595
|
+
let fiber = null;
|
|
1596
|
+
let resolvedBy = null;
|
|
1597
|
+
if (probeIdRaw != null) {
|
|
1598
|
+
const resolved = getProbeFiber(probeIdRaw);
|
|
1599
|
+
if (resolved?.error) return { error: resolved.error };
|
|
1600
|
+
fiber = resolved.fiber;
|
|
1601
|
+
resolvedBy = 'probe';
|
|
1602
|
+
}
|
|
1603
|
+
if (!fiber && fiberIdRaw) {
|
|
1604
|
+
fiber = getRememberedFiber(fiberIdRaw);
|
|
1605
|
+
if (fiber) resolvedBy = 'fiberId';
|
|
1606
|
+
}
|
|
1607
|
+
if (!fiber && nameRaw) {
|
|
1608
|
+
fiber = findFiberByName(nameRaw);
|
|
1609
|
+
if (fiber) resolvedBy = 'name';
|
|
1610
|
+
}
|
|
1611
|
+
if (!fiber) return { error: fiberIdRaw ? ('Unknown fiberId: ' + fiberIdRaw) : ('No fiber matches name: ' + nameRaw) };
|
|
1612
|
+
return { fiber, resolvedBy };
|
|
1613
|
+
}
|
|
1614
|
+
|
|
1615
|
+
async function evalDescribeComponent(fiberIdRaw, nameRaw) {
|
|
1616
|
+
const b = getBridge();
|
|
1617
|
+
if (!b) return { ok: false, ...BRIDGE_MODULE_MISSING };
|
|
1618
|
+
const target = resolveFiberTarget(null, fiberIdRaw, nameRaw);
|
|
1619
|
+
if (target.error) return { ok: false, error: target.error };
|
|
1620
|
+
if (b.resolveFiberSource) await b.resolveFiberSource(target.fiber);
|
|
1621
|
+
return b.evalDescribeComponent(fiberIdRaw, nameRaw, {
|
|
1622
|
+
// Keep source resolution, hook reads, and replay on the same fiber even
|
|
1623
|
+
// if React commits while the source map is being fetched.
|
|
1624
|
+
resolveFiberTarget: () => target,
|
|
1625
|
+
rememberFiber,
|
|
1626
|
+
getRememberedFiber,
|
|
1627
|
+
evalFiberHooks,
|
|
1628
|
+
replayComponentHooks,
|
|
1629
|
+
renderTreeText: b.renderTreeText,
|
|
1630
|
+
});
|
|
1631
|
+
}
|
|
1632
|
+
|
|
1633
|
+
function renderCompactComponentText(componentName, source, summary, records, warnings) {
|
|
1634
|
+
const b = getBridge();
|
|
1635
|
+
return b ? b.renderCompactComponentText(componentName, source, summary, records, warnings) : componentName;
|
|
1636
|
+
}
|
|
1637
|
+
|
|
1638
|
+
async function evalExplainComponent(probeIdRaw, fiberIdRaw, nameRaw) {
|
|
1639
|
+
const target = resolveFiberTarget(probeIdRaw, fiberIdRaw, nameRaw);
|
|
1640
|
+
if (target.error) return { ok: false, error: target.error };
|
|
1641
|
+
const { fiber, resolvedBy } = target;
|
|
1642
|
+
const b = getBridge();
|
|
1643
|
+
if (b?.resolveFiberSource) await b.resolveFiberSource(fiber);
|
|
1644
|
+
const fiberId = fiberIdRaw && getRememberedFiber(fiberIdRaw) ? String(fiberIdRaw) : rememberFiber(fiber);
|
|
1645
|
+
const raw = { fiberId, ...evalFiberHooks(fiber) };
|
|
1646
|
+
const summary = summarizeHookList(raw);
|
|
1647
|
+
const { componentName, records, replayAttempted, replayError, replayErrorDetails, replayStatus } = replayComponentHooks(fiber);
|
|
1648
|
+
summary.liveHookCount = summary.hookCount;
|
|
1649
|
+
summary.replayedHookCount = records.filter((r) => r.index != null).length;
|
|
1650
|
+
const source = getFiberSource(fiber);
|
|
1651
|
+
const warnings = [];
|
|
1652
|
+
if (!replayAttempted) warnings.push('fiber-only: component functions and hooks were not executed; custom-hook ancestry is unavailable unless captured metadata exists');
|
|
1653
|
+
if (replayError) warnings.push('replay-error: ' + replayError + ' (stack logged to browser console)');
|
|
1654
|
+
const text = renderCompactComponentText(componentName, source, summary, records, warnings);
|
|
1655
|
+
return {
|
|
1656
|
+
ok: true,
|
|
1657
|
+
componentName,
|
|
1658
|
+
fiberId,
|
|
1659
|
+
resolvedBy,
|
|
1660
|
+
source: source ? { fileName: source.fileName || null, lineNumber: source.lineNumber || null } : null,
|
|
1661
|
+
hookCount: summary.hookCount,
|
|
1662
|
+
liveHookCount: summary.liveHookCount,
|
|
1663
|
+
replayedHookCount: summary.replayedHookCount,
|
|
1664
|
+
replayStatus: replayStatus || 'not-run-fiber-only',
|
|
1665
|
+
text,
|
|
1666
|
+
captureEngineOk: true,
|
|
1667
|
+
warnings,
|
|
1668
|
+
replayError: replayError || undefined,
|
|
1669
|
+
replayErrorDetails: replayErrorDetails || undefined,
|
|
1670
|
+
};
|
|
1671
|
+
}
|
|
1672
|
+
|
|
1673
|
+
// Delegated to avalanche.iife.js — see src/chippy-bridge/fiber-introspect.ts
|
|
1674
|
+
function evalInspectFiberOwners(fiberId) {
|
|
1675
|
+
const fiber = getRememberedFiber(fiberId);
|
|
1676
|
+
if (!fiber) return { error: 'Unknown fiberId: ' + fiberId };
|
|
1677
|
+
const b = getBridge();
|
|
1678
|
+
if (!b) return { ...BRIDGE_MODULE_MISSING };
|
|
1679
|
+
return b.inspectFiberOwners(fiber, fiberId, buildScriptHelpers([]));
|
|
1680
|
+
}
|
|
1681
|
+
|
|
1682
|
+
function evalInspectFiberParents(fiberId, max) {
|
|
1683
|
+
const fiber = getRememberedFiber(fiberId);
|
|
1684
|
+
if (!fiber) return { ok: false, error: 'Unknown fiberId: ' + fiberId };
|
|
1685
|
+
return evalFiberParents(fiber, String(fiberId), max);
|
|
1686
|
+
}
|
|
1687
|
+
|
|
1688
|
+
|
|
1689
|
+
// ─── Connection ──────────────────────────────────────────────────────────
|
|
1690
|
+
|
|
1691
|
+
function connect() {
|
|
1692
|
+
const wsUrl = deriveWsUrl(config.helperBaseUrl);
|
|
1693
|
+
socket = new WebSocket(wsUrl);
|
|
1694
|
+
|
|
1695
|
+
socket.addEventListener('open', () => {
|
|
1696
|
+
setConnState('connected');
|
|
1697
|
+
// Defer hello + snapshot slightly so React has time to render
|
|
1698
|
+
// and detectBuildMode() has fibers to walk.
|
|
1699
|
+
global.setTimeout(() => {
|
|
1700
|
+
if (destroyed) return;
|
|
1701
|
+
buildMode = detectBuildMode();
|
|
1702
|
+
sendHello('open');
|
|
1703
|
+
if (config.activated && !probesInstalled()) {
|
|
1704
|
+
// A navigation clears the DOM; an ordinary socket reconnect does not.
|
|
1705
|
+
chippy.layoutLab?.exposeDefaultMarkers?.();
|
|
1706
|
+
} else if (config.activated) {
|
|
1707
|
+
chippy.layoutLab?.updatePanel?.();
|
|
1708
|
+
}
|
|
1709
|
+
scheduleSnapshot('open');
|
|
1710
|
+
}, 350);
|
|
1711
|
+
});
|
|
1712
|
+
|
|
1713
|
+
socket.addEventListener('message', (event) => {
|
|
1714
|
+
try {
|
|
1715
|
+
const payload = JSON.parse(event.data);
|
|
1716
|
+
if (payload.type === 'connected') {
|
|
1717
|
+
// server echoes back our real clientId if it assigned one
|
|
1718
|
+
}
|
|
1719
|
+
if (payload.type === 'command') {
|
|
1720
|
+
handleCommand(payload.command);
|
|
1721
|
+
}
|
|
1722
|
+
} catch (e) {
|
|
1723
|
+
console.error('[chippy-bridge] message parse error:', e?.message || e);
|
|
1724
|
+
}
|
|
1725
|
+
});
|
|
1726
|
+
|
|
1727
|
+
socket.addEventListener('close', () => {
|
|
1728
|
+
if (destroyed) return;
|
|
1729
|
+
setConnState('reconnecting');
|
|
1730
|
+
reconnectTimer = global.setTimeout(connect, config.reconnectDelayMs);
|
|
1731
|
+
});
|
|
1732
|
+
|
|
1733
|
+
socket.addEventListener('error', () => {
|
|
1734
|
+
setConnState('failed');
|
|
1735
|
+
});
|
|
1736
|
+
}
|
|
1737
|
+
|
|
1738
|
+
// ─── Config update ───────────────────────────────────────────────────────
|
|
1739
|
+
|
|
1740
|
+
function applyConfig(nextConfig) {
|
|
1741
|
+
const previousBaseUrl = config.helperBaseUrl;
|
|
1742
|
+
const wasActivated = config.activated;
|
|
1743
|
+
|
|
1744
|
+
const contextUi = nextConfig?.contextUiEnabled;
|
|
1745
|
+
config = {
|
|
1746
|
+
...config,
|
|
1747
|
+
...nextConfig,
|
|
1748
|
+
helperBaseUrl: normalizeBaseUrl(nextConfig?.helperBaseUrl || config.helperBaseUrl),
|
|
1749
|
+
activated: nextConfig?.activated ?? config.activated,
|
|
1750
|
+
...(contextUi === undefined ? {} : { contextUiEnabled: contextUi !== false }),
|
|
1751
|
+
};
|
|
1752
|
+
chippy.config = { ...config };
|
|
1753
|
+
if (config.ui === 'storybook-panel' || config.ui === 'none') {
|
|
1754
|
+
chippy.layoutLab?.removeInPageUi?.();
|
|
1755
|
+
}
|
|
1756
|
+
|
|
1757
|
+
if (config.activated && !wasActivated && !probesInstalled()) {
|
|
1758
|
+
chippy.layoutLab?.exposeDefaultMarkers?.();
|
|
1759
|
+
}
|
|
1760
|
+
if (!config.activated && wasActivated) {
|
|
1761
|
+
chippy.layoutLab?.clearAll?.();
|
|
1762
|
+
}
|
|
1763
|
+
|
|
1764
|
+
if (socket && previousBaseUrl !== config.helperBaseUrl) {
|
|
1765
|
+
try { socket.close(); } catch {}
|
|
1766
|
+
}
|
|
1767
|
+
}
|
|
1768
|
+
|
|
1769
|
+
// ─── Event listeners ─────────────────────────────────────────────────────
|
|
1770
|
+
|
|
1771
|
+
// Publish a fresh snapshot whenever layout-lab updates (markers moved/dropped/docked).
|
|
1772
|
+
onLayoutLabUpdate = (event) => {
|
|
1773
|
+
const reason = event.detail?.reason || 'layout-lab-update';
|
|
1774
|
+
// A boundary selection is a structured signal, not just a snapshot refresh:
|
|
1775
|
+
// forward the collateral boundary + distance to the helper as activity so it
|
|
1776
|
+
// is recorded distinctly from generic marker movement.
|
|
1777
|
+
if (reason === 'inspect-boundary') {
|
|
1778
|
+
sendActivity('inspect-boundary', {
|
|
1779
|
+
markerId: event.detail?.markerId ?? null,
|
|
1780
|
+
boundaryKind: event.detail?.boundaryKind ?? null,
|
|
1781
|
+
boundary: event.detail?.boundary ?? null,
|
|
1782
|
+
distance: event.detail?.distance ?? event.detail?.boundary?.distance ?? null,
|
|
1783
|
+
});
|
|
1784
|
+
}
|
|
1785
|
+
scheduleSnapshot(reason);
|
|
1786
|
+
};
|
|
1787
|
+
global.addEventListener('layout-lab:update', onLayoutLabUpdate);
|
|
1788
|
+
|
|
1789
|
+
// Legacy postMessage path from the extension content-script
|
|
1790
|
+
onWindowMessage = (event) => {
|
|
1791
|
+
if (event.source !== global || event.data?.source !== 'chippy-extension') return;
|
|
1792
|
+
if (event.data.type === 'config') { applyConfig(event.data.payload || {}); scheduleSnapshot('config'); }
|
|
1793
|
+
if (event.data.type === 'activate') activate();
|
|
1794
|
+
if (event.data.type === 'deactivate') deactivate();
|
|
1795
|
+
};
|
|
1796
|
+
global.addEventListener('message', onWindowMessage);
|
|
1797
|
+
|
|
1798
|
+
// CSP-safe commands from the extension content-script via CustomEvent
|
|
1799
|
+
onExtCommand = (event) => {
|
|
1800
|
+
const detail = event.detail || {};
|
|
1801
|
+
if (detail.type === 'deactivate') deactivate();
|
|
1802
|
+
if (detail.type === 'activate') activate();
|
|
1803
|
+
if (detail.type === 'apply-config') {
|
|
1804
|
+
applyConfig({ helperBaseUrl: detail.helperBaseUrl, activated: detail.activated });
|
|
1805
|
+
scheduleSnapshot('apply-config');
|
|
1806
|
+
}
|
|
1807
|
+
};
|
|
1808
|
+
global.document.addEventListener('chippy-ext-command', onExtCommand);
|
|
1809
|
+
|
|
1810
|
+
// An extension bridge that is already running stands down when another
|
|
1811
|
+
// integration claims the page; the background's owner check covers bridges
|
|
1812
|
+
// that have not loaded yet.
|
|
1813
|
+
function standDown(owner) {
|
|
1814
|
+
if (destroyed) return;
|
|
1815
|
+
destroy();
|
|
1816
|
+
try { chippy.layoutLab?.clearAll?.(); } catch (e) {}
|
|
1817
|
+
try { chippy.layoutLab?.removeInPageUi?.(); } catch (e) {}
|
|
1818
|
+
chippy.standDown = { at: Date.now(), owner: owner?.integration || null };
|
|
1819
|
+
setConnState('yielded');
|
|
1820
|
+
}
|
|
1821
|
+
onOwnerClaimed = () => {
|
|
1822
|
+
if (isExtensionBridge() && isLiveForeignOwner(global.__CHIPPY_OWNER__)) standDown(global.__CHIPPY_OWNER__);
|
|
1823
|
+
};
|
|
1824
|
+
global.addEventListener('chippy-owner-claimed', onOwnerClaimed);
|
|
1825
|
+
|
|
1826
|
+
locationInterval = global.setInterval(() => {
|
|
1827
|
+
if (global.location.href !== lastLocationHref) {
|
|
1828
|
+
lastLocationHref = global.location.href;
|
|
1829
|
+
sendHello('location-change');
|
|
1830
|
+
scheduleSnapshot('location-change');
|
|
1831
|
+
}
|
|
1832
|
+
}, 1000);
|
|
1833
|
+
|
|
1834
|
+
heartbeatInterval = global.setInterval(() => {
|
|
1835
|
+
send({
|
|
1836
|
+
type: 'heartbeat',
|
|
1837
|
+
clientId,
|
|
1838
|
+
bridgeVersion: BRIDGE_VERSION,
|
|
1839
|
+
buildMode: resolveBuildMode(),
|
|
1840
|
+
page: getPageState(),
|
|
1841
|
+
integration: getIntegrationIdentity(),
|
|
1842
|
+
activated: config.activated,
|
|
1843
|
+
probesActive: probesActive(),
|
|
1844
|
+
});
|
|
1845
|
+
}, 15000);
|
|
1846
|
+
|
|
1847
|
+
function handleFocusActivity() {
|
|
1848
|
+
const now = Date.now();
|
|
1849
|
+
if (now - lastFocusActivityAt < 750) return;
|
|
1850
|
+
lastFocusActivityAt = now;
|
|
1851
|
+
sendActivity('focus', { visibilityState: global.document.visibilityState || 'unknown' });
|
|
1852
|
+
}
|
|
1853
|
+
|
|
1854
|
+
function handleVisibilityActivity() {
|
|
1855
|
+
const nextState = global.document.visibilityState || 'unknown';
|
|
1856
|
+
if (nextState === lastVisibilityState) return;
|
|
1857
|
+
lastVisibilityState = nextState;
|
|
1858
|
+
sendActivity(nextState === 'visible' ? 'visible' : 'hidden', { visibilityState: nextState });
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1861
|
+
function handlePointerActivity() {
|
|
1862
|
+
const now = Date.now();
|
|
1863
|
+
if (now - lastPointerActivityAt < 3000) return;
|
|
1864
|
+
lastPointerActivityAt = now;
|
|
1865
|
+
sendActivity('pointer', { visibilityState: global.document.visibilityState || 'unknown' });
|
|
1866
|
+
}
|
|
1867
|
+
|
|
1868
|
+
global.addEventListener('focus', handleFocusActivity, true);
|
|
1869
|
+
global.addEventListener('pointerdown', handlePointerActivity, true);
|
|
1870
|
+
global.document.addEventListener('visibilitychange', handleVisibilityActivity, true);
|
|
1871
|
+
|
|
1872
|
+
function destroy() {
|
|
1873
|
+
destroyed = true;
|
|
1874
|
+
try { if (reconnectTimer) global.clearTimeout(reconnectTimer); } catch (e) {}
|
|
1875
|
+
try { if (locationInterval) global.clearInterval(locationInterval); } catch (e) {}
|
|
1876
|
+
try { if (heartbeatInterval) global.clearInterval(heartbeatInterval); } catch (e) {}
|
|
1877
|
+
try { if (socket) socket.close(); } catch (e) {}
|
|
1878
|
+
try { if (onLayoutLabUpdate) global.removeEventListener('layout-lab:update', onLayoutLabUpdate); } catch (e) {}
|
|
1879
|
+
try { if (onWindowMessage) global.removeEventListener('message', onWindowMessage); } catch (e) {}
|
|
1880
|
+
try { if (onExtCommand) global.document.removeEventListener('chippy-ext-command', onExtCommand); } catch (e) {}
|
|
1881
|
+
try { if (onOwnerClaimed) global.removeEventListener('chippy-owner-claimed', onOwnerClaimed); } catch (e) {}
|
|
1882
|
+
}
|
|
1883
|
+
|
|
1884
|
+
// ─── Public API ──────────────────────────────────────────────────────────
|
|
1885
|
+
|
|
1886
|
+
chippy.evalElementDom = evalElementDom;
|
|
1887
|
+
chippy.evalFiberProps = evalFiberProps;
|
|
1888
|
+
chippy.evalFiberPropValue = evalFiberPropValue;
|
|
1889
|
+
chippy.evalFiberHooks = evalFiberHooks;
|
|
1890
|
+
|
|
1891
|
+
chippy.pageBridge = {
|
|
1892
|
+
version: BRIDGE_VERSION,
|
|
1893
|
+
helpers: buildScriptHelpers([]),
|
|
1894
|
+
__debug: {
|
|
1895
|
+
replayComponentHooks,
|
|
1896
|
+
evalDescribeComponent,
|
|
1897
|
+
evalExplainComponent,
|
|
1898
|
+
evalInspectFiberStructure,
|
|
1899
|
+
rememberFiber,
|
|
1900
|
+
},
|
|
1901
|
+
destroy,
|
|
1902
|
+
applyConfig,
|
|
1903
|
+
activate,
|
|
1904
|
+
deactivate,
|
|
1905
|
+
publishSnapshot,
|
|
1906
|
+
setProbeAnnotation,
|
|
1907
|
+
ensureProbes,
|
|
1908
|
+
clearProbes,
|
|
1909
|
+
showStatus,
|
|
1910
|
+
hideStatus,
|
|
1911
|
+
avalanche: {
|
|
1912
|
+
install() { return evalAvalanche({ action: 'install' }); },
|
|
1913
|
+
start(options) { return evalAvalanche({ action: 'start', max: options?.max }); },
|
|
1914
|
+
stop() { return evalAvalanche({ action: 'stop' }); },
|
|
1915
|
+
clear() { return evalAvalanche({ action: 'clear' }); },
|
|
1916
|
+
uninstall() { return evalAvalanche({ action: 'uninstall' }); },
|
|
1917
|
+
status() { return evalAvalanche({ action: 'status' }); },
|
|
1918
|
+
export() { return evalAvalanche({ action: 'export' }); },
|
|
1919
|
+
exportLast(count) { return evalAvalanche({ action: 'export-last', count }); },
|
|
1920
|
+
debugHook(sourceId, hookIndex, windowSize) { return evalAvalanche({ action: 'debug-hook', sourceId, hookIndex, window: windowSize }); },
|
|
1921
|
+
metrics() { return evalAvalanche({ action: 'metrics' }); },
|
|
1922
|
+
help() { return evalAvalanche({ action: 'help' }); },
|
|
1923
|
+
},
|
|
1924
|
+
getState() {
|
|
1925
|
+
return {
|
|
1926
|
+
clientId,
|
|
1927
|
+
config,
|
|
1928
|
+
owner: global.__CHIPPY_OWNER__ || null,
|
|
1929
|
+
ownerResult: global.__CHIPPY_OWNER_RESULT__ || null,
|
|
1930
|
+
integration: getIntegrationIdentity(),
|
|
1931
|
+
connState: chippy.connState || 'unknown',
|
|
1932
|
+
buildMode: resolveBuildMode(),
|
|
1933
|
+
page: getPageState(),
|
|
1934
|
+
activated: config.activated,
|
|
1935
|
+
probesActive: probesActive(),
|
|
1936
|
+
markers: summarizeMarkers(),
|
|
1937
|
+
avalanche: readAvalancheStatus(),
|
|
1938
|
+
};
|
|
1939
|
+
},
|
|
1940
|
+
};
|
|
1941
|
+
|
|
1942
|
+
// If bootstrap/config asked for activation, expose probes immediately.
|
|
1943
|
+
// Do not wait for helper WebSocket open or an external command.
|
|
1944
|
+
if (config.activated && !probesInstalled()) {
|
|
1945
|
+
chippy.layoutLab?.exposeDefaultMarkers?.();
|
|
1946
|
+
}
|
|
1947
|
+
|
|
1948
|
+
if (isExtensionBridge() && isLiveForeignOwner(global.__CHIPPY_OWNER__)) {
|
|
1949
|
+
standDown(global.__CHIPPY_OWNER__);
|
|
1950
|
+
} else {
|
|
1951
|
+
connect();
|
|
1952
|
+
}
|
|
1953
|
+
})(window);
|