Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 8 additions & 0 deletions extension/dist/assets/protocol-Z52ThYIj.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
const DAEMON_PORT = 19825;
const DAEMON_HOST = "localhost";
const DAEMON_WS_URL = `ws://${DAEMON_HOST}:${DAEMON_PORT}/ext`;
const DAEMON_PING_URL = `http://${DAEMON_HOST}:${DAEMON_PORT}/ping`;
const WS_RECONNECT_BASE_DELAY = 2e3;
const WS_RECONNECT_MAX_DELAY = 6e4;

export { DAEMON_PING_URL as D, WS_RECONNECT_BASE_DELAY as W, DAEMON_WS_URL as a, WS_RECONNECT_MAX_DELAY as b };
144 changes: 144 additions & 0 deletions extension/dist/offscreen.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,144 @@
import { a as DAEMON_WS_URL, W as WS_RECONNECT_BASE_DELAY, b as WS_RECONNECT_MAX_DELAY } from './assets/protocol-Z52ThYIj.js';

let ws = null;
let reconnectTimer = null;
let reconnectAttempts = 0;
let pendingFrames = [];
let flushTimer = null;
let flushingFrames = false;
const MAX_EAGER_ATTEMPTS = 6;
const FRAME_RETRY_DELAY = 1e3;
function sendLog(level, msg) {
chrome.runtime.sendMessage({ type: "log", level, msg, ts: Date.now() }).catch(() => {
});
}
const _origLog = console.log.bind(console);
const _origWarn = console.warn.bind(console);
const _origError = console.error.bind(console);
console.log = (...args) => {
_origLog(...args);
sendLog("info", args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" "));
};
console.warn = (...args) => {
_origWarn(...args);
sendLog("warn", args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" "));
};
console.error = (...args) => {
_origError(...args);
sendLog("error", args.map((a) => typeof a === "string" ? a : JSON.stringify(a)).join(" "));
};
async function probeDaemon() {
try {
const resp = await chrome.runtime.sendMessage({ type: "ws-probe" });
return resp?.ok === true;
} catch {
return false;
}
}
async function connect() {
if (ws?.readyState === WebSocket.OPEN || ws?.readyState === WebSocket.CONNECTING) return;
if (!await probeDaemon()) {
scheduleReconnect();
return;
}
try {
ws = new WebSocket(DAEMON_WS_URL);
} catch {
scheduleReconnect();
return;
}
ws.onopen = () => {
console.log("[opencli/offscreen] Connected to daemon");
reconnectAttempts = 0;
if (reconnectTimer) {
clearTimeout(reconnectTimer);
reconnectTimer = null;
}
ws?.send(JSON.stringify({ type: "hello", version: chrome.runtime.getManifest().version }));
void flushPendingFrames();
};
ws.onmessage = (event) => {
pendingFrames.push(event.data);
void flushPendingFrames();
};
ws.onclose = () => {
console.log("[opencli/offscreen] Disconnected from daemon");
ws = null;
scheduleReconnect();
};
ws.onerror = () => {
ws?.close();
};
}
function scheduleFlush() {
if (flushTimer) return;
flushTimer = setTimeout(() => {
flushTimer = null;
void flushPendingFrames();
}, FRAME_RETRY_DELAY);
}
async function flushPendingFrames() {
if (flushingFrames || pendingFrames.length === 0) return;
flushingFrames = true;
try {
while (pendingFrames.length > 0) {
let delivered = false;
try {
const resp = await chrome.runtime.sendMessage({
type: "ws-message",
data: pendingFrames[0]
});
delivered = resp?.ok === true;
} catch {
delivered = false;
}
if (!delivered) {
scheduleFlush();
break;
}
pendingFrames.shift();
}
} finally {
flushingFrames = false;
if (pendingFrames.length > 0 && !flushTimer) scheduleFlush();
}
}
function scheduleReconnect() {
if (reconnectTimer) return;
reconnectAttempts++;
if (reconnectAttempts > MAX_EAGER_ATTEMPTS) return;
const delay = Math.min(WS_RECONNECT_BASE_DELAY * Math.pow(2, reconnectAttempts - 1), WS_RECONNECT_MAX_DELAY);
reconnectTimer = setTimeout(() => {
reconnectTimer = null;
void connect();
}, delay);
}
chrome.runtime.onMessage.addListener((msg, _sender, sendResponse) => {
if (msg?.type === "ws-connect") {
reconnectTimer = null;
reconnectAttempts = 0;
void connect();
sendResponse({ ok: true });
return false;
}
if (msg?.type === "ws-send") {
if (ws?.readyState === WebSocket.OPEN) {
ws.send(msg.payload);
sendResponse({ ok: true });
} else {
sendResponse({ ok: false, error: "WebSocket not open" });
}
return false;
}
if (msg?.type === "ws-status") {
sendResponse({
type: "ws-status-reply",
connected: ws?.readyState === WebSocket.OPEN,
reconnecting: reconnectTimer !== null
});
return false;
}
return false;
});
void connect();
console.log("[opencli/offscreen] Offscreen document ready");
3 changes: 2 additions & 1 deletion extension/manifest.json
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,8 @@
"tabs",
"cookies",
"activeTab",
"alarms"
"alarms",
"offscreen"
],
"host_permissions": [
"<all_urls>"
Expand Down
4 changes: 4 additions & 0 deletions extension/offscreen.html
Original file line number Diff line number Diff line change
@@ -0,0 +1,4 @@
<!DOCTYPE html>
<html><head><title>OpenCLI Offscreen</title></head>
<body><script src="dist/offscreen.js" type="module"></script></body>
</html>
51 changes: 51 additions & 0 deletions extension/scripts/package-release.mjs
Original file line number Diff line number Diff line change
Expand Up @@ -65,6 +65,10 @@ function collectManifestEntrypoints(manifest) {
for (const entry of manifest.web_accessible_resources ?? []) {
for (const resource of entry.resources ?? []) addLocalAsset(files, resource);
}
// MV3 offscreen documents are created at runtime via chrome.offscreen.createDocument()
// and are not referenced directly from manifest entry fields, so include the
// conventional offscreen page explicitly when the permission is present.
if ((manifest.permissions ?? []).includes('offscreen')) files.add('offscreen.html');
if (manifest.default_locale) files.add('_locales');

return [...files];
Expand Down Expand Up @@ -94,16 +98,43 @@ async function collectHtmlDependencies(relativeHtmlPath, files, visited) {
}
}

async function collectScriptDependencies(relativeScriptPath, files, visited) {
if (visited.has(relativeScriptPath)) return;
visited.add(relativeScriptPath);

const scriptPath = path.join(extensionDir, relativeScriptPath);
const source = await fs.readFile(scriptPath, 'utf8');
const importRe = /\bimport\s+(?:[^"'()]+?\s+from\s+)?["']([^"']+)["']|\bimport\(\s*["']([^"']+)["']\s*\)/g;

for (const match of source.matchAll(importRe)) {
const rawRef = match[1] ?? match[2];
const cleanRef = rawRef?.split('?')[0];
if (!isLocalAsset(cleanRef)) continue;

const resolvedRelativePath = cleanRef.startsWith('/')
? cleanRef.slice(1)
: path.posix.normalize(path.posix.join(path.posix.dirname(relativeScriptPath), cleanRef));

addLocalAsset(files, resolvedRelativePath);

if (resolvedRelativePath.endsWith('.js') || resolvedRelativePath.endsWith('.mjs')) {
await collectScriptDependencies(resolvedRelativePath, files, visited);
}
}
}

async function collectManifestAssets(manifest) {
const files = new Set(collectManifestEntrypoints(manifest));
const htmlPages = [];
const scriptEntries = [];

if (manifest.action?.default_popup) {
htmlPages.push(manifest.action.default_popup);
}
if (manifest.options_page) htmlPages.push(manifest.options_page);
if (manifest.devtools_page) htmlPages.push(manifest.devtools_page);
if (manifest.side_panel?.default_path) htmlPages.push(manifest.side_panel.default_path);
if ((manifest.permissions ?? []).includes('offscreen')) htmlPages.push('offscreen.html');
for (const page of manifest.sandbox?.pages ?? []) htmlPages.push(page);
for (const overridePage of Object.values(manifest.chrome_url_overrides ?? {})) htmlPages.push(overridePage);

Expand All @@ -114,6 +145,26 @@ async function collectManifestAssets(manifest) {
}
}

if (manifest.background?.service_worker && isLocalAsset(manifest.background.service_worker)) {
scriptEntries.push(manifest.background.service_worker);
}
for (const contentScript of manifest.content_scripts ?? []) {
for (const jsFile of contentScript.js ?? []) {
if (isLocalAsset(jsFile)) scriptEntries.push(jsFile);
}
}

for (const file of files) {
if (typeof file === 'string' && (file.endsWith('.js') || file.endsWith('.mjs'))) {
scriptEntries.push(file);
}
}

const scriptVisited = new Set();
for (const scriptEntry of new Set(scriptEntries)) {
await collectScriptDependencies(scriptEntry, files, scriptVisited);
}

return [...files];
}

Expand Down
Loading