extension-develop 4.0.13 → 4.0.14-canary.1784515910.7d7da9cc
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/dist/101.mjs +1 -1
- package/dist/839.mjs +155 -17
- package/dist/dev-server/control-bridge/producer-runtime.d.ts +2 -2
- package/dist/extension-js-devtools/chrome/content_scripts/content-0.js +2 -2
- package/dist/extension-js-devtools/chrome/pages/centralized-logger.js +4 -4
- package/dist/extension-js-devtools/chrome/pages/welcome.js +1 -1
- package/dist/extension-js-devtools/chromium/content_scripts/content-0.js +2 -2
- package/dist/extension-js-devtools/chromium/pages/centralized-logger.js +4 -4
- package/dist/extension-js-devtools/chromium/pages/welcome.js +1 -1
- package/dist/extension-js-devtools/edge/content_scripts/content-0.js +2 -2
- package/dist/extension-js-devtools/edge/pages/centralized-logger.js +4 -4
- package/dist/extension-js-devtools/edge/pages/welcome.js +1 -1
- package/dist/extension-js-devtools/extension-js/chrome/events.ndjson +2 -2
- package/dist/extension-js-devtools/extension-js/chrome/ready.json +8 -8
- package/dist/extension-js-devtools/extension-js/chromium/events.ndjson +2 -2
- package/dist/extension-js-devtools/extension-js/chromium/ready.json +8 -8
- package/dist/extension-js-devtools/extension-js/edge/events.ndjson +2 -2
- package/dist/extension-js-devtools/extension-js/edge/ready.json +8 -8
- package/dist/extension-js-devtools/extension-js/firefox/events.ndjson +2 -2
- package/dist/extension-js-devtools/extension-js/firefox/ready.json +8 -8
- package/dist/extension-js-devtools/firefox/content_scripts/content-0.js +2 -2
- package/dist/extension-js-devtools/firefox/pages/centralized-logger.js +4 -4
- package/dist/extension-js-devtools/firefox/pages/welcome.js +1 -1
- package/dist/extension-js-theme/extension-js/chrome/events.ndjson +2 -2
- package/dist/extension-js-theme/extension-js/chrome/ready.json +8 -8
- package/dist/extension-js-theme/extension-js/chromium/events.ndjson +2 -2
- package/dist/extension-js-theme/extension-js/chromium/ready.json +8 -8
- package/dist/extension-js-theme/extension-js/edge/events.ndjson +2 -2
- package/dist/extension-js-theme/extension-js/edge/ready.json +8 -8
- package/dist/extension-js-theme/extension-js/firefox/events.ndjson +2 -2
- package/dist/extension-js-theme/extension-js/firefox/ready.json +8 -8
- package/package.json +1 -1
package/dist/101.mjs
CHANGED
|
@@ -695,7 +695,7 @@ async function config_loader_isUsingExperimentalConfig(projectPath) {
|
|
|
695
695
|
}
|
|
696
696
|
return false;
|
|
697
697
|
}
|
|
698
|
-
var package_namespaceObject = /*#__PURE__*/ JSON.parse('{"rE":"4.0.
|
|
698
|
+
var package_namespaceObject = /*#__PURE__*/ JSON.parse('{"rE":"4.0.14-canary.1784515910.7d7da9cc","El":{"@prefresh/core":"1.5.9","@prefresh/utils":"1.2.1","@rspack/core":"^2.1.1","@rspack/dev-server":"2.1.0","@rspack/plugin-preact-refresh":"2.0.1","@rspack/plugin-react-refresh":"2.0.2","@vue/compiler-sfc":"3.5.26","adm-zip":"^0.6.0","browser-extension-manifest-fields":"^2.2.9","case-sensitive-paths-webpack-plugin":"^2.4.0","content-security-policy-parser":"^0.6.0","dotenv":"^17.2.3","es-module-lexer":"^2.1.0","extension-from-store":"^0.1.1","go-git-it":"^5.1.5","ignore":"^7.0.5","less":"4.6.7","less-loader":"13.0.0","parse5-utilities":"^1.0.0","pintor":"0.3.0","postcss":"8.5.10","postcss-loader":"8.2.1","postcss-preset-env":"11.1.1","postcss-scss":"4.0.9","preact":"10.27.3","prefers-yarn":"2.0.1","react-refresh":"0.18.0","sass-loader":"17.0.0","schema-utils":"^4.3.3","svelte-loader":"3.2.4","tiny-glob":"^0.2.9","typescript":"5.9.3","vue":"3.5.26","vue-loader":"17.4.2","webextension-polyfill":"^0.12.0","webpack-merge":"^6.0.1","webpack-target-webextension":"^2.1.3","ws":"^8.20.1"}}');
|
|
699
699
|
function asAbsolute(p) {
|
|
700
700
|
return __rspack_external_path.isAbsolute(p) ? p : __rspack_external_path.resolve(p);
|
|
701
701
|
}
|
package/dist/839.mjs
CHANGED
|
@@ -3485,14 +3485,34 @@ const BRIDGE_PRODUCER_SOURCE = `;(function () {
|
|
|
3485
3485
|
}
|
|
3486
3486
|
return;
|
|
3487
3487
|
}
|
|
3488
|
-
if (op === "storage.get") {
|
|
3489
|
-
chrome
|
|
3490
|
-
|
|
3491
|
-
return
|
|
3492
|
-
|
|
3493
|
-
|
|
3494
|
-
|
|
3495
|
-
|
|
3488
|
+
if (op === "storage.get" || op === "storage.set") {
|
|
3489
|
+
// chrome.* storage is callback-only on Gecko (only browser.* is
|
|
3490
|
+
// promisified there); some older Chromium builds also predate the
|
|
3491
|
+
// promise form. Calling .then() on the callback-only return (undefined)
|
|
3492
|
+
// is the §54 TypeError. Prefer the promisified browser.* namespace,
|
|
3493
|
+
// then fall back to the callback shape — the same promise-or-callback
|
|
3494
|
+
// duality getActionPopup handles below.
|
|
3495
|
+
var storageNS = (g.browser && g.browser.storage) ? g.browser.storage : chrome.storage;
|
|
3496
|
+
var storageArea = storageNS && storageNS[args.area || "local"];
|
|
3497
|
+
if (!storageArea) { replyErr(cmdId, "StorageError", "storage." + (args.area || "local") + " unavailable"); return; }
|
|
3498
|
+
var isSet = op === "storage.set";
|
|
3499
|
+
var callArgs = isSet ? [args.items || {}] : [args.key != null ? args.key : null];
|
|
3500
|
+
var onStorageOk = function (r) { replyOk(cmdId, isSet ? {set: Object.keys(args.items || {})} : r); };
|
|
3501
|
+
var onStorageErr = function (e) { replyErr(cmdId, "StorageError", (e && e.message) || String(e)); };
|
|
3502
|
+
var storageFn = isSet ? storageArea.set : storageArea.get;
|
|
3503
|
+
try {
|
|
3504
|
+
var storageRet = storageFn.apply(storageArea, callArgs);
|
|
3505
|
+
if (storageRet && typeof storageRet.then === "function") {
|
|
3506
|
+
storageRet.then(onStorageOk, onStorageErr);
|
|
3507
|
+
return;
|
|
3508
|
+
}
|
|
3509
|
+
} catch (e) { /* not thenable / threw synchronously: fall back to callback */ }
|
|
3510
|
+
try {
|
|
3511
|
+
storageFn.apply(storageArea, callArgs.concat([function (r) {
|
|
3512
|
+
var le = chrome.runtime && chrome.runtime.lastError;
|
|
3513
|
+
if (le) onStorageErr(le); else onStorageOk(r);
|
|
3514
|
+
}]));
|
|
3515
|
+
} catch (e) { onStorageErr(e); }
|
|
3496
3516
|
return;
|
|
3497
3517
|
}
|
|
3498
3518
|
if (op === "reload") {
|
|
@@ -4052,12 +4072,39 @@ const BRIDGE_PRODUCER_SOURCE = `;(function () {
|
|
|
4052
4072
|
};
|
|
4053
4073
|
}
|
|
4054
4074
|
|
|
4075
|
+
// §55: uncaught errors and unhandled rejections don't route through the
|
|
4076
|
+
// patched console (the browser reports them internally), so background/SW
|
|
4077
|
+
// crashes never reach \`extension logs\`. The global handlers below forward
|
|
4078
|
+
// them as level:"error"; these signatures let us skip a throw already shipped
|
|
4079
|
+
// via console.error (user code that logs THEN rethrows the same Error).
|
|
4080
|
+
var recentErrorSigs = [];
|
|
4081
|
+
function errorSig(message, stack) {
|
|
4082
|
+
return String(message == null ? "" : message) + "::" + String(stack == null ? "" : stack).slice(0, 200);
|
|
4083
|
+
}
|
|
4084
|
+
function noteErrorSig(sig) {
|
|
4085
|
+
recentErrorSigs.push({sig: sig, t: Date.now()});
|
|
4086
|
+
if (recentErrorSigs.length > 50) recentErrorSigs.shift();
|
|
4087
|
+
}
|
|
4088
|
+
function errorSigSeenRecently(sig) {
|
|
4089
|
+
var now = Date.now();
|
|
4090
|
+
for (var i = recentErrorSigs.length - 1; i >= 0; i--) {
|
|
4091
|
+
if (now - recentErrorSigs[i].t > 3000) { recentErrorSigs.splice(i, 1); continue; }
|
|
4092
|
+
if (recentErrorSigs[i].sig === sig) return true;
|
|
4093
|
+
}
|
|
4094
|
+
return false;
|
|
4095
|
+
}
|
|
4096
|
+
|
|
4055
4097
|
LEVELS.forEach(function (level) {
|
|
4056
4098
|
var orig = typeof consoleRef[level] === "function"
|
|
4057
4099
|
? consoleRef[level].bind(consoleRef)
|
|
4058
4100
|
: function () {};
|
|
4059
4101
|
consoleRef[level] = function () {
|
|
4060
4102
|
try {
|
|
4103
|
+
if (level === "error") {
|
|
4104
|
+
for (var ai = 0; ai < arguments.length; ai++) {
|
|
4105
|
+
if (arguments[ai] instanceof Error) noteErrorSig(errorSig(arguments[ai].message, arguments[ai].stack));
|
|
4106
|
+
}
|
|
4107
|
+
}
|
|
4061
4108
|
send({
|
|
4062
4109
|
type: "log",
|
|
4063
4110
|
event: {
|
|
@@ -4075,6 +4122,44 @@ const BRIDGE_PRODUCER_SOURCE = `;(function () {
|
|
|
4075
4122
|
};
|
|
4076
4123
|
});
|
|
4077
4124
|
|
|
4125
|
+
// Forward uncaught throws / unhandled rejections as level:"error" so a silent
|
|
4126
|
+
// background crash is visible in \`extension logs\` (§55). Deduped against the
|
|
4127
|
+
// console.error signatures above so a logged-then-rethrown Error emits once.
|
|
4128
|
+
function shipUncaughtError(message, stack, url) {
|
|
4129
|
+
try {
|
|
4130
|
+
var sig = errorSig(message, stack);
|
|
4131
|
+
if (errorSigSeenRecently(sig)) return;
|
|
4132
|
+
noteErrorSig(sig);
|
|
4133
|
+
send({
|
|
4134
|
+
type: "log",
|
|
4135
|
+
event: {
|
|
4136
|
+
v: 1,
|
|
4137
|
+
id: nowId(),
|
|
4138
|
+
timestamp: Date.now(),
|
|
4139
|
+
level: "error",
|
|
4140
|
+
context: CONTEXT,
|
|
4141
|
+
messageParts: sanitize([String(message) + (stack ? "\\n" + stack : "")]),
|
|
4142
|
+
url: url,
|
|
4143
|
+
runId: INSTANCE_ID
|
|
4144
|
+
}
|
|
4145
|
+
});
|
|
4146
|
+
} catch (e) {}
|
|
4147
|
+
}
|
|
4148
|
+
try {
|
|
4149
|
+
if (typeof g.addEventListener === "function") {
|
|
4150
|
+
g.addEventListener("error", function (ev) {
|
|
4151
|
+
var err = ev && ev.error;
|
|
4152
|
+
var message = (err && err.message) || (ev && ev.message) || "Uncaught error";
|
|
4153
|
+
shipUncaughtError(message, err && err.stack, ev && ev.filename);
|
|
4154
|
+
});
|
|
4155
|
+
g.addEventListener("unhandledrejection", function (ev) {
|
|
4156
|
+
var reason = ev && ev.reason;
|
|
4157
|
+
var message = (reason && reason.message) || (reason != null ? String(reason) : "Unhandled rejection");
|
|
4158
|
+
shipUncaughtError("Unhandled promise rejection: " + message, reason && reason.stack, undefined);
|
|
4159
|
+
});
|
|
4160
|
+
}
|
|
4161
|
+
} catch (e) {}
|
|
4162
|
+
|
|
4078
4163
|
// Multi-context console forwarding: other contexts (content scripts, surface
|
|
4079
4164
|
// pages) can't reliably open ws://127.0.0.1 (page CSP / connect-src), so they
|
|
4080
4165
|
// relay console to this SW, which owns the WS. The relay travels a NAMED
|
|
@@ -4230,26 +4315,79 @@ const BRIDGE_RELAY_SOURCE = `;(function () {
|
|
|
4230
4315
|
return logPort;
|
|
4231
4316
|
}
|
|
4232
4317
|
|
|
4318
|
+
// Relay one log payload to the SW producer over the named port, redialing
|
|
4319
|
+
// once if the port went stale (SW restarted). Shared by the console patch
|
|
4320
|
+
// and the uncaught-error handlers below.
|
|
4321
|
+
function postLog(payload) {
|
|
4322
|
+
var p = getLogPort();
|
|
4323
|
+
if (!p) return;
|
|
4324
|
+
try {
|
|
4325
|
+
p.postMessage({__extjsBridgeLog: payload});
|
|
4326
|
+
} catch (e) {
|
|
4327
|
+
logPort = null;
|
|
4328
|
+
var p2 = getLogPort();
|
|
4329
|
+
if (p2) { try { p2.postMessage({__extjsBridgeLog: payload}); } catch (e2) {} }
|
|
4330
|
+
}
|
|
4331
|
+
}
|
|
4332
|
+
|
|
4333
|
+
// §55: uncaught throws / unhandled rejections in a page or content script
|
|
4334
|
+
// never route through the patched console, so they'd be invisible in
|
|
4335
|
+
// \`extension logs\`. Track console.error Error signatures to dedupe them.
|
|
4336
|
+
var recentErrorSigs = [];
|
|
4337
|
+
function errorSig(message, stack) {
|
|
4338
|
+
return String(message == null ? "" : message) + "::" + String(stack == null ? "" : stack).slice(0, 200);
|
|
4339
|
+
}
|
|
4340
|
+
function noteErrorSig(sig) {
|
|
4341
|
+
recentErrorSigs.push({sig: sig, t: Date.now()});
|
|
4342
|
+
if (recentErrorSigs.length > 50) recentErrorSigs.shift();
|
|
4343
|
+
}
|
|
4344
|
+
function errorSigSeenRecently(sig) {
|
|
4345
|
+
var now = Date.now();
|
|
4346
|
+
for (var i = recentErrorSigs.length - 1; i >= 0; i--) {
|
|
4347
|
+
if (now - recentErrorSigs[i].t > 3000) { recentErrorSigs.splice(i, 1); continue; }
|
|
4348
|
+
if (recentErrorSigs[i].sig === sig) return true;
|
|
4349
|
+
}
|
|
4350
|
+
return false;
|
|
4351
|
+
}
|
|
4352
|
+
|
|
4233
4353
|
LEVELS.forEach(function (level) {
|
|
4234
4354
|
var orig = typeof consoleRef[level] === "function" ? consoleRef[level].bind(consoleRef) : function () {};
|
|
4235
4355
|
consoleRef[level] = function () {
|
|
4236
4356
|
try {
|
|
4237
|
-
|
|
4238
|
-
|
|
4239
|
-
|
|
4240
|
-
p.postMessage({__extjsBridgeLog: {level: level, context: CONTEXT, messageParts: sanitize([].slice.call(arguments)), url: here()}});
|
|
4241
|
-
} catch (e) {
|
|
4242
|
-
// stale port (SW restarted): drop it and redial once
|
|
4243
|
-
logPort = null;
|
|
4244
|
-
var p2 = getLogPort();
|
|
4245
|
-
if (p2) { try { p2.postMessage({__extjsBridgeLog: {level: level, context: CONTEXT, messageParts: sanitize([].slice.call(arguments)), url: here()}}); } catch (e2) {} }
|
|
4357
|
+
if (level === "error") {
|
|
4358
|
+
for (var ai = 0; ai < arguments.length; ai++) {
|
|
4359
|
+
if (arguments[ai] instanceof Error) noteErrorSig(errorSig(arguments[ai].message, arguments[ai].stack));
|
|
4246
4360
|
}
|
|
4247
4361
|
}
|
|
4362
|
+
postLog({level: level, context: CONTEXT, messageParts: sanitize([].slice.call(arguments)), url: here()});
|
|
4248
4363
|
} catch (e) {}
|
|
4249
4364
|
return orig.apply(consoleRef, arguments);
|
|
4250
4365
|
};
|
|
4251
4366
|
});
|
|
4252
4367
|
|
|
4368
|
+
function shipUncaughtError(message, stack, url) {
|
|
4369
|
+
try {
|
|
4370
|
+
var sig = errorSig(message, stack);
|
|
4371
|
+
if (errorSigSeenRecently(sig)) return;
|
|
4372
|
+
noteErrorSig(sig);
|
|
4373
|
+
postLog({level: "error", context: CONTEXT, messageParts: sanitize([String(message) + (stack ? "\\n" + stack : "")]), url: url || here()});
|
|
4374
|
+
} catch (e) {}
|
|
4375
|
+
}
|
|
4376
|
+
try {
|
|
4377
|
+
if (typeof g.addEventListener === "function") {
|
|
4378
|
+
g.addEventListener("error", function (ev) {
|
|
4379
|
+
var err = ev && ev.error;
|
|
4380
|
+
var message = (err && err.message) || (ev && ev.message) || "Uncaught error";
|
|
4381
|
+
shipUncaughtError(message, err && err.stack, (ev && ev.filename) || undefined);
|
|
4382
|
+
});
|
|
4383
|
+
g.addEventListener("unhandledrejection", function (ev) {
|
|
4384
|
+
var reason = ev && ev.reason;
|
|
4385
|
+
var message = (reason && reason.message) || (reason != null ? String(reason) : "Unhandled rejection");
|
|
4386
|
+
shipUncaughtError("Unhandled promise rejection: " + message, reason && reason.stack, undefined);
|
|
4387
|
+
});
|
|
4388
|
+
}
|
|
4389
|
+
} catch (e) {}
|
|
4390
|
+
|
|
4253
4391
|
// Surface DOM inspection: the SW executor can't reach popup/options/sidebar
|
|
4254
4392
|
// DOM (separate extension pages), and the sidecar can't either (cross-extension
|
|
4255
4393
|
// isolation). So THIS page — the user extension's own surface — answers an
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
export declare const BRIDGE_PRODUCER_SOURCE = ";(function () {\n try {\n var g = (typeof globalThis === \"object\" && globalThis) ? globalThis : this;\n if (!g || g.__extjsBridgeProducerInstalled) return;\n\n var PORT = parseInt(\"__EXTJS_CONTROL_PORT__\", 10);\n var INSTANCE_ID = \"__EXTJS_INSTANCE_ID__\";\n var CONTEXT = \"__EXTJS_CONTEXT__\";\n // Connectable host of the dev-server control WS. Baked from the resolved\n // connectable host (loopback locally; the public host for remote/devcontainer).\n var HOST = \"__EXTJS_CONTROL_HOST__\";\n if (!HOST || HOST.indexOf(\"__EXTJS\") === 0) HOST = \"127.0.0.1\";\n if (!PORT || PORT < 1) return;\n\n var WS = g.WebSocket;\n if (typeof WS !== \"function\") return;\n\n var consoleRef = g.console || {};\n g.__extjsBridgeProducerInstalled = true;\n\n // Capture extension event listeners at install time. The producer is\n // prepended to the background bundle, so these wraps run BEFORE user code\n // registers its listeners \u2014 letting the bridge replay them on demand. The\n // platform exposes no API to dispatch these events, and CDP attaches too\n // late to wrap addListener, so this is the only path. Replay invokes the\n // handler WITHOUT a user gesture, so the gesture-derived activeTab grant\n // does NOT apply (callers are told gesture:false). This is engine-agnostic:\n // it works on Chromium and Gecko because it only touches addListener.\n //\n // captureEvent transparently wraps addListener/removeListener (delegating to\n // the originals) and records callbacks into the sink array.\n function captureEvent(event, sink) {\n try {\n if (!event || typeof event.addListener !== \"function\") return;\n var origAdd = event.addListener.bind(event);\n event.addListener = function (cb) {\n if (typeof cb === \"function\" && sink.indexOf(cb) === -1) sink.push(cb);\n return origAdd(cb);\n };\n if (typeof event.removeListener === \"function\") {\n var origRemove = event.removeListener.bind(event);\n event.removeListener = function (cb) {\n var i = sink.indexOf(cb);\n if (i !== -1) sink.splice(i, 1);\n return origRemove(cb);\n };\n }\n } catch (e) { /* non-fatal: trigger falls back to its no-listener reply */ }\n }\n\n // Use g.chrome (not the hoisted chrome var below) \u2014 this runs first.\n var actionClickedListeners = [];\n var commandListeners = [];\n if (g.chrome) {\n captureEvent(g.chrome.action && g.chrome.action.onClicked, actionClickedListeners);\n captureEvent(g.chrome.commands && g.chrome.commands.onCommand, commandListeners);\n }\n\n var LEVELS = [\"log\", \"info\", \"warn\", \"error\", \"debug\", \"trace\"];\n var socket = null;\n var open = false;\n var queue = [];\n var backoff = 250;\n var MAX_BACKOFF = 5000;\n var MAX_QUEUE = 1000;\n var MAX_RESULT_BYTES = 256 * 1024;\n\n function nowId() {\n return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);\n }\n\n function engineName() {\n try {\n return (typeof navigator !== \"undefined\" && /firefox/i.test(navigator.userAgent)) ? \"firefox\" : \"chromium\";\n } catch (e) { return \"chromium\"; }\n }\n\n // JSON-safe, byte-capped clone of a result value.\n function safeValue(v) {\n try {\n var s = JSON.stringify(v);\n if (s === undefined) return {value: undefined, truncated: false};\n if (s.length > MAX_RESULT_BYTES) {\n return {value: {__type: \"truncated\", preview: s.slice(0, 1024)}, truncated: true};\n }\n return {value: JSON.parse(s), truncated: false};\n } catch (e) {\n return {value: {__type: \"unserializable\", preview: String(v).slice(0, 512)}, truncated: false};\n }\n }\n\n function replyOk(cmdId, value) {\n var s = safeValue(value);\n var frame = {type: \"result\", cmdId: cmdId, ok: true, value: s.value};\n if (s.truncated) frame.truncated = true;\n try { socket && socket.send(JSON.stringify(frame)); } catch (e) {}\n }\n\n function replyErr(cmdId, name, message) {\n var frame = {type: \"result\", cmdId: cmdId, ok: false, error: {name: name, message: String(message), engine: engineName()}};\n try { socket && socket.send(JSON.stringify(frame)); } catch (e) {}\n }\n\n // Resolve a numeric tab id for content/page targeting when the caller did\n // not pass one: match target.url against open tabs (Chrome match pattern,\n // then a substring fallback), else default to the active tab. cb(tabId, err)\n // \u2014 tabId is null with a human message when nothing matches. (#51)\n function resolveTargetTab(target, cb) {\n var chrome = g.chrome;\n if (!chrome || !chrome.tabs) { cb(null, \"chrome.tabs is not available\"); return; }\n var url = target && target.url;\n if (url) {\n var substringMatch = function () {\n try {\n chrome.tabs.query({}, function (all) {\n var hit = (all || []).filter(function (t) { return t.url && String(t.url).indexOf(url) !== -1; })[0];\n if (hit && typeof hit.id === \"number\") { cb(hit.id, null); return; }\n cb(null, \"no open tab matches url: \" + url);\n });\n } catch (e2) { cb(null, \"tab query failed\"); }\n };\n try {\n // chrome.tabs.query accepts URL match patterns; a bare url/glob may\n // not be a valid pattern, so fall back to a substring scan.\n chrome.tabs.query({url: url}, function (tabs) {\n if (chrome.runtime && chrome.runtime.lastError) { substringMatch(); return; }\n if (tabs && tabs.length && typeof tabs[0].id === \"number\") { cb(tabs[0].id, null); return; }\n substringMatch();\n });\n } catch (e) { substringMatch(); }\n return;\n }\n try {\n chrome.tabs.query({active: true, lastFocusedWindow: true}, function (tabs) {\n var t = tabs && tabs[0];\n if (t && typeof t.id === \"number\") { cb(t.id, null); return; }\n cb(null, \"no active tab to target\");\n });\n } catch (e3) { cb(null, \"tab query failed\"); }\n }\n\n // Execute one authorized command in the SW (or route to a tab). chrome.*\n // promise APIs are used; callback-only APIs are wrapped.\n function executeCommand(cmd) {\n var op = cmd.op, target = cmd.target || {}, args = cmd.args || {};\n var ctx = target.context || \"background\";\n var chrome = g.chrome;\n var cmdId = cmd.cmdId;\n // content/page eval & inspect need a numeric tab id. When the caller gave\n // a --url (or nothing), resolve it to a tab (url match, else the active\n // tab) and re-dispatch with target.tabId filled in. (#51)\n if ((op === \"eval\" || op === \"inspect\") && (ctx === \"content\" || ctx === \"page\") && target.tabId == null) {\n resolveTargetTab(target, function (tabId, err) {\n if (tabId == null) {\n replyErr(cmdId, \"Unsupported\", err || (\"eval/inspect in context \" + ctx + \" needs a --tab id, a --url to match, or an active tab\"));\n return;\n }\n var next = {};\n for (var k in cmd) { if (Object.prototype.hasOwnProperty.call(cmd, k)) next[k] = cmd[k]; }\n next.target = {};\n for (var tk in target) { if (Object.prototype.hasOwnProperty.call(target, tk)) next.target[tk] = target[tk]; }\n next.target.tabId = tabId;\n executeCommand(next);\n });\n return;\n }\n try {\n if (!chrome) { replyErr(cmdId, \"Unsupported\", \"chrome.* not available in this context\"); return; }\n if (op === \"eval\") {\n if (ctx === \"background\") {\n Promise.resolve().then(function () { return (0, eval)(args.expression); })\n .then(function (r) { replyOk(cmdId, r); }, function (e) {\n var msg = (e && e.message) || String(e);\n // MV3 forbids eval of strings in the SW/extension pages; Chrome\n // rejects 'unsafe-eval' in MV3 extension_pages, so this can't be\n // relaxed. Surface it honestly with a usable alternative.\n if (/Content Security Policy|unsafe-eval/i.test(msg)) {\n replyErr(cmdId, \"Unsupported\", \"eval is blocked in the MV3 service worker by CSP. Use --context page --tab <id> (eval runs in the page's MAIN world), or run on an MV2/Firefox build. Engine: \" + engineName());\n } else {\n replyErr(cmdId, (e && e.name) || \"EvalError\", msg);\n }\n });\n } else if ((ctx === \"content\" || ctx === \"page\") && target.tabId && chrome.scripting) {\n chrome.scripting.executeScript({\n target: {tabId: target.tabId},\n world: ctx === \"page\" ? \"MAIN\" : \"ISOLATED\",\n func: function (src) { return eval(src); },\n args: [String(args.expression)]\n }).then(function (res) { replyOk(cmdId, res && res[0] ? res[0].result : undefined); },\n function (e) { replyErr(cmdId, \"EvalError\", e); });\n } else {\n replyErr(cmdId, \"Unsupported\", \"eval in context \" + ctx + \" requires a tabId\");\n }\n return;\n }\n if (op === \"storage.get\") {\n chrome.storage[args.area || \"local\"].get(args.key != null ? args.key : null)\n .then(function (r) { replyOk(cmdId, r); }, function (e) { replyErr(cmdId, \"StorageError\", e); });\n return;\n }\n if (op === \"storage.set\") {\n chrome.storage[args.area || \"local\"].set(args.items || {})\n .then(function () { replyOk(cmdId, {set: Object.keys(args.items || {})}); }, function (e) { replyErr(cmdId, \"StorageError\", e); });\n return;\n }\n if (op === \"reload\") {\n if (ctx === \"background\") {\n replyOk(cmdId, {reloading: true});\n setTimeout(function () { try { chrome.runtime.reload(); } catch (e) {} }, 50);\n } else if (target.tabId) {\n chrome.tabs.reload(target.tabId).then(function () { replyOk(cmdId, {reloaded: target.tabId}); }, function (e) { replyErr(cmdId, \"ReloadError\", e); });\n } else {\n replyErr(cmdId, \"Unsupported\", \"reload needs background or a tabId\");\n }\n return;\n }\n if (op === \"open\") {\n var surface = args.surface || ctx;\n // getPopup is promise-style on Gecko and (MV3) Chromium; fall back to\n // callback-style for older Chromium. Always resolves to a string.\n var getActionPopup = function (cb) {\n try {\n var r = chrome.action && chrome.action.getPopup && chrome.action.getPopup({});\n if (r && typeof r.then === \"function\") {\n r.then(function (p) { cb(p || \"\"); }, function () { cb(\"\"); });\n return;\n }\n } catch (e) {}\n try { chrome.action.getPopup({}, function (p) { cb(p || \"\"); }); }\n catch (e) { cb(\"\"); }\n };\n // Resolve the tab a replayed event should carry: an explicit args.tabId,\n // else the active tab of the focused window.\n var resolveActiveTab = function (a, cb) {\n if (a && typeof a.tabId === \"number\") {\n try { chrome.tabs.get(a.tabId, function (t) { cb(t || {id: a.tabId}); }); return; }\n catch (e) {}\n }\n try { chrome.tabs.query({active: true, lastFocusedWindow: true}, function (tabs) { cb((tabs && tabs[0]) || undefined); }); }\n catch (e) { cb(undefined); }\n };\n // Replaying a listener carries no user gesture, so activeTab is never\n // granted. Warn when the manifest declares it (handler will diverge\n // from a real click).\n var activeTabWarning = function () {\n try {\n var m = chrome.runtime.getManifest();\n var perms = (m && m.permissions) || [];\n if (perms.indexOf(\"activeTab\") !== -1) {\n return \"replayed without a user gesture: activeTab is NOT granted, so APIs that depend on it (scripting on the active tab, captureVisibleTab) behave differently than a real click\";\n }\n } catch (e) {}\n return null;\n };\n if (surface === \"popup\") {\n if (chrome.action && chrome.action.openPopup) {\n chrome.action.openPopup().then(function () { replyOk(cmdId, {opened: \"popup\"}); }, function (e) { replyErr(cmdId, \"Unsupported\", \"openPopup: \" + e); });\n } else { replyErr(cmdId, \"Unsupported\", \"action.openPopup not available\"); }\n } else if (surface === \"options\") {\n try { chrome.runtime.openOptionsPage(function () { replyOk(cmdId, {opened: \"options\"}); }); }\n catch (e) { replyErr(cmdId, \"Unsupported\", \"openOptionsPage: \" + e); }\n } else if (surface === \"sidebar\") {\n if (chrome.sidePanel && chrome.sidePanel.open && chrome.windows) {\n chrome.windows.getCurrent(function (w) {\n chrome.sidePanel.open({windowId: w.id}).then(function () { replyOk(cmdId, {opened: \"sidebar\"}); }, function (e) { replyErr(cmdId, \"Unsupported\", \"sidePanel.open: \" + e); });\n });\n } else { replyErr(cmdId, \"Unsupported\", \"sidePanel not available (engine: \" + engineName() + \")\"); }\n } else if (surface === \"action\") {\n // Trigger the toolbar action. With a default_popup, clicking the icon\n // opens it (reuse openPopup). Without a popup, clicking fires\n // chrome.action.onClicked \u2014 we replay the listeners captured at install.\n if (chrome.action) {\n getActionPopup(function (popup) {\n if (popup) {\n try {\n chrome.action.openPopup().then(function () { replyOk(cmdId, {triggered: \"popup\"}); }, function (e) { replyErr(cmdId, \"Unsupported\", \"openPopup: \" + e); });\n } catch (e) { replyErr(cmdId, \"Unsupported\", \"openPopup: \" + e); }\n } else if (actionClickedListeners.length) {\n resolveActiveTab(args, function (tab) {\n var fired = 0;\n for (var i = 0; i < actionClickedListeners.length; i++) {\n try { actionClickedListeners[i](tab); fired++; } catch (e) {}\n }\n var reply = {triggered: \"onClicked\", listeners: fired, gesture: false};\n var warning = activeTabWarning();\n if (warning) reply.warning = warning;\n replyOk(cmdId, reply);\n });\n } else {\n replyErr(cmdId, \"Unsupported\", \"action has no popup and no onClicked listener registered\");\n }\n });\n } else { replyErr(cmdId, \"Unsupported\", \"chrome.action not available (engine: \" + engineName() + \")\"); }\n } else if (surface === \"command\") {\n // Replay a captured chrome.commands.onCommand listener (keyboard\n // shortcut). Same no-gesture caveat as onClicked.\n var commandName = (args && args.name) || undefined;\n if (!commandListeners.length) {\n replyErr(cmdId, \"Unsupported\", \"no chrome.commands.onCommand listener registered\");\n } else {\n resolveActiveTab(args, function (tab) {\n var fired = 0;\n for (var i = 0; i < commandListeners.length; i++) {\n try { commandListeners[i](commandName, tab); fired++; } catch (e) {}\n }\n replyOk(cmdId, {triggered: \"command\", command: commandName || null, listeners: fired, gesture: false});\n });\n }\n } else { replyErr(cmdId, \"BadRequest\", \"unknown surface: \" + surface); }\n return;\n }\n if (op === \"tabs.query\") {\n chrome.tabs.query(args || {}).then(function (tabs) {\n replyOk(cmdId, (tabs || []).map(function (t) { return {id: t.id, url: t.url, title: t.title, active: t.active, windowId: t.windowId}; }));\n }, function (e) { replyErr(cmdId, \"TabsError\", e); });\n return;\n }\n if (op === \"inspect\") {\n // Slice 3 sidecar/default path: extract a DOM snapshot from the target\n // page via chrome.scripting (CDP-free). Closed shadow roots need the\n // --deep-dom CDP escape hatch (separate tool); here we read open ones.\n if ((ctx === \"content\" || ctx === \"page\") && target.tabId && chrome.scripting) {\n var maxBytes = (args && args.maxBytes) || 262144;\n var includeHtml = !args || !args.include || args.include.indexOf(\"html\") !== -1;\n chrome.scripting.executeScript({\n target: {tabId: target.tabId},\n world: ctx === \"page\" ? \"MAIN\" : \"ISOLATED\",\n func: function (wantHtml, cap) {\n function countShadow(root) {\n var n = 0, els = root.querySelectorAll(\"*\");\n for (var i = 0; i < els.length; i++) { if (els[i].shadowRoot) { n++; n += countShadow(els[i].shadowRoot); } }\n return n;\n }\n var doc = document;\n var roots = doc.querySelectorAll('#extension-root,[data-extension-root]');\n var out = {\n url: location.href,\n title: doc.title,\n summary: {\n htmlLength: doc.documentElement.outerHTML.length,\n scriptCount: doc.querySelectorAll(\"script\").length,\n styleCount: doc.querySelectorAll(\"style,link[rel=stylesheet]\").length,\n extensionRootCount: roots.length,\n openShadowRoots: countShadow(doc),\n bodyChildCount: doc.body ? doc.body.children.length : 0\n }\n };\n if (wantHtml) {\n var html = doc.documentElement.outerHTML;\n if (cap > 0 && html.length > cap) { out.html = html.slice(0, cap); out.htmlTruncated = true; }\n else { out.html = html; }\n }\n return out;\n },\n args: [includeHtml, maxBytes]\n }).then(function (res) { replyOk(cmdId, res && res[0] ? res[0].result : undefined); },\n function (e) { replyErr(cmdId, \"InspectError\", e); });\n } else if (ctx === \"popup\" || ctx === \"options\" || ctx === \"sidebar\" || ctx === \"devtools\") {\n // The SW can't read a surface page's DOM; ask the surface's own\n // in-bundle relay (only the open, matching-context page responds).\n chrome.runtime.sendMessage(\n {__extjsInspectRequest: true, target: target, args: args},\n function (resp) {\n if (chrome.runtime.lastError || !resp) {\n replyErr(cmdId, \"Unsupported\", \"surface '\" + ctx + \"' is not open (open it first: extension open \" + ctx + \")\");\n } else if (resp.ok) {\n replyOk(cmdId, resp.value);\n } else {\n replyErr(cmdId, (resp.error && resp.error.name) || \"InspectError\", (resp.error && resp.error.message) || \"inspect failed\");\n }\n }\n );\n } else if (ctx === \"background\") {\n replyErr(cmdId, \"Unsupported\", \"the service worker has no DOM; inspect a content/page (with --tab) or an open surface\");\n } else {\n replyErr(cmdId, \"Unsupported\", \"inspect of \" + ctx + \" requires a tabId (content/page) or an open surface\");\n }\n return;\n }\n replyErr(cmdId, \"BadRequest\", \"unknown op: \" + op);\n } catch (e) {\n replyErr(cmdId, \"ExecutorError\", e);\n }\n }\n\n function noopLastError() { try { void g.chrome.runtime.lastError; } catch (e) {} }\n\n // Only http(s)/file/ftp tabs can run a content script; skip chrome://, the\n // extension's own pages, about:blank, etc.\n function isInjectableUrl(url) {\n return typeof url === \"string\" && /^(https?|file|ftp):/i.test(url);\n }\n\n // Re-inject a single declared content-script entry's fresh files into every\n // open tab it matches, in place. The wrapper's reinject runtime tears down\n // the previous mount (matched by data-extjs-reinject-owner + build hash) and\n // mounts the new one \u2014 so this is the controller-less equivalent of the CDP\n // controller's reinjection, just driven from inside the extension.\n //\n // exclude_matches MUST be honored: the browser's own static injection never\n // touches excluded pages, and extensions can rely on that (e.g. a script\n // whose message handler opens the very page it excludes \u2014 injecting there\n // anyway creates an open-a-tab \u2192 inject \u2192 open-a-tab runaway loop). Chrome's\n // tabs.query has no exclude support, so query the exclude patterns\n // separately and subtract \u2014 the browser stays the single pattern matcher.\n function reinjectContentScriptEntry(entry) {\n var chrome = g.chrome;\n var matches = (entry && entry.matches) || [];\n if (!Array.isArray(matches) || !matches.length) return;\n var excludeMatches = (entry && entry.exclude_matches) || [];\n var jsFiles = ((entry && entry.js) || []).filter(function (f) { return typeof f === \"string\"; });\n var cssFiles = ((entry && entry.css) || []).filter(function (f) { return typeof f === \"string\"; });\n if (!jsFiles.length && !cssFiles.length) return;\n var world = entry.world === \"MAIN\" ? \"MAIN\" : \"ISOLATED\";\n var allFrames = !!entry.all_frames;\n function injectInto(excludedTabIds) {\n try {\n chrome.tabs.query({url: matches}, function (tabs) {\n var err = null;\n try { err = chrome.runtime.lastError; } catch (e) {}\n if (err || !tabs) return;\n for (var i = 0; i < tabs.length; i++) {\n (function (tab) {\n if (!tab || tab.id == null || !isInjectableUrl(tab.url)) return;\n if (excludedTabIds[tab.id]) return;\n var target = {tabId: tab.id, allFrames: allFrames};\n if (cssFiles.length && chrome.scripting.insertCSS) {\n try { chrome.scripting.insertCSS({target: target, files: cssFiles}, noopLastError); } catch (e) {}\n }\n if (jsFiles.length && chrome.scripting.executeScript) {\n try {\n chrome.scripting.executeScript(\n {target: target, files: jsFiles, world: world, injectImmediately: true},\n noopLastError\n );\n } catch (e) {}\n }\n })(tabs[i]);\n }\n });\n } catch (e) {}\n }\n if (Array.isArray(excludeMatches) && excludeMatches.length) {\n try {\n chrome.tabs.query({url: excludeMatches}, function (excludedTabs) {\n try { void chrome.runtime.lastError; } catch (e) {}\n var excluded = {};\n if (excludedTabs) {\n for (var i = 0; i < excludedTabs.length; i++) {\n if (excludedTabs[i] && excludedTabs[i].id != null) excluded[excludedTabs[i].id] = true;\n }\n }\n injectInto(excluded);\n });\n return;\n } catch (e) {}\n }\n injectInto({});\n }\n\n // Re-inject all content scripts into their open tabs. Reads the manifest\n // FROM DISK (not chrome.runtime.getManifest(), which is frozen at extension-\n // registration time) because dev content-script filenames are content-hashed\n // and change on every edit \u2014 disk has the new js/css paths.\n function reinjectContentScripts(onDone) {\n var chrome = g.chrome;\n try {\n if (typeof g.fetch !== \"function\" || !chrome.scripting) return false;\n g.fetch(chrome.runtime.getURL(\"manifest.json\"), {cache: \"no-store\"})\n .then(function (r) { return r.json(); })\n .then(function (manifest) {\n var entries = (manifest && manifest.content_scripts) || [];\n for (var i = 0; i < entries.length; i++) reinjectContentScriptEntry(entries[i]);\n reregisterForFutureNavigations(entries);\n if (onDone) { try { onDone(); } catch (e) {} }\n })\n .catch(function () {});\n return true;\n } catch (e) { return false; }\n }\n\n function mapRunAt(runAt) {\n if (runAt === \"document_start\") return \"document_start\";\n if (runAt === \"document_end\") return \"document_end\";\n return \"document_idle\";\n }\n\n // Re-register the content scripts dynamically (chrome.scripting) pointing at\n // the fresh files, so tabs opened AFTER an edit also get the new build. The\n // static manifest registration still points at the old content-hashed file\n // (it only updates on an extension reload) and keeps firing on new\n // navigations; the dynamic registration injects the new file alongside it,\n // and the wrapper's reinject runtime converges the mount to the newest build.\n // Idempotent: update an existing id, else register it.\n function reregisterForFutureNavigations(entries) {\n var chrome = g.chrome;\n if (\n !chrome.scripting ||\n !chrome.scripting.registerContentScripts ||\n !chrome.scripting.getRegisteredContentScripts\n ) return;\n\n var scripts = [];\n for (var i = 0; i < entries.length; i++) {\n var e = entries[i] || {};\n if (!Array.isArray(e.matches) || !e.matches.length) continue;\n var js = (e.js || []).filter(function (f) { return typeof f === \"string\"; });\n var css = (e.css || []).filter(function (f) { return typeof f === \"string\"; });\n if (!js.length && !css.length) continue;\n var s = {\n id: \"extjs-dev-cs-\" + i,\n matches: e.matches,\n runAt: mapRunAt(e.run_at),\n allFrames: !!e.all_frames,\n world: e.world === \"MAIN\" ? \"MAIN\" : \"ISOLATED\"\n };\n // Dev registration must not be BROADER than the static one: dropping\n // exclude_matches injects into pages the browser itself would skip.\n if (Array.isArray(e.exclude_matches) && e.exclude_matches.length) {\n s.excludeMatches = e.exclude_matches;\n }\n if (js.length) s.js = js;\n if (css.length) s.css = css;\n scripts.push(s);\n }\n if (!scripts.length) return;\n\n try {\n chrome.scripting.getRegisteredContentScripts(function (existing) {\n try { void chrome.runtime.lastError; } catch (e) {}\n var have = {};\n if (existing) for (var k = 0; k < existing.length; k++) have[existing[k].id] = true;\n var toRegister = [], toUpdate = [];\n for (var j = 0; j < scripts.length; j++) {\n (have[scripts[j].id] ? toUpdate : toRegister).push(scripts[j]);\n }\n if (toRegister.length) {\n try { chrome.scripting.registerContentScripts(toRegister, noopLastError); } catch (e) {}\n }\n if (toUpdate.length) {\n try { chrome.scripting.updateContentScripts(toUpdate, noopLastError); } catch (e) {}\n }\n });\n } catch (e) {}\n }\n\n // Perform a dev-loop reload without the CDP controller:\n // - content-scripts: re-inject the fresh script into open matching tabs in\n // place (no extension restart), the controller-less equivalent of CDP\n // reinjection. Needs the scripting permission + host access to the tab \u2014\n // both injected in dev by ApplyDevDefaults. Falls back to a full reload\n // if scripting is unavailable.\n // - service-worker / full / manifest: restart the whole extension so it\n // re-registers from the fresh manifest.\n function performDevReload(type, onDone) {\n var chrome = g.chrome;\n if (!chrome) return;\n\n var fullReload = function () {\n // Leave a pending-reinject flag for the NEXT producer generation:\n // Chrome never re-runs content scripts in tabs that were already open\n // when the extension reloads, and runtime.reload() does NOT fire\n // onInstalled (verified Chromium 146), so the boot-time heal below is\n // the only thing that converges open tabs after a SW/full reload \u2014\n // without it a shared SW+content module edit restarts the SW while\n // every open tab keeps the stale content world (family F).\n try {\n if (chrome.storage && chrome.storage.local) {\n chrome.storage.local.set({__extjsDevPendingReinject: Date.now()}, noopLastError);\n }\n } catch (e) {}\n // Deferred so any in-flight result/log frame \u2014 and the \"Reloading\u2026\"\n // console announcement dispatched into tabs \u2014 flushes before the SW\n // dies. runtime.reload() restarts the whole extension; the devtools\n // companion confirms completion via chrome.management events, so no\n // onDone here.\n try { setTimeout(function () { try { chrome.runtime.reload(); } catch (e) {} }, 150); } catch (e) {}\n };\n\n if (type === \"content-scripts\" && chrome.scripting && chrome.tabs && chrome.tabs.query) {\n if (reinjectContentScripts(onDone)) return;\n }\n\n fullReload();\n }\n\n // ---- Dev-reload announcement surfaces --------------------------------\n // One server-built label travels the ReloadFrame; the producer echoes it\n // 1:1 into (a) the page's devtools console and (b) the bundled\n // extension-js-devtools companion (the bottom-left pill). Both fire HERE,\n // next to the actual reload action, so a surface can only say\n // \"Reloading\u2026\" when a reload is actually being performed.\n\n // Stable IDs of the bundled extension-js-devtools companion: Chromium pins\n // via the manifest \"key\"; Firefox via browser_specific_settings.gecko.id.\n // Absent companion (e.g. --no-browser in the user's own browser) is fine \u2014\n // sendMessage just reports no receiver and we swallow it.\n var DEVTOOLS_COMPANION_ID_CHROMIUM = \"kgdaecdpfkikjncaalnmmnjjfpofkcbl\";\n var DEVTOOLS_COMPANION_ID_FIREFOX = \"devtools@extension.js\";\n\n function notifyDevtoolsCompanion(phase, label, kind) {\n try {\n var chrome = g.chrome;\n if (!chrome || !chrome.runtime || typeof chrome.runtime.sendMessage !== \"function\") return;\n var id = engineName() === \"firefox\" ? DEVTOOLS_COMPANION_ID_FIREFOX : DEVTOOLS_COMPANION_ID_CHROMIUM;\n chrome.runtime.sendMessage(\n id,\n {type: \"extjs-dev-reload-state\", phase: phase, label: label || \"\", kind: kind || \"\", instanceId: INSTANCE_ID},\n function () { noopLastError(); }\n );\n } catch (e) {}\n }\n\n // console.log the reload line into every open injectable tab. Runs in the\n // user extension's ISOLATED world, where the bridge relay patched console \u2014\n // so the same line also lands in the centralized dev-server log stream.\n function announceReloadInTabs(text) {\n var chrome = g.chrome;\n if (!chrome || !chrome.tabs || !chrome.tabs.query) return;\n try {\n chrome.tabs.query({}, function (tabs) {\n noopLastError();\n if (!tabs) return;\n for (var i = 0; i < tabs.length; i++) {\n (function (tab) {\n if (!tab || tab.id == null || !isInjectableUrl(tab.url)) return;\n if (chrome.scripting && chrome.scripting.executeScript) {\n try {\n chrome.scripting.executeScript(\n {target: {tabId: tab.id}, func: function (t) { console.log(t); }, args: [text]},\n noopLastError\n );\n return;\n } catch (e) {}\n }\n // MV2 fallback (Firefox without the scripting API).\n if (chrome.tabs.executeScript) {\n try {\n chrome.tabs.executeScript(tab.id, {code: \"console.log(\" + JSON.stringify(text) + \");\"}, noopLastError);\n } catch (e) {}\n }\n })(tabs[i]);\n }\n });\n } catch (e) {}\n }\n\n function handleDevReloadFrame(frame) {\n var kind = frame.reloadType || \"full\";\n var label = typeof frame.label === \"string\" ? frame.label : \"\";\n // The server always builds the label; the kind-derived fallback only\n // covers a malformed frame so the announcement stays truthful.\n var fallback = kind === \"content-scripts\" ? \"content_script\"\n : kind === \"service-worker\" ? \"service_worker\"\n : \"extension\";\n var announced = \"[extension.js] Reloading \" + (label || fallback) + \"\u2026\";\n\n notifyDevtoolsCompanion(\"reloading\", label, kind);\n\n if (kind === \"page\") {\n // Notify-only: rspack-dev-server's livereload refreshes the open\n // surface; reloading the extension here would race it. No tab console\n // line either \u2014 the reloading page clears its own console anyway.\n return;\n }\n\n // Delivery ack for the broker: proves this SW's message pump actually\n // processed the frame \u2014 a successful socket WRITE proves nothing when\n // the worker is wedged-but-connected (bug 27). The broker latches a\n // content-scripts reload until this ack and replays it to the next\n // producer hello, so an edit that landed on a dead pump still converges.\n // Sent on receipt (not after reinjection): the scripting-unavailable\n // fallback restarts the whole extension, and an unacked latch would\n // replay into the fresh SW and restart it a second time.\n send({type: \"reload-ack\", reloadType: kind, label: label});\n\n announceReloadInTabs(announced);\n performDevReload(kind, function () {\n // Only the content-scripts path confirms from here (reinjection ran to\n // completion in this same SW). Full/SW reloads are confirmed by the\n // devtools companion's chrome.management listeners.\n notifyDevtoolsCompanion(\"reloaded\", label, kind);\n });\n }\n\n function sanitize(args) {\n var out = [];\n for (var i = 0; i < args.length; i++) {\n var p = args[i];\n try {\n if (typeof p === \"string\") out.push(p.length > 2048 ? p.slice(0, 2048) + \"...\" : p);\n else if (p instanceof Error) out.push(p.name + \": \" + p.message);\n else out.push(JSON.stringify(p));\n } catch (e) {\n out.push(String(p));\n }\n if (out.join(\" \").length > 8192) break;\n }\n return out;\n }\n\n function flush() {\n while (queue.length && open && socket) {\n var f = queue.shift();\n try { socket.send(JSON.stringify(f)); } catch (e) { break; }\n }\n }\n\n function send(frame) {\n if (open && socket) {\n try { socket.send(JSON.stringify(frame)); return; } catch (e) {}\n }\n if (queue.length < MAX_QUEUE) queue.push(frame);\n }\n\n function schedule() {\n var delay = backoff;\n backoff = Math.min(backoff * 2, MAX_BACKOFF);\n try { setTimeout(connect, delay); } catch (e) {}\n }\n\n function connect() {\n try {\n socket = new WS(\"ws://\" + HOST + \":\" + PORT + \"/extjs-control\");\n } catch (e) {\n schedule();\n return;\n }\n socket.onopen = function () {\n open = true;\n backoff = 250;\n try {\n socket.send(JSON.stringify({type: \"hello\", v: 1, role: \"producer\", instanceId: INSTANCE_ID}));\n } catch (e) {}\n flush();\n };\n socket.onmessage = function (ev) {\n // Slice 2: the broker routes authorized command frames to this SW.\n var frame;\n try { frame = JSON.parse(typeof ev.data === \"string\" ? ev.data : \"\"); } catch (e) { return; }\n if (frame && frame.type === \"command\") {\n try { executeCommand(frame); } catch (e) { replyErr(frame.cmdId, \"ExecutorError\", e); }\n } else if (frame && frame.type === \"reload\") {\n // Dev-loop reload broadcast (see broker.broadcastReload).\n // Fire-and-forget: no result frame is expected.\n try { handleDevReloadFrame(frame); } catch (e) {}\n } else if (frame && frame.type === \"ping\") {\n // Server keepalive: merely RECEIVING this message resets the MV3\n // service worker's ~30s idle timer (Chrome 116+), keeping this SW\n // reachable for reload broadcasts. Nothing to do.\n }\n };\n socket.onclose = function () {\n open = false;\n socket = null;\n schedule();\n };\n socket.onerror = function () {\n try { socket && socket.close(); } catch (e) {}\n };\n }\n\n LEVELS.forEach(function (level) {\n var orig = typeof consoleRef[level] === \"function\"\n ? consoleRef[level].bind(consoleRef)\n : function () {};\n consoleRef[level] = function () {\n try {\n send({\n type: \"log\",\n event: {\n v: 1,\n id: nowId(),\n timestamp: Date.now(),\n level: level,\n context: CONTEXT,\n messageParts: sanitize([].slice.call(arguments)),\n runId: INSTANCE_ID\n }\n });\n } catch (e) {}\n return orig.apply(consoleRef, arguments);\n };\n });\n\n // Multi-context console forwarding: other contexts (content scripts, surface\n // pages) can't reliably open ws://127.0.0.1 (page CSP / connect-src), so they\n // relay console to this SW, which owns the WS. The relay travels a NAMED\n // runtime.Port \u2014 never runtime.sendMessage \u2014 because port traffic is\n // invisible to the extension's own onMessage listeners. With sendMessage, an\n // extension whose SW echoes every message back to its tabs (wild newtube:\n // onMessage \u2192 tabs.sendMessage(msg) to all tabs) turned each relayed log\n // into a new tab message, whose subject-side console.log became another\n // relayed envelope \u2014 a self-sustaining dev-only message storm (25k rows/10min\n // observed) that starved the extension system and wedged Chromium's DevTools\n // endpoint (family B). We enrich with the real tabId/url from the port sender.\n function shipRelayedLog(ev, sender) {\n try {\n send({\n type: \"log\",\n event: {\n v: 1,\n id: nowId(),\n timestamp: Date.now(),\n level: ev.level || \"log\",\n context: ev.context || \"content\",\n messageParts: Array.isArray(ev.messageParts) ? ev.messageParts : [],\n url: ev.url || (sender ? sender.url : undefined),\n tabId: sender && sender.tab ? sender.tab.id : undefined,\n frameId: sender ? sender.frameId : undefined,\n runId: INSTANCE_ID\n }\n });\n } catch (e) {}\n }\n try {\n var rtPort = g.chrome;\n if (rtPort && rtPort.runtime && rtPort.runtime.onConnect) {\n rtPort.runtime.onConnect.addListener(function (port) {\n if (!port || port.name !== \"__extjs-bridge-log__\") return;\n try {\n port.onMessage.addListener(function (msg) {\n if (!msg || !msg.__extjsBridgeLog) return;\n shipRelayedLog(msg.__extjsBridgeLog, port.sender);\n });\n } catch (e) {}\n });\n }\n } catch (e) {}\n // Legacy sendMessage path: kept for any surface still relaying the old way\n // (harmless one-predicate listener; new relays never use it).\n try {\n var rt = g.chrome;\n if (rt && rt.runtime && rt.runtime.onMessage) {\n rt.runtime.onMessage.addListener(function (msg, sender) {\n if (!msg || !msg.__extjsBridgeLog) return;\n shipRelayedLog(msg.__extjsBridgeLog, sender);\n // No async response.\n });\n }\n } catch (e) {}\n\n // Chrome never injects manifest content scripts into tabs that were\n // already open when the extension (re)loads, so after a dev full reload\n // (manifest edit, SW edit, chrome.runtime.reload) every open tab keeps the\n // PREVIOUS build's content-script DOM as a zombie: the nodes still render\n // but their listeners died with the old world, and nothing heals the tab\n // until the next recompile happens to reinject. Fire one producer-driven\n // reinject when this SW generation is a fresh install/reload so open tabs\n // converge to the new build immediately. onInstalled is the gate: it fires\n // for install/update/reload (fresh worlds) but NOT for idle-stop SW wakes,\n // where reinjecting would churn mounted UI for no reason.\n try {\n var rtBoot = g.chrome;\n if (rtBoot && rtBoot.runtime && rtBoot.runtime.onInstalled) {\n rtBoot.runtime.onInstalled.addListener(function () {\n try {\n setTimeout(function () {\n try { reinjectContentScripts(); } catch (e) {}\n }, 250);\n } catch (e) {}\n });\n }\n } catch (e) {}\n\n // runtime.reload() does NOT fire onInstalled (verified Chromium 146), so\n // a dev-driven SW/full reload leaves the listener above silent and every\n // open tab keeps the previous build's content world. fullReload() stamps\n // __extjsDevPendingReinject into storage.local right before reloading;\n // consume it here (fresh flags only \u2014 a stale one from a crashed session\n // is dropped) and heal open tabs from the on-disk manifest. Idle-stop SW\n // wakes never see the flag, so this cannot churn mounted UI.\n try {\n var stBoot = g.chrome;\n if (stBoot && stBoot.storage && stBoot.storage.local) {\n stBoot.storage.local.get(\"__extjsDevPendingReinject\", function (res) {\n noopLastError();\n var ts = res && res.__extjsDevPendingReinject;\n if (ts == null) return;\n try { stBoot.storage.local.remove(\"__extjsDevPendingReinject\", noopLastError); } catch (e) {}\n if (typeof ts !== \"number\" || Date.now() - ts > 30000) return;\n setTimeout(function () {\n try { reinjectContentScripts(); } catch (e) {}\n }, 250);\n });\n }\n } catch (e) {}\n\n connect();\n } catch (e) {}\n})();\n";
|
|
1
|
+
export declare const BRIDGE_PRODUCER_SOURCE = ";(function () {\n try {\n var g = (typeof globalThis === \"object\" && globalThis) ? globalThis : this;\n if (!g || g.__extjsBridgeProducerInstalled) return;\n\n var PORT = parseInt(\"__EXTJS_CONTROL_PORT__\", 10);\n var INSTANCE_ID = \"__EXTJS_INSTANCE_ID__\";\n var CONTEXT = \"__EXTJS_CONTEXT__\";\n // Connectable host of the dev-server control WS. Baked from the resolved\n // connectable host (loopback locally; the public host for remote/devcontainer).\n var HOST = \"__EXTJS_CONTROL_HOST__\";\n if (!HOST || HOST.indexOf(\"__EXTJS\") === 0) HOST = \"127.0.0.1\";\n if (!PORT || PORT < 1) return;\n\n var WS = g.WebSocket;\n if (typeof WS !== \"function\") return;\n\n var consoleRef = g.console || {};\n g.__extjsBridgeProducerInstalled = true;\n\n // Capture extension event listeners at install time. The producer is\n // prepended to the background bundle, so these wraps run BEFORE user code\n // registers its listeners \u2014 letting the bridge replay them on demand. The\n // platform exposes no API to dispatch these events, and CDP attaches too\n // late to wrap addListener, so this is the only path. Replay invokes the\n // handler WITHOUT a user gesture, so the gesture-derived activeTab grant\n // does NOT apply (callers are told gesture:false). This is engine-agnostic:\n // it works on Chromium and Gecko because it only touches addListener.\n //\n // captureEvent transparently wraps addListener/removeListener (delegating to\n // the originals) and records callbacks into the sink array.\n function captureEvent(event, sink) {\n try {\n if (!event || typeof event.addListener !== \"function\") return;\n var origAdd = event.addListener.bind(event);\n event.addListener = function (cb) {\n if (typeof cb === \"function\" && sink.indexOf(cb) === -1) sink.push(cb);\n return origAdd(cb);\n };\n if (typeof event.removeListener === \"function\") {\n var origRemove = event.removeListener.bind(event);\n event.removeListener = function (cb) {\n var i = sink.indexOf(cb);\n if (i !== -1) sink.splice(i, 1);\n return origRemove(cb);\n };\n }\n } catch (e) { /* non-fatal: trigger falls back to its no-listener reply */ }\n }\n\n // Use g.chrome (not the hoisted chrome var below) \u2014 this runs first.\n var actionClickedListeners = [];\n var commandListeners = [];\n if (g.chrome) {\n captureEvent(g.chrome.action && g.chrome.action.onClicked, actionClickedListeners);\n captureEvent(g.chrome.commands && g.chrome.commands.onCommand, commandListeners);\n }\n\n var LEVELS = [\"log\", \"info\", \"warn\", \"error\", \"debug\", \"trace\"];\n var socket = null;\n var open = false;\n var queue = [];\n var backoff = 250;\n var MAX_BACKOFF = 5000;\n var MAX_QUEUE = 1000;\n var MAX_RESULT_BYTES = 256 * 1024;\n\n function nowId() {\n return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);\n }\n\n function engineName() {\n try {\n return (typeof navigator !== \"undefined\" && /firefox/i.test(navigator.userAgent)) ? \"firefox\" : \"chromium\";\n } catch (e) { return \"chromium\"; }\n }\n\n // JSON-safe, byte-capped clone of a result value.\n function safeValue(v) {\n try {\n var s = JSON.stringify(v);\n if (s === undefined) return {value: undefined, truncated: false};\n if (s.length > MAX_RESULT_BYTES) {\n return {value: {__type: \"truncated\", preview: s.slice(0, 1024)}, truncated: true};\n }\n return {value: JSON.parse(s), truncated: false};\n } catch (e) {\n return {value: {__type: \"unserializable\", preview: String(v).slice(0, 512)}, truncated: false};\n }\n }\n\n function replyOk(cmdId, value) {\n var s = safeValue(value);\n var frame = {type: \"result\", cmdId: cmdId, ok: true, value: s.value};\n if (s.truncated) frame.truncated = true;\n try { socket && socket.send(JSON.stringify(frame)); } catch (e) {}\n }\n\n function replyErr(cmdId, name, message) {\n var frame = {type: \"result\", cmdId: cmdId, ok: false, error: {name: name, message: String(message), engine: engineName()}};\n try { socket && socket.send(JSON.stringify(frame)); } catch (e) {}\n }\n\n // Resolve a numeric tab id for content/page targeting when the caller did\n // not pass one: match target.url against open tabs (Chrome match pattern,\n // then a substring fallback), else default to the active tab. cb(tabId, err)\n // \u2014 tabId is null with a human message when nothing matches. (#51)\n function resolveTargetTab(target, cb) {\n var chrome = g.chrome;\n if (!chrome || !chrome.tabs) { cb(null, \"chrome.tabs is not available\"); return; }\n var url = target && target.url;\n if (url) {\n var substringMatch = function () {\n try {\n chrome.tabs.query({}, function (all) {\n var hit = (all || []).filter(function (t) { return t.url && String(t.url).indexOf(url) !== -1; })[0];\n if (hit && typeof hit.id === \"number\") { cb(hit.id, null); return; }\n cb(null, \"no open tab matches url: \" + url);\n });\n } catch (e2) { cb(null, \"tab query failed\"); }\n };\n try {\n // chrome.tabs.query accepts URL match patterns; a bare url/glob may\n // not be a valid pattern, so fall back to a substring scan.\n chrome.tabs.query({url: url}, function (tabs) {\n if (chrome.runtime && chrome.runtime.lastError) { substringMatch(); return; }\n if (tabs && tabs.length && typeof tabs[0].id === \"number\") { cb(tabs[0].id, null); return; }\n substringMatch();\n });\n } catch (e) { substringMatch(); }\n return;\n }\n try {\n chrome.tabs.query({active: true, lastFocusedWindow: true}, function (tabs) {\n var t = tabs && tabs[0];\n if (t && typeof t.id === \"number\") { cb(t.id, null); return; }\n cb(null, \"no active tab to target\");\n });\n } catch (e3) { cb(null, \"tab query failed\"); }\n }\n\n // Execute one authorized command in the SW (or route to a tab). chrome.*\n // promise APIs are used; callback-only APIs are wrapped.\n function executeCommand(cmd) {\n var op = cmd.op, target = cmd.target || {}, args = cmd.args || {};\n var ctx = target.context || \"background\";\n var chrome = g.chrome;\n var cmdId = cmd.cmdId;\n // content/page eval & inspect need a numeric tab id. When the caller gave\n // a --url (or nothing), resolve it to a tab (url match, else the active\n // tab) and re-dispatch with target.tabId filled in. (#51)\n if ((op === \"eval\" || op === \"inspect\") && (ctx === \"content\" || ctx === \"page\") && target.tabId == null) {\n resolveTargetTab(target, function (tabId, err) {\n if (tabId == null) {\n replyErr(cmdId, \"Unsupported\", err || (\"eval/inspect in context \" + ctx + \" needs a --tab id, a --url to match, or an active tab\"));\n return;\n }\n var next = {};\n for (var k in cmd) { if (Object.prototype.hasOwnProperty.call(cmd, k)) next[k] = cmd[k]; }\n next.target = {};\n for (var tk in target) { if (Object.prototype.hasOwnProperty.call(target, tk)) next.target[tk] = target[tk]; }\n next.target.tabId = tabId;\n executeCommand(next);\n });\n return;\n }\n try {\n if (!chrome) { replyErr(cmdId, \"Unsupported\", \"chrome.* not available in this context\"); return; }\n if (op === \"eval\") {\n if (ctx === \"background\") {\n Promise.resolve().then(function () { return (0, eval)(args.expression); })\n .then(function (r) { replyOk(cmdId, r); }, function (e) {\n var msg = (e && e.message) || String(e);\n // MV3 forbids eval of strings in the SW/extension pages; Chrome\n // rejects 'unsafe-eval' in MV3 extension_pages, so this can't be\n // relaxed. Surface it honestly with a usable alternative.\n if (/Content Security Policy|unsafe-eval/i.test(msg)) {\n replyErr(cmdId, \"Unsupported\", \"eval is blocked in the MV3 service worker by CSP. Use --context page --tab <id> (eval runs in the page's MAIN world), or run on an MV2/Firefox build. Engine: \" + engineName());\n } else {\n replyErr(cmdId, (e && e.name) || \"EvalError\", msg);\n }\n });\n } else if ((ctx === \"content\" || ctx === \"page\") && target.tabId && chrome.scripting) {\n chrome.scripting.executeScript({\n target: {tabId: target.tabId},\n world: ctx === \"page\" ? \"MAIN\" : \"ISOLATED\",\n func: function (src) { return eval(src); },\n args: [String(args.expression)]\n }).then(function (res) { replyOk(cmdId, res && res[0] ? res[0].result : undefined); },\n function (e) { replyErr(cmdId, \"EvalError\", e); });\n } else {\n replyErr(cmdId, \"Unsupported\", \"eval in context \" + ctx + \" requires a tabId\");\n }\n return;\n }\n if (op === \"storage.get\" || op === \"storage.set\") {\n // chrome.* storage is callback-only on Gecko (only browser.* is\n // promisified there); some older Chromium builds also predate the\n // promise form. Calling .then() on the callback-only return (undefined)\n // is the \u00A754 TypeError. Prefer the promisified browser.* namespace,\n // then fall back to the callback shape \u2014 the same promise-or-callback\n // duality getActionPopup handles below.\n var storageNS = (g.browser && g.browser.storage) ? g.browser.storage : chrome.storage;\n var storageArea = storageNS && storageNS[args.area || \"local\"];\n if (!storageArea) { replyErr(cmdId, \"StorageError\", \"storage.\" + (args.area || \"local\") + \" unavailable\"); return; }\n var isSet = op === \"storage.set\";\n var callArgs = isSet ? [args.items || {}] : [args.key != null ? args.key : null];\n var onStorageOk = function (r) { replyOk(cmdId, isSet ? {set: Object.keys(args.items || {})} : r); };\n var onStorageErr = function (e) { replyErr(cmdId, \"StorageError\", (e && e.message) || String(e)); };\n var storageFn = isSet ? storageArea.set : storageArea.get;\n try {\n var storageRet = storageFn.apply(storageArea, callArgs);\n if (storageRet && typeof storageRet.then === \"function\") {\n storageRet.then(onStorageOk, onStorageErr);\n return;\n }\n } catch (e) { /* not thenable / threw synchronously: fall back to callback */ }\n try {\n storageFn.apply(storageArea, callArgs.concat([function (r) {\n var le = chrome.runtime && chrome.runtime.lastError;\n if (le) onStorageErr(le); else onStorageOk(r);\n }]));\n } catch (e) { onStorageErr(e); }\n return;\n }\n if (op === \"reload\") {\n if (ctx === \"background\") {\n replyOk(cmdId, {reloading: true});\n setTimeout(function () { try { chrome.runtime.reload(); } catch (e) {} }, 50);\n } else if (target.tabId) {\n chrome.tabs.reload(target.tabId).then(function () { replyOk(cmdId, {reloaded: target.tabId}); }, function (e) { replyErr(cmdId, \"ReloadError\", e); });\n } else {\n replyErr(cmdId, \"Unsupported\", \"reload needs background or a tabId\");\n }\n return;\n }\n if (op === \"open\") {\n var surface = args.surface || ctx;\n // getPopup is promise-style on Gecko and (MV3) Chromium; fall back to\n // callback-style for older Chromium. Always resolves to a string.\n var getActionPopup = function (cb) {\n try {\n var r = chrome.action && chrome.action.getPopup && chrome.action.getPopup({});\n if (r && typeof r.then === \"function\") {\n r.then(function (p) { cb(p || \"\"); }, function () { cb(\"\"); });\n return;\n }\n } catch (e) {}\n try { chrome.action.getPopup({}, function (p) { cb(p || \"\"); }); }\n catch (e) { cb(\"\"); }\n };\n // Resolve the tab a replayed event should carry: an explicit args.tabId,\n // else the active tab of the focused window.\n var resolveActiveTab = function (a, cb) {\n if (a && typeof a.tabId === \"number\") {\n try { chrome.tabs.get(a.tabId, function (t) { cb(t || {id: a.tabId}); }); return; }\n catch (e) {}\n }\n try { chrome.tabs.query({active: true, lastFocusedWindow: true}, function (tabs) { cb((tabs && tabs[0]) || undefined); }); }\n catch (e) { cb(undefined); }\n };\n // Replaying a listener carries no user gesture, so activeTab is never\n // granted. Warn when the manifest declares it (handler will diverge\n // from a real click).\n var activeTabWarning = function () {\n try {\n var m = chrome.runtime.getManifest();\n var perms = (m && m.permissions) || [];\n if (perms.indexOf(\"activeTab\") !== -1) {\n return \"replayed without a user gesture: activeTab is NOT granted, so APIs that depend on it (scripting on the active tab, captureVisibleTab) behave differently than a real click\";\n }\n } catch (e) {}\n return null;\n };\n if (surface === \"popup\") {\n if (chrome.action && chrome.action.openPopup) {\n chrome.action.openPopup().then(function () { replyOk(cmdId, {opened: \"popup\"}); }, function (e) { replyErr(cmdId, \"Unsupported\", \"openPopup: \" + e); });\n } else { replyErr(cmdId, \"Unsupported\", \"action.openPopup not available\"); }\n } else if (surface === \"options\") {\n try { chrome.runtime.openOptionsPage(function () { replyOk(cmdId, {opened: \"options\"}); }); }\n catch (e) { replyErr(cmdId, \"Unsupported\", \"openOptionsPage: \" + e); }\n } else if (surface === \"sidebar\") {\n if (chrome.sidePanel && chrome.sidePanel.open && chrome.windows) {\n chrome.windows.getCurrent(function (w) {\n chrome.sidePanel.open({windowId: w.id}).then(function () { replyOk(cmdId, {opened: \"sidebar\"}); }, function (e) { replyErr(cmdId, \"Unsupported\", \"sidePanel.open: \" + e); });\n });\n } else { replyErr(cmdId, \"Unsupported\", \"sidePanel not available (engine: \" + engineName() + \")\"); }\n } else if (surface === \"action\") {\n // Trigger the toolbar action. With a default_popup, clicking the icon\n // opens it (reuse openPopup). Without a popup, clicking fires\n // chrome.action.onClicked \u2014 we replay the listeners captured at install.\n if (chrome.action) {\n getActionPopup(function (popup) {\n if (popup) {\n try {\n chrome.action.openPopup().then(function () { replyOk(cmdId, {triggered: \"popup\"}); }, function (e) { replyErr(cmdId, \"Unsupported\", \"openPopup: \" + e); });\n } catch (e) { replyErr(cmdId, \"Unsupported\", \"openPopup: \" + e); }\n } else if (actionClickedListeners.length) {\n resolveActiveTab(args, function (tab) {\n var fired = 0;\n for (var i = 0; i < actionClickedListeners.length; i++) {\n try { actionClickedListeners[i](tab); fired++; } catch (e) {}\n }\n var reply = {triggered: \"onClicked\", listeners: fired, gesture: false};\n var warning = activeTabWarning();\n if (warning) reply.warning = warning;\n replyOk(cmdId, reply);\n });\n } else {\n replyErr(cmdId, \"Unsupported\", \"action has no popup and no onClicked listener registered\");\n }\n });\n } else { replyErr(cmdId, \"Unsupported\", \"chrome.action not available (engine: \" + engineName() + \")\"); }\n } else if (surface === \"command\") {\n // Replay a captured chrome.commands.onCommand listener (keyboard\n // shortcut). Same no-gesture caveat as onClicked.\n var commandName = (args && args.name) || undefined;\n if (!commandListeners.length) {\n replyErr(cmdId, \"Unsupported\", \"no chrome.commands.onCommand listener registered\");\n } else {\n resolveActiveTab(args, function (tab) {\n var fired = 0;\n for (var i = 0; i < commandListeners.length; i++) {\n try { commandListeners[i](commandName, tab); fired++; } catch (e) {}\n }\n replyOk(cmdId, {triggered: \"command\", command: commandName || null, listeners: fired, gesture: false});\n });\n }\n } else { replyErr(cmdId, \"BadRequest\", \"unknown surface: \" + surface); }\n return;\n }\n if (op === \"tabs.query\") {\n chrome.tabs.query(args || {}).then(function (tabs) {\n replyOk(cmdId, (tabs || []).map(function (t) { return {id: t.id, url: t.url, title: t.title, active: t.active, windowId: t.windowId}; }));\n }, function (e) { replyErr(cmdId, \"TabsError\", e); });\n return;\n }\n if (op === \"inspect\") {\n // Slice 3 sidecar/default path: extract a DOM snapshot from the target\n // page via chrome.scripting (CDP-free). Closed shadow roots need the\n // --deep-dom CDP escape hatch (separate tool); here we read open ones.\n if ((ctx === \"content\" || ctx === \"page\") && target.tabId && chrome.scripting) {\n var maxBytes = (args && args.maxBytes) || 262144;\n var includeHtml = !args || !args.include || args.include.indexOf(\"html\") !== -1;\n chrome.scripting.executeScript({\n target: {tabId: target.tabId},\n world: ctx === \"page\" ? \"MAIN\" : \"ISOLATED\",\n func: function (wantHtml, cap) {\n function countShadow(root) {\n var n = 0, els = root.querySelectorAll(\"*\");\n for (var i = 0; i < els.length; i++) { if (els[i].shadowRoot) { n++; n += countShadow(els[i].shadowRoot); } }\n return n;\n }\n var doc = document;\n var roots = doc.querySelectorAll('#extension-root,[data-extension-root]');\n var out = {\n url: location.href,\n title: doc.title,\n summary: {\n htmlLength: doc.documentElement.outerHTML.length,\n scriptCount: doc.querySelectorAll(\"script\").length,\n styleCount: doc.querySelectorAll(\"style,link[rel=stylesheet]\").length,\n extensionRootCount: roots.length,\n openShadowRoots: countShadow(doc),\n bodyChildCount: doc.body ? doc.body.children.length : 0\n }\n };\n if (wantHtml) {\n var html = doc.documentElement.outerHTML;\n if (cap > 0 && html.length > cap) { out.html = html.slice(0, cap); out.htmlTruncated = true; }\n else { out.html = html; }\n }\n return out;\n },\n args: [includeHtml, maxBytes]\n }).then(function (res) { replyOk(cmdId, res && res[0] ? res[0].result : undefined); },\n function (e) { replyErr(cmdId, \"InspectError\", e); });\n } else if (ctx === \"popup\" || ctx === \"options\" || ctx === \"sidebar\" || ctx === \"devtools\") {\n // The SW can't read a surface page's DOM; ask the surface's own\n // in-bundle relay (only the open, matching-context page responds).\n chrome.runtime.sendMessage(\n {__extjsInspectRequest: true, target: target, args: args},\n function (resp) {\n if (chrome.runtime.lastError || !resp) {\n replyErr(cmdId, \"Unsupported\", \"surface '\" + ctx + \"' is not open (open it first: extension open \" + ctx + \")\");\n } else if (resp.ok) {\n replyOk(cmdId, resp.value);\n } else {\n replyErr(cmdId, (resp.error && resp.error.name) || \"InspectError\", (resp.error && resp.error.message) || \"inspect failed\");\n }\n }\n );\n } else if (ctx === \"background\") {\n replyErr(cmdId, \"Unsupported\", \"the service worker has no DOM; inspect a content/page (with --tab) or an open surface\");\n } else {\n replyErr(cmdId, \"Unsupported\", \"inspect of \" + ctx + \" requires a tabId (content/page) or an open surface\");\n }\n return;\n }\n replyErr(cmdId, \"BadRequest\", \"unknown op: \" + op);\n } catch (e) {\n replyErr(cmdId, \"ExecutorError\", e);\n }\n }\n\n function noopLastError() { try { void g.chrome.runtime.lastError; } catch (e) {} }\n\n // Only http(s)/file/ftp tabs can run a content script; skip chrome://, the\n // extension's own pages, about:blank, etc.\n function isInjectableUrl(url) {\n return typeof url === \"string\" && /^(https?|file|ftp):/i.test(url);\n }\n\n // Re-inject a single declared content-script entry's fresh files into every\n // open tab it matches, in place. The wrapper's reinject runtime tears down\n // the previous mount (matched by data-extjs-reinject-owner + build hash) and\n // mounts the new one \u2014 so this is the controller-less equivalent of the CDP\n // controller's reinjection, just driven from inside the extension.\n //\n // exclude_matches MUST be honored: the browser's own static injection never\n // touches excluded pages, and extensions can rely on that (e.g. a script\n // whose message handler opens the very page it excludes \u2014 injecting there\n // anyway creates an open-a-tab \u2192 inject \u2192 open-a-tab runaway loop). Chrome's\n // tabs.query has no exclude support, so query the exclude patterns\n // separately and subtract \u2014 the browser stays the single pattern matcher.\n function reinjectContentScriptEntry(entry) {\n var chrome = g.chrome;\n var matches = (entry && entry.matches) || [];\n if (!Array.isArray(matches) || !matches.length) return;\n var excludeMatches = (entry && entry.exclude_matches) || [];\n var jsFiles = ((entry && entry.js) || []).filter(function (f) { return typeof f === \"string\"; });\n var cssFiles = ((entry && entry.css) || []).filter(function (f) { return typeof f === \"string\"; });\n if (!jsFiles.length && !cssFiles.length) return;\n var world = entry.world === \"MAIN\" ? \"MAIN\" : \"ISOLATED\";\n var allFrames = !!entry.all_frames;\n function injectInto(excludedTabIds) {\n try {\n chrome.tabs.query({url: matches}, function (tabs) {\n var err = null;\n try { err = chrome.runtime.lastError; } catch (e) {}\n if (err || !tabs) return;\n for (var i = 0; i < tabs.length; i++) {\n (function (tab) {\n if (!tab || tab.id == null || !isInjectableUrl(tab.url)) return;\n if (excludedTabIds[tab.id]) return;\n var target = {tabId: tab.id, allFrames: allFrames};\n if (cssFiles.length && chrome.scripting.insertCSS) {\n try { chrome.scripting.insertCSS({target: target, files: cssFiles}, noopLastError); } catch (e) {}\n }\n if (jsFiles.length && chrome.scripting.executeScript) {\n try {\n chrome.scripting.executeScript(\n {target: target, files: jsFiles, world: world, injectImmediately: true},\n noopLastError\n );\n } catch (e) {}\n }\n })(tabs[i]);\n }\n });\n } catch (e) {}\n }\n if (Array.isArray(excludeMatches) && excludeMatches.length) {\n try {\n chrome.tabs.query({url: excludeMatches}, function (excludedTabs) {\n try { void chrome.runtime.lastError; } catch (e) {}\n var excluded = {};\n if (excludedTabs) {\n for (var i = 0; i < excludedTabs.length; i++) {\n if (excludedTabs[i] && excludedTabs[i].id != null) excluded[excludedTabs[i].id] = true;\n }\n }\n injectInto(excluded);\n });\n return;\n } catch (e) {}\n }\n injectInto({});\n }\n\n // Re-inject all content scripts into their open tabs. Reads the manifest\n // FROM DISK (not chrome.runtime.getManifest(), which is frozen at extension-\n // registration time) because dev content-script filenames are content-hashed\n // and change on every edit \u2014 disk has the new js/css paths.\n function reinjectContentScripts(onDone) {\n var chrome = g.chrome;\n try {\n if (typeof g.fetch !== \"function\" || !chrome.scripting) return false;\n g.fetch(chrome.runtime.getURL(\"manifest.json\"), {cache: \"no-store\"})\n .then(function (r) { return r.json(); })\n .then(function (manifest) {\n var entries = (manifest && manifest.content_scripts) || [];\n for (var i = 0; i < entries.length; i++) reinjectContentScriptEntry(entries[i]);\n reregisterForFutureNavigations(entries);\n if (onDone) { try { onDone(); } catch (e) {} }\n })\n .catch(function () {});\n return true;\n } catch (e) { return false; }\n }\n\n function mapRunAt(runAt) {\n if (runAt === \"document_start\") return \"document_start\";\n if (runAt === \"document_end\") return \"document_end\";\n return \"document_idle\";\n }\n\n // Re-register the content scripts dynamically (chrome.scripting) pointing at\n // the fresh files, so tabs opened AFTER an edit also get the new build. The\n // static manifest registration still points at the old content-hashed file\n // (it only updates on an extension reload) and keeps firing on new\n // navigations; the dynamic registration injects the new file alongside it,\n // and the wrapper's reinject runtime converges the mount to the newest build.\n // Idempotent: update an existing id, else register it.\n function reregisterForFutureNavigations(entries) {\n var chrome = g.chrome;\n if (\n !chrome.scripting ||\n !chrome.scripting.registerContentScripts ||\n !chrome.scripting.getRegisteredContentScripts\n ) return;\n\n var scripts = [];\n for (var i = 0; i < entries.length; i++) {\n var e = entries[i] || {};\n if (!Array.isArray(e.matches) || !e.matches.length) continue;\n var js = (e.js || []).filter(function (f) { return typeof f === \"string\"; });\n var css = (e.css || []).filter(function (f) { return typeof f === \"string\"; });\n if (!js.length && !css.length) continue;\n var s = {\n id: \"extjs-dev-cs-\" + i,\n matches: e.matches,\n runAt: mapRunAt(e.run_at),\n allFrames: !!e.all_frames,\n world: e.world === \"MAIN\" ? \"MAIN\" : \"ISOLATED\"\n };\n // Dev registration must not be BROADER than the static one: dropping\n // exclude_matches injects into pages the browser itself would skip.\n if (Array.isArray(e.exclude_matches) && e.exclude_matches.length) {\n s.excludeMatches = e.exclude_matches;\n }\n if (js.length) s.js = js;\n if (css.length) s.css = css;\n scripts.push(s);\n }\n if (!scripts.length) return;\n\n try {\n chrome.scripting.getRegisteredContentScripts(function (existing) {\n try { void chrome.runtime.lastError; } catch (e) {}\n var have = {};\n if (existing) for (var k = 0; k < existing.length; k++) have[existing[k].id] = true;\n var toRegister = [], toUpdate = [];\n for (var j = 0; j < scripts.length; j++) {\n (have[scripts[j].id] ? toUpdate : toRegister).push(scripts[j]);\n }\n if (toRegister.length) {\n try { chrome.scripting.registerContentScripts(toRegister, noopLastError); } catch (e) {}\n }\n if (toUpdate.length) {\n try { chrome.scripting.updateContentScripts(toUpdate, noopLastError); } catch (e) {}\n }\n });\n } catch (e) {}\n }\n\n // Perform a dev-loop reload without the CDP controller:\n // - content-scripts: re-inject the fresh script into open matching tabs in\n // place (no extension restart), the controller-less equivalent of CDP\n // reinjection. Needs the scripting permission + host access to the tab \u2014\n // both injected in dev by ApplyDevDefaults. Falls back to a full reload\n // if scripting is unavailable.\n // - service-worker / full / manifest: restart the whole extension so it\n // re-registers from the fresh manifest.\n function performDevReload(type, onDone) {\n var chrome = g.chrome;\n if (!chrome) return;\n\n var fullReload = function () {\n // Leave a pending-reinject flag for the NEXT producer generation:\n // Chrome never re-runs content scripts in tabs that were already open\n // when the extension reloads, and runtime.reload() does NOT fire\n // onInstalled (verified Chromium 146), so the boot-time heal below is\n // the only thing that converges open tabs after a SW/full reload \u2014\n // without it a shared SW+content module edit restarts the SW while\n // every open tab keeps the stale content world (family F).\n try {\n if (chrome.storage && chrome.storage.local) {\n chrome.storage.local.set({__extjsDevPendingReinject: Date.now()}, noopLastError);\n }\n } catch (e) {}\n // Deferred so any in-flight result/log frame \u2014 and the \"Reloading\u2026\"\n // console announcement dispatched into tabs \u2014 flushes before the SW\n // dies. runtime.reload() restarts the whole extension; the devtools\n // companion confirms completion via chrome.management events, so no\n // onDone here.\n try { setTimeout(function () { try { chrome.runtime.reload(); } catch (e) {} }, 150); } catch (e) {}\n };\n\n if (type === \"content-scripts\" && chrome.scripting && chrome.tabs && chrome.tabs.query) {\n if (reinjectContentScripts(onDone)) return;\n }\n\n fullReload();\n }\n\n // ---- Dev-reload announcement surfaces --------------------------------\n // One server-built label travels the ReloadFrame; the producer echoes it\n // 1:1 into (a) the page's devtools console and (b) the bundled\n // extension-js-devtools companion (the bottom-left pill). Both fire HERE,\n // next to the actual reload action, so a surface can only say\n // \"Reloading\u2026\" when a reload is actually being performed.\n\n // Stable IDs of the bundled extension-js-devtools companion: Chromium pins\n // via the manifest \"key\"; Firefox via browser_specific_settings.gecko.id.\n // Absent companion (e.g. --no-browser in the user's own browser) is fine \u2014\n // sendMessage just reports no receiver and we swallow it.\n var DEVTOOLS_COMPANION_ID_CHROMIUM = \"kgdaecdpfkikjncaalnmmnjjfpofkcbl\";\n var DEVTOOLS_COMPANION_ID_FIREFOX = \"devtools@extension.js\";\n\n function notifyDevtoolsCompanion(phase, label, kind) {\n try {\n var chrome = g.chrome;\n if (!chrome || !chrome.runtime || typeof chrome.runtime.sendMessage !== \"function\") return;\n var id = engineName() === \"firefox\" ? DEVTOOLS_COMPANION_ID_FIREFOX : DEVTOOLS_COMPANION_ID_CHROMIUM;\n chrome.runtime.sendMessage(\n id,\n {type: \"extjs-dev-reload-state\", phase: phase, label: label || \"\", kind: kind || \"\", instanceId: INSTANCE_ID},\n function () { noopLastError(); }\n );\n } catch (e) {}\n }\n\n // console.log the reload line into every open injectable tab. Runs in the\n // user extension's ISOLATED world, where the bridge relay patched console \u2014\n // so the same line also lands in the centralized dev-server log stream.\n function announceReloadInTabs(text) {\n var chrome = g.chrome;\n if (!chrome || !chrome.tabs || !chrome.tabs.query) return;\n try {\n chrome.tabs.query({}, function (tabs) {\n noopLastError();\n if (!tabs) return;\n for (var i = 0; i < tabs.length; i++) {\n (function (tab) {\n if (!tab || tab.id == null || !isInjectableUrl(tab.url)) return;\n if (chrome.scripting && chrome.scripting.executeScript) {\n try {\n chrome.scripting.executeScript(\n {target: {tabId: tab.id}, func: function (t) { console.log(t); }, args: [text]},\n noopLastError\n );\n return;\n } catch (e) {}\n }\n // MV2 fallback (Firefox without the scripting API).\n if (chrome.tabs.executeScript) {\n try {\n chrome.tabs.executeScript(tab.id, {code: \"console.log(\" + JSON.stringify(text) + \");\"}, noopLastError);\n } catch (e) {}\n }\n })(tabs[i]);\n }\n });\n } catch (e) {}\n }\n\n function handleDevReloadFrame(frame) {\n var kind = frame.reloadType || \"full\";\n var label = typeof frame.label === \"string\" ? frame.label : \"\";\n // The server always builds the label; the kind-derived fallback only\n // covers a malformed frame so the announcement stays truthful.\n var fallback = kind === \"content-scripts\" ? \"content_script\"\n : kind === \"service-worker\" ? \"service_worker\"\n : \"extension\";\n var announced = \"[extension.js] Reloading \" + (label || fallback) + \"\u2026\";\n\n notifyDevtoolsCompanion(\"reloading\", label, kind);\n\n if (kind === \"page\") {\n // Notify-only: rspack-dev-server's livereload refreshes the open\n // surface; reloading the extension here would race it. No tab console\n // line either \u2014 the reloading page clears its own console anyway.\n return;\n }\n\n // Delivery ack for the broker: proves this SW's message pump actually\n // processed the frame \u2014 a successful socket WRITE proves nothing when\n // the worker is wedged-but-connected (bug 27). The broker latches a\n // content-scripts reload until this ack and replays it to the next\n // producer hello, so an edit that landed on a dead pump still converges.\n // Sent on receipt (not after reinjection): the scripting-unavailable\n // fallback restarts the whole extension, and an unacked latch would\n // replay into the fresh SW and restart it a second time.\n send({type: \"reload-ack\", reloadType: kind, label: label});\n\n announceReloadInTabs(announced);\n performDevReload(kind, function () {\n // Only the content-scripts path confirms from here (reinjection ran to\n // completion in this same SW). Full/SW reloads are confirmed by the\n // devtools companion's chrome.management listeners.\n notifyDevtoolsCompanion(\"reloaded\", label, kind);\n });\n }\n\n function sanitize(args) {\n var out = [];\n for (var i = 0; i < args.length; i++) {\n var p = args[i];\n try {\n if (typeof p === \"string\") out.push(p.length > 2048 ? p.slice(0, 2048) + \"...\" : p);\n else if (p instanceof Error) out.push(p.name + \": \" + p.message);\n else out.push(JSON.stringify(p));\n } catch (e) {\n out.push(String(p));\n }\n if (out.join(\" \").length > 8192) break;\n }\n return out;\n }\n\n function flush() {\n while (queue.length && open && socket) {\n var f = queue.shift();\n try { socket.send(JSON.stringify(f)); } catch (e) { break; }\n }\n }\n\n function send(frame) {\n if (open && socket) {\n try { socket.send(JSON.stringify(frame)); return; } catch (e) {}\n }\n if (queue.length < MAX_QUEUE) queue.push(frame);\n }\n\n function schedule() {\n var delay = backoff;\n backoff = Math.min(backoff * 2, MAX_BACKOFF);\n try { setTimeout(connect, delay); } catch (e) {}\n }\n\n function connect() {\n try {\n socket = new WS(\"ws://\" + HOST + \":\" + PORT + \"/extjs-control\");\n } catch (e) {\n schedule();\n return;\n }\n socket.onopen = function () {\n open = true;\n backoff = 250;\n try {\n socket.send(JSON.stringify({type: \"hello\", v: 1, role: \"producer\", instanceId: INSTANCE_ID}));\n } catch (e) {}\n flush();\n };\n socket.onmessage = function (ev) {\n // Slice 2: the broker routes authorized command frames to this SW.\n var frame;\n try { frame = JSON.parse(typeof ev.data === \"string\" ? ev.data : \"\"); } catch (e) { return; }\n if (frame && frame.type === \"command\") {\n try { executeCommand(frame); } catch (e) { replyErr(frame.cmdId, \"ExecutorError\", e); }\n } else if (frame && frame.type === \"reload\") {\n // Dev-loop reload broadcast (see broker.broadcastReload).\n // Fire-and-forget: no result frame is expected.\n try { handleDevReloadFrame(frame); } catch (e) {}\n } else if (frame && frame.type === \"ping\") {\n // Server keepalive: merely RECEIVING this message resets the MV3\n // service worker's ~30s idle timer (Chrome 116+), keeping this SW\n // reachable for reload broadcasts. Nothing to do.\n }\n };\n socket.onclose = function () {\n open = false;\n socket = null;\n schedule();\n };\n socket.onerror = function () {\n try { socket && socket.close(); } catch (e) {}\n };\n }\n\n // \u00A755: uncaught errors and unhandled rejections don't route through the\n // patched console (the browser reports them internally), so background/SW\n // crashes never reach `extension logs`. The global handlers below forward\n // them as level:\"error\"; these signatures let us skip a throw already shipped\n // via console.error (user code that logs THEN rethrows the same Error).\n var recentErrorSigs = [];\n function errorSig(message, stack) {\n return String(message == null ? \"\" : message) + \"::\" + String(stack == null ? \"\" : stack).slice(0, 200);\n }\n function noteErrorSig(sig) {\n recentErrorSigs.push({sig: sig, t: Date.now()});\n if (recentErrorSigs.length > 50) recentErrorSigs.shift();\n }\n function errorSigSeenRecently(sig) {\n var now = Date.now();\n for (var i = recentErrorSigs.length - 1; i >= 0; i--) {\n if (now - recentErrorSigs[i].t > 3000) { recentErrorSigs.splice(i, 1); continue; }\n if (recentErrorSigs[i].sig === sig) return true;\n }\n return false;\n }\n\n LEVELS.forEach(function (level) {\n var orig = typeof consoleRef[level] === \"function\"\n ? consoleRef[level].bind(consoleRef)\n : function () {};\n consoleRef[level] = function () {\n try {\n if (level === \"error\") {\n for (var ai = 0; ai < arguments.length; ai++) {\n if (arguments[ai] instanceof Error) noteErrorSig(errorSig(arguments[ai].message, arguments[ai].stack));\n }\n }\n send({\n type: \"log\",\n event: {\n v: 1,\n id: nowId(),\n timestamp: Date.now(),\n level: level,\n context: CONTEXT,\n messageParts: sanitize([].slice.call(arguments)),\n runId: INSTANCE_ID\n }\n });\n } catch (e) {}\n return orig.apply(consoleRef, arguments);\n };\n });\n\n // Forward uncaught throws / unhandled rejections as level:\"error\" so a silent\n // background crash is visible in `extension logs` (\u00A755). Deduped against the\n // console.error signatures above so a logged-then-rethrown Error emits once.\n function shipUncaughtError(message, stack, url) {\n try {\n var sig = errorSig(message, stack);\n if (errorSigSeenRecently(sig)) return;\n noteErrorSig(sig);\n send({\n type: \"log\",\n event: {\n v: 1,\n id: nowId(),\n timestamp: Date.now(),\n level: \"error\",\n context: CONTEXT,\n messageParts: sanitize([String(message) + (stack ? \"\\n\" + stack : \"\")]),\n url: url,\n runId: INSTANCE_ID\n }\n });\n } catch (e) {}\n }\n try {\n if (typeof g.addEventListener === \"function\") {\n g.addEventListener(\"error\", function (ev) {\n var err = ev && ev.error;\n var message = (err && err.message) || (ev && ev.message) || \"Uncaught error\";\n shipUncaughtError(message, err && err.stack, ev && ev.filename);\n });\n g.addEventListener(\"unhandledrejection\", function (ev) {\n var reason = ev && ev.reason;\n var message = (reason && reason.message) || (reason != null ? String(reason) : \"Unhandled rejection\");\n shipUncaughtError(\"Unhandled promise rejection: \" + message, reason && reason.stack, undefined);\n });\n }\n } catch (e) {}\n\n // Multi-context console forwarding: other contexts (content scripts, surface\n // pages) can't reliably open ws://127.0.0.1 (page CSP / connect-src), so they\n // relay console to this SW, which owns the WS. The relay travels a NAMED\n // runtime.Port \u2014 never runtime.sendMessage \u2014 because port traffic is\n // invisible to the extension's own onMessage listeners. With sendMessage, an\n // extension whose SW echoes every message back to its tabs (wild newtube:\n // onMessage \u2192 tabs.sendMessage(msg) to all tabs) turned each relayed log\n // into a new tab message, whose subject-side console.log became another\n // relayed envelope \u2014 a self-sustaining dev-only message storm (25k rows/10min\n // observed) that starved the extension system and wedged Chromium's DevTools\n // endpoint (family B). We enrich with the real tabId/url from the port sender.\n function shipRelayedLog(ev, sender) {\n try {\n send({\n type: \"log\",\n event: {\n v: 1,\n id: nowId(),\n timestamp: Date.now(),\n level: ev.level || \"log\",\n context: ev.context || \"content\",\n messageParts: Array.isArray(ev.messageParts) ? ev.messageParts : [],\n url: ev.url || (sender ? sender.url : undefined),\n tabId: sender && sender.tab ? sender.tab.id : undefined,\n frameId: sender ? sender.frameId : undefined,\n runId: INSTANCE_ID\n }\n });\n } catch (e) {}\n }\n try {\n var rtPort = g.chrome;\n if (rtPort && rtPort.runtime && rtPort.runtime.onConnect) {\n rtPort.runtime.onConnect.addListener(function (port) {\n if (!port || port.name !== \"__extjs-bridge-log__\") return;\n try {\n port.onMessage.addListener(function (msg) {\n if (!msg || !msg.__extjsBridgeLog) return;\n shipRelayedLog(msg.__extjsBridgeLog, port.sender);\n });\n } catch (e) {}\n });\n }\n } catch (e) {}\n // Legacy sendMessage path: kept for any surface still relaying the old way\n // (harmless one-predicate listener; new relays never use it).\n try {\n var rt = g.chrome;\n if (rt && rt.runtime && rt.runtime.onMessage) {\n rt.runtime.onMessage.addListener(function (msg, sender) {\n if (!msg || !msg.__extjsBridgeLog) return;\n shipRelayedLog(msg.__extjsBridgeLog, sender);\n // No async response.\n });\n }\n } catch (e) {}\n\n // Chrome never injects manifest content scripts into tabs that were\n // already open when the extension (re)loads, so after a dev full reload\n // (manifest edit, SW edit, chrome.runtime.reload) every open tab keeps the\n // PREVIOUS build's content-script DOM as a zombie: the nodes still render\n // but their listeners died with the old world, and nothing heals the tab\n // until the next recompile happens to reinject. Fire one producer-driven\n // reinject when this SW generation is a fresh install/reload so open tabs\n // converge to the new build immediately. onInstalled is the gate: it fires\n // for install/update/reload (fresh worlds) but NOT for idle-stop SW wakes,\n // where reinjecting would churn mounted UI for no reason.\n try {\n var rtBoot = g.chrome;\n if (rtBoot && rtBoot.runtime && rtBoot.runtime.onInstalled) {\n rtBoot.runtime.onInstalled.addListener(function () {\n try {\n setTimeout(function () {\n try { reinjectContentScripts(); } catch (e) {}\n }, 250);\n } catch (e) {}\n });\n }\n } catch (e) {}\n\n // runtime.reload() does NOT fire onInstalled (verified Chromium 146), so\n // a dev-driven SW/full reload leaves the listener above silent and every\n // open tab keeps the previous build's content world. fullReload() stamps\n // __extjsDevPendingReinject into storage.local right before reloading;\n // consume it here (fresh flags only \u2014 a stale one from a crashed session\n // is dropped) and heal open tabs from the on-disk manifest. Idle-stop SW\n // wakes never see the flag, so this cannot churn mounted UI.\n try {\n var stBoot = g.chrome;\n if (stBoot && stBoot.storage && stBoot.storage.local) {\n stBoot.storage.local.get(\"__extjsDevPendingReinject\", function (res) {\n noopLastError();\n var ts = res && res.__extjsDevPendingReinject;\n if (ts == null) return;\n try { stBoot.storage.local.remove(\"__extjsDevPendingReinject\", noopLastError); } catch (e) {}\n if (typeof ts !== \"number\" || Date.now() - ts > 30000) return;\n setTimeout(function () {\n try { reinjectContentScripts(); } catch (e) {}\n }, 250);\n });\n }\n } catch (e) {}\n\n connect();\n } catch (e) {}\n})();\n";
|
|
2
2
|
/**
|
|
3
3
|
* Lightweight RELAY for non-SW contexts (content scripts, surface pages). It
|
|
4
4
|
* patches console and forwards each call to the background SW over a NAMED
|
|
@@ -9,7 +9,7 @@ export declare const BRIDGE_PRODUCER_SOURCE = ";(function () {\n try {\n var
|
|
|
9
9
|
* listeners never see relay traffic: an SW that echoes messages back to tabs
|
|
10
10
|
* otherwise turns the relay into an infinite dev-only message storm (family B).
|
|
11
11
|
*/
|
|
12
|
-
export declare const BRIDGE_RELAY_SOURCE = ";(function () {\n try {\n var g = (typeof globalThis === \"object\" && globalThis) ? globalThis : this;\n if (!g || g.__extjsBridgeRelayInstalled) return;\n\n var CONTEXT = \"__EXTJS_CONTEXT__\";\n var chrome = g.chrome;\n if (!chrome || !chrome.runtime) return;\n var canRelay = typeof chrome.runtime.connect === \"function\";\n\n g.__extjsBridgeRelayInstalled = true;\n var consoleRef = g.console || {};\n var LEVELS = [\"log\", \"info\", \"warn\", \"error\", \"debug\", \"trace\"];\n\n function sanitize(args) {\n var out = [];\n for (var i = 0; i < args.length; i++) {\n var p = args[i];\n try {\n if (typeof p === \"string\") out.push(p.length > 2048 ? p.slice(0, 2048) + \"...\" : p);\n else if (p instanceof Error) out.push(p.name + \": \" + p.message);\n else out.push(JSON.stringify(p));\n } catch (e) { out.push(String(p)); }\n if (out.join(\" \").length > 8192) break;\n }\n return out;\n }\n\n function here() { try { return g.location ? g.location.href : undefined; } catch (e) { return undefined; } }\n\n // Lazy named port to the SW producer. Connecting wakes an idle SW like\n // sendMessage does, but port frames never reach the extension's own\n // onMessage listeners (see the loop note above). Reconnect on demand: the\n // SW idling out (or reloading) disconnects the port; the next log redials.\n var logPort = null;\n function getLogPort() {\n if (!canRelay) return null;\n if (logPort) return logPort;\n try {\n logPort = chrome.runtime.connect({name: \"__extjs-bridge-log__\"});\n logPort.onDisconnect.addListener(function () {\n try { void chrome.runtime.lastError; } catch (e) {}\n logPort = null;\n });\n } catch (e) { logPort = null; }\n return logPort;\n }\n\n LEVELS.forEach(function (level) {\n var orig = typeof consoleRef[level] === \"function\" ? consoleRef[level].bind(consoleRef) : function () {};\n consoleRef[level] = function () {\n try {\n var
|
|
12
|
+
export declare const BRIDGE_RELAY_SOURCE = ";(function () {\n try {\n var g = (typeof globalThis === \"object\" && globalThis) ? globalThis : this;\n if (!g || g.__extjsBridgeRelayInstalled) return;\n\n var CONTEXT = \"__EXTJS_CONTEXT__\";\n var chrome = g.chrome;\n if (!chrome || !chrome.runtime) return;\n var canRelay = typeof chrome.runtime.connect === \"function\";\n\n g.__extjsBridgeRelayInstalled = true;\n var consoleRef = g.console || {};\n var LEVELS = [\"log\", \"info\", \"warn\", \"error\", \"debug\", \"trace\"];\n\n function sanitize(args) {\n var out = [];\n for (var i = 0; i < args.length; i++) {\n var p = args[i];\n try {\n if (typeof p === \"string\") out.push(p.length > 2048 ? p.slice(0, 2048) + \"...\" : p);\n else if (p instanceof Error) out.push(p.name + \": \" + p.message);\n else out.push(JSON.stringify(p));\n } catch (e) { out.push(String(p)); }\n if (out.join(\" \").length > 8192) break;\n }\n return out;\n }\n\n function here() { try { return g.location ? g.location.href : undefined; } catch (e) { return undefined; } }\n\n // Lazy named port to the SW producer. Connecting wakes an idle SW like\n // sendMessage does, but port frames never reach the extension's own\n // onMessage listeners (see the loop note above). Reconnect on demand: the\n // SW idling out (or reloading) disconnects the port; the next log redials.\n var logPort = null;\n function getLogPort() {\n if (!canRelay) return null;\n if (logPort) return logPort;\n try {\n logPort = chrome.runtime.connect({name: \"__extjs-bridge-log__\"});\n logPort.onDisconnect.addListener(function () {\n try { void chrome.runtime.lastError; } catch (e) {}\n logPort = null;\n });\n } catch (e) { logPort = null; }\n return logPort;\n }\n\n // Relay one log payload to the SW producer over the named port, redialing\n // once if the port went stale (SW restarted). Shared by the console patch\n // and the uncaught-error handlers below.\n function postLog(payload) {\n var p = getLogPort();\n if (!p) return;\n try {\n p.postMessage({__extjsBridgeLog: payload});\n } catch (e) {\n logPort = null;\n var p2 = getLogPort();\n if (p2) { try { p2.postMessage({__extjsBridgeLog: payload}); } catch (e2) {} }\n }\n }\n\n // \u00A755: uncaught throws / unhandled rejections in a page or content script\n // never route through the patched console, so they'd be invisible in\n // `extension logs`. Track console.error Error signatures to dedupe them.\n var recentErrorSigs = [];\n function errorSig(message, stack) {\n return String(message == null ? \"\" : message) + \"::\" + String(stack == null ? \"\" : stack).slice(0, 200);\n }\n function noteErrorSig(sig) {\n recentErrorSigs.push({sig: sig, t: Date.now()});\n if (recentErrorSigs.length > 50) recentErrorSigs.shift();\n }\n function errorSigSeenRecently(sig) {\n var now = Date.now();\n for (var i = recentErrorSigs.length - 1; i >= 0; i--) {\n if (now - recentErrorSigs[i].t > 3000) { recentErrorSigs.splice(i, 1); continue; }\n if (recentErrorSigs[i].sig === sig) return true;\n }\n return false;\n }\n\n LEVELS.forEach(function (level) {\n var orig = typeof consoleRef[level] === \"function\" ? consoleRef[level].bind(consoleRef) : function () {};\n consoleRef[level] = function () {\n try {\n if (level === \"error\") {\n for (var ai = 0; ai < arguments.length; ai++) {\n if (arguments[ai] instanceof Error) noteErrorSig(errorSig(arguments[ai].message, arguments[ai].stack));\n }\n }\n postLog({level: level, context: CONTEXT, messageParts: sanitize([].slice.call(arguments)), url: here()});\n } catch (e) {}\n return orig.apply(consoleRef, arguments);\n };\n });\n\n function shipUncaughtError(message, stack, url) {\n try {\n var sig = errorSig(message, stack);\n if (errorSigSeenRecently(sig)) return;\n noteErrorSig(sig);\n postLog({level: \"error\", context: CONTEXT, messageParts: sanitize([String(message) + (stack ? \"\\n\" + stack : \"\")]), url: url || here()});\n } catch (e) {}\n }\n try {\n if (typeof g.addEventListener === \"function\") {\n g.addEventListener(\"error\", function (ev) {\n var err = ev && ev.error;\n var message = (err && err.message) || (ev && ev.message) || \"Uncaught error\";\n shipUncaughtError(message, err && err.stack, (ev && ev.filename) || undefined);\n });\n g.addEventListener(\"unhandledrejection\", function (ev) {\n var reason = ev && ev.reason;\n var message = (reason && reason.message) || (reason != null ? String(reason) : \"Unhandled rejection\");\n shipUncaughtError(\"Unhandled promise rejection: \" + message, reason && reason.stack, undefined);\n });\n }\n } catch (e) {}\n\n // Surface DOM inspection: the SW executor can't reach popup/options/sidebar\n // DOM (separate extension pages), and the sidecar can't either (cross-extension\n // isolation). So THIS page \u2014 the user extension's own surface \u2014 answers an\n // inspect request for its own context. The SW broadcasts the request; only the\n // matching-context surface responds with its DOM snapshot.\n try {\n if (chrome.runtime && chrome.runtime.onMessage) {\n chrome.runtime.onMessage.addListener(function (msg, _sender, sendResponse) {\n if (!msg || !msg.__extjsInspectRequest) return;\n if (!msg.target || msg.target.context !== CONTEXT) return; // not for me\n try {\n var args = msg.args || {};\n var wantHtml = !args.include || args.include.indexOf(\"html\") !== -1;\n var cap = args.maxBytes || 262144;\n function countShadow(root) {\n var n = 0, els = root.querySelectorAll(\"*\");\n for (var i = 0; i < els.length; i++) { if (els[i].shadowRoot) { n++; n += countShadow(els[i].shadowRoot); } }\n return n;\n }\n var doc = g.document;\n var roots = doc.querySelectorAll('#extension-root,[data-extension-root]');\n var snap = {\n context: CONTEXT,\n url: here(),\n title: doc.title,\n summary: {\n htmlLength: doc.documentElement.outerHTML.length,\n scriptCount: doc.querySelectorAll(\"script\").length,\n styleCount: doc.querySelectorAll(\"style,link[rel=stylesheet]\").length,\n extensionRootCount: roots.length,\n openShadowRoots: countShadow(doc),\n bodyChildCount: doc.body ? doc.body.children.length : 0\n }\n };\n if (wantHtml) {\n var html = doc.documentElement.outerHTML;\n if (cap > 0 && html.length > cap) { snap.html = html.slice(0, cap); snap.htmlTruncated = true; }\n else { snap.html = html; }\n }\n sendResponse({ok: true, value: snap});\n } catch (e) {\n sendResponse({ok: false, error: {name: \"InspectError\", message: String(e)}});\n }\n return true; // responded\n });\n }\n } catch (e) {}\n } catch (e) {}\n})();\n";
|
|
13
13
|
export interface BuildRelayOptions {
|
|
14
14
|
context: string;
|
|
15
15
|
}
|