1/**
2 * Impeccable DevTools Extension - Content Script
3 *
4 * Bridges between the extension messaging system and the page-context detector.
5 * The detector must run in page context (not isolated world) because it needs
6 * access to getComputedStyle, document.styleSheets.cssRules, etc.
7 *
8 * Wrapped in an IIFE with an idempotency flag so re-injection (via
9 * chrome.scripting.executeScript) is a no-op and doesn't cause:
10 * - SyntaxError: Identifier 'foo' has already been declared
11 * - Duplicate event listeners accumulating over time
12 */
13(function () {
14 if (window.__IMPECCABLE_CS_LOADED__) return;
15 window.__IMPECCABLE_CS_LOADED__ = true;
16
17 let injected = false;
18 let pendingScan = false;
19 let scanConfig = null;
20
21 // Listen for commands from the service worker
22 chrome.runtime.onMessage.addListener((msg, sender, sendResponse) => {
23 if (msg.action === 'scan') {
24 scanConfig = msg.config || null;
25 injectAndScan();
26 sendResponse({ ok: true });
27 } else if (msg.action === 'toggle-overlays') {
28 window.postMessage({ source: 'impeccable-command', action: 'toggle-overlays' }, '*');
29 sendResponse({ ok: true });
30 } else if (msg.action === 'remove') {
31 window.postMessage({ source: 'impeccable-command', action: 'remove' }, '*');
32 injected = false;
33 sendResponse({ ok: true });
34 } else if (msg.action === 'highlight') {
35 window.postMessage({ source: 'impeccable-command', action: 'highlight', selector: msg.selector }, '*');
36 sendResponse({ ok: true });
37 } else if (msg.action === 'unhighlight') {
38 window.postMessage({ source: 'impeccable-command', action: 'unhighlight' }, '*');
39 sendResponse({ ok: true });
40 }
41 return true;
42 });
43
44 // Listen for results and state changes from the detector in page context
45 window.addEventListener('message', (e) => {
46 if (e.source !== window || !e.data) return;
47
48 if (e.data.source === 'impeccable-results') {
49 chrome.runtime.sendMessage({
50 action: 'findings',
51 findings: e.data.findings,
52 count: e.data.count,
53 }).catch(() => {});
54 }
55
56 if (e.data.source === 'impeccable-overlays-toggled') {
57 chrome.runtime.sendMessage({
58 action: 'overlays-toggled',
59 visible: e.data.visible,
60 }).catch(() => {});
61 }
62
63 if (e.data.source === 'impeccable-ready') {
64 injected = true;
65 if (pendingScan) {
66 pendingScan = false;
67 sendScanCommand();
68 }
69 }
70 });
71
72 // Forward "page is active" signal to the extension when the cursor moves over the page.
73 // This is the reliable way to know the user has left the DevTools panel — the panel's
74 // own pointerleave/mouseleave events are unreliable on fast cursor movement.
75 let lastPageActive = 0;
76 document.addEventListener('pointermove', () => {
77 const now = Date.now();
78 if (now - lastPageActive < 150) return; // throttle
79 lastPageActive = now;
80 chrome.runtime.sendMessage({ action: 'page-pointer-active' }).catch(() => {});
81 }, { passive: true, capture: true });
82
83 // SPA navigation detection (pushState/replaceState don't fire events, but
84 // popstate and hashchange cover back/forward and hash navigation)
85 let lastUrl = location.href;
86 function onPossibleNavigation() {
87 if (location.href === lastUrl) return;
88 lastUrl = location.href;
89 if (injected) {
90 // Detector is still loaded in page context, just re-scan after DOM settles
91 setTimeout(sendScanCommand, 500);
92 }
93 }
94 window.addEventListener('popstate', onPossibleNavigation);
95 window.addEventListener('hashchange', onPossibleNavigation);
96
97 function sendScanCommand() {
98 const msg = { source: 'impeccable-command', action: 'scan' };
99 if (scanConfig) msg.config = scanConfig;
100 window.postMessage(msg, '*');
101 }
102
103 function injectAndScan() {
104 if (injected) {
105 sendScanCommand();
106 return;
107 }
108
109 // Set the extension flag via a data attribute (CSP-safe: content scripts share the DOM)
110 document.documentElement.dataset.impeccableExtension = 'true';
111
112 // Inject the detector script into page context
113 const script = document.createElement('script');
114 script.src = chrome.runtime.getURL('detector/detect.js');
115 script.dataset.impeccableExtension = 'true';
116 pendingScan = true;
117 script.onload = () => script.remove();
118 script.onerror = () => {
119 script.remove();
120 // Fallback: use chrome.scripting.executeScript for strict CSP pages
121 chrome.runtime.sendMessage({ action: 'inject-fallback' });
122 };
123 (document.head || document.documentElement).appendChild(script);
124 }
125})();