Yay a devtools!

This commit is contained in:
Emmaline Autumn 2025-02-15 12:46:42 -07:00
parent 9124abb749
commit 8d379461c3
8 changed files with 265 additions and 217 deletions

View File

@ -1,24 +1,54 @@
console.log("background.js loaded"); console.log("background.js loaded");
// Listen for messages from the devtools panel
browser.runtime.onMessage.addListener((message, sender, sendResponse) => { browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
// You could add a check to see if the message is from the devtools panel.
if (message.type === "GET_CONTEXT_STACK") { if (message.type === "GET_CONTEXT_STACK") {
// Forward the message to the content script running in the active tab. browser.tabs.sendMessage(message.tabId, message)
browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => { .then((res) => {
if (tabs.length) { console.log("RESPONSE", res);
browser.tabs.sendMessage(tabs[0].id, message).then(sendResponse); sendResponse(res);
} })
}); .catch((err) => {
// Return true to indicate you wish to send a response asynchronously console.log("Error sending message to content script:", err);
sendResponse({ error: err });
});
// browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => {
// if (tabs.length) {
// }
// });
return true; return true;
} else if (message.type === "UPDATE_CONTEXT_VALUE") { } else if (message.type === "UPDATE_CONTEXT_VALUE") {
// Forward update messages similarly browser.tabs.sendMessage(message.tabId, message).then(sendResponse);
browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => { // browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => {
if (tabs.length) { // if (tabs.length) {
browser.tabs.sendMessage(tabs[0].id, message).then(sendResponse); // }
} // });
});
return true; return true;
} }
}); });
let devtoolsPort = null;
browser.runtime.onConnect.addListener((port) => {
if (port.name === "devtools") {
devtoolsPort = port;
console.log("Devtools panel connected.");
// port.onMessage.addListener((msg) => {
// console.log("Received message from devtools panel:", msg);
// });
port.onDisconnect.addListener(() => {
devtoolsPort = null;
});
}
});
// Relay messages from content scripts to the devtools panel.
browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === "PAGE_LOADED") {
console.log("Background received PAGE_LOADED message from content script.");
if (devtoolsPort) {
devtoolsPort.postMessage({ type: "PAGE_LOADED" });
}
}
});

View File

