Skip to content

Commit 5163eff

Browse files
committed
Chrome extension stuff
1 parent 6cc7005 commit 5163eff

26 files changed

+5809
-54
lines changed

docs/demos/index.tsx

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,8 @@
1+
import "@preact/signals-debug";
12
import { render } from "preact";
23
import { LocationProvider, Router, useLocation, lazy } from "preact-iso";
34
import { signal, useSignal } from "@preact/signals";
45
import { setFlashingEnabled, constrainFlashToChildren } from "./render-flasher";
5-
import "@preact/signals-debug";
66

77
// disable flashing during initial render:
88
setFlashingEnabled(false);

extension/.gitignore

Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# Extension artifacts
2+
*.crx
3+
*.pem
4+
*.zip
5+
6+
# Build outputs
7+
dist/
8+
build/
9+
10+
# Node modules
11+
node_modules/
12+
13+
# Temporary files
14+
.tmp/
15+
.cache/
16+
17+
# OS files
18+
.DS_Store
19+
Thumbs.db
20+
21+
# IDE files
22+
.vscode/
23+
.idea/
24+
*.swp
25+
*.swo
26+
27+
# Web-ext artifacts
28+
web-ext-artifacts/
29+
30+
# Logs
31+
*.log
32+
npm-debug.log*
33+
yarn-debug.log*
34+
yarn-error.log*

extension/background.js

Lines changed: 129 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,129 @@
1+
// Background service worker for the extension
2+
3+
// Maps to store connections by tab ID
4+
const contentConnections = new Map(); // tab ID -> content script port
5+
const devtoolsConnections = new Map(); // tab ID -> devtools port
6+
7+
chrome.runtime.onConnect.addListener(port => {
8+
if (port.name === "content-to-background") {
9+
handleContentScriptConnection(port);
10+
} else if (port.name === "devtools-to-background") {
11+
handleDevToolsConnection(port);
12+
} else {
13+
console.warn("Unknown connection type:", port.name);
14+
port.disconnect();
15+
}
16+
});
17+
18+
function handleContentScriptConnection(port) {
19+
const tabId = port.sender?.tab?.id;
20+
21+
if (!tabId) {
22+
console.error("Content script connection missing tab ID");
23+
port.disconnect();
24+
return;
25+
}
26+
27+
contentConnections.set(tabId, port);
28+
29+
port.onMessage.addListener(message => {
30+
// Forward message to devtools if connected
31+
const devtoolsPort = devtoolsConnections.get(tabId);
32+
if (devtoolsPort) {
33+
try {
34+
devtoolsPort.postMessage(message);
35+
} catch (error) {
36+
console.error("Failed to forward message to devtools:", error);
37+
devtoolsConnections.delete(tabId);
38+
}
39+
} else {
40+
console.log(
41+
`No devtools connection for tab ${tabId}, message queued:`,
42+
message.type
43+
);
44+
}
45+
});
46+
47+
port.onDisconnect.addListener(() => {
48+
contentConnections.delete(tabId);
49+
50+
// Notify devtools if connected
51+
const devtoolsPort = devtoolsConnections.get(tabId);
52+
if (devtoolsPort) {
53+
try {
54+
devtoolsPort.postMessage({ type: "CONTENT_SCRIPT_DISCONNECTED" });
55+
} catch (error) {
56+
console.error(
57+
"Failed to notify devtools of content script disconnect:",
58+
error
59+
);
60+
}
61+
}
62+
});
63+
}
64+
65+
function handleDevToolsConnection(port) {
66+
let tabId = null;
67+
let isInitialized = false;
68+
69+
// Listen for the initial tab ID message
70+
const tabIdListener = message => {
71+
if (message.type === "DEVTOOLS_TAB_ID" && !isInitialized) {
72+
tabId = message.tabId;
73+
isInitialized = true;
74+
75+
devtoolsConnections.set(tabId, port);
76+
77+
// Remove the tab ID listener
78+
port.onMessage.removeListener(tabIdListener);
79+
80+
// Set up the main message listener
81+
port.onMessage.addListener(message => {
82+
// Forward message to content script if connected
83+
const contentPort = contentConnections.get(tabId);
84+
if (contentPort) {
85+
try {
86+
contentPort.postMessage(message);
87+
} catch (error) {
88+
console.error(
89+
"Failed to forward message to content script:",
90+
error
91+
);
92+
contentConnections.delete(tabId);
93+
}
94+
} else {
95+
console.log(`No content script connection for tab ${tabId}`);
96+
}
97+
});
98+
99+
// Send initial status to devtools
100+
const contentPort = contentConnections.get(tabId);
101+
try {
102+
port.postMessage({
103+
type: "BACKGROUND_READY",
104+
contentScriptConnected: !!contentPort,
105+
});
106+
} catch (error) {
107+
console.error("Failed to send initial status to devtools:", error);
108+
}
109+
}
110+
};
111+
112+
port.onMessage.addListener(tabIdListener);
113+
114+
port.onDisconnect.addListener(() => {
115+
if (tabId) {
116+
devtoolsConnections.delete(tabId);
117+
}
118+
});
119+
}
120+
121+
chrome.action.onClicked.addListener(tab => {
122+
chrome.tabs.sendMessage(tab.id, { type: "OPEN_DEVTOOLS_HINT" });
123+
});
124+
125+
// Clean up connections when tabs are closed
126+
chrome.tabs.onRemoved.addListener(tabId => {
127+
contentConnections.delete(tabId);
128+
devtoolsConnections.delete(tabId);
129+
});

