extension-develop 4.1.24 → 4.1.25

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/832~0.mjs CHANGED
@@ -19,7 +19,7 @@ import * as __rspack_external_node_fs_1b05aee1 from "node:fs";
19
19
  import * as __rspack_external_node_os_4f3c9d58 from "node:os";
20
20
  import * as __rspack_external_node_path_806ed179 from "node:path";
21
21
  import * as __rspack_external_node_vm_ef240610 from "node:vm";
22
- var package_namespaceObject = /*#__PURE__*/ JSON.parse('{"rE":"4.1.24","El":{"@prefresh/core":"1.5.9","@prefresh/utils":"1.2.1","@rspack/core":"2.2.3","@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","acorn":"^8.16.0","browser-extension-manifest-fields":"^3.0.1","case-sensitive-paths-webpack-plugin":"^2.4.0","content-security-policy-parser":"^0.6.0","cross-spawn":"^7.0.6","dotenv":"^17.2.3","es-module-lexer":"^3.0.2","extension-from-store":"^0.2.5","fflate":"^0.8.3","go-git-it":"^5.1.5","ignore":"^7.0.5","less":"4.9.1","less-loader":"13.0.0","parse5-utilities":"^1.0.0","pintor":"0.3.0","postcss":"8.5.23","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","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"}}');
22
+ var package_namespaceObject = /*#__PURE__*/ JSON.parse('{"rE":"4.1.25","El":{"@prefresh/core":"1.5.9","@prefresh/utils":"1.2.1","@rspack/core":"2.2.3","@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","acorn":"^8.16.0","browser-extension-manifest-fields":"^3.0.1","case-sensitive-paths-webpack-plugin":"^2.4.0","content-security-policy-parser":"^0.6.0","cross-spawn":"^7.0.6","dotenv":"^17.2.3","es-module-lexer":"^3.0.2","extension-from-store":"^0.2.5","fflate":"^0.8.3","go-git-it":"^5.1.5","ignore":"^7.0.5","less":"4.9.1","less-loader":"13.0.0","parse5-utilities":"^1.0.0","pintor":"0.3.0","postcss":"8.5.23","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","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"}}');
23
23
  const preloadedEnvKeys = new Set();
24
24
  function getPreloadedEnvKeys() {
25
25
  return preloadedEnvKeys;
package/dist/840~0.mjs CHANGED
@@ -2841,6 +2841,9 @@ function emittedScriptFiles(chunk, name) {
2841
2841
  `${name}.js`
2842
2842
  ];
2843
2843
  }
