Yay a devtools!

This commit is contained in:
Emmaline Autumn 2025-02-15 12:46:14 -07:00
parent 8e6294c96f
commit 9124abb749
9 changed files with 303 additions and 0 deletions

24
devtools/background.js Normal file
View File

@ -0,0 +1,24 @@
console.log("background.js loaded");
// Listen for messages from the devtools panel
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") {
// Forward the message to the content script running in the active tab.
browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => {
if (tabs.length) {
browser.tabs.sendMessage(tabs[0].id, message).then(sendResponse);
}
});
// Return true to indicate you wish to send a response asynchronously
return true;
} else if (message.type === "UPDATE_CONTEXT_VALUE") {
// Forward update messages similarly
browser.tabs.query({ active: true, currentWindow: true }).then((tabs) => {
if (tabs.length) {
browser.tabs.sendMessage(tabs[0].id, message).then(sendResponse);
}
});
return true;
}
});

27
devtools/content.js Normal file
View File

@ -0,0 +1,27 @@
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) => {
if (message.type === "GET_CONTEXT_STACK") {
const contextStack = getContextStack();
sendResponse({ contextStack });
} else if (message.type === "UPDATE_CONTEXT_VALUE") {
const { key, value, depth } = message;
if (glowindow.updateContextValue) {
updateContextValue(key, value, depth);
sendResponse({ success: true });
} else {
sendResponse({
success: false,
error: "updateContextValue not defined",
});
}
}
});
// }, 0);

12
devtools/devtools.html Normal file
View File

@ -0,0 +1,12 @@
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<script src="devtools.js"></script>
</head>
<body></body>
</html>

7
devtools/devtools.js Normal file
View File

@ -0,0 +1,7 @@
// Create a new devtools panel named "Context Stack"
browser.devtools.panels.create(
"Context Stack", // Tab title
"train icon.png", // Icon for your panel (optional)
"panel.html", // HTML page for your panel content
);
console.log("devtools loaded");

27
devtools/manifest.json Normal file
View File

@ -0,0 +1,27 @@
{
"manifest_version": 2,
"name": "Context Stack DevTools",
"version": "1.0",
"description": "A devtools panel to view and edit context stack values.",
"devtools_page": "devtools.html",
"background": {
"scripts": [
"background.js"
]
},
"content_scripts": [
{
"matches": [
"<all_urls>"
],
"js": [
"content.js"
]
}
],
"permissions": [
"devtools",
"tabs",
"*://*/*"
]
}

39
devtools/panel.html Normal file
View File

@ -0,0 +1,39 @@
<!DOCTYPE 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;
}
.context-row {
display: flex;
margin-bottom: 8px;
}
.context-key {
width: 150px;
font-weight: bold;
}
.context-value input {
width: 100%;
}
</style>
</head>
<body>
<h1>Context Stack</h1>
<div id="contextContainer"></div>
<button id="refresh">Refresh</button>
<script src="panel.js"></script>
</body>
</html>

52
devtools/panel.js Normal file
View File

@ -0,0 +1,52 @@
document.getElementById("refresh").addEventListener("click", loadContextStack);
function loadContextStack() {
const container = document.getElementById("contextContainer");
container.innerHTML = "";
browser.runtime.sendMessage({ type: "GET_CONTEXT_STACK" }).then(
(response) => {
if (!response || !response.contextStack) {
container.innerHTML = "<p>No context stack found.</p>";
return;
}
const contextStack = response.contextStack;
for (const key in contextStack) {
const row = document.createElement("div");
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.
loadContextStack();

BIN
devtools/train icon.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.7 KiB

115
lib/context.ts Normal file
View File

@ -0,0 +1,115 @@
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;
}