extension/content.js

Lines changed: 213 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,213 @@
1+
// Content script that injects the bridge script into the page context
2+
// and communicates with the DevTools panel
3+
4+
let connectionPort = null;
5+
let isSignalsAvailable = false;
6+
let connectionAttempts = 0;
7+
const MAX_CONNECTION_ATTEMPTS = 5;
8+
let __HAS_BRIDGE_SCRIPT__ = false;
9+
10+
// Inject the bridge script into the page context
11+
function injectBridgeScript() {
12+
try {
13+
const script = document.createElement("script");
14+
script.src = chrome.runtime.getURL("inject.js");
15+
script.setAttribute("data-signals-devtools", "true");
16+
17+
script.onload = function () {
18+
script.remove();
19+
};
20+
21+
script.onerror = function () {
22+
console.error("Failed to load Preact Signals DevTools inject script");
23+
script.remove();
24+
};
25+
26+
// Inject into the page as early as possible
27+
const target = document.head || document.documentElement || document;
28+
target.appendChild(script);
29+
__HAS_BRIDGE_SCRIPT__ = true;
30+
} catch (error) {
31+
console.error("Failed to inject bridge script:", error);
32+
}
33+
}
34+
35+
// Inject the script as early as possible
36+
if (!__HAS_BRIDGE_SCRIPT__) {
37+
injectBridgeScript();
38+
}
39+
40+
// Listen for messages from the injected script
41+
window.addEventListener("message", event => {
42+
// Only accept messages from same origin for security
43+
if (event.source !== window || event.origin !== window.location.origin) {
44+
return;
45+
}
46+
47+
const { type, payload } = event.data;
48+
49+
switch (type) {
50+
case "SIGNALS_UPDATE":
51+
forwardToDevTools({
52+
type: "SIGNALS_UPDATE",
53+
payload: payload,
54+
timestamp: event.data.timestamp,
55+
});
56+
break;
57+
58+
case "SIGNALS_INIT_FROM_PAGE":
59+
forwardToDevTools({
60+
type: "SIGNALS_INIT",
61+
timestamp: event.data.timestamp,
62+
});
63+
break;
64+
65+
case "SIGNALS_AVAILABLE":
66+
isSignalsAvailable = payload.available;
67+
forwardToDevTools({
68+
type: "SIGNALS_AVAILABILITY",
69+
payload: payload,
70+
});
71+
break;
72+
73+
case "SIGNALS_CONFIG_FROM_PAGE":
74+
forwardToDevTools({
75+
type: "SIGNALS_CONFIG",
76+
payload: payload,
77+
});
78+
break;
79+
}
80+
});
81+
82+
// Forward messages to DevTools panel via background script
83+
function forwardToDevTools(message) {
84+
if (connectionPort) {
85+
try {
86+
connectionPort.postMessage(message);
87+
} catch (error) {
88+
console.error("Failed to send message to background:", error);
89+
// Port might be disconnected, try to reconnect
90+
connectionPort = null;
91+
connectToBackground();
92+
if (connectionPort) {
93+
try {
94+
connectionPort.postMessage(message);
95+
} catch (retryError) {
96+
console.error(
97+
"Failed to send message after reconnection:",
98+
retryError
99+
);
100+
}
101+
}
102+
}
103+
} else {
104+
// Try to establish connection
105+
connectToBackground();
106+
if (connectionPort) {
107+
try {
108+
connectionPort.postMessage(message);
109+
} catch (error) {
110+
console.error("Failed to send message on new connection:", error);
111+
}
112+
}
113+
}
114+
}
115+
116+
// Connect to background script
117+
function connectToBackground() {
118+
if (connectionAttempts >= MAX_CONNECTION_ATTEMPTS) {
119+
console.error("Max connection attempts reached, giving up");
120+
return;
121+
}
122+
123+
try {
124+
connectionAttempts++;
125+
connectionPort = chrome.runtime.connect({ name: "content-to-background" });
126+
127+
connectionPort.onMessage.addListener(message => {
128+
handleMessageFromDevTools(message);
129+
});
130+
131+
connectionPort.onDisconnect.addListener(() => {
132+
connectionPort = null;
133+
134+
// Send disconnect message to page
135+
window.postMessage(
136+
{
137+
type: "DEVTOOLS_DISCONNECTED",
138+
},
139+
"*"
140+
);
141+
142+
// Reset connection attempts after a delay
143+
setTimeout(() => {
144+
connectionAttempts = 0;
145+
}, 5000);
146+
});
147+
148+
// Reset connection attempts on successful connection
149+
connectionAttempts = 0;
150+
} catch (error) {
151+
console.error("Failed to connect to background script:", error);
152+
connectionPort = null;
153+
}
154+
}
155+
156+
// Handle messages from DevTools panel (via background script)
157+
function handleMessageFromDevTools(message) {
158+
const { type, payload } = message;
159+
160+
switch (type) {
161+
case "CONFIGURE_DEBUG":
162+
window.postMessage(
163+
{
164+
type: "CONFIGURE_DEBUG_FROM_EXTENSION",
165+
payload: payload,
166+
},
167+
"*"
168+
);
169+
break;
170+
171+
case "REQUEST_STATE":
172+
window.postMessage(
173+
{
174+
type: "REQUEST_STATE_FROM_EXTENSION",
175+
},
176+
"*"
177+
);
178+
179+
// Also manually trigger a state check
180+
setTimeout(() => {
181+
window.postMessage(
182+
{
183+
type: "CONTENT_SCRIPT_READY",
184+
},
185+
"*"
186+
);
187+
}, 100);
188+
break;
189+
190+
case "OPEN_DEVTOOLS_HINT":
191+
break;
192+
193+
default:
194+
console.log("Unhandled message from DevTools:", message);
195+
}
196+
}
197+
198+
// Listen for messages from background script (for extension icon clicks, etc.)
199+
chrome.runtime.onMessage.addListener((message, sender, sendResponse) => {
200+
handleMessageFromDevTools(message);
201+
return true; // Keep the message channel open for async response
202+
});
203+
204+
// Initial connection attempt
205+
connectToBackground();
206+
207+
// Announce presence to the page
208+
window.postMessage(
209+
{
210+
type: "CONTENT_SCRIPT_READY",
211+
},
212+
"*"
213+
);

0 commit comments

Comments
 (0)