2844
+ function isHtmlSource(rel) {
2845
+ return /\.html?$/i.test(rel);
2846
+ }
2844
2847
  function changedScriptFilesFor(changedSources, index) {
2845
2848
  const out = new Set();
2846
2849
  for (const rel of changedSources){
@@ -2890,6 +2893,10 @@ function classifyReloadFromSources(opts) {
2890
2893
  pageChanged.push(rel);
2891
2894
  known = true;
2892
2895
  }
2896
+ if (!known && isHtmlSource(rel)) {
2897
+ pageChanged.push(rel);
2898
+ known = true;
2899
+ }
2893
2900
  if (!known) unknown.push(rel);
2894
2901
  }
2895
2902
  const publicRoots = index?.publicRoots;
@@ -1,5 +1,5 @@
1
1
  export declare const CONTROL_PORT_ASSET_NAME = "extension-js-control.json";
2
- 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: these wraps run\n // BEFORE user code, and no platform API exists to dispatch these events.\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 the raw globals (not the hoisted chrome var below). This runs first.\n // On Gecko browser.* is a DISTINCT wrapper object: hook BOTH namespaces.\n var actionClickedListeners = [];\n var commandListeners = [];\n var capturedEvents = [];\n function captureEventOnce(event, sink) {\n // Polyfilled extensions expose ONE event object on both namespaces:\n // wrap it once, and the sink dedupes callbacks shared across wrappers.\n if (!event || capturedEvents.indexOf(event) !== -1) return;\n capturedEvents.push(event);\n captureEvent(event, sink);\n }\n var captureNamespaces = [g.chrome, g.browser];\n for (var cn = 0; cn < captureNamespaces.length; cn++) {\n var capNS = captureNamespaces[cn];\n if (!capNS) continue;\n captureEventOnce(capNS.action && capNS.action.onClicked, actionClickedListeners);\n captureEventOnce(capNS.commands && capNS.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 connectFailures = 0;\n var MAX_QUEUE = 1000;\n var MAX_RESULT_BYTES = 256 * 1024;\n // Where error frames are parked so a context that dies before its socket\n // opens does not take them along. Capped, and old entries are dropped.\n var PENDING_ERRORS_KEY = \"__extjsPendingErrors\";\n var MAX_PENDING_ERRORS = 20;\n var PENDING_ERROR_TTL = 300000;\n\n function nowId() {\n return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);\n }\n\n // The extension's own URL scheme names the engine outright, which beats a\n // user agent where Chrome and Safari both say \"Safari\".\n function engineName() {\n try {\n var scheme = (typeof location !== \"undefined\" && location.protocol) || \"\";\n if (scheme.indexOf(\"safari-web-extension\") === 0) return \"webkit\";\n if (scheme.indexOf(\"moz-extension\") === 0) return \"firefox\";\n if (scheme.indexOf(\"chrome-extension\") === 0) return \"chromium\";\n var ua = (typeof navigator !== \"undefined\" && navigator.userAgent) || \"\";\n if (/firefox/i.test(ua)) return \"firefox\";\n if (/safari/i.test(ua) && !/chrome|chromium|edg/i.test(ua)) return \"webkit\";\n return \"chromium\";\n } catch (e) { return \"chromium\"; }\n }\n\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 // Ignore\n }\n }\n\n function replyErr(cmdId, name, message, code) {\n var frame = {type: \"result\", cmdId: cmdId, ok: false, error: {name: name, message: String(message), engine: engineName()}};\n if (code) frame.error.code = code;\n try { socket && socket.send(JSON.stringify(frame)); } catch (e) {\n // Ignore\n }\n }\n\n // Name the refusal the browser wrote as prose, so a consumer branches on\n // error.code instead of matching the engine's sentence.\n function openRefusalCode(message) {\n var m = String(message == null ? \"\" : message);\n if (/user gesture|user input|user interaction|user action/i.test(m)) return \"needs_user_gesture\";\n if (/browser window|active window/i.test(m)) return \"needs_headed_window\";\n return undefined;\n }\n\n // Resolve a numeric tab id when the caller did not pass one: match\n // target.url against open tabs, else default to the active tab. (#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 // chrome.* async APIs are callback-only on Gecko. Prefer promisified\n // browser.* when present, else the chrome.* callback form. cb(err, result).\n function nsCall(nsName, method, callArgs, cb) {\n var chromeG = g.chrome;\n var done = false;\n var once = function (err, r) { if (!done) { done = true; cb(err, r); } };\n var viaPromise = function (ns) {\n try {\n var ret = ns[method].apply(ns, callArgs);\n if (ret && typeof ret.then === \"function\") {\n ret.then(function (r) { once(null, r); }, function (e) { once(e); });\n return true;\n }\n } catch (e) { once(e); return true; }\n return false;\n };\n var bNS = g.browser && g.browser[nsName];\n if (bNS && typeof bNS[method] === \"function\" && viaPromise(bNS)) return;\n var cNS = chromeG && chromeG[nsName];\n if (!cNS || typeof cNS[method] !== \"function\") {\n once(new Error(\"chrome.\" + nsName + \".\" + method + \" is not available (engine: \" + engineName() + \")\"));\n return;\n }\n try {\n var ret2 = cNS[method].apply(cNS, callArgs.concat([function (r) {\n var le = chromeG.runtime && chromeG.runtime.lastError;\n if (le) once(le); else once(null, r);\n }]));\n // A promise-only implementation ignores the trailing callback and\n // returns a thenable instead; resolve through it (once() dedupes).\n if (ret2 && typeof ret2.then === \"function\") {\n ret2.then(function (r) { once(null, r); }, function (e) { once(e); });\n }\n } catch (e2) {\n if (!viaPromise(cNS)) once(e2);\n }\n }\n\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: resolve --url (or\n // nothing) to a 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 and\n // rejects 'unsafe-eval'. Surface it honestly with an alternative.\n if (/Content Security Policy|unsafe-eval/i.test(msg)) {\n replyErr(cmdId, \"Unsupported\", \"eval is blocked in the extension background by CSP, which allows no unsafe-eval. Use --context page --tab <id> (eval runs in the page's MAIN world), or run on a build whose CSP permits it. 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 // Wrap the eval so a throw INSIDE the tab comes back as data:\n // Chrome swallows an injected function's exception (null result).\n nsCall(\"scripting\", \"executeScript\", [{\n target: {tabId: target.tabId},\n world: ctx === \"page\" ? \"MAIN\" : \"ISOLATED\",\n func: function (src) {\n try { return {__extjsEval: 1, ok: true, value: eval(src)}; }\n catch (e) { return {__extjsEval: 1, ok: false, name: (e && e.name) || \"EvalError\", message: (e && e.message) || String(e)}; }\n },\n args: [String(args.expression)]\n }], function (err, res) {\n if (err) { replyErr(cmdId, \"TargetNotFound\", (err && err.message) || err); return; }\n var frame = res && res[0] ? res[0].result : undefined;\n if (!frame || frame.__extjsEval !== 1) {\n replyErr(cmdId, \"TargetNotFound\", \"the expression never executed in tab \" + target.tabId + \": no injectable frame returned a result (restricted page, or outside host_permissions)\");\n return;\n }\n if (frame.ok) { replyOk(cmdId, frame.value); return; }\n if (ctx === \"content\" && /Content Security Policy|unsafe-eval|EvalError/i.test(String(frame.name) + \" \" + String(frame.message))) {\n replyErr(cmdId, \"Unsupported\", \"eval of a string is blocked in the ISOLATED (content) world by the extension CSP. Use --context page (runs in the page's MAIN world). Original: \" + frame.message);\n return;\n }\n replyErr(cmdId, frame.name || \"EvalError\", frame.message);\n });\n } else if (ctx === \"popup\" || ctx === \"options\" || ctx === \"sidebar\" || ctx === \"devtools\" || ctx === \"newtab\" || ctx === \"history\" || ctx === \"bookmarks\") {\n // The SW can't eval in another extension page; ask the surface's\n // own in-bundle relay (only the open, matching-context page responds).\n chrome.runtime.sendMessage(\n {__extjsEvalRequest: true, target: target, args: {expression: String(args.expression)}},\n function (resp) {\n if ((chrome.runtime && chrome.runtime.lastError) || !resp) {\n replyErr(cmdId, \"Unsupported\", \"surface '\" + ctx + \"' is not open (open it first: extension open \" + ctx + \")\", \"surface_not_open\");\n } else if (resp.ok) {\n replyOk(cmdId, resp.value);\n } else {\n replyErr(cmdId, (resp.error && resp.error.name) || \"EvalError\", (resp.error && resp.error.message) || \"eval failed\");\n }\n }\n );\n } else if (ctx === \"content\" || ctx === \"page\") {\n replyErr(cmdId, \"Unsupported\", target.tabId\n ? \"chrome.scripting is not available on this engine (MV2 has no scripting API); use --context background\"\n : \"eval in context \" + ctx + \" requires a tabId\");\n } else {\n replyErr(cmdId, \"BadRequest\", \"unknown eval context: \" + ctx);\n }\n return;\n }\n if (op === \"storage.get\" || op === \"storage.set\") {\n // chrome.* storage is callback-only on Gecko and older Chromium.\n // Prefer promisified browser.*, then fall back to the callback shape.\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) {\n // Ignore\n } }, 50);\n } else if (target.tabId) {\n nsCall(\"tabs\", \"reload\", [target.tabId], function (err) {\n if (err) replyErr(cmdId, \"ReloadError\", (err && err.message) || err);\n else replyOk(cmdId, {reloaded: target.tabId});\n });\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 // Ignore\n }\n try { chrome.action.getPopup({}, function (p) { cb(p || \"\"); }); }\n catch (e) { cb(\"\"); }\n };\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 // Ignore\n }\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.\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 // Ignore\n }\n return null;\n };\n if (surface === \"popup\") {\n if (chrome.action && chrome.action.openPopup) {\n nsCall(\"action\", \"openPopup\", [], function (err) {\n if (err) {\n var openMsg = (err && err.message) || err;\n replyErr(cmdId, \"Unsupported\", \"openPopup: \" + openMsg, openRefusalCode(openMsg));\n } else replyOk(cmdId, {opened: \"popup\"});\n });\n } else { replyErr(cmdId, \"Unsupported\", \"action.openPopup not available\", \"api_unavailable\"); }\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 // The two member reads below are exactly what the Safari\n // unsupported-API scan looks for, and this runtime is prepended to\n // the background asset at PROCESS_ASSETS_STAGE_REPORT + 101, after\n // that scan runs at SUMMARIZE + 1. Move the injection earlier and\n // every Safari dev session warns about this line.\n if (chrome.sidePanel && chrome.sidePanel.open && chrome.windows) {\n chrome.windows.getCurrent(function (w) {\n nsCall(\"sidePanel\", \"open\", [{windowId: w.id}], function (err) {\n if (err) {\n var sideMsg = (err && err.message) || err;\n replyErr(cmdId, \"Unsupported\", \"sidePanel.open: \" + sideMsg, openRefusalCode(sideMsg));\n } else replyOk(cmdId, {opened: \"sidebar\"});\n });\n });\n } else { replyErr(cmdId, \"Unsupported\", \"sidePanel not available (engine: \" + engineName() + \")\", \"api_unavailable\"); }\n } else if (surface === \"action\") {\n // With a default_popup the click opens it; without one, replay the\n // onClicked listeners captured at install time.\n if (chrome.action) {\n getActionPopup(function (popup) {\n if (popup) {\n nsCall(\"action\", \"openPopup\", [], function (err) {\n if (err) {\n var actMsg = (err && err.message) || err;\n replyErr(cmdId, \"Unsupported\", \"openPopup: \" + actMsg, openRefusalCode(actMsg));\n } else replyOk(cmdId, {triggered: \"popup\"});\n });\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 // Ignore\n }\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() + \")\", \"api_unavailable\"); }\n } else if (surface === \"command\") {\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 // Ignore\n }\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 nsCall(\"tabs\", \"query\", [args || {}], function (err, tabs) {\n if (err) { replyErr(cmdId, \"TabsError\", (err && err.message) || err); return; }\n replyOk(cmdId, (tabs || []).map(function (t) { return {id: t.id, url: t.url, title: t.title, active: t.active, windowId: t.windowId}; }));\n });\n return;\n }\n if (op === \"inspect\") {\n // Extract a DOM snapshot from the target page via chrome.scripting\n // (CDP-free). Closed shadow roots need CDP; 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 nsCall(\"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 }], function (err, res) {\n if (err) { replyErr(cmdId, \"TargetNotFound\", (err && err.message) || err); return; }\n var snap = res && res[0] ? res[0].result : undefined;\n if (snap == null) { replyErr(cmdId, \"TargetNotFound\", \"no injectable frame returned a snapshot for tab \" + target.tabId + \" (restricted page, or outside host_permissions)\"); return; }\n replyOk(cmdId, snap);\n });\n } else if (ctx === \"popup\" || ctx === \"options\" || ctx === \"sidebar\" || ctx === \"devtools\" || ctx === \"newtab\" || ctx === \"history\" || ctx === \"bookmarks\") {\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 + \")\", \"surface_not_open\");\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 // Ignore\n } }\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 one declared entry's fresh files into every matching open tab.\n // exclude_matches MUST be honored: query the excludes and subtract them.\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 // Ignore\n }\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 // Ignore\n }\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 // Ignore\n }\n }\n })(tabs[i]);\n }\n });\n } catch (e) {\n // Ignore\n }\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 // Ignore\n }\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 // Ignore\n }\n }\n injectInto({});\n }\n\n // Re-inject all content scripts into their open tabs. Reads the manifest\n // FROM DISK: getManifest() is frozen and dev filenames are content-hashed.\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 // Ignore\n } }\n })\n .catch(function () {\n // Ignore\n });\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 content scripts dynamically at the fresh files, so tabs\n // opened AFTER an edit also get the new build. Idempotent per entry id.\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 // Ignore\n }\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 // Ignore\n }\n }\n if (toUpdate.length) {\n try { chrome.scripting.updateContentScripts(toUpdate, noopLastError); } catch (e) {\n // Ignore\n }\n }\n });\n } catch (e) {\n // Ignore\n }\n }\n\n // Dev-loop reload without the CDP controller: content-scripts re-inject in\n // place into open tabs; service-worker/full/manifest restart the extension.\n function performDevReload(type, onDone) {\n var chrome = g.chrome;\n if (!chrome) return;\n\n var fullReload = function () {\n // Flag a pending reinject for the NEXT producer generation: the\n // boot-time heal is what converges open tabs after a SW/full reload.\n try {\n if (chrome.storage && chrome.storage.local) {\n chrome.storage.local.set({__extjsDevPendingReinject: Date.now()}, noopLastError);\n }\n } catch (e) {\n // Ignore\n }\n // Deferred so in-flight frames and the tab console announcement flush\n // before the SW dies; the devtools companion confirms completion.\n try { setTimeout(function () { try { chrome.runtime.reload(); } catch (e) {\n // Ignore\n } }, 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 // One server-built label travels the ReloadFrame; the announcement\n // fires HERE, next to the actual reload action, so it stays true.\n\n // console.log the reload line into every open injectable tab; the patched\n // ISOLATED-world console also lands it in the 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 // Ignore\n }\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 // Ignore\n }\n }\n })(tabs[i]);\n }\n });\n } catch (e) {\n // Ignore\n }\n }\n\n // The scripts-replay shim (plugin-reload) keeps the SW's own\n // executeScript registry; the replay is fire-and-forget by design.\n function replayProgrammaticScripts(changedScriptFiles) {\n if (!Array.isArray(changedScriptFiles) || changedScriptFiles.length === 0) return;\n var replay = g.__extjsScriptsReplay;\n if (typeof replay !== \"function\") return;\n try {\n var outcome = replay(changedScriptFiles);\n if (outcome && typeof outcome.then === \"function\") {\n outcome.then(null, function () {});\n }\n } catch (e) {\n // Ignore\n }\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 // A scripts/ bundle edit: re-run the executeScript calls that named it\n // on their recorded tabs, before any reload decision below.\n replayProgrammaticScripts(frame.changedScriptFiles);\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.\n return;\n }\n\n // Delivery ack for the broker: proves this SW's message pump processed\n // the frame (a socket write proves nothing). Sent on receipt, by design.\n send({type: \"reload-ack\", reloadType: kind, label: label});\n\n announceReloadInTabs(announced);\n performDevReload(kind, function () {});\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 // Ignore\n }\n }\n if (queue.length < MAX_QUEUE) queue.push(frame);\n parkPendingError(frame);\n }\n\n // The queue above lives in this context's memory only, so a background\n // that throws while it evaluates dies with the error still queued: the\n // socket has not finished its handshake yet. Park error frames in\n // extension storage too, where the NEXT context to connect can ship them.\n // Best effort by construction: the write is asynchronous, so a teardown\n // can cut it before the browser records anything.\n var parkedErrors = [];\n function storageLocalArea() {\n try {\n var ns = (g.chrome && g.chrome.storage) || (g.browser && g.browser.storage);\n var area = ns && ns.local;\n if (!area || typeof area.get !== \"function\" || typeof area.set !== \"function\") return null;\n return area;\n } catch (e) { return null; }\n }\n function parkPendingError(frame) {\n if (open) return;\n if (!frame || frame.type !== \"log\" || !frame.event || frame.event.level !== \"error\") return;\n var area = storageLocalArea();\n if (!area) return;\n // The whole list is rewritten on every park, so two errors in one turn\n // cannot lose each other to a read-modify-write, and the cap holds.\n parkedErrors.push(frame);\n while (parkedErrors.length > MAX_PENDING_ERRORS) parkedErrors.shift();\n var items = {};\n items[PENDING_ERRORS_KEY] = parkedErrors;\n try { area.set(items, noopLastError); } catch (e) {\n // Ignore\n }\n }\n function parkedHere(id) {\n for (var i = 0; i < parkedErrors.length; i++) {\n var ev = parkedErrors[i] && parkedErrors[i].event;\n if (ev && ev.id === id) return true;\n }\n return false;\n }\n // Ship what an earlier context parked and never got to send. Entries from\n // another build (a different instance id) or from an older run are dropped,\n // and the key is cleared on read so one failure is reported once.\n function flushPendingErrors() {\n var area = storageLocalArea();\n if (!area) return;\n var read = false;\n function onRead(res) {\n if (read) return;\n read = true;\n var list = res && res[PENDING_ERRORS_KEY];\n if (!list || !list.length) return;\n try {\n if (typeof area.remove === \"function\") area.remove(PENDING_ERRORS_KEY, noopLastError);\n } catch (e) {\n // Ignore\n }\n var now = Date.now();\n for (var i = 0; i < list.length; i++) {\n var frame = list[i];\n var ev = frame && frame.event;\n if (!ev || ev.runId !== INSTANCE_ID) continue;\n // This context parked it, so the queue flush above already has it.\n if (parkedHere(ev.id)) continue;\n if (typeof ev.timestamp !== \"number\" || now - ev.timestamp > PENDING_ERROR_TTL) continue;\n send(frame);\n }\n }\n try {\n var ret = area.get(PENDING_ERRORS_KEY, function (res) { noopLastError(); onRead(res); });\n if (ret && typeof ret.then === \"function\") ret.then(onRead, function () {});\n } catch (e) {\n // Ignore\n }\n }\n\n // The baked PORT is a snapshot of an ephemeral port. After a dev-server\n // restart, the port file shipped in the unpacked extension names the\n // live one, and unpacked resources are read from disk on every fetch.\n function refreshControlPort(done) {\n try {\n var runtime = (g.chrome && g.chrome.runtime) || (g.browser && g.browser.runtime);\n if (!runtime || typeof runtime.getURL !== \"function\" || typeof g.fetch !== \"function\") { done(); return; }\n g.fetch(runtime.getURL(\"extension-js-control.json\"), {cache: \"no-store\"})\n .then(function (res) { return res.json(); })\n .then(function (data) {\n var next = data && parseInt(data.port, 10);\n if (next && next > 0 && next < 65536 && next !== PORT) PORT = next;\n done();\n })\n .catch(function () { done(); });\n } catch (e) { done(); }\n }\n\n function schedule() {\n var delay = backoff;\n backoff = Math.min(backoff * 2, MAX_BACKOFF);\n connectFailures++;\n try {\n setTimeout(function () {\n // Two straight failures make the baked port suspect (a restarted\n // server dials out on a new port): re-resolve before re-dialing.\n if (connectFailures >= 2) refreshControlPort(connect);\n else connect();\n }, delay);\n } catch (e) {\n // Ignore\n }\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 connectFailures = 0;\n try {\n socket.send(JSON.stringify({type: \"hello\", v: 1, role: \"producer\", instanceId: INSTANCE_ID}));\n } catch (e) {\n // Ignore\n }\n flush();\n // Hello went first and this context's own queue is out; now replay any\n // error a previous context parked before it died.\n flushPendingErrors();\n };\n socket.onmessage = function (ev) {\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 // Ignore\n }\n } else if (frame && frame.type === \"ping\") {\n // Server keepalive: merely RECEIVING this resets the MV3 SW's ~30s\n // idle timer (Chrome 116+), keeping it reachable. 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 // Ignore\n }\n };\n }\n\n // Uncaught errors don't route through the patched console; the handlers\n // below forward them, using these signatures to skip already-logged throws.\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 // Ignore\n }\n return orig.apply(consoleRef, arguments);\n };\n });\n\n // Forward uncaught throws / unhandled rejections as level:\"error\", deduped\n // 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 // Ignore\n }\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 // Ignore\n }\n\n // Other contexts relay console logs to this SW over a NAMED runtime.Port:\n // they can't open the WS under page CSP, and sendMessage can echo-loop.\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 // Ignore\n }\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 // Ignore\n }\n });\n }\n } catch (e) {\n // Ignore\n }\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 });\n }\n } catch (e) {\n // Ignore\n }\n\n // Chrome never injects manifest content scripts into already-open tabs, so\n // fire one reinject on onInstalled (which skips idle-stop SW wakes).\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 // Ignore\n }\n }, 250);\n } catch (e) {\n // Ignore\n }\n });\n }\n } catch (e) {\n // Ignore\n }\n\n // runtime.reload() does NOT fire onInstalled (verified Chromium 146), so\n // consume the pre-reload storage flag here (fresh only) to heal open tabs.\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 // Ignore\n }\n if (typeof ts !== \"number\" || Date.now() - ts > 30000) return;\n setTimeout(function () {\n try { reinjectContentScripts(); } catch (e) {\n // Ignore\n }\n }, 250);\n });\n }\n } catch (e) {\n // Ignore\n }\n\n connect();\n } catch (e) {\n // Ignore\n }\n})();\n";
2
+ 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: these wraps run\n // BEFORE user code, and no platform API exists to dispatch these events.\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 the raw globals (not the hoisted chrome var below). This runs first.\n // On Gecko browser.* is a DISTINCT wrapper object: hook BOTH namespaces.\n var actionClickedListeners = [];\n var commandListeners = [];\n var capturedEvents = [];\n function captureEventOnce(event, sink) {\n // Polyfilled extensions expose ONE event object on both namespaces:\n // wrap it once, and the sink dedupes callbacks shared across wrappers.\n if (!event || capturedEvents.indexOf(event) !== -1) return;\n capturedEvents.push(event);\n captureEvent(event, sink);\n }\n var captureNamespaces = [g.chrome, g.browser];\n for (var cn = 0; cn < captureNamespaces.length; cn++) {\n var capNS = captureNamespaces[cn];\n if (!capNS) continue;\n captureEventOnce(capNS.action && capNS.action.onClicked, actionClickedListeners);\n captureEventOnce(capNS.commands && capNS.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 connectFailures = 0;\n var MAX_QUEUE = 1000;\n var MAX_RESULT_BYTES = 256 * 1024;\n // Where error frames are parked so a context that dies before its socket\n // opens does not take them along. Capped, and old entries are dropped.\n var PENDING_ERRORS_KEY = \"__extjsPendingErrors\";\n var MAX_PENDING_ERRORS = 20;\n var PENDING_ERROR_TTL = 300000;\n\n function nowId() {\n return Date.now().toString(36) + Math.random().toString(36).slice(2, 8);\n }\n\n // The extension's own URL scheme names the engine outright, which beats a\n // user agent where Chrome and Safari both say \"Safari\".\n function engineName() {\n try {\n var scheme = (typeof location !== \"undefined\" && location.protocol) || \"\";\n if (scheme.indexOf(\"safari-web-extension\") === 0) return \"webkit\";\n if (scheme.indexOf(\"moz-extension\") === 0) return \"firefox\";\n if (scheme.indexOf(\"chrome-extension\") === 0) return \"chromium\";\n var ua = (typeof navigator !== \"undefined\" && navigator.userAgent) || \"\";\n if (/firefox/i.test(ua)) return \"firefox\";\n if (/safari/i.test(ua) && !/chrome|chromium|edg/i.test(ua)) return \"webkit\";\n return \"chromium\";\n } catch (e) { return \"chromium\"; }\n }\n\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 // Ignore\n }\n }\n\n function replyErr(cmdId, name, message, code) {\n var frame = {type: \"result\", cmdId: cmdId, ok: false, error: {name: name, message: String(message), engine: engineName()}};\n if (code) frame.error.code = code;\n try { socket && socket.send(JSON.stringify(frame)); } catch (e) {\n // Ignore\n }\n }\n\n // Name the refusal the browser wrote as prose, so a consumer branches on\n // error.code instead of matching the engine's sentence.\n function openRefusalCode(message) {\n var m = String(message == null ? \"\" : message);\n if (/user gesture|user input|user interaction|user action/i.test(m)) return \"needs_user_gesture\";\n if (/browser window|active window/i.test(m)) return \"needs_headed_window\";\n return undefined;\n }\n\n // Resolve a numeric tab id when the caller did not pass one: match\n // target.url against open tabs, else default to the active tab. (#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 // chrome.* async APIs are callback-only on Gecko. Prefer promisified\n // browser.* when present, else the chrome.* callback form. cb(err, result).\n function nsCall(nsName, method, callArgs, cb) {\n var chromeG = g.chrome;\n var done = false;\n var once = function (err, r) { if (!done) { done = true; cb(err, r); } };\n var viaPromise = function (ns) {\n try {\n var ret = ns[method].apply(ns, callArgs);\n if (ret && typeof ret.then === \"function\") {\n ret.then(function (r) { once(null, r); }, function (e) { once(e); });\n return true;\n }\n } catch (e) { once(e); return true; }\n return false;\n };\n var bNS = g.browser && g.browser[nsName];\n if (bNS && typeof bNS[method] === \"function\" && viaPromise(bNS)) return;\n var cNS = chromeG && chromeG[nsName];\n if (!cNS || typeof cNS[method] !== \"function\") {\n once(new Error(\"chrome.\" + nsName + \".\" + method + \" is not available (engine: \" + engineName() + \")\"));\n return;\n }\n try {\n var ret2 = cNS[method].apply(cNS, callArgs.concat([function (r) {\n var le = chromeG.runtime && chromeG.runtime.lastError;\n if (le) once(le); else once(null, r);\n }]));\n // A promise-only implementation ignores the trailing callback and\n // returns a thenable instead; resolve through it (once() dedupes).\n if (ret2 && typeof ret2.then === \"function\") {\n ret2.then(function (r) { once(null, r); }, function (e) { once(e); });\n }\n } catch (e2) {\n if (!viaPromise(cNS)) once(e2);\n }\n }\n\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: resolve --url (or\n // nothing) to a 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 and\n // rejects 'unsafe-eval'. Surface it honestly with an alternative.\n if (/Content Security Policy|unsafe-eval/i.test(msg)) {\n replyErr(cmdId, \"Unsupported\", \"eval is blocked in the extension background by CSP, which allows no unsafe-eval. Use --context page --tab <id> (eval runs in the page's MAIN world), or run on a build whose CSP permits it. 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 // Wrap the eval so a throw INSIDE the tab comes back as data:\n // Chrome swallows an injected function's exception (null result).\n nsCall(\"scripting\", \"executeScript\", [{\n target: {tabId: target.tabId},\n world: ctx === \"page\" ? \"MAIN\" : \"ISOLATED\",\n func: function (src) {\n try { return {__extjsEval: 1, ok: true, value: eval(src)}; }\n catch (e) { return {__extjsEval: 1, ok: false, name: (e && e.name) || \"EvalError\", message: (e && e.message) || String(e)}; }\n },\n args: [String(args.expression)]\n }], function (err, res) {\n if (err) { replyErr(cmdId, \"TargetNotFound\", (err && err.message) || err); return; }\n var frame = res && res[0] ? res[0].result : undefined;\n if (!frame || frame.__extjsEval !== 1) {\n replyErr(cmdId, \"TargetNotFound\", \"the expression never executed in tab \" + target.tabId + \": no injectable frame returned a result (restricted page, or outside host_permissions)\");\n return;\n }\n if (frame.ok) { replyOk(cmdId, frame.value); return; }\n if (ctx === \"content\" && /Content Security Policy|unsafe-eval|EvalError/i.test(String(frame.name) + \" \" + String(frame.message))) {\n replyErr(cmdId, \"Unsupported\", \"eval of a string is blocked in the ISOLATED (content) world by the extension CSP. Use --context page (runs in the page's MAIN world). Original: \" + frame.message);\n return;\n }\n replyErr(cmdId, frame.name || \"EvalError\", frame.message);\n });\n } else if (ctx === \"popup\" || ctx === \"options\" || ctx === \"sidebar\" || ctx === \"devtools\" || ctx === \"newtab\" || ctx === \"history\" || ctx === \"bookmarks\") {\n // The SW can't eval in another extension page; ask the surface's\n // own in-bundle relay (only the open, matching-context page responds).\n chrome.runtime.sendMessage(\n {__extjsEvalRequest: true, target: target, args: {expression: String(args.expression)}},\n function (resp) {\n if ((chrome.runtime && chrome.runtime.lastError) || !resp) {\n replyErr(cmdId, \"Unsupported\", \"surface '\" + ctx + \"' is not open (open it first: extension open \" + ctx + \")\", \"surface_not_open\");\n } else if (resp.ok) {\n replyOk(cmdId, resp.value);\n } else {\n replyErr(cmdId, (resp.error && resp.error.name) || \"EvalError\", (resp.error && resp.error.message) || \"eval failed\");\n }\n }\n );\n } else if (ctx === \"content\" || ctx === \"page\") {\n replyErr(cmdId, \"Unsupported\", target.tabId\n ? \"chrome.scripting is not available on this engine (MV2 has no scripting API); use --context background\"\n : \"eval in context \" + ctx + \" requires a tabId\");\n } else {\n replyErr(cmdId, \"BadRequest\", \"unknown eval context: \" + ctx);\n }\n return;\n }\n if (op === \"storage.get\" || op === \"storage.set\") {\n // chrome.* storage is callback-only on Gecko and older Chromium.\n // Prefer promisified browser.*, then fall back to the callback shape.\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) {\n // Ignore\n } }, 50);\n } else if (target.tabId) {\n nsCall(\"tabs\", \"reload\", [target.tabId], function (err) {\n if (err) replyErr(cmdId, \"ReloadError\", (err && err.message) || err);\n else replyOk(cmdId, {reloaded: target.tabId});\n });\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 // Ignore\n }\n try { chrome.action.getPopup({}, function (p) { cb(p || \"\"); }); }\n catch (e) { cb(\"\"); }\n };\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 // Ignore\n }\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.\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 // Ignore\n }\n return null;\n };\n if (surface === \"popup\") {\n if (chrome.action && chrome.action.openPopup) {\n nsCall(\"action\", \"openPopup\", [], function (err) {\n if (err) {\n var openMsg = (err && err.message) || err;\n replyErr(cmdId, \"Unsupported\", \"openPopup: \" + openMsg, openRefusalCode(openMsg));\n } else replyOk(cmdId, {opened: \"popup\"});\n });\n } else { replyErr(cmdId, \"Unsupported\", \"action.openPopup not available\", \"api_unavailable\"); }\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 // The two member reads below are exactly what the Safari\n // unsupported-API scan looks for, and this runtime is prepended to\n // the background asset at PROCESS_ASSETS_STAGE_REPORT + 101, after\n // that scan runs at SUMMARIZE + 1. Move the injection earlier and\n // every Safari dev session warns about this line.\n if (chrome.sidePanel && chrome.sidePanel.open && chrome.windows) {\n chrome.windows.getCurrent(function (w) {\n nsCall(\"sidePanel\", \"open\", [{windowId: w.id}], function (err) {\n if (err) {\n var sideMsg = (err && err.message) || err;\n replyErr(cmdId, \"Unsupported\", \"sidePanel.open: \" + sideMsg, openRefusalCode(sideMsg));\n } else replyOk(cmdId, {opened: \"sidebar\"});\n });\n });\n } else { replyErr(cmdId, \"Unsupported\", \"sidePanel not available (engine: \" + engineName() + \")\", \"api_unavailable\"); }\n } else if (surface === \"action\") {\n // With a default_popup the click opens it; without one, replay the\n // onClicked listeners captured at install time.\n if (chrome.action) {\n getActionPopup(function (popup) {\n if (popup) {\n nsCall(\"action\", \"openPopup\", [], function (err) {\n if (err) {\n var actMsg = (err && err.message) || err;\n replyErr(cmdId, \"Unsupported\", \"openPopup: \" + actMsg, openRefusalCode(actMsg));\n } else replyOk(cmdId, {triggered: \"popup\"});\n });\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 // Ignore\n }\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() + \")\", \"api_unavailable\"); }\n } else if (surface === \"command\") {\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 // Ignore\n }\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 nsCall(\"tabs\", \"query\", [args || {}], function (err, tabs) {\n if (err) { replyErr(cmdId, \"TabsError\", (err && err.message) || err); return; }\n replyOk(cmdId, (tabs || []).map(function (t) { return {id: t.id, url: t.url, title: t.title, active: t.active, windowId: t.windowId}; }));\n });\n return;\n }\n if (op === \"inspect\") {\n // Extract a DOM snapshot from the target page via chrome.scripting\n // (CDP-free). Closed shadow roots need CDP; 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 nsCall(\"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 }], function (err, res) {\n if (err) { replyErr(cmdId, \"TargetNotFound\", (err && err.message) || err); return; }\n var snap = res && res[0] ? res[0].result : undefined;\n if (snap == null) { replyErr(cmdId, \"TargetNotFound\", \"no injectable frame returned a snapshot for tab \" + target.tabId + \" (restricted page, or outside host_permissions)\"); return; }\n replyOk(cmdId, snap);\n });\n } else if (ctx === \"popup\" || ctx === \"options\" || ctx === \"sidebar\" || ctx === \"devtools\" || ctx === \"newtab\" || ctx === \"history\" || ctx === \"bookmarks\") {\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 + \")\", \"surface_not_open\");\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 // Ignore\n } }\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 one declared entry's fresh files into every matching open tab.\n // exclude_matches MUST be honored: query the excludes and subtract them.\n // opts.completeOnly skips a tab still loading: Chrome injects the static\n // script into it when it lands, and a second copy here would run twice.\n function reinjectContentScriptEntry(entry, opts) {\n var chrome = g.chrome;\n var completeOnly = !!(opts && opts.completeOnly);\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 // Ignore\n }\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 if (completeOnly && tab.status === \"loading\") 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 // Ignore\n }\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 // Ignore\n }\n }\n })(tabs[i]);\n }\n });\n } catch (e) {\n // Ignore\n }\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 // Ignore\n }\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 // Ignore\n }\n }\n injectInto({});\n }\n\n // Re-inject all content scripts into their open tabs. Reads the manifest\n // FROM DISK: getManifest() is frozen and dev filenames are content-hashed.\n // opts.register false skips the dynamic registration: at boot the static\n // manifest already names the current files, and a copy injects twice.\n function reinjectContentScripts(onDone, opts) {\n var chrome = g.chrome;\n var register = !opts || opts.register !== false;\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], opts);\n if (register) reregisterForFutureNavigations(entries);\n if (onDone) { try { onDone(); } catch (e) {\n // Ignore\n } }\n })\n .catch(function () {\n // Ignore\n });\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 content scripts dynamically at the fresh files, so tabs\n // opened AFTER an edit also get the new build. Idempotent per entry id.\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 // A browser restart loads the static manifest fresh; a copy that\n // outlived the session would inject every page twice.\n persistAcrossSessions: false\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 // Ignore\n }\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 // Ignore\n }\n }\n if (toUpdate.length) {\n try { chrome.scripting.updateContentScripts(toUpdate, noopLastError); } catch (e) {\n // Ignore\n }\n }\n });\n } catch (e) {\n // Ignore\n }\n }\n\n // The dev content-script runtime (plugin-reload) owns registration, the\n // boot heal and the tab reloads when its registry exists; absent, the\n // manifest-based paths below stay in charge.\n function devContentScripts() {\n try { return g.__extjsDevContentScripts || null; } catch (e) { return null; }\n }\n\n // Dev-loop reload without the CDP controller: content-scripts reload the\n // matching tabs at the new bundle, or re-inject in place where no registry\n // exists; service-worker/full/manifest restart the extension.\n function performDevReload(type, onDone, frame) {\n var chrome = g.chrome;\n if (!chrome) return;\n\n var fullReload = function () {\n // Flag a pending reinject for the NEXT producer generation: the\n // boot-time heal is what converges open tabs after a SW/full reload.\n try {\n if (chrome.storage && chrome.storage.local) {\n chrome.storage.local.set({__extjsDevPendingReinject: Date.now()}, noopLastError);\n }\n } catch (e) {\n // Ignore\n }\n // Deferred so in-flight frames and the tab console announcement flush\n // before the SW dies; the devtools companion confirms completion.\n try { setTimeout(function () { try { chrome.runtime.reload(); } catch (e) {\n // Ignore\n } }, 150); } catch (e) {}\n };\n\n if (type === \"content-scripts\" && chrome.scripting && chrome.tabs && chrome.tabs.query) {\n var dev = devContentScripts();\n if (dev && typeof dev.reload === \"function\") {\n try {\n dev.reload(frame && frame.changedContentScriptEntries, function (handled) {\n if (handled) { if (onDone) { try { onDone(); } catch (e) {\n // Ignore\n } } return; }\n if (reinjectContentScripts(onDone)) return;\n fullReload();\n });\n return;\n } catch (e) {\n // Ignore\n }\n }\n if (reinjectContentScripts(onDone)) return;\n }\n\n fullReload();\n }\n\n // One server-built label travels the ReloadFrame; the announcement\n // fires HERE, next to the actual reload action, so it stays true.\n\n // console.log the reload line into every open injectable tab; the patched\n // ISOLATED-world console also lands it in the 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 // Ignore\n }\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 // Ignore\n }\n }\n })(tabs[i]);\n }\n });\n } catch (e) {\n // Ignore\n }\n }\n\n // The scripts-replay shim (plugin-reload) keeps the SW's own\n // executeScript registry; the replay is fire-and-forget by design.\n function replayProgrammaticScripts(changedScriptFiles) {\n if (!Array.isArray(changedScriptFiles) || changedScriptFiles.length === 0) return;\n var replay = g.__extjsScriptsReplay;\n if (typeof replay !== \"function\") return;\n try {\n var outcome = replay(changedScriptFiles);\n if (outcome && typeof outcome.then === \"function\") {\n outcome.then(null, function () {});\n }\n } catch (e) {\n // Ignore\n }\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 // A scripts/ bundle edit: re-run the executeScript calls that named it\n // on their recorded tabs, before any reload decision below.\n replayProgrammaticScripts(frame.changedScriptFiles);\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.\n return;\n }\n\n // Delivery ack for the broker: proves this SW's message pump processed\n // the frame (a socket write proves nothing). Sent on receipt, by design.\n send({type: \"reload-ack\", reloadType: kind, label: label});\n\n announceReloadInTabs(announced);\n performDevReload(kind, function () {}, frame);\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 // Ignore\n }\n }\n if (queue.length < MAX_QUEUE) queue.push(frame);\n parkPendingError(frame);\n }\n\n // The queue above lives in this context's memory only, so a background\n // that throws while it evaluates dies with the error still queued: the\n // socket has not finished its handshake yet. Park error frames in\n // extension storage too, where the NEXT context to connect can ship them.\n // Best effort by construction: the write is asynchronous, so a teardown\n // can cut it before the browser records anything.\n var parkedErrors = [];\n function storageLocalArea() {\n try {\n var ns = (g.chrome && g.chrome.storage) || (g.browser && g.browser.storage);\n var area = ns && ns.local;\n if (!area || typeof area.get !== \"function\" || typeof area.set !== \"function\") return null;\n return area;\n } catch (e) { return null; }\n }\n function parkPendingError(frame) {\n if (open) return;\n if (!frame || frame.type !== \"log\" || !frame.event || frame.event.level !== \"error\") return;\n var area = storageLocalArea();\n if (!area) return;\n // The whole list is rewritten on every park, so two errors in one turn\n // cannot lose each other to a read-modify-write, and the cap holds.\n parkedErrors.push(frame);\n while (parkedErrors.length > MAX_PENDING_ERRORS) parkedErrors.shift();\n var items = {};\n items[PENDING_ERRORS_KEY] = parkedErrors;\n try { area.set(items, noopLastError); } catch (e) {\n // Ignore\n }\n }\n function parkedHere(id) {\n for (var i = 0; i < parkedErrors.length; i++) {\n var ev = parkedErrors[i] && parkedErrors[i].event;\n if (ev && ev.id === id) return true;\n }\n return false;\n }\n // Ship what an earlier context parked and never got to send. Entries from\n // another build (a different instance id) or from an older run are dropped,\n // and the key is cleared on read so one failure is reported once.\n function flushPendingErrors() {\n var area = storageLocalArea();\n if (!area) return;\n var read = false;\n function onRead(res) {\n if (read) return;\n read = true;\n var list = res && res[PENDING_ERRORS_KEY];\n if (!list || !list.length) return;\n try {\n if (typeof area.remove === \"function\") area.remove(PENDING_ERRORS_KEY, noopLastError);\n } catch (e) {\n // Ignore\n }\n var now = Date.now();\n for (var i = 0; i < list.length; i++) {\n var frame = list[i];\n var ev = frame && frame.event;\n if (!ev || ev.runId !== INSTANCE_ID) continue;\n // This context parked it, so the queue flush above already has it.\n if (parkedHere(ev.id)) continue;\n if (typeof ev.timestamp !== \"number\" || now - ev.timestamp > PENDING_ERROR_TTL) continue;\n send(frame);\n }\n }\n try {\n var ret = area.get(PENDING_ERRORS_KEY, function (res) { noopLastError(); onRead(res); });\n if (ret && typeof ret.then === \"function\") ret.then(onRead, function () {});\n } catch (e) {\n // Ignore\n }\n }\n\n // The baked PORT is a snapshot of an ephemeral port. After a dev-server\n // restart, the port file shipped in the unpacked extension names the\n // live one, and unpacked resources are read from disk on every fetch.\n function refreshControlPort(done) {\n try {\n var runtime = (g.chrome && g.chrome.runtime) || (g.browser && g.browser.runtime);\n if (!runtime || typeof runtime.getURL !== \"function\" || typeof g.fetch !== \"function\") { done(); return; }\n g.fetch(runtime.getURL(\"extension-js-control.json\"), {cache: \"no-store\"})\n .then(function (res) { return res.json(); })\n .then(function (data) {\n var next = data && parseInt(data.port, 10);\n if (next && next > 0 && next < 65536 && next !== PORT) PORT = next;\n done();\n })\n .catch(function () { done(); });\n } catch (e) { done(); }\n }\n\n function schedule() {\n var delay = backoff;\n backoff = Math.min(backoff * 2, MAX_BACKOFF);\n connectFailures++;\n try {\n setTimeout(function () {\n // Two straight failures make the baked port suspect (a restarted\n // server dials out on a new port): re-resolve before re-dialing.\n if (connectFailures >= 2) refreshControlPort(connect);\n else connect();\n }, delay);\n } catch (e) {\n // Ignore\n }\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 connectFailures = 0;\n try {\n socket.send(JSON.stringify({type: \"hello\", v: 1, role: \"producer\", instanceId: INSTANCE_ID}));\n } catch (e) {\n // Ignore\n }\n flush();\n // Hello went first and this context's own queue is out; now replay any\n // error a previous context parked before it died.\n flushPendingErrors();\n };\n socket.onmessage = function (ev) {\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 // Ignore\n }\n } else if (frame && frame.type === \"ping\") {\n // Server keepalive: merely RECEIVING this resets the MV3 SW's ~30s\n // idle timer (Chrome 116+), keeping it reachable. 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 // Ignore\n }\n };\n }\n\n // Uncaught errors don't route through the patched console; the handlers\n // below forward them, using these signatures to skip already-logged throws.\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 // Ignore\n }\n return orig.apply(consoleRef, arguments);\n };\n });\n\n // Forward uncaught throws / unhandled rejections as level:\"error\", deduped\n // 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 // Ignore\n }\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 // Ignore\n }\n\n // Other contexts relay console logs to this SW over a NAMED runtime.Port:\n // they can't open the WS under page CSP, and sendMessage can echo-loop.\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 // Ignore\n }\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 // Ignore\n }\n });\n }\n } catch (e) {\n // Ignore\n }\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 });\n }\n } catch (e) {\n // Ignore\n }\n\n // Dev registrations outlive runtime.reload(), while the new generation's\n // static manifest already names the current files: drop them first, or\n // every navigation from here on runs the content script twice.\n function unregisterDevContentScripts(onDone) {\n var chrome = g.chrome;\n var done = function () { if (onDone) { try { onDone(); } catch (e) {\n // Ignore\n } } };\n try {\n if (\n !chrome.scripting ||\n !chrome.scripting.getRegisteredContentScripts ||\n !chrome.scripting.unregisterContentScripts\n ) return done();\n chrome.scripting.getRegisteredContentScripts(function (existing) {\n try { void chrome.runtime.lastError; } catch (e) {\n // Ignore\n }\n var ids = [];\n if (existing) {\n for (var i = 0; i < existing.length; i++) {\n var id = existing[i] && existing[i].id;\n if (typeof id === \"string\" && id.indexOf(\"extjs-dev-cs-\") === 0) ids.push(id);\n }\n }\n if (!ids.length) return done();\n try {\n chrome.scripting.unregisterContentScripts({ids: ids}, function () { noopLastError(); done(); });\n } catch (e) { done(); }\n });\n } catch (e) { done(); }\n }\n\n // One heal per producer generation: onInstalled and the pre-reload storage\n // flag can both fire on the same boot, and each would inject once more.\n var bootHealDone = false;\n function healOpenTabsAtBoot() {\n if (bootHealDone) return;\n bootHealDone = true;\n setTimeout(function () {\n var dev = devContentScripts();\n if (dev && typeof dev.heal === \"function\") {\n try { dev.heal(); return; } catch (e) {\n // Ignore\n }\n }\n unregisterDevContentScripts(function () {\n try { reinjectContentScripts(undefined, {register: false, completeOnly: true}); } catch (e) {\n // Ignore\n }\n });\n }, 250);\n }\n\n // Chrome never injects manifest content scripts into already-open tabs, so\n // fire one reinject on onInstalled (which skips idle-stop SW wakes). Not\n // on the first install: the tabs the browser launched with load beside\n // the extension and Chrome injects them itself.\n try {\n var rtBoot = g.chrome;\n if (rtBoot && rtBoot.runtime && rtBoot.runtime.onInstalled) {\n rtBoot.runtime.onInstalled.addListener(function (details) {\n if (details && details.reason === \"install\") return;\n try { healOpenTabsAtBoot(); } catch (e) {\n // Ignore\n }\n });\n }\n } catch (e) {\n // Ignore\n }\n\n // runtime.reload() does NOT fire onInstalled (verified Chromium 146), so\n // consume the pre-reload storage flag here (fresh only) to heal open tabs.\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 // Ignore\n }\n if (typeof ts !== \"number\" || Date.now() - ts > 30000) return;\n healOpenTabsAtBoot();\n });\n }\n } catch (e) {\n // Ignore\n }\n\n connect();\n } catch (e) {\n // Ignore\n }\n})();\n";
3
3
  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, but\n // port frames never reach the extension's own onMessage listeners.\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 // Ignore\n }\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).\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 // Ignore\n } }\n }\n }\n\n // Uncaught throws / unhandled rejections never route through the patched\n // console; forward them, deduped against console.error Error signatures.\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 // Ignore\n }\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 // Ignore\n }\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 // Ignore\n }\n\n // The SW executor can't reach another extension page's DOM or eval there,\n // so THIS surface answers inspect/eval requests for its own context.\n try {\n if (chrome.runtime && chrome.runtime.onMessage) {\n chrome.runtime.onMessage.addListener(function (msg, _sender, sendResponse) {\n if (!msg || !msg.__extjsEvalRequest) return;\n if (!msg.target || msg.target.context !== CONTEXT) return; // not for me\n try {\n var value = (0, eval)(String((msg.args && msg.args.expression) || \"\"));\n try { sendResponse({ok: true, value: value}); }\n catch (eSend) { sendResponse({ok: true, value: String(value)}); }\n } catch (e) {\n var emsg = (e && e.message) || String(e);\n if (/Content Security Policy|unsafe-eval|call to eval/i.test(emsg)) {\n sendResponse({ok: false, error: {name: \"Unsupported\", message: \"eval of a string is blocked in the \" + CONTEXT + \" page by the MV3 extension CSP. Use extension inspect \" + CONTEXT + \" to read its DOM, or --context background on an MV2/Firefox build. Original: \" + emsg}});\n } else {\n sendResponse({ok: false, error: {name: (e && e.name) || \"EvalError\", message: emsg}});\n }\n }\n return true; // responded\n });\n }\n } catch (e) {\n // Ignore\n }\n\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 // Ignore\n }\n } catch (e) {\n // Ignore\n }\n})();\n";
4
4
  export interface BuildRelayOptions {
5
5
  context: string;
@@ -0,0 +1,33 @@
1
+ import type { Manifest } from '../../types.js';
2
+ export declare const DEV_CONTENT_SCRIPT_REGISTRY_ASSET = "content_scripts/dev-registry.json";
3
+ export declare const DEV_CONTENT_SCRIPT_MARKER_KEY = "__extjsDevContentScripts";
4
+ export declare const DEV_CONTENT_SCRIPT_STUB_MESSAGE_KEY = "__extjsDevCsStub";
5
+ export declare const DEV_CONTENT_SCRIPT_ID_PREFIX = "extjs-dev-cs-";
6
+ export interface DevContentScriptRegistryEntry {
7
+ id: string;
8
+ entry: string;
9
+ matches: string[];
10
+ excludeMatches?: string[];
11
+ js: string[];
12
+ css: string[];
13
+ runAt: 'document_start' | 'document_end' | 'document_idle';
14
+ allFrames: boolean;
15
+ world: 'ISOLATED' | 'MAIN';
16
+ matchOriginAsFallback?: boolean;
17
+ stubOnly: boolean;
18
+ }
19
+ export interface DevContentScriptRegistry {
20
+ version: 1;
21
+ entries: DevContentScriptRegistryEntry[];
22
+ }
23
+ export interface DevContentScriptPlan {
24
+ manifest: Manifest;
25
+ registry: DevContentScriptRegistry;
26
+ stubs: Record<string, string>;
27
+ }
28
+ export declare function devContentScriptStubAssetName(index: number): string;
29
+ export declare function contentScriptEntryForAsset(assetName: string): string | undefined;
30
+ export declare function planDevContentScripts(manifest: Manifest): DevContentScriptPlan | undefined;
31
+ export declare function buildDevContentScriptStubSource(entryName: string): string;
32
+ export declare function buildDevContentScriptMarkerPrelude(entryName: string, assetName: string): string;
33
+ export declare const DEV_CONTENT_SCRIPTS_RUNTIME_SOURCE: string;
@@ -0,0 +1,4 @@
1
+ import { type Compiler } from '@rspack/core';
2
+ export declare class SetupDevContentScripts {
3
+ apply(compiler: Compiler): void;
4
+ }
@@ -4130,8 +4130,11 @@ const BRIDGE_PRODUCER_SOURCE = `;(function () {
4130
4130
 
4131
4131
  // Re-inject one declared entry's fresh files into every matching open tab.
4132
4132
  // exclude_matches MUST be honored: query the excludes and subtract them.
4133
- function reinjectContentScriptEntry(entry) {
4133
+ // opts.completeOnly skips a tab still loading: Chrome injects the static
4134
+ // script into it when it lands, and a second copy here would run twice.
4135
+ function reinjectContentScriptEntry(entry, opts) {
4134
4136
  var chrome = g.chrome;
4137
+ var completeOnly = !!(opts && opts.completeOnly);
4135
4138
  var matches = (entry && entry.matches) || [];
4136
4139
  if (!Array.isArray(matches) || !matches.length) return;
4137
4140
  var excludeMatches = (entry && entry.exclude_matches) || [];
@@ -4152,6 +4155,7 @@ const BRIDGE_PRODUCER_SOURCE = `;(function () {
4152
4155
  (function (tab) {
4153
4156
  if (!tab || tab.id == null || !isInjectableUrl(tab.url)) return;
4154
4157
  if (excludedTabIds[tab.id]) return;
4158
+ if (completeOnly && tab.status === "loading") return;
4155
4159
  var target = {tabId: tab.id, allFrames: allFrames};
4156
4160
  if (cssFiles.length && chrome.scripting.insertCSS) {
4157
4161
  try { chrome.scripting.insertCSS({target: target, files: cssFiles}, noopLastError); } catch (e) {
@@ -4199,16 +4203,19 @@ const BRIDGE_PRODUCER_SOURCE = `;(function () {
4199
4203
 
4200
4204
  // Re-inject all content scripts into their open tabs. Reads the manifest
4201
4205
  // FROM DISK: getManifest() is frozen and dev filenames are content-hashed.
4202
- function reinjectContentScripts(onDone) {
4206
+ // opts.register false skips the dynamic registration: at boot the static
4207
+ // manifest already names the current files, and a copy injects twice.
4208
+ function reinjectContentScripts(onDone, opts) {
4203
4209
  var chrome = g.chrome;
4210
+ var register = !opts || opts.register !== false;
4204
4211
  try {
4205
4212
  if (typeof g.fetch !== "function" || !chrome.scripting) return false;
4206
4213
  g.fetch(chrome.runtime.getURL("manifest.json"), {cache: "no-store"})
4207
4214
  .then(function (r) { return r.json(); })
4208
4215
  .then(function (manifest) {
4209
4216
  var entries = (manifest && manifest.content_scripts) || [];
4210
- for (var i = 0; i < entries.length; i++) reinjectContentScriptEntry(entries[i]);
4211
- reregisterForFutureNavigations(entries);
4217
+ for (var i = 0; i < entries.length; i++) reinjectContentScriptEntry(entries[i], opts);
4218
+ if (register) reregisterForFutureNavigations(entries);
4212
4219
  if (onDone) { try { onDone(); } catch (e) {
4213
4220
  // Ignore
4214
4221
  } }
@@ -4248,7 +4255,10 @@ const BRIDGE_PRODUCER_SOURCE = `;(function () {
4248
4255
  matches: e.matches,
4249
4256
  runAt: mapRunAt(e.run_at),
4250
4257
  allFrames: !!e.all_frames,
4251
- world: e.world === "MAIN" ? "MAIN" : "ISOLATED"
4258
+ world: e.world === "MAIN" ? "MAIN" : "ISOLATED",
4259
+ // A browser restart loads the static manifest fresh; a copy that
4260
+ // outlived the session would inject every page twice.
4261
+ persistAcrossSessions: false
4252
4262
  };
4253
4263
  // Dev registration must not be BROADER than the static one: dropping
4254
4264
  // exclude_matches injects into pages the browser itself would skip.
@@ -4288,9 +4298,17 @@ const BRIDGE_PRODUCER_SOURCE = `;(function () {
4288
4298
  }
4289
4299
  }
4290
4300
 
4291
- // Dev-loop reload without the CDP controller: content-scripts re-inject in
4292
- // place into open tabs; service-worker/full/manifest restart the extension.
4293
- function performDevReload(type, onDone) {
4301
+ // The dev content-script runtime (plugin-reload) owns registration, the
4302
+ // boot heal and the tab reloads when its registry exists; absent, the
4303
+ // manifest-based paths below stay in charge.
4304
+ function devContentScripts() {
4305
+ try { return g.__extjsDevContentScripts || null; } catch (e) { return null; }
4306
+ }
4307
+
4308
+ // Dev-loop reload without the CDP controller: content-scripts reload the
4309
+ // matching tabs at the new bundle, or re-inject in place where no registry
4310
+ // exists; service-worker/full/manifest restart the extension.
4311
+ function performDevReload(type, onDone, frame) {
4294
4312
  var chrome = g.chrome;
4295
4313
  if (!chrome) return;
4296
4314
 
@@ -4312,6 +4330,21 @@ const BRIDGE_PRODUCER_SOURCE = `;(function () {
4312
4330
  };
4313
4331
 
4314
4332
  if (type === "content-scripts" && chrome.scripting && chrome.tabs && chrome.tabs.query) {
4333
+ var dev = devContentScripts();
4334
+ if (dev && typeof dev.reload === "function") {
4335
+ try {
4336
+ dev.reload(frame && frame.changedContentScriptEntries, function (handled) {
4337
+ if (handled) { if (onDone) { try { onDone(); } catch (e) {
4338
+ // Ignore
4339
+ } } return; }
4340
+ if (reinjectContentScripts(onDone)) return;
4341
+ fullReload();
4342
+ });
4343
+ return;
4344
+ } catch (e) {
4345
+ // Ignore
4346
+ }
4347
+ }
4315
4348
  if (reinjectContentScripts(onDone)) return;
4316
4349
  }
4317
4350
 
@@ -4401,7 +4434,7 @@ const BRIDGE_PRODUCER_SOURCE = `;(function () {
4401
4434
  send({type: "reload-ack", reloadType: kind, label: label});
4402
4435
 
4403
4436
  announceReloadInTabs(announced);
4404
- performDevReload(kind, function () {});
4437
+ performDevReload(kind, function () {}, frame);
4405
4438
  }
4406
4439
 
4407
4440
  function sanitize(args) {
@@ -4738,19 +4771,70 @@ const BRIDGE_PRODUCER_SOURCE = `;(function () {
4738
4771
  // Ignore
4739
4772
  }
4740
4773
 
4774
+ // Dev registrations outlive runtime.reload(), while the new generation's
4775
+ // static manifest already names the current files: drop them first, or
4776
+ // every navigation from here on runs the content script twice.
4777
+ function unregisterDevContentScripts(onDone) {
4778
+ var chrome = g.chrome;
4779
+ var done = function () { if (onDone) { try { onDone(); } catch (e) {
4780
+ // Ignore
4781
+ } } };
4782
+ try {
4783
+ if (
4784
+ !chrome.scripting ||
4785
+ !chrome.scripting.getRegisteredContentScripts ||
4786
+ !chrome.scripting.unregisterContentScripts
4787
+ ) return done();
4788
+ chrome.scripting.getRegisteredContentScripts(function (existing) {
4789
+ try { void chrome.runtime.lastError; } catch (e) {
4790
+ // Ignore
4791
+ }
4792
+ var ids = [];
4793
+ if (existing) {
4794
+ for (var i = 0; i < existing.length; i++) {
4795
+ var id = existing[i] && existing[i].id;
4796
+ if (typeof id === "string" && id.indexOf("extjs-dev-cs-") === 0) ids.push(id);
4797
+ }
4798
+ }
4799
+ if (!ids.length) return done();
4800
+ try {
4801
+ chrome.scripting.unregisterContentScripts({ids: ids}, function () { noopLastError(); done(); });
4802
+ } catch (e) { done(); }
4803
+ });
4804
+ } catch (e) { done(); }
4805
+ }
4806
+
4807
+ // One heal per producer generation: onInstalled and the pre-reload storage
4808
+ // flag can both fire on the same boot, and each would inject once more.
4809
+ var bootHealDone = false;
4810
+ function healOpenTabsAtBoot() {
4811
+ if (bootHealDone) return;
4812
+ bootHealDone = true;
4813
+ setTimeout(function () {
4814
+ var dev = devContentScripts();
4815
+ if (dev && typeof dev.heal === "function") {
4816
+ try { dev.heal(); return; } catch (e) {
4817
+ // Ignore
4818
+ }
4819
+ }
4820
+ unregisterDevContentScripts(function () {
4821
+ try { reinjectContentScripts(undefined, {register: false, completeOnly: true}); } catch (e) {
4822
+ // Ignore
4823
+ }
4824
+ });
4825
+ }, 250);
4826
+ }
4827
+
4741
4828
  // Chrome never injects manifest content scripts into already-open tabs, so
4742
- // fire one reinject on onInstalled (which skips idle-stop SW wakes).
4829
+ // fire one reinject on onInstalled (which skips idle-stop SW wakes). Not
4830
+ // on the first install: the tabs the browser launched with load beside
4831
+ // the extension and Chrome injects them itself.
4743
4832
  try {
4744
4833
  var rtBoot = g.chrome;
4745
4834
  if (rtBoot && rtBoot.runtime && rtBoot.runtime.onInstalled) {
4746
- rtBoot.runtime.onInstalled.addListener(function () {
4747
- try {
4748
- setTimeout(function () {
4749
- try { reinjectContentScripts(); } catch (e) {
4750
- // Ignore
4751
- }
4752
- }, 250);
4753
- } catch (e) {
4835
+ rtBoot.runtime.onInstalled.addListener(function (details) {
4836
+ if (details && details.reason === "install") return;
4837
+ try { healOpenTabsAtBoot(); } catch (e) {
4754
4838
  // Ignore
4755
4839
  }
4756
4840
  });
@@ -4772,11 +4856,7 @@ const BRIDGE_PRODUCER_SOURCE = `;(function () {
4772
4856
  // Ignore
4773
4857
  }
4774
4858
  if (typeof ts !== "number" || Date.now() - ts > 30000) return;
4775
- setTimeout(function () {
4776
- try { reinjectContentScripts(); } catch (e) {
4777
- // Ignore
4778
- }
4779
- }, 250);
4859
+ healOpenTabsAtBoot();
4780
4860
  });
4781
4861
  }
4782
4862
  } catch (e) {
@@ -5280,6 +5360,404 @@ class PruneStaleHotUpdates {
5280
5360
  });
5281
5361
  }
5282
5362
  }
5363
+ const DEV_CONTENT_SCRIPT_REGISTRY_ASSET = "content_scripts/dev-registry.json";
5364
+ const DEV_CONTENT_SCRIPT_MARKER_KEY = '__extjsDevContentScripts';
5365
+ const DEV_CONTENT_SCRIPT_STUB_MESSAGE_KEY = '__extjsDevCsStub';
5366
+ const DEV_CONTENT_SCRIPT_ID_PREFIX = 'extjs-dev-cs-';
5367
+ function devContentScriptStubAssetName(index) {
5368
+ return `content_scripts/dev-stub-${index}.js`;
5369
+ }
5370
+ const HASHED_CONTENT_SCRIPT_ASSET = /^content_scripts\/content-(\d+)(?:\.[a-f0-9]+)?\.js$/i;
5371
+ function contentScriptEntryForAsset(assetName) {
5372
+ const match = HASHED_CONTENT_SCRIPT_ASSET.exec(assetName);
5373
+ if (!match) return;
5374
+ return getCanonicalContentScriptEntryName(Number(match[1]));
5375
+ }
5376
+ function stringList(value) {
5377
+ return Array.isArray(value) ? value.filter((item)=>'string' == typeof item) : [];
5378
+ }
5379
+ function runAtOf(value) {
5380
+ if ('document_start' === value || 'document_end' === value) return value;
5381
+ return 'document_idle';
5382
+ }
5383
+ function canonicalIndexOf(entry) {
5384
+ for (const file of [
5385
+ ...stringList(entry.js),
5386
+ ...stringList(entry.css)
5387
+ ]){
5388
+ const parsed = parseCanonicalContentScriptAsset(file);
5389
+ if (parsed) return parsed.index;
5390
+ }
5391
+ }
5392
+ function planDevContentScripts(manifest) {
5393
+ if (3 !== manifest.manifest_version) return;
5394
+ const groups = manifest.content_scripts;
5395
+ if (!Array.isArray(groups) || 0 === groups.length) return;
5396
+ const entries = [];
5397
+ const stubs = {};
5398
+ const nextGroups = groups.map((group)=>{
5399
+ const index = canonicalIndexOf(group);
5400
+ const matches = stringList(group.matches);
5401
+ const js = stringList(group.js);
5402
+ const css = stringList(group.css);
5403
+ if (void 0 === index || 0 === matches.length) return group;
5404
+ if (0 === js.length && 0 === css.length) return group;
5405
+ const entryName = getCanonicalContentScriptEntryName(index);
5406
+ const excludeMatches = stringList(group.exclude_matches);
5407
+ const stubOnly = stringList(group.include_globs).length > 0 || stringList(group.exclude_globs).length > 0;
5408
+ entries.push({
5409
+ id: `${DEV_CONTENT_SCRIPT_ID_PREFIX}${index}`,
5410
+ entry: entryName,
5411
+ matches,
5412
+ ...excludeMatches.length > 0 ? {
5413
+ excludeMatches
5414
+ } : {},
5415
+ js,
5416
+ css,
5417
+ runAt: runAtOf(group.run_at),
5418
+ allFrames: true === group.all_frames,
5419
+ world: 'MAIN' === group.world ? 'MAIN' : 'ISOLATED',
5420
+ ...true === group.match_origin_as_fallback ? {
5421
+ matchOriginAsFallback: true
5422
+ } : {},
5423
+ stubOnly
5424
+ });
5425
+ const stubAsset = devContentScriptStubAssetName(index);
5426
+ stubs[stubAsset] = buildDevContentScriptStubSource(entryName);
5427
+ const { css: _css, world: _world, ...rest } = group;
5428
+ return {
5429
+ ...rest,
5430
+ js: [
5431
+ stubAsset
5432
+ ]
5433
+ };
5434
+ });
5435
+ if (0 === entries.length) return;
5436
+ return {
5437
+ manifest: {
5438
+ ...manifest,
5439
+ content_scripts: nextGroups
5440
+ },
5441
+ registry: {
5442
+ version: 1,
5443
+ entries
5444
+ },
5445
+ stubs
5446
+ };
5447
+ }
5448
+ function buildDevContentScriptStubSource(entryName) {
5449
+ return `;(function(){try{var c=globalThis.chrome;if(!c||!c.runtime||!c.runtime.sendMessage)return;c.runtime.sendMessage({${DEV_CONTENT_SCRIPT_STUB_MESSAGE_KEY}:{entry:${JSON.stringify(entryName)}}},function(){try{void c.runtime.lastError}catch(e){}})}catch(e){}})();\n`;
5450
+ }
5451
+ function buildDevContentScriptMarkerPrelude(entryName, assetName) {
5452
+ return `;(function(){try{var g=globalThis,k=${JSON.stringify(DEV_CONTENT_SCRIPT_MARKER_KEY)};(g[k]=g[k]||{})[${JSON.stringify(entryName)}]=${JSON.stringify(assetName)}}catch(e){}})();\n`;
5453
+ }
5454
+ const DEV_CONTENT_SCRIPTS_RUNTIME_SOURCE = `;(function () {
5455
+ try {
5456
+ var g = (typeof globalThis !== "undefined" ? globalThis : self);
5457
+ if (!g || g.__extjsDevContentScriptsInstalled) return;
5458
+ var chrome = g.chrome;
5459
+ if (!chrome || !chrome.runtime || !chrome.scripting || !chrome.tabs) return;
5460
+ g.__extjsDevContentScriptsInstalled = true;
5461
+
5462
+ var REGISTRY = ${JSON.stringify(DEV_CONTENT_SCRIPT_REGISTRY_ASSET)};
5463
+ var MARKER = ${JSON.stringify(DEV_CONTENT_SCRIPT_MARKER_KEY)};
5464
+ var STUB = ${JSON.stringify(DEV_CONTENT_SCRIPT_STUB_MESSAGE_KEY)};
5465
+ var ID_PREFIX = ${JSON.stringify(DEV_CONTENT_SCRIPT_ID_PREFIX)};
5466
+ var registry = null;
5467
+ var ready = false;
5468
+ var whenReady = [];
5469
+
5470
+ function noop() { try { void chrome.runtime.lastError; } catch (e) {
5471
+ // Ignore
5472
+ } }
5473
+ function isString(v) { return typeof v === "string"; }
5474
+ function injectableUrl(url) { return typeof url === "string" && /^(https?|file|ftp):/i.test(url); }
5475
+ function call(fn) { if (fn) { try { fn(); } catch (e) {
5476
+ // Ignore
5477
+ } } }
5478
+ function onReady(fn) { if (ready) call(fn); else whenReady.push(fn); }
5479
+ function markReady() {
5480
+ ready = true;
5481
+ var fns = whenReady;
5482
+ whenReady = [];
5483
+ for (var i = 0; i < fns.length; i++) call(fns[i]);
5484
+ }
5485
+
5486
+ // The registry is read FROM DISK every time: the bundles it names are
5487
+ // content-hashed and the worker outlives many edits.
5488
+ function load(cb) {
5489
+ try {
5490
+ if (typeof g.fetch !== "function") return cb(null);
5491
+ g.fetch(chrome.runtime.getURL(REGISTRY), {cache: "no-store"})
5492
+ .then(function (r) { return r && r.ok ? r.json() : null; })
5493
+ .then(function (reg) { cb(reg && Array.isArray(reg.entries) ? reg : null); })
5494
+ .catch(function () { cb(null); });
5495
+ } catch (e) { cb(null); }
5496
+ }
5497
+
5498
+ function registrationOf(e) {
5499
+ var s = {
5500
+ id: e.id,
5501
+ matches: e.matches,
5502
+ runAt: e.runAt === "document_start" || e.runAt === "document_end" ? e.runAt : "document_idle",
5503
+ allFrames: !!e.allFrames,
5504
+ world: e.world === "MAIN" ? "MAIN" : "ISOLATED",
5505
+ persistAcrossSessions: false
5506
+ };
5507
+ var js = (e.js || []).filter(isString), css = (e.css || []).filter(isString);
5508
+ if (Array.isArray(e.excludeMatches) && e.excludeMatches.length) s.excludeMatches = e.excludeMatches;
5509
+ if (js.length) s.js = js;
5510
+ if (css.length) s.css = css;
5511
+ if (e.matchOriginAsFallback) s.matchOriginAsFallback = true;
5512
+ return s;
5513
+ }
5514
+
5515
+ // Register what the registry names, update what is already registered,
5516
+ // drop dev registrations it no longer names. Idempotent per entry id.
5517
+ function sync(reg, done) {
5518
+ try {
5519
+ if (!chrome.scripting.registerContentScripts || !chrome.scripting.getRegisteredContentScripts) return call(done);
5520
+ var wanted = [];
5521
+ for (var i = 0; i < reg.entries.length; i++) {
5522
+ var e = reg.entries[i] || {};
5523
+ if (e.stubOnly || !isString(e.id) || !Array.isArray(e.matches) || !e.matches.length) continue;
5524
+ var r = registrationOf(e);
5525
+ if (!r.js && !r.css) continue;
5526
+ wanted.push(r);
5527
+ }
5528
+ chrome.scripting.getRegisteredContentScripts(function (existing) {
5529
+ noop();
5530
+ var have = {};
5531
+ if (existing) {
5532
+ for (var k = 0; k < existing.length; k++) {
5533
+ if (existing[k] && isString(existing[k].id)) have[existing[k].id] = true;
5534
+ }
5535
+ }
5536
+ var toRegister = [], toUpdate = [], toDrop = [], keep = {};
5537
+ for (var j = 0; j < wanted.length; j++) {
5538
+ keep[wanted[j].id] = true;
5539
+ (have[wanted[j].id] ? toUpdate : toRegister).push(wanted[j]);
5540
+ }
5541
+ for (var id in have) {
5542
+ if (id.indexOf(ID_PREFIX) === 0 && !keep[id]) toDrop.push(id);
5543
+ }
5544
+ var ops = [];
5545
+ if (toDrop.length && chrome.scripting.unregisterContentScripts) ops.push(["unregisterContentScripts", {ids: toDrop}]);
5546
+ if (toUpdate.length && chrome.scripting.updateContentScripts) ops.push(["updateContentScripts", toUpdate]);
5547
+ if (toRegister.length) ops.push(["registerContentScripts", toRegister]);
5548
+ var pending = ops.length;
5549
+ if (!pending) return call(done);
5550
+ var step = function () { pending--; if (pending <= 0) call(done); };
5551
+ for (var o = 0; o < ops.length; o++) {
5552
+ try { chrome.scripting[ops[o][0]](ops[o][1], function () { noop(); step(); }); } catch (e) { step(); }
5553
+ }
5554
+ });
5555
+ } catch (e) { call(done); }
5556
+ }
5557
+
5558
+ // Inject one entry into the frames of a tab whose world does not already
5559
+ // run the current file. Chromium's own injection sets the same marker,
5560
+ // so a frame it covered is skipped and nothing ever runs twice.
5561
+ function inject(e, target, done) {
5562
+ var world = e.world === "MAIN" ? "MAIN" : "ISOLATED";
5563
+ var js = (e.js || []).filter(isString), css = (e.css || []).filter(isString);
5564
+ var probe = {target: target, world: world, injectImmediately: true, args: [MARKER, e.entry, js[0] || ""],
5565
+ func: function (key, entry, file) { try { var m = globalThis[key]; return !!(m && m[entry] === file); } catch (x) { return false; } }};
5566
+ try {
5567
+ chrome.scripting.executeScript(probe, function (results) {
5568
+ noop();
5569
+ var frameIds = [];
5570
+ if (results) {
5571
+ for (var i = 0; i < results.length; i++) {
5572
+ var r = results[i];
5573
+ if (!r || r.result === true) continue;
5574
+ frameIds.push(typeof r.frameId === "number" ? r.frameId : 0);
5575
+ }
5576
+ }
5577
+ if (!frameIds.length) return call(done);
5578
+ var into = {tabId: target.tabId, frameIds: frameIds};
5579
+ if (css.length && chrome.scripting.insertCSS) {
5580
+ try { chrome.scripting.insertCSS({target: into, files: css}, noop); } catch (x) {
5581
+ // Ignore
5582
+ }
5583
+ }
5584
+ if (!js.length) return call(done);
5585
+ try {
5586
+ chrome.scripting.executeScript({target: into, files: js, world: world, injectImmediately: true}, function () { noop(); call(done); });
5587
+ } catch (x) { call(done); }
5588
+ });
5589
+ } catch (x) { call(done); }
5590
+ }
5591
+
5592
+ // The open tabs an entry matches, exclude_matches honored, tabs still
5593
+ // loading skipped when asked: Chromium injects those itself on landing.
5594
+ function tabsFor(e, completeOnly, cb) {
5595
+ var matches = Array.isArray(e.matches) ? e.matches : [];
5596
+ if (!matches.length) return cb([]);
5597
+ var excludes = Array.isArray(e.excludeMatches) ? e.excludeMatches : [];
5598
+ var collect = function (excludedIds) {
5599
+ try {
5600
+ chrome.tabs.query({url: matches}, function (tabs) {
5601
+ noop();
5602
+ var out = [];
5603
+ for (var i = 0; tabs && i < tabs.length; i++) {
5604
+ var t = tabs[i];
5605
+ if (!t || t.id == null || !injectableUrl(t.url) || excludedIds[t.id]) continue;
5606
+ if (completeOnly && t.status === "loading") continue;
5607
+ out.push(t);
5608
+ }
5609
+ cb(out);
5610
+ });
5611
+ } catch (x) { cb([]); }
5612
+ };
5613
+ if (!excludes.length) return collect({});
5614
+ try {
5615
+ chrome.tabs.query({url: excludes}, function (tabs) {
5616
+ noop();
5617
+ var ids = {};
5618
+ for (var i = 0; tabs && i < tabs.length; i++) if (tabs[i] && tabs[i].id != null) ids[tabs[i].id] = true;
5619
+ collect(ids);
5620
+ });
5621
+ } catch (x) { collect({}); }
5622
+ }
5623
+
5624
+ function onStub(msg, sender) {
5625
+ if (!msg || !isString(msg.entry) || !sender || !sender.tab || sender.tab.id == null) return;
5626
+ var tabId = sender.tab.id;
5627
+ var frameId = typeof sender.frameId === "number" ? sender.frameId : 0;
5628
+ onReady(function () {
5629
+ if (!registry) return;
5630
+ for (var i = 0; i < registry.entries.length; i++) {
5631
+ var e = registry.entries[i];
5632
+ if (e && e.entry === msg.entry) inject(e, {tabId: tabId, frameIds: [frameId]});
5633
+ }
5634
+ });
5635
+ }
5636
+
5637
+ // After a worker restart the open tabs still run the previous generation,
5638
+ // and Chromium never injects into a tab that finished loading before it.
5639
+ function heal(done) {
5640
+ onReady(function () {
5641
+ if (!registry) return call(done);
5642
+ var pending = 1;
5643
+ var step = function () { pending--; if (pending <= 0) call(done); };
5644
+ for (var i = 0; i < registry.entries.length; i++) {
5645
+ (function (e) {
5646
+ if (!e || e.stubOnly) return;
5647
+ pending++;
5648
+ tabsFor(e, true, function (tabs) {
5649
+ var left = tabs.length;
5650
+ if (!left) return step();
5651
+ for (var t = 0; t < tabs.length; t++) {
5652
+ inject(e, {tabId: tabs[t].id, allFrames: !!e.allFrames}, function () { if (--left === 0) step(); });
5653
+ }
5654
+ });
5655
+ })(registry.entries[i]);
5656
+ }
5657
+ step();
5658
+ });
5659
+ }
5660
+
5661
+ // A content script edit: re-register at the new files, then reload every
5662
+ // tab the edited entries match so each runs exactly one fresh copy. Returns
5663
+ // false when no registry exists, and the caller keeps its own path.
5664
+ function reload(names, done) {
5665
+ load(function (reg) {
5666
+ if (!reg) return call(function () { done(false); });
5667
+ registry = reg;
5668
+ sync(reg, function () {
5669
+ markReady();
5670
+ var wanted = Array.isArray(names) && names.length ? names : null;
5671
+ var seen = {};
5672
+ var pending = 1;
5673
+ var step = function () { pending--; if (pending <= 0) call(function () { done(true); }); };
5674
+ for (var i = 0; i < reg.entries.length; i++) {
5675
+ (function (e) {
5676
+ if (!e || (wanted && wanted.indexOf(e.entry) === -1)) return;
5677
+ pending++;
5678
+ tabsFor(e, false, function (tabs) {
5679
+ for (var t = 0; t < tabs.length; t++) {
5680
+ var id = tabs[t].id;
5681
+ if (seen[id]) continue;
5682
+ seen[id] = true;
5683
+ try { chrome.tabs.reload(id, {}, noop); } catch (x) {
5684
+ // Ignore
5685
+ }
5686
+ }
5687
+ step();
5688
+ });
5689
+ })(reg.entries[i]);
5690
+ }
5691
+ step();
5692
+ });
5693
+ });
5694
+ }
5695
+
5696
+ try {
5697
+ chrome.runtime.onMessage.addListener(function (msg, sender) {
5698
+ if (!msg || !msg[STUB]) return;
5699
+ onStub(msg[STUB], sender);
5700
+ });
5701
+ } catch (e) {
5702
+ // Ignore
5703
+ }
5704
+
5705
+ g.__extjsDevContentScripts = {heal: heal, reload: reload, isReady: function () { return ready; }};
5706
+
5707
+ load(function (reg) {
5708
+ registry = reg;
5709
+ if (!reg) return markReady();
5710
+ sync(reg, markReady);
5711
+ });
5712
+ } catch (e) {
5713
+ // Ignore
5714
+ }
5715
+ })();
5716
+ `;
5717
+ const setup_dev_content_scripts_BACKGROUND_ASSET = /(^|\/)background\/(?:service_worker|scripts?|index)\.js$/i;
5718
+ function putAsset(compilation, name, text) {
5719
+ const source = new core_sources.RawSource(text);
5720
+ if (compilation.getAsset(name)) compilation.updateAsset(name, source);
5721
+ else compilation.emitAsset(name, source);
5722
+ }
5723
+ class SetupDevContentScripts {
5724
+ apply(compiler) {
5725
+ compiler.hooks.thisCompilation.tap(SetupDevContentScripts.name, (compilation)=>{
5726
+ compilation.hooks.processAssets.tap({
5727
+ name: SetupDevContentScripts.name,
5728
+ stage: core_Compilation.PROCESS_ASSETS_STAGE_REPORT + 100
5729
+ }, ()=>{
5730
+ if (compilation.errors.length > 0) return;
5731
+ const background = compilation.getAssets().find((asset)=>setup_dev_content_scripts_BACKGROUND_ASSET.test(asset.name));
5732
+ if (!background) return;
5733
+ const manifestAsset = compilation.getAsset('manifest.json');
5734
+ if (!manifestAsset) return;
5735
+ let manifest;
5736
+ try {
5737
+ manifest = JSON.parse(getCurrentManifestContent(compilation) || manifestAsset.source.source().toString());
5738
+ } catch {
5739
+ return;
5740
+ }
5741
+ const plan = planDevContentScripts(manifest);
5742
+ if (!plan) return;
5743
+ for (const asset of compilation.getAssets()){
5744
+ const entry = contentScriptEntryForAsset(asset.name);
5745
+ if (!entry) continue;
5746
+ const text = asset.source.source().toString();
5747
+ if (!text.includes(buildDevContentScriptMarkerPrelude(entry, asset.name))) prependToEmittedAsset(compilation, asset, buildDevContentScriptMarkerPrelude(entry, asset.name));
5748
+ }
5749
+ for (const [name, text] of Object.entries(plan.stubs))putAsset(compilation, name, text);
5750
+ putAsset(compilation, DEV_CONTENT_SCRIPT_REGISTRY_ASSET, `${JSON.stringify(plan.registry, null, 2)}\n`);
5751
+ const manifestText = JSON.stringify(plan.manifest, null, 2);
5752
+ putAsset(compilation, 'manifest.json', manifestText);
5753
+ setCurrentManifestContent(compilation, manifestText);
5754
+ const backgroundText = background.source.source().toString();
5755
+ if (backgroundText.includes('__extjsDevContentScriptsInstalled')) return;
5756
+ prependToEmittedAsset(compilation, background, DEV_CONTENT_SCRIPTS_RUNTIME_SOURCE);
5757
+ });
5758
+ });
5759
+ }
5760
+ }
5283
5761
  const utils_isRemoteUrl = (entry)=>/^([a-z][a-z0-9+.-]*:)?\/\//i.test(entry);
5284
5762
  function resolveScriptEntryPath(entry, manifestDir, projectPath) {
5285
5763
  if (!entry || utils_isRemoteUrl(entry)) return entry;
@@ -6563,6 +7041,7 @@ class ReloadPlugin {
6563
7041
  browser: this.browser
6564
7042
  }).apply(compiler);
6565
7043
  new InjectScriptsReplayShim().apply(compiler);
7044
+ if (isChromiumBasedBrowser(String(this.browser))) new SetupDevContentScripts().apply(compiler);
6566
7045
  new InjectBridgeProducer().apply(compiler);
6567
7046
  new InjectBridgeRelay().apply(compiler);
6568
7047
  new PruneStaleHotUpdates().apply(compiler);
package/package.json CHANGED
@@ -43,7 +43,7 @@
43
43
  "runtime"
44
44
  ],
45
45
  "name": "extension-develop",
46
- "version": "4.1.24",
46
+ "version": "4.1.25",
47
47
  "description": "Develop, build, preview, and package Extension.js projects.",
48
48
  "author": {
49
49
  "name": "Cezar Augusto",