@ -1,20 +1,12 @@
console.log("content.js loaded", window.wrappedJSObject);
// setTimeout(() => {
const getContextStack = () => window.wrappedJSObject.getContextStack();
const updateContextValue = (key, value, depth) =>
window.wrappedJSObject.updateContextValue(key, value, depth);
console.log(getContextStack());
browser.runtime.onMessage.addListener((message, sender, sendResponse) => { browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
if (message.type === "GET_CONTEXT_STACK") { if (message.type === "GET_CONTEXT_STACK") {
const contextStack = getContextStack(); const contextStack = window.wrappedJSObject.getContextStack();
sendResponse({ contextStack }); sendResponse({ contextStack });
} else if (message.type === "UPDATE_CONTEXT_VALUE") { } else if (message.type === "UPDATE_CONTEXT_VALUE") {
const { key, value, depth } = message; const { key, value, depth } = message;
if (glowindow.updateContextValue) { if (window.wrappedJSObject.updateContextValue) {
updateContextValue(key, value, depth); window.wrappedJSObject.updateContextValue(key, value, depth);
sendResponse({ success: true }); sendResponse({ success: true });
} else { } else {
sendResponse({ sendResponse({
@ -24,4 +16,5 @@ browser.runtime.onMessage.addListener((message, sender, sendResponse) => {
} }
} }
}); });
// }, 0);
browser.runtime.sendMessage({ type: "PAGE_LOADED" });

View File

@ -16,7 +16,8 @@
], ],
"js": [ "js": [
"content.js" "content.js"
] ],
"all_frames": true
} }
], ],
"permissions": [ "permissions": [

View File

@ -1,39 +1,48 @@
<!DOCTYPE html> <!DOCTYPE html>
<html> <html>
<head>
<meta charset="utf-8">
<title>Context Stack</title>
<style>
body {
font-family: sans-serif;
/* margin: 10px; */
/* width: 100%;
height: 100%; */
background-color: #302040;
color: #fff;
/* text-align: right; */
}
<head> .context-row {
<meta charset="utf-8"> display: flex;
<title>Context Stack</title> margin-bottom: 8px;
<style> }
body {
font-family: sans-serif;
/* margin: 10px; */
width: 100%;
height: 100%;
background-color: #302040;
}
.context-row { .context-key {
display: flex; width: 150px;
margin-bottom: 8px; font-weight: bold;
} }
.context-key { .context-value input {
width: 150px; width: 100%;
font-weight: bold; }
}
.context-value input { form {
width: 100%; margin-left: 1rem;
} margin-right: 1rem;
</style> }
</head> label {
display: inline-block;
margin-right: 1rem;
}
</style>
</head>
<body> <body>
<h1>Context Stack</h1> <h1>Context Stack</h1>
<div id="contextContainer"></div> <div id="contextContainer"></div>
<button id="refresh">Refresh</button> <button id="refresh">Refresh</button>
<script src="panel.js"></script> <script src="panel.js"></script>
</body> </body>
</html>
</html>

View File

@ -1,52 +1,127 @@
const port = browser.runtime.connect({ name: "devtools" });
port.onMessage.addListener((message) => {
if (message.type === "PAGE_LOADED") {
loadContextStack(); // Refresh your context stack here.
}
});
const tabId = browser.devtools.inspectedWindow.tabId;
document.getElementById("refresh").addEventListener("click", loadContextStack); document.getElementById("refresh").addEventListener("click", loadContextStack);
function loadContextStack() { function loadContextStack() {
const container = document.getElementById("contextContainer"); const container = document.getElementById("contextContainer");
container.innerHTML = ""; container.innerHTML = "";
browser.runtime.sendMessage({ type: "GET_CONTEXT_STACK" }).then( browser.runtime.sendMessage({ type: "GET_CONTEXT_STACK", tabId }).then(
(response) => { (response) => {
if (response.error) {
container.innerHTML = `<p>Error: ${response.error}</p>`;
return;
}
if (!response || !response.contextStack) { if (!response || !response.contextStack) {
container.innerHTML = "<p>No context stack found.</p>"; container.innerHTML = "<p>No context stack found.</p>";
return; return;
} }
const contextStack = response.contextStack; const contextStack = coalesceContextStack(response.contextStack);
for (const key in contextStack) { const form = generateObjectForm(contextStack);
const row = document.createElement("div"); container.appendChild(form);
row.classList.add("context-row");
const keyDiv = document.createElement("div");
keyDiv.classList.add("context-key");
keyDiv.textContent = key;
const valueDiv = document.createElement("div");
valueDiv.classList.add("context-value");
const input = document.createElement("input");
input.value = contextStack[key];
input.addEventListener("change", () => {
browser.runtime.sendMessage({
type: "UPDATE_CONTEXT_VALUE",
key,
value: input.value,
}).then((updateResponse) => {
if (updateResponse && updateResponse.success) {
console.log(`Updated ${key} to ${input.value}`);
} else {
console.error(`Failed to update ${key}:`, updateResponse.error);
}
});
});
valueDiv.appendChild(input);
row.appendChild(keyDiv);
row.appendChild(valueDiv);
container.appendChild(row);
}
}, },
); );
} }
// Initial load when the panel opens. function generateObjectForm(obj, path = "") {
loadContextStack(); const detail = document.createElement("details");
detail.open = path === "";
const summary = document.createElement("summary");
summary.textContent = path.split(".").at(-1);
detail.appendChild(summary);
const form = document.createElement("form");
let count = 0;
for (const [key, value] of Object.entries(obj)) {
if (value == undefined || value === "[Circular]") continue;
const isObject = value.constructor === Object;
const isArray = Array.isArray(value);
if (isObject || isArray) {
const nestedForm = generateObjectForm(
value,
path ? path + "." + key : key,
);
if (!nestedForm) {
continue;
}
const div = document.createElement("div");
div.appendChild(nestedForm);
form.appendChild(div);
count++;
continue;
}
const div = document.createElement("div");
const label = document.createElement("label");
label.textContent = key;
div.appendChild(label);
const input = document.createElement("input");
input.name = key;
const isBoolean = typeof value === "boolean";
if (isBoolean) {
input.type = "checkbox";
input.checked = value;
input.addEventListener("change", () => {
browser.runtime.sendMessage({
type: "UPDATE_CONTEXT_VALUE",
key: path ? path + "." + key : key,
value: input.checked,
tabId,
}).then((updateResponse) => {
if (updateResponse && updateResponse.success) {
console.log(`Updated ${key} to ${input.checked}`);
} else {
console.error(`Failed to update ${key}:`, updateResponse.error);
}
});
});
} else {
input.type = "text";
input.value = value;
input.addEventListener("change", () => {
browser.runtime.sendMessage({
type: "UPDATE_CONTEXT_VALUE",
key: path ? path + "." + key : key,
value: input.value,
tabId,
}).then((updateResponse) => {
if (updateResponse && updateResponse.success) {
console.log(`Updated ${key} to ${input.value}`);
} else {
console.error(`Failed to update ${key}:`, updateResponse.error);
}
});
});
}
div.appendChild(input);
form.appendChild(div);
count++;
}
if (count === 0) {
const pre = document.createElement("pre");
pre.textContent = JSON.stringify(obj, null, 2);
detail.appendChild(pre);
} else {
detail.appendChild(form);
}
return detail;
}
function coalesceContextStack(contextStack) {
const obj = {};
for (const ctx of contextStack) {
Object.assign(obj, ctx);
}
return obj;
}
document.addEventListener("DOMContentLoaded", () => {
loadContextStack();
});

View File

@ -1,115 +0,0 @@
type ContextStore = Record<string, any>;
const defaultContext: ContextStore = {};
const contextStack: ContextStore[] = [defaultContext];
const debug = JSON.parse(localStorage.getItem("debug") || "false");
export function setDefaultContext(context: ContextStore) {
Object.assign(defaultContext, context);
}
export function withContext<T>(context: ContextStore, fn: () => T): T {
contextStack.push(context);
try {
return fn();
} finally {
contextStack.pop();
}
}
export const ctx = new Proxy(
{},
{
get(_, prop: string) {
for (let i = contextStack.length - 1; i >= 0; i--) {
if (prop in contextStack[i]) return contextStack[i][prop];
}
if (prop in defaultContext) return defaultContext[prop];
throw new Error(`Context variable '${prop}' is not defined.`);
},
},
) as Record<string, unknown>;
export function getContext() {
return ctx;
}
export function getContextItem<T>(prop: string): T {
return ctx[prop] as T;
}
export function getContextItemOrDefault<T>(prop: string, defaultValue: T): T {
try {
return ctx[prop] as T;
} catch {
return defaultValue;
}
}
export function setContextItem<T>(prop: string, value: T) {
Object.assign(contextStack[contextStack.length - 1] ?? defaultContext, {
[prop]: value,
});
}
if (debug) {
setInterval(() => {
let ctxEl = document.getElementById("context");
if (!ctxEl) {
ctxEl = document.createElement("div");
ctxEl.id = "context";
document.body.append(ctxEl);
}
ctxEl.innerHTML = "";
const div = document.createElement("div");
const pre = document.createElement("pre");
const h3 = document.createElement("h3");
h3.textContent = "Default";
div.append(h3);
pre.textContent = safeStringify(defaultContext);
div.append(pre);
ctxEl.append(div);
for (const [idx, ctx] of contextStack.entries()) {
const div = document.createElement("div");
const pre = document.createElement("pre");
const h3 = document.createElement("h3");
h3.textContent = "CTX " + idx;
div.append(h3);
pre.textContent = safeStringify(ctx);
div.append(pre);
ctxEl.append(div);
}
}, 1000);
}
function safeStringify(obj: any) {
const seen = new WeakSet();
return JSON.stringify(obj, (key, value) => {
if (typeof value === "object" && value !== null) {
if (seen.has(value)) {
return "[Circular]"; // Replace circular references
}
seen.add(value);
}
return value;
}, 2);
}
const getContextStack = () => contextStack.slice();
const updateContextValue = (
key: string,
value: unknown,
depth = contextStack.length - 1,
) => {
const context = contextStack[depth] ?? defaultContext;
context[key] = value;
};
if (location.hostname === "localhost") {
globalThis.getContextStack = getContextStack;
globalThis.updateContextValue = updateContextValue;
}
console.log("globalThis.getContextStack", globalThis.getContextStack);
declare global {
var getContextStack: () => ContextStore[];
var updateContextValue: (key: string, value: unknown, depth?: number) => void;
}

View File

@ -1,3 +1,5 @@
import { TrackSegment } from "../track/system.ts";
type ContextStore = Record<string, any>; type ContextStore = Record<string, any>;
const defaultContext: ContextStore = {}; const defaultContext: ContextStore = {};
@ -83,6 +85,27 @@ if (debug) {
function safeStringify(obj: any) { function safeStringify(obj: any) {
const seen = new WeakSet(); const seen = new WeakSet();
return JSON.stringify(obj, (key, value) => { return JSON.stringify(obj, (key, value) => {
if (value instanceof Map) {
const val: Record<string, unknown> = {};
for (const [k, v] of value) {
if (typeof k !== "string") continue;
val[k] = v;
}
return val;
}
if (value instanceof Set) {
return Array.from(value);
}
if (value instanceof TrackSegment) {
return {
...value,
frontNeighbours: value.frontNeighbours.map((n) => n.id),
backNeighbours: value.backNeighbours.map((n) => n.id),
};
}
if (typeof value === "object" && value !== null) { if (typeof value === "object" && value !== null) {
if (seen.has(value)) { if (seen.has(value)) {
return "[Circular]"; // Replace circular references return "[Circular]"; // Replace circular references
@ -92,3 +115,35 @@ function safeStringify(obj: any) {
return value; return value;
}, 2); }, 2);
} }
const getContextStack = () => JSON.parse(safeStringify(contextStack));
const updateContextValue = (
key: string,
value: unknown,
depth = contextStack.length - 1,
) => {
const keys = key.split(".");
let context = contextStack[depth] ?? defaultContext;
for (let i = 0; i < keys.length - 1; i++) {
if (context instanceof Map) {
context = context.get(keys[i]);
} else {
context = context[keys[i]];
}
}
if (context instanceof Map) {
context.set(keys.at(-1)!, value);
} else {
context[keys.at(-1)!] = value;
}
};
if (location.hostname === "localhost") {
globalThis.getContextStack = getContextStack;
globalThis.updateContextValue = updateContextValue;
globalThis.contextStack = contextStack;
}
declare global {
var getContextStack: () => ContextStore[];
var updateContextValue: (key: string, value: unknown, depth?: number) => void;
var contextStack: ContextStore[];
}

View File

@ -42,12 +42,12 @@ export class LoadState extends State<States> {
}); });
const doodler = getContextItem<Doodler>("doodler"); const doodler = getContextItem<Doodler>("doodler");
this.layers.push(doodler.createLayer((_, __, dTime) => { // this.layers.push(doodler.createLayer((_, __, dTime) => {
doodler.clearRect(new Vector(0, 0), doodler.width, doodler.height); // doodler.clearRect(new Vector(0, 0), doodler.width, doodler.height);
doodler.fillRect(new Vector(0, 0), doodler.width, doodler.height, { // doodler.fillRect(new Vector(0, 0), doodler.width, doodler.height, {
color: "#302040", // color: "#302040",
}); // });
})); // }));
} }
override stop(): void { override stop(): void {
// noop // noop