@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
|
@@ -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);
|
package/lib/mcp-skill/README.md
CHANGED
|
@@ -73,7 +73,7 @@ Chippy supports three page-side integrations:
|
|
|
73
73
|
| Module import | A page without the extension UI | Applies to the current document; import again after reload/navigation |
|
|
74
74
|
| Storybook addon | A Storybook preview | Reconnects through preview annotations |
|
|
75
75
|
|
|
76
|
-
On extension-owned pages, Chippy starts as a small bottom-corner launcher. Expanding it reveals one unified panel with compact `P1 — drag to source` and `P2 — drag to source` rows that act as both dock and probe detail. Dragging a probe out keeps the panel expanded; once placed, the probe is document-anchored and follows its target during page scroll.
|
|
76
|
+
On extension-owned pages, Chippy starts as a small bottom-corner launcher. Expanding it reveals one unified panel with compact `P1 — drag to source` and `P2 — drag to source` rows that act as both dock and probe detail. Dragging a probe out keeps the panel expanded; once placed, the probe is document-anchored and follows its target during page scroll. The helper URL and plugin switches live behind its gear. Probe and connection updates patch existing controls in place, so drafts, focus, and the settings form survive ordinary refreshes. Active probes commit their annotation when the intent changes or Enter is pressed in the comment; Shift+Enter inserts a newline. The toolbar popup shows the Chippy mark and name with Enable and Disable for the current origin. Storybook is the intentional exception: its details live in the manager panel, so its preview frame retains a standalone probe dock.
|
|
77
77
|
|
|
78
78
|
When the extension workflow fits, load this skill's `extension` folder through **Load unpacked** in the browser's extension developer mode. The module-import and Playwright recipes live in [playwright-cli-setup](references/playwright-cli-setup.md).
|
|
79
79
|
|
|
@@ -97,7 +97,7 @@ Chippy deliberately keeps different facts separate:
|
|
|
97
97
|
- **DOM facts:** the exact element, text, attributes, styles, and nearest test-id boundary.
|
|
98
98
|
- **React facts:** the fiber, component, owners, parents, props, hooks, and source metadata for the current snapshot.
|
|
99
99
|
- **Declarative boundary facts:** every Error/Suspense ancestor around a target, or the whole page reduced to a boundary-only containment forest, including current Suspense content/fallback state.
|
|
100
|
-
- **Jira context:** optional probe enrichment for recognized ownership markers and `JSErrorBoundary` metadata; it appears in both the in-page and Storybook probe details
|
|
100
|
+
- **Jira context:** optional probe enrichment for recognized ownership markers and `JSErrorBoundary` metadata; it appears in both the in-page and Storybook probe details once the Jira plugin is enabled (it is off by default), and does not replace source evidence.
|
|
101
101
|
- **Source facts:** what the reported file and code can do.
|
|
102
102
|
- **Render facts:** which commits, sources, restarts, effects, context updates, and collateral occurred in the measured window.
|
|
103
103
|
- **Action facts:** what the user or automation did immediately before a measured commit.
|
|
@@ -142,8 +142,6 @@ Performance work has three distinct entry points:
|
|
|
142
142
|
- Probes and commit captures are evidence anchors. Preserve them until the question is answered and any edit is verified.
|
|
143
143
|
- Development source resolution supports React <=18 `_debugSource` and React 19 `_debugStack` plus the dev server's source map. React 19 requires no JSX-runtime instrumentation when its development build and maps are available.
|
|
144
144
|
- Production or minified pages can reduce source and component confidence. Chippy reports the boundary instead of inventing missing detail.
|
|
145
|
-
- Browser extension file changes require an extension publish cycle; rebuilding the bundled runtime alone does not publish it.
|
|
146
|
-
- New commands, options, fields, and behavior changes require shell, wire, type/DTO, test, and documentation parity.
|
|
147
145
|
|
|
148
146
|
## License
|
|
149
147
|
|
package/lib/mcp-skill/SKILL.md
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: chippy
|
|
3
|
-
description: Debug a running React page with live runtime evidence. Use to explore components (find by name, text, test id, file, or prop; explain structure and hooks), debug where an element, value, prop, or loading/error boundary comes from, compare two rendered states or pages, and investigate slow interactions, re-renders, and initial load with Avalanche commit capture. Connects through the
|
|
3
|
+
description: Debug a running React page with live runtime evidence. Use to explore components (find by name, text, test id, file, or prop; explain structure and hooks), debug where an element, value, prop, or loading/error boundary comes from, compare two rendered states or pages, and investigate slow interactions, re-renders, and initial load with Avalanche commit capture. Connects a local helper to the page through the browser extension, an agent-driven Playwright browser, the helper's local proxy, the Storybook addon, or a console/module import; not for static code review without a running page.
|
|
4
4
|
allowed-tools: Bash(chippy.sh:*)
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -21,15 +21,15 @@ Use the running page as evidence. Connect visible React behavior to exact source
|
|
|
21
21
|
|
|
22
22
|
## Start every task
|
|
23
23
|
|
|
24
|
-
1. If neither Chippy MCP tools nor this skill's scripts are installed, have the user run `npx
|
|
24
|
+
1. If neither Chippy MCP tools nor this skill's scripts are installed, have the user run `npx skills add atlassian-labs/chippy --skill chippy -g`; the skill then lives in `~/.agents/skills/chippy`, and its `extension` folder is the unpacked browser extension. Resolve this skill's absolute path; do not assume the current repository contains a relative copy. Prefer available Chippy MCP tools when permitted by the host; otherwise run this skill's `scripts/chippy.sh`.
|
|
25
25
|
2. With MCP available, use `chippy_status` → `chippy_pages` → `chippy_snapshot` (refresh, then read). MCP starts/reuses the helper automatically; follow [MCP setup and tools](references/mcp.md). MCP covers inspection, Avalanche capture and analysis, interaction, and annotation waits; compare (`fiber-state-*`), `page-boundaries`, `avalanche-arm`, `reload-with-avalanche`, and `recipes` are CLI-only, so use `scripts/chippy.sh` for them. Otherwise run `health` → `pages` → `request-snapshot <clientId>`; helper-contacting shell commands must run outside the agent sandbox. The agent owns helper startup and command execution.
|
|
26
|
-
3. Select the page by URL, title, integration identity, and recency. Resolve a fresh `clientId` every turn; a reload or navigation can replace it. When two pages match closely, ask which page is in scope.
|
|
26
|
+
3. Select the page by URL, title, integration identity, and recency. When no page is connected, choose a route in [Connection and bootstrap](#connection-and-bootstrap); the extension is one route, not a prerequisite. Resolve a fresh `clientId` every turn; a reload or navigation can replace it. When two pages match closely, ask which page is in scope.
|
|
27
27
|
4. Use the first read-only signal to leave ❓ Disorder and name the Cynefin domain. Then iterate with OODA inside that domain: observe one fact, orient it to the invariant and proof gap, decide one permitted move, act, and observe again.
|
|
28
28
|
5. Choose the narrowest route below. Do not edit until the proof packet names the fix layer and a repeatable verification command.
|
|
29
29
|
|
|
30
30
|
`health`, `pages`, `request-snapshot`, and reads of existing probe state are orientation. Active stimuli, diagnosis, and source edits are not.
|
|
31
31
|
|
|
32
|
-
When
|
|
32
|
+
When a page connected in the user's own browser already has the target state, work there first. Use Playwright only when automation or replay is required, the user asks for it, or the connected page cannot provide the evidence.
|
|
33
33
|
|
|
34
34
|
## Route by question
|
|
35
35
|
|
|
@@ -45,7 +45,8 @@ Before `explain-component`, `describe-component`, or `inspect-fiber-structure`,
|
|
|
45
45
|
| What is inside this component? | `explain-component`; use `describe-component` for the detailed hook tree | The component's structure is described, with replay status and warnings disclosed. |
|
|
46
46
|
| What changed between two rendered states or pages? | `fiber-state-capture` before and after → `fiber-state-compare` | The bounded diff names matched fibers and added, removed, or changed props without returning either full capture. |
|
|
47
47
|
| Why is an interaction slow or re-rendering? | `avalanche-arm` → reproduce the interaction → `avalanche-status` → hot sources / commit list / commit peek | Trigger, hotspot, collateral, and fix layer are distinguished. |
|
|
48
|
-
| Why is initial load slow? | `reload-with-avalanche` → status / hot sources / commit analysis | The measured load window is bounded and repeatable. |
|
|
48
|
+
| Why is initial load slow? | Confirm the page's route records the initial load ([Connection and bootstrap](#connection-and-bootstrap)) → `reload-with-avalanche` → status / hot sources / commit analysis | The measured load window is bounded and repeatable. |
|
|
49
|
+
| Does the user want render evidence in tests or scripts, with no agent at run time? | Point to the standalone Avalanche detector in the [npm package documentation](https://github.com/atlassian-labs/chippy/blob/main/docs/npm-package.md#standalone-avalanche) | The test installs the detector before React DOM loads and reads `detector.report()`. |
|
|
49
50
|
| Is this subtree stable? | Load [stability-render](references/stability-render.md) and run one controlled nudge | The capture yields clean, leak, leak+cascade, or trasher evidence. |
|
|
50
51
|
| Which hook is suspicious? | Use `avalanche-debug-hook` only after a capture identifies `sourceId + hookIndex` | Primitive/custom-hook ownership and next remediation branch are named. |
|
|
51
52
|
| Are state dispatches or memo/effect recomputations driving the cost? | Load [hook-metrics](references/hook-metrics.md) | A before/after counter or tracker result is joined to the same interaction. |
|
|
@@ -62,7 +63,7 @@ Use [bdd-router](references/bdd-router.md) when the situation needs the complete
|
|
|
62
63
|
|
|
63
64
|
- A probe identifies one exact DOM element rendered by one bundle. Its source location, fiber, owner chain, and marker coordinates are facts for that snapshot.
|
|
64
65
|
- Open `source.fileName` at `lineNumber` directly once it is an authored path. On React 19, an initial `http(s)` module URL is the generated fallback; read the following `*-source-resolved` snapshot or run `describe-component` before treating its line as a repository position. Do not grep for a file the probe already identified. Search is the last resort after the runtime-to-source chain is exhausted.
|
|
65
|
-
- Development source metadata is version-dependent: React <=18 supplies `_debugSource`; React 19 supplies `_debugStack`. Chippy filters dependency frames, fetches the served module in the page realm, decodes its inline or external source map, and republishes the authored file/line. Absence of `_debugSource` alone is not production evidence.
|
|
66
|
+
- Development source metadata is version-dependent: React <=18 supplies `_debugSource`; React 19 supplies `_debugStack`. Chippy filters dependency frames, fetches the served module in the page realm, decodes its inline or external source map, and republishes the authored file/line. It also corrects `_debugSource` lines that a dev-server transform shifted before JSX compilation, such as Vite's Fast Refresh header. `find-fibers`, `describe-component`, and `explain-component` return the mapped position. Absence of `_debugSource` alone is not production evidence.
|
|
66
67
|
- Read every returned field. Props, owner indices, keys, test IDs, hook counts, and source locations can each change the next move.
|
|
67
68
|
- Static source proves what code can do. Runtime evidence proves what happened in this run. Never promote code shape, a global's presence, or a hot component into a runtime cause.
|
|
68
69
|
- Screenshots confirm appearance; they do not replace probe, fiber, action, or commit evidence.
|
|
@@ -118,13 +119,17 @@ Chippy has a local helper and one page-side integration. `chippy_pages` or
|
|
|
118
119
|
helper startup without a shell command; it needs host access to the browser's
|
|
119
120
|
network environment. Closing an MCP session preserves the shared helper.
|
|
120
121
|
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
|
124
|
-
|
|
125
|
-
|
|
|
122
|
+
Each route below loads the same page runtime. The extension is one delivery route; choose by who owns the browser and whether the task needs initial-load capture.
|
|
123
|
+
|
|
124
|
+
| Route | Who sets it up | Reload behavior | Initial-load capture |
|
|
125
|
+
|---|---|---|---|
|
|
126
|
+
| Browser extension | User, once per browser and origin | Re-injects automatically for enabled origins. | Yes, with `reload-with-avalanche`: the armed reload runs the early scripts at `document_start`. |
|
|
127
|
+
| Agent-driven Playwright browser | Agent; no extension | Init scripts load again on every navigation; the ad-hoc import must be repeated. | Only with the [pre-page start-up](references/mode-a-injection.md#pre-page-start-up) variant, which installs the detector before React on every document. The packaged loader and the ad-hoc import start the detector after connection. |
|
|
128
|
+
| Local proxy (`proxy-start <targetPort> [originalUrl]`) | Agent; the user or Playwright opens the page on the helper port | The proxy adds Chippy to every load of the `originalUrl` path, so pass `originalUrl`. | Yes, with `reload-with-avalanche`: the shim and Avalanche run before page scripts. Targets on this machine only. |
|
|
129
|
+
| Storybook addon (`addons: ['@atlassian/chippy/storybook']`, Storybook 10.3+) | User, in the Storybook configuration | `page-reload` uses Storybook `FORCE_REMOUNT` to remount the SUT without reloading the preview iframe. | The story's mount, through the remount. |
|
|
130
|
+
| Console/module import, including `connectChippy()` from `@atlassian/chippy/browser` | User or page code, in the current document | Full reload/navigation removes Chippy; run the import again. | No: use mid-page capture. |
|
|
126
131
|
|
|
127
|
-
The extension toolbar popup shows the Chippy mark and name with Enable and Disable for the current origin. The in-page UI starts as one small bottom-corner Chippy button. Expand it to reveal compact `P1 — drag to source` and `P2 — drag to source` rows; each row is the probe's dock and expands into its details once placed. Dragging a probe out keeps the panel expanded, and a placed probe is document-anchored so it moves with its target during page scroll. The gear contains the helper URL
|
|
132
|
+
The extension toolbar popup shows the Chippy mark and name with Enable and Disable for the current origin. The in-page UI starts as one small bottom-corner Chippy button. Expand it to reveal compact `P1 — drag to source` and `P2 — drag to source` rows; each row is the probe's dock and expands into its details once placed. Dragging a probe out keeps the panel expanded, and a placed probe is document-anchored so it moves with its target during page scroll. The gear contains the helper URL and the Jira and ATL plugin switches. Active probes commit their annotation when the intent changes or Enter is pressed in the comment; Shift+Enter inserts a newline. Storybook intentionally keeps its probe dock in the preview because its details panel lives in the manager frame.
|
|
128
133
|
|
|
129
134
|
Each connected page reports `integration.kind`, `who`, `source`, `reinjectsOnReload`, and `reloadAction`. Use those fields instead of inferring how Chippy arrived.
|
|
130
135
|
|
|
@@ -136,14 +141,14 @@ For a user-controlled page without the extension, run this in the browser consol
|
|
|
136
141
|
import('http://127.0.0.1:7823/__chippy/inject.mjs')
|
|
137
142
|
```
|
|
138
143
|
|
|
139
|
-
The single `import(...)` is all that is needed: the ESM bootstrap installs Chippy, loads the Avalanche bridge runtime (so fiber-backed reads and captures work immediately — no second asset), and activates probes for the current document. It renders no in-page control panel/overlay. After reload or navigation, import again.
|
|
144
|
+
The single `import(...)` is all that is needed: the ESM bootstrap installs Chippy, loads the Avalanche bridge runtime (so fiber-backed reads and captures work immediately — no second asset), and activates probes for the current document. It renders no in-page control panel/overlay. After reload or navigation, import again. A strict Content Security Policy blocks the import; on a local dev server, use the proxy instead, which removes the page's CSP headers.
|
|
140
145
|
|
|
141
|
-
For
|
|
146
|
+
For pages where reload destroys Chippy (console import or `connectChippy()`), **do not use `reload-with-avalanche`**. The correct workflow is mid-page capture: import Chippy once, install/arm Avalanche on the live page (`avalanche-install` or `avalanche-arm`), then trigger a controlled render stimulus such as `fiber-schedule-update` / the stability nudge. `reload-with-avalanche` is for routes that load Chippy before page scripts on the reloaded document: the extension, the local proxy, and the Storybook addon. On Storybook, its reload step is a `FORCE_REMOUNT` of the current SUT, not an iframe navigation. A Playwright page with the pre-page start-up variant installs the detector on every document itself, so a plain `page.reload()` captures the load.
|
|
142
147
|
|
|
143
148
|
The helper serves matching source maps alongside its Avalanche bundles; browser
|
|
144
149
|
developer tools can resolve the runtime frames without missing-map requests.
|
|
145
150
|
|
|
146
|
-
For Playwright-
|
|
151
|
+
For an agent-driven Playwright browser, follow [mode-a-injection](references/mode-a-injection.md) for the loader variants and their capture limits, and [playwright-cli-setup](references/playwright-cli-setup.md) for ad-hoc `playwright-cli` sessions. The classic fallback requires `inject.js` followed by `avalanche.iife.js`; `bridge module not loaded` means the second asset is missing.
|
|
147
152
|
|
|
148
153
|
### Explicit wait mode
|
|
149
154
|
|
|
@@ -276,7 +281,7 @@ Arm before the user or harness performs the slow interaction. Probes drill into
|
|
|
276
281
|
|
|
277
282
|
`latestAction` is a clue, not causality. A confirmed root cause needs a source-to-collateral chain or source code that proves the update origin. Unmount and Suspense fields are branch signals; report DOM regions before wrapper names when the question is about DOM teardown.
|
|
278
283
|
|
|
279
|
-
|
|
284
|
+
Every mode records which components rendered and their prop changes (`commit-peek include=rendered`). `stats` leaves `isSelfSource` false, and only `full` or `amplifiers` fill `trasherCounts`. When amplifier evidence is required and triage began in the default `sources` mode, run `avalanche-recapture <clientId> full`, reproduce the identical interaction, and only then read `trasherCounts`. Load [avalanche-detector](references/avalanche-detector.md) for exact schemas and [fixing-performance](references/fixing-performance.md) for fix-layer selection.
|
|
280
285
|
|
|
281
286
|
## Snippets and active stimuli
|
|
282
287
|
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
/* eslint-disable */
|
|
2
|
+
// One-shot early startup for an armed reload, loaded into the background
|
|
3
|
+
// service worker with importScripts().
|
|
4
|
+
//
|
|
5
|
+
// The background's normal startup injection waits on several asynchronous
|
|
6
|
+
// lookups after navigation commits, so a fast page can commit React before
|
|
7
|
+
// Avalanche installs. Before an armed reload, the page bridge stores the arm in
|
|
8
|
+
// sessionStorage and asks for this registration. Chrome then runs the scripts
|
|
9
|
+
// below synchronously at document_start of the next document, and
|
|
10
|
+
// startup-handoff.js turns the stored arm into the startup config.
|
|
11
|
+
//
|
|
12
|
+
// The registration covers one navigation of one frame: it is removed when that
|
|
13
|
+
// document finishes parsing, when its navigation fails, or when it is older
|
|
14
|
+
// than PENDING_MAX_AGE_MS at the next navigation event.
|
|
15
|
+
(function chippyArmedReloadStartup(global) {
|
|
16
|
+
const SCRIPT_ID = 'chippy-armed-reload-startup';
|
|
17
|
+
const PENDING_KEY = 'chippyArmedReloadPending';
|
|
18
|
+
const PENDING_MAX_AGE_MS = 60000;
|
|
19
|
+
const FILES = ['devtools-hook-shim.js', 'startup-handoff.js', 'avalanche.iife.js', 'early-bootstrap.js'];
|
|
20
|
+
|
|
21
|
+
function originPattern(url) {
|
|
22
|
+
const parsed = new URL(url);
|
|
23
|
+
if (parsed.protocol !== 'http:' && parsed.protocol !== 'https:') throw new Error(`Unsupported page URL: ${url}`);
|
|
24
|
+
return `${parsed.origin}/*`;
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function unregister() {
|
|
28
|
+
try { await chrome.scripting.unregisterContentScripts({ ids: [SCRIPT_ID] }); } catch {}
|
|
29
|
+
try { await chrome.storage.session.remove(PENDING_KEY); } catch {}
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
// Registration calls run one at a time; Chrome rejects a concurrent
|
|
33
|
+
// registration of the same id.
|
|
34
|
+
let queue = Promise.resolve();
|
|
35
|
+
function serialized(task) {
|
|
36
|
+
const run = queue.then(task, task);
|
|
37
|
+
queue = run.catch(() => {});
|
|
38
|
+
return run;
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async function register({ tabId, frameId, url }) {
|
|
42
|
+
const pattern = originPattern(url);
|
|
43
|
+
await unregister();
|
|
44
|
+
await chrome.scripting.registerContentScripts([{
|
|
45
|
+
id: SCRIPT_ID,
|
|
46
|
+
matches: [pattern],
|
|
47
|
+
js: FILES,
|
|
48
|
+
runAt: 'document_start',
|
|
49
|
+
world: 'MAIN',
|
|
50
|
+
allFrames: frameId !== 0,
|
|
51
|
+
persistAcrossSessions: false,
|
|
52
|
+
}]);
|
|
53
|
+
await chrome.storage.session.set({ [PENDING_KEY]: { tabId, frameId, pattern, at: Date.now() } });
|
|
54
|
+
return { ok: true, pattern };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function settle(details) {
|
|
58
|
+
let pending;
|
|
59
|
+
try { pending = (await chrome.storage.session.get(PENDING_KEY))[PENDING_KEY]; } catch { return; }
|
|
60
|
+
if (!pending) return;
|
|
61
|
+
const ours = pending.tabId === details.tabId && pending.frameId === details.frameId;
|
|
62
|
+
if (ours || Date.now() - pending.at > PENDING_MAX_AGE_MS) await serialized(unregister);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
|
|
66
|
+
if (message?.type !== 'chippy-register-armed-reload') return false;
|
|
67
|
+
const tabId = sender.tab?.id;
|
|
68
|
+
if (tabId == null || !sender.url) {
|
|
69
|
+
sendResponse({ ok: false, error: 'No sender tab for the armed reload' });
|
|
70
|
+
return false;
|
|
71
|
+
}
|
|
72
|
+
serialized(() => register({ tabId, frameId: sender.frameId ?? 0, url: sender.url }))
|
|
73
|
+
.then(sendResponse)
|
|
74
|
+
.catch((e) => sendResponse({ ok: false, error: e.message }));
|
|
75
|
+
return true;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
// document_start injection has happened by DOMContentLoaded.
|
|
79
|
+
chrome.webNavigation.onDOMContentLoaded.addListener((details) => { settle(details); });
|
|
80
|
+
chrome.webNavigation.onErrorOccurred.addListener((details) => { settle(details); });
|
|
81
|
+
|
|
82
|
+
global.chippyArmedReloadStartup = { SCRIPT_ID, FILES, originPattern };
|
|
83
|
+
})(globalThis);
|