@atlassian/chippy 0.0.1-beta.1 → 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/README.md +142 -54
- package/bin/chippy.mjs +53 -95
- package/bin/pack.mjs +20 -0
- package/bin/skill-dir.mjs +15 -0
- package/lib/avalanche.js +54 -23
- package/lib/avalanche.js.map +2 -2
- package/lib/bootstrap.js +20 -1
- package/lib/bootstrap.js.map +3 -3
- package/lib/extension/avalanche.es.js +173 -34
- package/lib/extension/avalanche.es.js.map +1 -1
- package/lib/extension/chippy-page-bridge.js +164 -44
- package/lib/mcp-skill/README.md +2 -4
- package/lib/mcp-skill/SKILL.md +21 -16
- package/lib/mcp-skill/extension/armed-reload-startup.js +83 -0
- package/lib/mcp-skill/extension/avalanche.es.js +173 -34
- package/lib/mcp-skill/extension/avalanche.es.js.map +1 -1
- package/lib/mcp-skill/extension/avalanche.iife.js +173 -34
- package/lib/mcp-skill/extension/avalanche.iife.js.map +1 -1
- package/lib/mcp-skill/extension/background.js +2 -0
- package/lib/mcp-skill/extension/chippy-page-bridge.js +164 -44
- package/lib/mcp-skill/extension/content-script.js +23 -0
- package/lib/mcp-skill/extension/devtools-hook-shim.js +18 -1
- package/lib/mcp-skill/extension/early-bootstrap.js +3 -1
- package/lib/mcp-skill/extension/manifest.json +1 -1
- package/lib/mcp-skill/extension/panel-settings.js +2 -20
- package/lib/mcp-skill/extension/startup-handoff.js +30 -0
- package/lib/mcp-skill/references/README.md +2 -2
- package/lib/mcp-skill/references/avalanche-detector.md +3 -3
- package/lib/mcp-skill/references/browser-runtime-api.md +4 -4
- package/lib/mcp-skill/references/command-output-formats.md +12 -2
- package/lib/mcp-skill/references/command-surface.md +6 -6
- package/lib/mcp-skill/references/mode-a-injection.md +43 -9
- package/lib/mcp-skill/references/playwright-cli-setup.md +5 -5
- package/lib/mcp-skill/references/playwright-probes.md +1 -1
- package/lib/mcp-skill/references/recipes.txt +1 -1
- package/lib/mcp-skill/references/stability-render.md +4 -4
- package/lib/mcp-skill/references/test-mode-playbook.md +2 -1
- package/lib/mcp-skill/references/troubleshooting.md +6 -1
- package/lib/mcp-skill/scripts/chippy-helper-server.js +116 -58
- package/lib/mcp-skill/scripts/chippy.sh +41 -7
- package/lib/mcp-skill/scripts/helper-access.js +20 -6
- package/lib/mcp-skill/scripts/helper-proxy.js +87 -0
- package/lib/mcp-skill/scripts/page-socket-trust.js +81 -0
- package/lib/plugin.js.map +2 -2
- package/lib/storybook/connection-fallback.jsx +1 -1
- package/lib/storybook/manager-owner.js +42 -0
- package/lib/storybook/manager.jsx +22 -0
- package/lib/storybook/preview.js +53 -18
- package/lib/storybook/setup.js +15 -4
- package/lib/types/chippy-bridge/fiber-source.d.ts +14 -3
- package/lib/types/chippy-bridge/react-renderer.d.ts +30 -5
- package/lib/types/chippy-bridge/recorded-source.d.ts +44 -0
- package/lib/types/debug-render/core.d.ts +6 -0
- package/package.json +3 -1
|
@@ -13,6 +13,8 @@ const DEFAULT_HELPER = 'http://127.0.0.1:7823';
|
|
|
13
13
|
// with the plugins it enables until the user saves their own selection.
|
|
14
14
|
const BUILD_DEFAULT_PLUGINS = Array.isArray(globalThis.__CHIPPY_BUILD?.defaultPlugins) ? globalThis.__CHIPPY_BUILD.defaultPlugins : [];
|
|
15
15
|
const CONTENT_SCRIPTS = ['panel-settings.js', 'content-script.js'];
|
|
16
|
+
// Registers early startup for the navigation after an armed reload.
|
|
17
|
+
importScripts('armed-reload-startup.js');
|
|
16
18
|
const EXTENSION_INTEGRATION = Object.freeze({
|
|
17
19
|
kind: 'extension',
|
|
18
20
|
who: 'Chippy browser extension',
|
|
@@ -8,6 +8,16 @@
|
|
|
8
8
|
/** @type {ChippyPageRuntime} */
|
|
9
9
|
const chippy = global.__CHIPPY = global.__CHIPPY || {};
|
|
10
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';
|
|
11
21
|
if (chippy.pageBridge) {
|
|
12
22
|
// Always tear down and reinstall — version-equality skip caused stale code to win.
|
|
13
23
|
try { chippy.pageBridge.destroy?.(); } catch (e) {}
|
|
@@ -37,18 +47,53 @@
|
|
|
37
47
|
return config?.integration || global.__CHIPPY_OWNER__?.integration || null;
|
|
38
48
|
}
|
|
39
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
|
+
|
|
40
73
|
function getStoredClientId() {
|
|
74
|
+
const key = '__chippyClientId' + frameKeySuffix();
|
|
41
75
|
try {
|
|
42
|
-
const existing = global.sessionStorage?.getItem(
|
|
76
|
+
const existing = global.sessionStorage?.getItem(key);
|
|
43
77
|
if (existing) return existing;
|
|
44
78
|
const next = global.crypto?.randomUUID?.() || `chippy-${Date.now()}`;
|
|
45
|
-
global.sessionStorage?.setItem(
|
|
79
|
+
global.sessionStorage?.setItem(key, next);
|
|
46
80
|
return next;
|
|
47
81
|
} catch {
|
|
48
82
|
return `chippy-${Date.now()}`;
|
|
49
83
|
}
|
|
50
84
|
}
|
|
51
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
|
+
|
|
52
97
|
function getStoryId() {
|
|
53
98
|
const params = new URLSearchParams(global.location.search || '');
|
|
54
99
|
return params.get('id') || null;
|
|
@@ -363,6 +408,7 @@
|
|
|
363
408
|
let onLayoutLabUpdate = null;
|
|
364
409
|
let onWindowMessage = null;
|
|
365
410
|
let onExtCommand = null;
|
|
411
|
+
let onOwnerClaimed = null;
|
|
366
412
|
let publishTimer = null;
|
|
367
413
|
let snapshotSequence = 0;
|
|
368
414
|
let lastLocationHref = global.location.href;
|
|
@@ -542,18 +588,55 @@
|
|
|
542
588
|
const b = getBridge();
|
|
543
589
|
return b
|
|
544
590
|
? b.readAvalancheStatus(getAvalancheApi(), getAvalancheModule())
|
|
545
|
-
: { ok: false, available: false,
|
|
591
|
+
: { ok: false, available: false, ...BRIDGE_MODULE_MISSING };
|
|
546
592
|
}
|
|
547
593
|
|
|
548
594
|
function evalAvalanche(command) {
|
|
549
595
|
const b = getBridge();
|
|
550
596
|
if (!b) {
|
|
551
597
|
const action = String(command?.action || 'status');
|
|
552
|
-
return { ok: false, available: false, action,
|
|
598
|
+
return { ok: false, available: false, action, ...BRIDGE_MODULE_MISSING };
|
|
553
599
|
}
|
|
554
600
|
return b.evalAvalanche(command, getAvalancheApi(), getAvalancheModule());
|
|
555
601
|
}
|
|
556
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
|
+
|
|
557
640
|
function handleCommand(command) {
|
|
558
641
|
switch (command?.type) {
|
|
559
642
|
case 'activate':
|
|
@@ -581,15 +664,19 @@
|
|
|
581
664
|
setContextUiEnabled(command.enabled !== false, 'set-context-ui');
|
|
582
665
|
break;
|
|
583
666
|
case 'reload':
|
|
584
|
-
|
|
585
|
-
|
|
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
|
+
});
|
|
586
671
|
break;
|
|
587
672
|
case 'storybook-force-remount': {
|
|
588
673
|
// The Storybook addon owns the host API. The bridge only relays the
|
|
589
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.
|
|
590
677
|
const remountEvent = new CustomEvent('chippy-storybook-force-remount', {
|
|
591
678
|
cancelable: true,
|
|
592
|
-
detail: { requestId: command.requestId || null },
|
|
679
|
+
detail: { requestId: command.requestId || null, avalanche: command.avalanche || null },
|
|
593
680
|
});
|
|
594
681
|
global.dispatchEvent(remountEvent);
|
|
595
682
|
try {
|
|
@@ -599,6 +686,7 @@
|
|
|
599
686
|
requestId: command.requestId || null,
|
|
600
687
|
action: 'storybook-force-remount',
|
|
601
688
|
ok: remountEvent.defaultPrevented,
|
|
689
|
+
error: remountEvent.defaultPrevented ? undefined : 'The Storybook preview did not remount: no rendered story matched the current selection',
|
|
602
690
|
});
|
|
603
691
|
} catch (e) {}
|
|
604
692
|
break;
|
|
@@ -655,8 +743,9 @@
|
|
|
655
743
|
}
|
|
656
744
|
case 'find-fibers': {
|
|
657
745
|
const requestId = command.requestId || null;
|
|
658
|
-
|
|
659
|
-
|
|
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) } }));
|
|
660
749
|
break;
|
|
661
750
|
}
|
|
662
751
|
case 'capture-fiber-state': {
|
|
@@ -873,35 +962,40 @@
|
|
|
873
962
|
if (!t) return null;
|
|
874
963
|
return t.displayName || t.name || (typeof t === 'string' ? t : null);
|
|
875
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`).
|
|
876
969
|
function getRootFibers() {
|
|
877
970
|
const hook = devtoolsHook();
|
|
878
971
|
const out = [];
|
|
879
|
-
if (!hook?.renderers) return out;
|
|
880
972
|
const seen = new Set();
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
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 {
|
|
884
986
|
const roots = hook.getFiberRoots(rendererId);
|
|
885
|
-
if (roots && typeof roots.forEach === 'function')
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
seen.add(root);
|
|
889
|
-
out.push(root);
|
|
890
|
-
}
|
|
891
|
-
});
|
|
892
|
-
}
|
|
893
|
-
}
|
|
894
|
-
} catch (_) {}
|
|
987
|
+
if (roots && typeof roots.forEach === 'function') roots.forEach(add);
|
|
988
|
+
} catch (_) {}
|
|
989
|
+
}
|
|
895
990
|
}
|
|
896
|
-
//
|
|
991
|
+
// Fallback for hooks without getFiberRoots: walk root containers in the document.
|
|
897
992
|
const containers = document.querySelectorAll('[id], body div, body main');
|
|
898
993
|
containers.forEach((el) => {
|
|
899
994
|
const key = Object.keys(el).find((k) => k.startsWith('__reactContainer$') || k.startsWith('__reactFiber$'));
|
|
900
995
|
if (!key) return;
|
|
901
996
|
let fiber = el[key];
|
|
902
|
-
// climb to root
|
|
903
997
|
while (fiber && fiber.return) fiber = fiber.return;
|
|
904
|
-
|
|
998
|
+
add(fiber);
|
|
905
999
|
});
|
|
906
1000
|
return out;
|
|
907
1001
|
}
|
|
@@ -1119,7 +1213,7 @@
|
|
|
1119
1213
|
// Delegated to avalanche.iife.js — see src/chippy-bridge/fiber-invoke.ts
|
|
1120
1214
|
function evalFiberPropContract(fiberId, propName) {
|
|
1121
1215
|
const b = getBridge();
|
|
1122
|
-
if (!b || !b.evalFiberPropContract) return { ok: false,
|
|
1216
|
+
if (!b || !b.evalFiberPropContract) return { ok: false, ...BRIDGE_MODULE_MISSING };
|
|
1123
1217
|
return b.evalFiberPropContract(fiberId, propName, {
|
|
1124
1218
|
normalizeRememberedFiberId,
|
|
1125
1219
|
getRememberedFiber,
|
|
@@ -1130,7 +1224,7 @@
|
|
|
1130
1224
|
// Returns a Promise. The handler routes async responses through `send` once resolved.
|
|
1131
1225
|
function evalFiberInvoke(fiberId, propName, args) {
|
|
1132
1226
|
const b = getBridge();
|
|
1133
|
-
if (!b || !b.evalFiberInvoke) return Promise.resolve({ ok: false,
|
|
1227
|
+
if (!b || !b.evalFiberInvoke) return Promise.resolve({ ok: false, ...BRIDGE_MODULE_MISSING });
|
|
1134
1228
|
return Promise.resolve(b.evalFiberInvoke(fiberId, propName, args, {
|
|
1135
1229
|
normalizeRememberedFiberId,
|
|
1136
1230
|
getRememberedFiber,
|
|
@@ -1143,7 +1237,7 @@
|
|
|
1143
1237
|
function evalFiberScheduleUpdate(fiberId, ownerIndex) {
|
|
1144
1238
|
const b = getBridge();
|
|
1145
1239
|
if (!b || !b.evalFiberScheduleUpdate) {
|
|
1146
|
-
return { ok: false, reason: 'no-scheduleUpdate',
|
|
1240
|
+
return { ok: false, reason: 'no-scheduleUpdate', ...BRIDGE_MODULE_MISSING };
|
|
1147
1241
|
}
|
|
1148
1242
|
return b.evalFiberScheduleUpdate(fiberId, ownerIndex, {
|
|
1149
1243
|
normalizeRememberedFiberId,
|
|
@@ -1154,7 +1248,7 @@
|
|
|
1154
1248
|
// Delegated to avalanche.iife.js — see src/chippy-bridge/eval-helpers.ts
|
|
1155
1249
|
function evalElementDom(element, parentLevel) {
|
|
1156
1250
|
const b = getBridge();
|
|
1157
|
-
if (!b) return {
|
|
1251
|
+
if (!b) return { ...BRIDGE_MODULE_MISSING };
|
|
1158
1252
|
return b.evalElementDom(element, parentLevel, window.getComputedStyle.bind(window));
|
|
1159
1253
|
}
|
|
1160
1254
|
|
|
@@ -1167,14 +1261,14 @@
|
|
|
1167
1261
|
// Delegated to avalanche.iife.js — see src/chippy-bridge/eval-helpers.ts
|
|
1168
1262
|
function evalFiberProps(baseFiber, ownerIndex) {
|
|
1169
1263
|
const b = getBridge();
|
|
1170
|
-
if (!b) return {
|
|
1264
|
+
if (!b) return { ...BRIDGE_MODULE_MISSING };
|
|
1171
1265
|
return b.evalFiberProps(baseFiber, ownerIndex, chippy.layoutLab?.getHookSummary, resolveOwnerFiber);
|
|
1172
1266
|
}
|
|
1173
1267
|
|
|
1174
1268
|
// Delegated to avalanche.iife.js — see src/chippy-bridge/eval-helpers.ts
|
|
1175
1269
|
function evalFiberPropValue(baseFiber, propName, ownerIndex) {
|
|
1176
1270
|
const b = getBridge();
|
|
1177
|
-
if (!b) return {
|
|
1271
|
+
if (!b) return { ...BRIDGE_MODULE_MISSING };
|
|
1178
1272
|
return b.evalFiberPropValue(baseFiber, propName, ownerIndex, chippy.layoutLab?.getHookSummary, resolveOwnerFiber, safeSerialize);
|
|
1179
1273
|
}
|
|
1180
1274
|
|
|
@@ -1188,7 +1282,7 @@
|
|
|
1188
1282
|
|
|
1189
1283
|
function summarizeHookList(raw) {
|
|
1190
1284
|
const b = getBridge();
|
|
1191
|
-
return b ? b.summarizeHookList(raw) : (raw || {
|
|
1285
|
+
return b ? b.summarizeHookList(raw) : (raw || { ...BRIDGE_MODULE_MISSING });
|
|
1192
1286
|
}
|
|
1193
1287
|
|
|
1194
1288
|
function formatCounts(counts) {
|
|
@@ -1372,15 +1466,20 @@
|
|
|
1372
1466
|
}
|
|
1373
1467
|
|
|
1374
1468
|
// Delegated to avalanche.iife.js — see src/chippy-bridge/eval-helpers.ts
|
|
1375
|
-
function evalFindFibers(filters) {
|
|
1469
|
+
async function evalFindFibers(filters) {
|
|
1376
1470
|
const b = getBridge();
|
|
1377
|
-
if (!b) return { count: 0, items: [],
|
|
1378
|
-
|
|
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;
|
|
1379
1478
|
}
|
|
1380
1479
|
|
|
1381
1480
|
function evalCaptureFiberState(options) {
|
|
1382
1481
|
const b = getBridge();
|
|
1383
|
-
if (!b?.captureFiberState) return { ok: false,
|
|
1482
|
+
if (!b?.captureFiberState) return { ok: false, ...BRIDGE_MODULE_MISSING };
|
|
1384
1483
|
const helpers = buildScriptHelpers([]);
|
|
1385
1484
|
return b.captureFiberState({
|
|
1386
1485
|
...options,
|
|
@@ -1393,7 +1492,7 @@
|
|
|
1393
1492
|
|
|
1394
1493
|
function evalCompareFiberStates(before, after, options) {
|
|
1395
1494
|
const b = getBridge();
|
|
1396
|
-
if (!b?.compareFiberStates) return { ok: false,
|
|
1495
|
+
if (!b?.compareFiberStates) return { ok: false, ...BRIDGE_MODULE_MISSING };
|
|
1397
1496
|
return b.compareFiberStates(before, after, options);
|
|
1398
1497
|
}
|
|
1399
1498
|
|
|
@@ -1401,7 +1500,7 @@
|
|
|
1401
1500
|
// extension shim stays version-coupled but implementation-free.
|
|
1402
1501
|
function evalBoundaryChain(fiber, max) {
|
|
1403
1502
|
const b = getBridge();
|
|
1404
|
-
if (!b) return { ok: false,
|
|
1503
|
+
if (!b) return { ok: false, ...BRIDGE_MODULE_MISSING };
|
|
1405
1504
|
return b.inspectDeclarativeBoundaryChain(fiber, max, {
|
|
1406
1505
|
rememberFiber,
|
|
1407
1506
|
getFiberDomNode: getAvalancheApi()?.layoutLab?.getFiberDomNode || getFiberDomNode,
|
|
@@ -1422,7 +1521,7 @@
|
|
|
1422
1521
|
|
|
1423
1522
|
function evalPageBoundaries(max) {
|
|
1424
1523
|
const b = getBridge();
|
|
1425
|
-
if (!b) return { ok: false,
|
|
1524
|
+
if (!b) return { ok: false, ...BRIDGE_MODULE_MISSING };
|
|
1426
1525
|
const helpers = buildScriptHelpers([]);
|
|
1427
1526
|
return b.inspectPageDeclarativeBoundaries(max, {
|
|
1428
1527
|
rememberFiber,
|
|
@@ -1470,7 +1569,7 @@
|
|
|
1470
1569
|
const fiber = getRememberedFiber(fiberId);
|
|
1471
1570
|
if (!fiber) return { error: 'Unknown fiberId: ' + fiberId };
|
|
1472
1571
|
const b = getBridge();
|
|
1473
|
-
if (!b) return {
|
|
1572
|
+
if (!b) return { ...BRIDGE_MODULE_MISSING };
|
|
1474
1573
|
return b.inspectFiberStructure(fiber, fiberId, hookIndex, windowSize, replayComponentHooks, evalFiberHooks);
|
|
1475
1574
|
}
|
|
1476
1575
|
|
|
@@ -1515,7 +1614,7 @@
|
|
|
1515
1614
|
|
|
1516
1615
|
async function evalDescribeComponent(fiberIdRaw, nameRaw) {
|
|
1517
1616
|
const b = getBridge();
|
|
1518
|
-
if (!b) return { ok: false,
|
|
1617
|
+
if (!b) return { ok: false, ...BRIDGE_MODULE_MISSING };
|
|
1519
1618
|
const target = resolveFiberTarget(null, fiberIdRaw, nameRaw);
|
|
1520
1619
|
if (target.error) return { ok: false, error: target.error };
|
|
1521
1620
|
if (b.resolveFiberSource) await b.resolveFiberSource(target.fiber);
|
|
@@ -1576,7 +1675,7 @@
|
|
|
1576
1675
|
const fiber = getRememberedFiber(fiberId);
|
|
1577
1676
|
if (!fiber) return { error: 'Unknown fiberId: ' + fiberId };
|
|
1578
1677
|
const b = getBridge();
|
|
1579
|
-
if (!b) return {
|
|
1678
|
+
if (!b) return { ...BRIDGE_MODULE_MISSING };
|
|
1580
1679
|
return b.inspectFiberOwners(fiber, fiberId, buildScriptHelpers([]));
|
|
1581
1680
|
}
|
|
1582
1681
|
|
|
@@ -1708,6 +1807,22 @@
|
|
|
1708
1807
|
};
|
|
1709
1808
|
global.document.addEventListener('chippy-ext-command', onExtCommand);
|
|
1710
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
|
+
|
|
1711
1826
|
locationInterval = global.setInterval(() => {
|
|
1712
1827
|
if (global.location.href !== lastLocationHref) {
|
|
1713
1828
|
lastLocationHref = global.location.href;
|
|
@@ -1763,6 +1878,7 @@
|
|
|
1763
1878
|
try { if (onLayoutLabUpdate) global.removeEventListener('layout-lab:update', onLayoutLabUpdate); } catch (e) {}
|
|
1764
1879
|
try { if (onWindowMessage) global.removeEventListener('message', onWindowMessage); } catch (e) {}
|
|
1765
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) {}
|
|
1766
1882
|
}
|
|
1767
1883
|
|
|
1768
1884
|
// ─── Public API ──────────────────────────────────────────────────────────
|
|
@@ -1829,5 +1945,9 @@
|
|
|
1829
1945
|
chippy.layoutLab?.exposeDefaultMarkers?.();
|
|
1830
1946
|
}
|
|
1831
1947
|
|
|
1832
|
-
|
|
1948
|
+
if (isExtensionBridge() && isLiveForeignOwner(global.__CHIPPY_OWNER__)) {
|
|
1949
|
+
standDown(global.__CHIPPY_OWNER__);
|
|
1950
|
+
} else {
|
|
1951
|
+
connect();
|
|
1952
|
+
}
|
|
1833
1953
|
})(window);
|
|
@@ -162,6 +162,29 @@ chrome.runtime.onMessage.addListener((message, _sender, sendResponse) => {
|
|
|
162
162
|
}
|
|
163
163
|
});
|
|
164
164
|
|
|
165
|
+
// The page bridge asks for early startup on the next navigation before an armed
|
|
166
|
+
// reload. Only the background can register the MAIN-world startup scripts. The
|
|
167
|
+
// background also re-runs this file, so the relay is installed once per document.
|
|
168
|
+
function relayArmedReload() {
|
|
169
|
+
const reply = (result) => {
|
|
170
|
+
try {
|
|
171
|
+
document.dispatchEvent(new CustomEvent('chippy-armed-reload-ready', { detail: JSON.stringify(result || {}) }));
|
|
172
|
+
} catch {}
|
|
173
|
+
};
|
|
174
|
+
try {
|
|
175
|
+
chrome.runtime.sendMessage({ type: 'chippy-register-armed-reload' }, (response) => {
|
|
176
|
+
if (chrome.runtime.lastError) reply({ ok: false, error: chrome.runtime.lastError.message });
|
|
177
|
+
else reply(response);
|
|
178
|
+
});
|
|
179
|
+
} catch (e) {
|
|
180
|
+
reply({ ok: false, error: e.message });
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
if (!globalThis.__chippyArmedReloadRelay) {
|
|
184
|
+
globalThis.__chippyArmedReloadRelay = true;
|
|
185
|
+
document.addEventListener('chippy-armed-reload-prepare', relayArmedReload);
|
|
186
|
+
}
|
|
187
|
+
|
|
165
188
|
applyStoredConfig();
|
|
166
189
|
|
|
167
190
|
})();
|
|
@@ -24,6 +24,15 @@
|
|
|
24
24
|
if (global.__REACT_DEVTOOLS_GLOBAL_HOOK__) return;
|
|
25
25
|
|
|
26
26
|
var renderers = new Map();
|
|
27
|
+
// Committed FiberRoots per renderer, tracked like React DevTools does so
|
|
28
|
+
// tree walks start from the live root instead of a container's stale
|
|
29
|
+
// HostRoot alternate.
|
|
30
|
+
var fiberRoots = new Map();
|
|
31
|
+
function rootsFor(id) {
|
|
32
|
+
var roots = fiberRoots.get(id);
|
|
33
|
+
if (!roots) { roots = new Set(); fiberRoots.set(id, roots); }
|
|
34
|
+
return roots;
|
|
35
|
+
}
|
|
27
36
|
var hook = {
|
|
28
37
|
__chippyShim: true,
|
|
29
38
|
supportsFiber: true,
|
|
@@ -33,8 +42,16 @@
|
|
|
33
42
|
renderers.set(id, renderer);
|
|
34
43
|
return id;
|
|
35
44
|
},
|
|
45
|
+
getFiberRoots: function getFiberRoots(id) { return rootsFor(id); },
|
|
36
46
|
onScheduleFiberRoot: function () {},
|
|
37
|
-
onCommitFiberRoot: function () {
|
|
47
|
+
onCommitFiberRoot: function onCommitFiberRoot(id, root) {
|
|
48
|
+
if (!root || !root.current) return;
|
|
49
|
+
var roots = rootsFor(id);
|
|
50
|
+
var state = root.current.memoizedState;
|
|
51
|
+
var unmounting = !state || state.element == null;
|
|
52
|
+
if (unmounting) roots.delete(root);
|
|
53
|
+
else roots.add(root);
|
|
54
|
+
},
|
|
38
55
|
onCommitFiberUnmount: function () {},
|
|
39
56
|
onPostCommitFiberRoot: function () {},
|
|
40
57
|
};
|
|
@@ -8,12 +8,14 @@
|
|
|
8
8
|
if (globalThis.__CHIPPY_EARLY_BOOTSTRAP_INSTALLED__) {
|
|
9
9
|
return;
|
|
10
10
|
}
|
|
11
|
-
globalThis.__CHIPPY_EARLY_BOOTSTRAP_INSTALLED__ = true;
|
|
12
11
|
|
|
12
|
+
// Without a config this run does nothing, so a later run that has one (the
|
|
13
|
+
// background's startup injection) must still be able to bootstrap.
|
|
13
14
|
const cfg = globalThis.__CHIPPY_STARTUP_CONFIG || null;
|
|
14
15
|
if (!cfg || !cfg.enabled) {
|
|
15
16
|
return;
|
|
16
17
|
}
|
|
18
|
+
globalThis.__CHIPPY_EARLY_BOOTSTRAP_INSTALLED__ = true;
|
|
17
19
|
|
|
18
20
|
globalThis.__CHIPPY = globalThis.__CHIPPY || {};
|
|
19
21
|
|
|
@@ -30,12 +30,6 @@
|
|
|
30
30
|
}
|
|
31
31
|
}
|
|
32
32
|
|
|
33
|
-
function parseForwardPort(value) {
|
|
34
|
-
if (value === '') return null;
|
|
35
|
-
const port = Number(value);
|
|
36
|
-
return Number.isInteger(port) && port >= 1 && port <= 65535 ? port : undefined;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
33
|
function escapeHtml(value) {
|
|
40
34
|
return String(value).replace(/[&<>"']/g, (c) => `&#${c.charCodeAt(0)};`);
|
|
41
35
|
}
|
|
@@ -74,10 +68,6 @@
|
|
|
74
68
|
<span>Chippy URL</span>
|
|
75
69
|
<input data-helper-url type="url" inputmode="url" required placeholder="${DEFAULT_HELPER}" />
|
|
76
70
|
</label>
|
|
77
|
-
<label>
|
|
78
|
-
<span>Forward to port · optional</span>
|
|
79
|
-
<input data-forward-port type="number" inputmode="numeric" min="1" max="65535" step="1" placeholder="Auto-detect from page URL" />
|
|
80
|
-
</label>
|
|
81
71
|
${pluginMarkup()}
|
|
82
72
|
<div class="actions">
|
|
83
73
|
<button type="submit">Save settings</button>
|
|
@@ -91,7 +81,7 @@
|
|
|
91
81
|
host.setAttribute('data-chippy-settings-host', 'true');
|
|
92
82
|
host.setAttribute('data-chippy-preserve', 'true');
|
|
93
83
|
const pluginRowsHeight = PLUGIN_OPTIONS.length ? 18 + 19 * PLUGIN_OPTIONS.length : 0;
|
|
94
|
-
host.style.cssText = `display:block;min-height:${
|
|
84
|
+
host.style.cssText = `display:block;min-height:${124 + pluginRowsHeight}px;`;
|
|
95
85
|
const root = host.attachShadow({ mode: 'closed' });
|
|
96
86
|
const surface = document.createElement('div');
|
|
97
87
|
surface.innerHTML = settingsMarkup();
|
|
@@ -99,7 +89,6 @@
|
|
|
99
89
|
|
|
100
90
|
const form = root.querySelector('form');
|
|
101
91
|
const helperInput = root.querySelector('[data-helper-url]');
|
|
102
|
-
const forwardPortInput = root.querySelector('[data-forward-port]');
|
|
103
92
|
const pluginInputs = Array.from(root.querySelectorAll('[data-plugin]'));
|
|
104
93
|
const statusNode = root.querySelector('[data-status]');
|
|
105
94
|
|
|
@@ -110,11 +99,9 @@
|
|
|
110
99
|
|
|
111
100
|
chrome.storage.local.get({
|
|
112
101
|
helperBaseUrl: DEFAULT_HELPER,
|
|
113
|
-
forwardPort: null,
|
|
114
102
|
plugins: DEFAULT_PLUGINS,
|
|
115
103
|
}).then((stored) => {
|
|
116
104
|
helperInput.value = normalizeBaseUrl(stored.helperBaseUrl);
|
|
117
|
-
forwardPortInput.value = stored.forwardPort || '';
|
|
118
105
|
const plugins = Array.isArray(stored.plugins) ? stored.plugins : [];
|
|
119
106
|
for (const input of pluginInputs) input.checked = plugins.includes(input.value);
|
|
120
107
|
}).catch((error) => setStatus(error.message || 'Could not load settings.', true));
|
|
@@ -122,19 +109,14 @@
|
|
|
122
109
|
form.addEventListener('submit', async (event) => {
|
|
123
110
|
event.preventDefault();
|
|
124
111
|
const helperBaseUrl = normalizeBaseUrl(helperInput.value);
|
|
125
|
-
const forwardPort = parseForwardPort(forwardPortInput.value);
|
|
126
112
|
if (!validateHelperUrl(helperBaseUrl)) {
|
|
127
113
|
setStatus('Use an http(s) URL.', true);
|
|
128
114
|
return;
|
|
129
115
|
}
|
|
130
|
-
if (forwardPort === undefined) {
|
|
131
|
-
setStatus('Port must be 1–65535.', true);
|
|
132
|
-
return;
|
|
133
|
-
}
|
|
134
116
|
|
|
135
117
|
try {
|
|
136
118
|
const plugins = pluginInputs.filter((input) => input.checked).map((input) => input.value);
|
|
137
|
-
await chrome.storage.local.set({ helperBaseUrl,
|
|
119
|
+
await chrome.storage.local.set({ helperBaseUrl, plugins });
|
|
138
120
|
document.dispatchEvent(new CustomEvent('chippy-ext-command', {
|
|
139
121
|
detail: { type: 'apply-config', helperBaseUrl, activated: true },
|
|
140
122
|
}));
|
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
// Runs in MAIN at document_start, before avalanche.iife.js, on the one
|
|
3
|
+
// navigation that follows an armed reload. The page bridge stored the arm in
|
|
4
|
+
// sessionStorage just before reloading; reading it here is synchronous, so the
|
|
5
|
+
// startup config exists when Avalanche evaluates and the detector installs
|
|
6
|
+
// before React's first commit. The entry is removed on read and ignored when old.
|
|
7
|
+
(function chippyStartupHandoff(global) {
|
|
8
|
+
var KEY = '__chippyStartupHandoff';
|
|
9
|
+
var MAX_AGE_MS = 60000;
|
|
10
|
+
var handoff = null;
|
|
11
|
+
try {
|
|
12
|
+
var raw = global.sessionStorage.getItem(KEY);
|
|
13
|
+
if (raw == null) return;
|
|
14
|
+
global.sessionStorage.removeItem(KEY);
|
|
15
|
+
handoff = JSON.parse(raw);
|
|
16
|
+
} catch (_) {
|
|
17
|
+
return;
|
|
18
|
+
}
|
|
19
|
+
if (!handoff || !handoff.avalanche || !handoff.avalanche.autoStart) return;
|
|
20
|
+
if (!(Date.now() - Number(handoff.at) <= MAX_AGE_MS)) return;
|
|
21
|
+
if (global.__CHIPPY_STARTUP_CONFIG) return;
|
|
22
|
+
global.__CHIPPY_STARTUP_CONFIG = {
|
|
23
|
+
enabled: true,
|
|
24
|
+
helperBaseUrl: handoff.helperBaseUrl,
|
|
25
|
+
avalanche: handoff.avalanche,
|
|
26
|
+
plugins: Array.isArray(handoff.plugins) ? handoff.plugins : undefined,
|
|
27
|
+
contextUiEnabled: true,
|
|
28
|
+
startupSource: 'armed-reload-handoff',
|
|
29
|
+
};
|
|
30
|
+
})(typeof globalThis !== 'undefined' ? globalThis : window);
|
|
@@ -39,9 +39,9 @@ These pages are siblings, not substitutes. Start with Avalanche for a real slow
|
|
|
39
39
|
|---|---|---|
|
|
40
40
|
| Load Chippy into a Playwright-controlled page | [Playwright CLI setup](playwright-cli-setup.md) | The shortest current-document bootstrap and reload recovery recipe |
|
|
41
41
|
| Build a repeatable browser loop | [Test mode playbook](test-mode-playbook.md) | Sidecar and Playwright-controlled automation contracts |
|
|
42
|
-
|
|
|
42
|
+
| Load Chippy before page scripts, or record the initial load without the extension | [Mode A injection](mode-a-injection.md) | Init-script loader variants, pre-page start-up, boot-time limits, and failure signatures |
|
|
43
43
|
|
|
44
|
-
Use Playwright CLI setup for ad-hoc inspection. Use the test-mode playbook
|
|
44
|
+
Use Playwright CLI setup for ad-hoc inspection. Use the test-mode playbook for scripted or CI workflows. Use Mode A injection when Chippy must stay across reloads or record the initial load in a Playwright browser.
|
|
45
45
|
|
|
46
46
|
## Look up a contract
|
|
47
47
|
|
|
@@ -92,7 +92,7 @@ Look for `installed`, `hasMetricsGlobal`, `commitCount`.
|
|
|
92
92
|
```bash
|
|
93
93
|
chippy.sh reload-with-avalanche my-client http://localhost:9000 0 15000 5000
|
|
94
94
|
```
|
|
95
|
-
Arms, reloads, waits for reconnect, waits settle window, then prints capture status.
|
|
95
|
+
Arms, reloads, waits for reconnect, waits settle window, then prints capture status. With the extension or proxy mode, the reloaded document installs Avalanche before React's first commit, so boot commits are included. The reload step's ack reports `earlyStartup`: `registered` (extension), `handoff-only` (no extension; proxy mode still captures boot), or `not-armed`.
|
|
96
96
|
|
|
97
97
|
### `avalanche-hot-sources <clientId> [limit]`
|
|
98
98
|
```bash
|
|
@@ -175,7 +175,7 @@ chippy.sh avalanche-offline .chippy-captures/raf-1846.json source-search --name
|
|
|
175
175
|
```
|
|
176
176
|
Use saved captures without a live page. `commit-list`, `commit-peek`, `hot-sources`, and `describe-action` run the same code as the live commands and return the same shapes, so sort keys and include values match the live tables above (`unmounts` is live-only).
|
|
177
177
|
|
|
178
|
-
`summary` returns commit count, first/last commit id, source count, and totals of the commit stats. `source-search --name <regex>` matches aggregate sources by `sourceId` and
|
|
178
|
+
`summary` returns commit count, first/last commit id, source count, and totals of the commit stats. `source-search --name <regex>` matches aggregate sources by `sourceId` and rendered components by name with per-instance render and self-source counts.
|
|
179
179
|
|
|
180
180
|
### `avalanche-arm <clientId> <origin> [maxCommits]`
|
|
181
181
|
```bash
|
|
@@ -263,7 +263,7 @@ Suspense reading:
|
|
|
263
263
|
|
|
264
264
|
## `renderedByCommit` — per-component render evidence
|
|
265
265
|
|
|
266
|
-
Populated
|
|
266
|
+
Populated in every detection mode. The mode decides which fields carry evidence: `stats` has no source tracking, so `isSelfSource` stays `false`; `trasherCounts` stay zero unless the mode is `full` or `amplifiers`. Pulled into `commit-peek` output via `include=rendered`.
|
|
267
267
|
|
|
268
268
|
Top-level shape (`?` = optional, may be absent or `null`; everything else is required):
|
|
269
269
|
|