diff --git a/extension/panel-bridge.js b/extension/panel-bridge.js index b1623d1..9333685 100644 --- a/extension/panel-bridge.js +++ b/extension/panel-bridge.js @@ -7,65 +7,80 @@ const status = document.getElementById('status'); const tabId = chrome.devtools.inspectedWindow.tabId; const LOCAL_HOSTS = ['localhost', '127.0.0.1']; -// Try to find the devframe connection on the inspected page +// Where devframe may be mounted. +const PATHS = ['/__ng-devtools/', '/__devframe/', '/']; +const CONNECTION_FILES = ['__devframe/__connection.json', '__connection.json']; +const PROBE_TIMEOUT_MS = 1500; + +let detection = 0; + +// Look for a devframe connection, but only on a loopback page: nothing else +// can be connected to, so nothing else is worth probing. function detectConnection() { - // Try common devframe mount paths - const paths = ['/__ng-devtools/', '/__devframe/', '/']; + const run = ++detection; + chrome.devtools.inspectedWindow.eval('location.origin', (origin, error) => { + if (run !== detection) return; + if (error || typeof origin !== 'string') { + loadPanel(null); + return; + } - chrome.devtools.inspectedWindow.eval( - `(function() { - const paths = ${JSON.stringify(paths)}; - for (const base of paths) { - try { - const xhr = new XMLHttpRequest(); - xhr.open('GET', base + '__devframe/__connection.json', false); - xhr.send(); - if (xhr.status === 200) { - return { base: base, connection: JSON.parse(xhr.responseText) }; - } - } catch(e) {} - try { - const xhr = new XMLHttpRequest(); - xhr.open('GET', base + '__connection.json', false); - xhr.send(); - if (xhr.status === 200) { - return { base: base, connection: JSON.parse(xhr.responseText) }; - } - } catch(e) {} - } - return null; - })()`, - (result, err) => { - if (result && paths.includes(result.base)) { - loadPanel(result.base); - } else { - // No live devframe found — load in standalone/static mode - loadPanel(null); + let hostname; + try { + hostname = new URL(origin).hostname; + } catch { + loadPanel(null); + return; + } + + if (!LOCAL_HOSTS.includes(hostname)) { + loadPanel(null); + return; + } + + findConnection(origin).then((base) => { + if (run === detection) loadPanel(base, origin); + }); + }); +} + +// The first mount path that answers with a connection file, or null. +async function findConnection(origin) { + for (const base of PATHS) { + for (const file of CONNECTION_FILES) { + try { + const response = await fetch(new URL(base + file, origin), { + credentials: 'omit', + cache: 'no-store', + redirect: 'error', + signal: AbortSignal.timeout(PROBE_TIMEOUT_MS), + }); + if (!response.ok) continue; + await response.json(); + return base; + } catch { + // Not mounted here; try the next one. } - }, - ); + } + } + return null; } -function loadPanel(baseURL) { +function loadPanel(baseURL, origin) { status.classList.add('hidden'); frame.style.display = 'block'; // The SPA is bundled inside the extension at ui/index.html const panelUrl = chrome.runtime.getURL('ui/index.html'); - if (baseURL) { - // Get the inspected page's origin to build the full baseURL - chrome.devtools.inspectedWindow.eval('location.origin', (origin) => { - const url = new URL(baseURL, origin); - if (!LOCAL_HOSTS.includes(url.hostname)) { - frame.src = panelUrl; - return; - } - frame.src = `${panelUrl}?baseURL=${encodeURIComponent(url.href)}`; - }); - } else { + if (!baseURL || !origin) { frame.src = panelUrl; + return; } + const url = new URL(baseURL, origin); + frame.src = LOCAL_HOSTS.includes(url.hostname) + ? `${panelUrl}?baseURL=${encodeURIComponent(url.href)}` + : panelUrl; } // Start detection after a short delay to let the page settle