From 9124abb749f509c14b4992e379097f4c5efd7ad7 Mon Sep 17 00:00:00 2001 From: Emma Date: Sat, 15 Feb 2025 12:46:14 -0700 Subject: [PATCH] Yay a devtools! --- devtools/background.js | 24 +++++++++ devtools/content.js | 27 ++++++++++ devtools/devtools.html | 12 +++++ devtools/devtools.js | 7 +++ devtools/manifest.json | 27 ++++++++++ devtools/panel.html | 39 ++++++++++++++ devtools/panel.js | 52 ++++++++++++++++++ devtools/train icon.png | Bin 0 -> 2813 bytes lib/context.ts | 115 ++++++++++++++++++++++++++++++++++++++++ 9 files changed, 303 insertions(+) create mode 100644 devtools/background.js create mode 100644 devtools/content.js create mode 100644 devtools/devtools.html create mode 100644 devtools/devtools.js create mode 100644 devtools/manifest.json create mode 100644 devtools/panel.html create mode 100644 devtools/panel.js create mode 100644 devtools/train icon.png create mode 100644 lib/context.ts diff --git a/devtools/background.js b/devtools/background.js new file mode 100644 index 0000000..6c8c76f --- /dev/null +++ b/devtools/background.js @@ -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; + } +}); diff --git a/devtools/content.js b/devtools/content.js new file mode 100644 index 0000000..b935d84 --- /dev/null +++ b/devtools/content.js @@ -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); diff --git a/devtools/devtools.html b/devtools/devtools.html new file mode 100644 index 0000000..89884ef --- /dev/null +++ b/devtools/devtools.html @@ -0,0 +1,12 @@ + + + + + + + + + + + + \ No newline at end of file diff --git a/devtools/devtools.js b/devtools/devtools.js new file mode 100644 index 0000000..cc71299 --- /dev/null +++ b/devtools/devtools.js @@ -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"); diff --git a/devtools/manifest.json b/devtools/manifest.json new file mode 100644 index 0000000..24558ae --- /dev/null +++ b/devtools/manifest.json @@ -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": [ + "" + ], + "js": [ + "content.js" + ] + } + ], + "permissions": [ + "devtools", + "tabs", + "*://*/*" + ] +} \ No newline at end of file diff --git a/devtools/panel.html b/devtools/panel.html new file mode 100644 index 0000000..c193569 --- /dev/null +++ b/devtools/panel.html @@ -0,0 +1,39 @@ + + + + + + Context Stack + + + + +

Context Stack

+
+ + + + + \ No newline at end of file diff --git a/devtools/panel.js b/devtools/panel.js new file mode 100644 index 0000000..820f5f5 --- /dev/null +++ b/devtools/panel.js @@ -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 = "

No context stack found.

"; + 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(); diff --git a/devtools/train icon.png b/devtools/train icon.png new file mode 100644 index 0000000000000000000000000000000000000000..b4a3826ac3c7465474e3a186d1d529d212edcaea GIT binary patch literal 2813 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 + +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(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; + +export function getContext() { + return ctx; +} +export function getContextItem(prop: string): T { + return ctx[prop] as T; +} +export function getContextItemOrDefault(prop: string, defaultValue: T): T { + try { + return ctx[prop] as T; + } catch { + return defaultValue; + } +} +export function setContextItem(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; +}