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 0000000..b4a3826 Binary files /dev/null and b/devtools/train icon.png differ diff --git a/lib/context.ts b/lib/context.ts new file mode 100644 index 0000000..9856aa5 --- /dev/null +++ b/lib/context.ts @@ -0,0 +1,115 @@ +type ContextStore = Record; + +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; +}