UNPKG

@tanstack/devtools

Version:

TanStack Devtools is a set of tools for building advanced devtools for your application.

666 lines (658 loc) 23 kB
import { initialState } from './CUZKAGZ3.js'; import { createContext, createEffect, createComponent, createSignal, onCleanup, createMemo, useContext } from 'solid-js'; import { createStore, reconcile } from 'solid-js/store'; import { createComponent as createComponent$1, delegateEvents } from 'solid-js/web'; import { ensureDevtoolsStyles } from '@tanstack/devtools-ui/internal'; // src/utils/constants.ts var MAX_ACTIVE_PLUGINS = 18; var PLUGIN_GROUP_TAB_HEIGHT = 30; var PLUGIN_SPLITTER_SIZE = 6; var MIN_PANE_SIZE = { w: 280, h: 160 }; var PANE_DROP_EDGE_RATIO = 0.25; var WORKBENCH_HEADER_HEIGHT = 36; var PLUGINS_STRIP_HEIGHT = 44; var WORKBENCH_GUTTER = 16; var WORKBENCH_GUTTER_NARROW = 12; var PANEL_CLOSE_THRESHOLD = 70; var PANEL_MAX_VIEWPORT_RATIO = 0.9; // src/utils/storage.ts var getStorageItem = (key) => { return localStorage.getItem(key); }; var setStorageItem = (key, value) => { try { localStorage.setItem(key, value); } catch (_e) { return; } }; var TANSTACK_DEVTOOLS = "tanstack_devtools"; var TANSTACK_DEVTOOLS_STATE = "tanstack_devtools_state"; var TANSTACK_DEVTOOLS_SETTINGS = "tanstack_devtools_settings"; // src/utils/get-default-active-plugins.ts function getDefaultActivePlugins(plugins) { if (plugins.length === 0) { return []; } if (plugins.length === 1) { return [plugins[0].id]; } return plugins.filter((plugin) => plugin.defaultOpen === true).slice(0, MAX_ACTIVE_PLUGINS).map((plugin) => plugin.id); } // src/utils/layout-tree.ts var EPSILON = 1e-9; var MAX_STORED_DEPTH = 32; var isGroup = (node) => node.kind === "group"; var isSplit = (node) => node.kind === "split"; var flattenTabs = (tree) => tree === null ? [] : isGroup(tree) ? [...tree.tabs] : tree.children.flatMap(flattenTabs); var allGroups = (tree) => tree === null ? [] : isGroup(tree) ? [tree] : tree.children.flatMap(allGroups); var findGroupOfTab = (tree, tabId) => allGroups(tree).find((group) => group.tabs.includes(tabId)) ?? null; var findGroupById = (tree, groupId) => allGroups(tree).find((group) => group.id === groupId) ?? null; var nextGroupId = (tree) => { let highest = -1; for (const group of allGroups(tree)) { const match = /^g(\d+)$/.exec(group.id); if (match) highest = Math.max(highest, Number(match[1])); } return `g${highest + 1}`; }; var singleGroup = (tabs, id = "g0") => tabs.length === 0 ? null : { kind: "group", id, tabs: [...tabs], active: 0 }; var normalise = (sizes, count) => { const usable = sizes.length === count && sizes.every((n) => Number.isFinite(n) && n > 0) ? sizes : Array.from({ length: count }, () => 1); const total = usable.reduce((sum, n) => sum + n, 0); return total > EPSILON ? usable.map((n) => n / total) : Array.from({ length: count }, () => 1 / count); }; var split = (dir, children, sizes) => ({ kind: "split", dir, sizes: normalise(sizes ?? [], children.length), children }); var prune = (node) => { if (node === null) return null; if (isGroup(node)) { if (node.tabs.length === 0) return null; const active = Math.min(Math.max(node.active, 0), node.tabs.length - 1); return active === node.active ? node : { ...node, active }; } const kept = []; const keptSizes = []; node.children.forEach((child, index) => { const pruned = prune(child); if (pruned === null) return; if (isSplit(pruned) && pruned.dir === node.dir) { const share = node.sizes[index] ?? 1 / node.children.length; pruned.children.forEach((grandchild, inner) => { kept.push(grandchild); keptSizes.push(share * (pruned.sizes[inner] ?? 0)); }); return; } kept.push(pruned); keptSizes.push(node.sizes[index] ?? 1 / node.children.length); }); if (kept.length === 0) return null; if (kept.length === 1) return kept[0]; return split(node.dir, kept, keptSizes); }; var closeTab = (tree, tabId) => { const strip = (node) => { if (isGroup(node)) { const index = node.tabs.indexOf(tabId); if (index === -1) return node; const tabs = node.tabs.filter((id) => id !== tabId); const active = node.active > index ? node.active - 1 : node.active; return { ...node, tabs, active }; } return { ...node, children: node.children.map(strip) }; }; return tree === null ? null : prune(strip(tree)); }; var setTabs = (tree, groupId, tabIds) => { const group = findGroupById(tree, groupId); if (tree === null || group === null) return tree; const existing = new Set(group.tabs); const seen = /* @__PURE__ */ new Set(); const reordered = tabIds.filter((id) => { if (!existing.has(id) || seen.has(id)) return false; seen.add(id); return true; }); const tabs = [...reordered, ...group.tabs.filter((id) => !seen.has(id))]; if (tabs.length === 0) return tree; const activeId = group.tabs[group.active]; const active = Math.max( tabs.findIndex((id) => id === activeId), 0 ); const visit = (node) => { if (isGroup(node)) { return node.id === groupId ? { ...node, tabs, active } : node; } return { ...node, children: node.children.map(visit) }; }; return visit(tree); }; var activateTab = (tree, tabId) => { if (tree === null) return null; const visit = (node) => { if (isGroup(node)) { const index = node.tabs.indexOf(tabId); return index === -1 || index === node.active ? node : { ...node, active: index }; } return { ...node, children: node.children.map(visit) }; }; return visit(tree); }; var moveTab = (tree, tabId, groupId, index) => { if (tree === null) return null; const target = findGroupById(tree, groupId); if (target === null) return tree; const source = findGroupOfTab(tree, tabId); const withoutTab = source === null ? tree : closeTab(tree, tabId) ?? null; if (withoutTab === null) return singleGroup([tabId], groupId); if (findGroupById(withoutTab, groupId) === null) return tree; const insert = (node) => { if (isGroup(node)) { if (node.id !== groupId) return node; const at = Math.min(Math.max(index, 0), node.tabs.length); const tabs = [...node.tabs.slice(0, at), tabId, ...node.tabs.slice(at)]; return { ...node, tabs, active: at }; } return { ...node, children: node.children.map(insert) }; }; return prune(insert(withoutTab)); }; var stackInto = (tree, groupId, tabId) => { const group = findGroupById(tree, groupId); return group === null ? tree : moveTab(tree, tabId, groupId, group.tabs.length); }; var zoneAxis = (zone) => zone === "left" || zone === "right" ? "row" : "col"; var zoneLeads = (zone) => zone === "left" || zone === "top"; var splitAt = (tree, groupId, zone, tabId) => { if (tree === null) return singleGroup([tabId], "g0"); if (zone === "center") return stackInto(tree, groupId, tabId); if (findGroupById(tree, groupId) === null) return tree; const lifted = closeTab(tree, tabId); if (lifted === null) return singleGroup([tabId], groupId); const host = findGroupById(lifted, groupId); if (host === null) return tree; const newGroup = { kind: "group", id: nextGroupId(lifted), tabs: [tabId], active: 0 }; const dir = zoneAxis(zone); const place = (node) => { if (isGroup(node)) { if (node.id !== groupId) return node; return split(dir, zoneLeads(zone) ? [newGroup, node] : [node, newGroup]); } return { ...node, children: node.children.map(place) }; }; return prune(place(lifted)); }; var appendPane = (tree, tabId, dir = "row") => { if (tree === null) return singleGroup([tabId]); const lifted = closeTab(tree, tabId); if (lifted === null) return singleGroup([tabId]); const newGroup = { kind: "group", id: nextGroupId(lifted), tabs: [tabId], active: 0 }; const children = isSplit(lifted) && lifted.dir === dir ? [...lifted.children, newGroup] : [lifted, newGroup]; return prune(split(dir, children)); }; var nodeAtPath = (tree, path) => { let node = tree; for (const index of path) { if (node === null || !isSplit(node)) return null; node = node.children[index] ?? null; } return node; }; var resize = (tree, path, gutterIndex, delta, minFraction = 0) => { const target = nodeAtPath(tree, path); if (tree === null || target === null || !isSplit(target)) return tree; const before = target.sizes[gutterIndex]; const after = target.sizes[gutterIndex + 1]; if (before === void 0 || after === void 0) return tree; const budget = before + after; const min = Math.min(minFraction, budget / 2); const nextBefore = Math.min(Math.max(before + delta, min), budget - min); if (Math.abs(nextBefore - before) < EPSILON) return tree; const sizes = [...target.sizes]; sizes[gutterIndex] = nextBefore; sizes[gutterIndex + 1] = budget - nextBefore; const replace = (node, depth) => { if (depth === path.length) return { ...node, sizes }; const index = path[depth]; const children = [...node.children]; children[index] = replace(children[index], depth + 1); return { ...node, children }; }; return replace(tree, 0); }; var layoutRects = (tree, box, gutter = 0) => { const out = {}; const walk = (node, rect) => { if (isGroup(node)) { out[node.id] = rect; return; } const horizontal = node.dir === "row"; const gutters = gutter * (node.children.length - 1); const available = Math.max( (horizontal ? rect.width : rect.height) - gutters, 0 ); let offset = horizontal ? rect.left : rect.top; node.children.forEach((child, index) => { const extent = available * (node.sizes[index] ?? 0); walk( child, horizontal ? { left: offset, top: rect.top, width: extent, height: rect.height } : { left: rect.left, top: offset, width: rect.width, height: extent } ); offset += extent + gutter; }); }; if (tree !== null) { walk(tree, { left: 0, top: 0, width: box.w, height: box.h }); } return out; }; var splitterHandles = (tree, box, gutter = 0) => { const handles = []; const walk = (node, rect, path) => { if (isGroup(node)) return; const horizontal = node.dir === "row"; const gutters = gutter * (node.children.length - 1); const available = Math.max( (horizontal ? rect.width : rect.height) - gutters, 0 ); let offset = horizontal ? rect.left : rect.top; node.children.forEach((child, index) => { const extent = available * (node.sizes[index] ?? 0); const childRect = horizontal ? { left: offset, top: rect.top, width: extent, height: rect.height } : { left: rect.left, top: offset, width: rect.width, height: extent }; walk(child, childRect, [...path, index]); offset += extent; if (index < node.children.length - 1) { handles.push({ path, gutterIndex: index, dir: node.dir, extent: available, rect: horizontal ? { left: offset, top: rect.top, width: gutter, height: rect.height } : { left: rect.left, top: offset, width: rect.width, height: gutter } }); offset += gutter; } }); }; if (tree !== null) { walk(tree, { left: 0, top: 0, width: box.w, height: box.h }, []); } return handles; }; var canSplit = (tree, groupId, zone, min, box, gutter = 0) => { if (zone === "center") return true; const rect = layoutRects(tree, box, gutter)[groupId]; if (!rect) return false; return zoneAxis(zone) === "row" ? (rect.width - gutter) / 2 >= min.w : (rect.height - gutter) / 2 >= min.h; }; var zoneAt = (point, rect, edge = 0.25) => { const x = rect.width > 0 ? (point.x - rect.left) / rect.width : 0.5; const y = rect.height > 0 ? (point.y - rect.top) / rect.height : 0.5; const distances = [ ["left", x], ["right", 1 - x], ["top", y], ["bottom", 1 - y] ]; const [zone, distance] = distances.reduce( (best, entry) => entry[1] < best[1] ? entry : best ); return distance <= edge ? zone : "center"; }; var isRawGroup = (value) => value.kind === "group" && typeof value.id === "string" && Array.isArray(value.tabs) && value.tabs.every((tab) => typeof tab === "string"); var isRawSplit = (value) => value.kind === "split" && (value.dir === "row" || value.dir === "col") && Array.isArray(value.children); var repairLayout = (raw, known) => { const seen = /* @__PURE__ */ new Set(); const rebuild = (value, depth = 0) => { if (depth > MAX_STORED_DEPTH) return null; if (typeof value !== "object" || value === null) return null; const record = value; if (isRawGroup(record)) { const tabs = record.tabs.filter((tab) => { if (!known.has(tab) || seen.has(tab)) return false; seen.add(tab); return true; }); if (tabs.length === 0) return null; const active = typeof record.active === "number" && Number.isInteger(record.active) ? Math.min(Math.max(record.active, 0), tabs.length - 1) : 0; return { kind: "group", id: String(record.id), tabs, active }; } if (isRawSplit(record)) { const children = record.children.map((child) => rebuild(child, depth + 1)).filter((child) => child !== null); if (children.length === 0) return null; const rawSizes = Array.isArray(record.sizes) ? record.sizes.filter( (size) => typeof size === "number" ) : []; return split(record.dir, children, rawSizes); } return null; }; const rebuilt = prune(rebuild(raw)); if (rebuilt !== null) return dedupeIds(rebuilt); const salvaged = collectKnownIds(raw, known); return singleGroup(salvaged); }; var collectKnownIds = (raw, known) => { const found = []; const seen = /* @__PURE__ */ new Set(); const visited = /* @__PURE__ */ new WeakSet(); const walk = (value) => { if (typeof value === "string") { if (known.has(value) && !seen.has(value)) { seen.add(value); found.push(value); } return; } if (typeof value !== "object" || value === null) return; if (visited.has(value)) return; visited.add(value); if (Array.isArray(value)) { value.forEach(walk); return; } Object.values(value).forEach(walk); }; walk(raw); return found; }; var dedupeIds = (tree) => { const used = /* @__PURE__ */ new Set(); let counter = 0; const visit = (node) => { if (isGroup(node)) { if (!used.has(node.id)) { used.add(node.id); return node; } let id = `g${counter++}`; while (used.has(id)) id = `g${counter++}`; used.add(id); return { ...node, id }; } return { ...node, children: node.children.map(visit) }; }; return visit(tree); }; // src/utils/sanitize.ts var tryParseJson = (json) => { if (!json) return void 0; try { return JSON.parse(json); } catch (_e) { return void 0; } }; var uppercaseFirstLetter = (value) => value.charAt(0).toUpperCase() + value.slice(1); var getAllPermutations = (arr) => { const res = []; function permutate(arr2, start) { if (start === arr2.length - 1) { res.push([...arr2]); return; } for (let i = start; i < arr2.length; i++) { [arr2[start], arr2[i]] = [arr2[i], arr2[start]]; permutate(arr2, start + 1); [arr2[start], arr2[i]] = [arr2[i], arr2[start]]; } } permutate(arr, 0); return res; }; // src/context/devtools-context.tsx var DevtoolsContext = createContext(); var getSettings = () => { const settingsString = getStorageItem(TANSTACK_DEVTOOLS_SETTINGS); const settings = tryParseJson(settingsString); return { ...settings }; }; var generatePluginId = (plugin, index) => { if (plugin.id) { return plugin.id; } if (typeof plugin.name === "string") { return `${plugin.name.toLowerCase().replace(" ", "-")}-${index}`; } return index.toString(); }; function getStateFromLocalStorage(plugins) { const existingStateString = getStorageItem(TANSTACK_DEVTOOLS_STATE); const existingState = tryParseJson(existingStateString); const pluginIds = plugins?.map((plugin, i) => generatePluginId(plugin, i)) || []; if (existingState) { const known = new Set(pluginIds); const before = JSON.stringify(existingState.layout ?? null); const raw = existingState.layout ?? singleGroup(existingState.activePlugins ?? []); existingState.layout = repairLayout(raw, known); delete existingState.activePlugins; if (JSON.stringify(existingState.layout ?? null) !== before) { setStorageItem(TANSTACK_DEVTOOLS_STATE, JSON.stringify(existingState)); } } return existingState; } var getExistingStateFromStorage = (config, plugins) => { const existingState = getStateFromLocalStorage(plugins); const settings = getSettings(); const pluginsWithIds = plugins?.map((plugin, i) => { const id = generatePluginId(plugin, i); return { ...plugin, id }; }) || []; let layout = existingState?.layout ?? null; const shouldFillWithDefaultOpenPlugins = flattenTabs(layout).length === 0 && pluginsWithIds.length > 0; if (shouldFillWithDefaultOpenPlugins) { layout = singleGroup(getDefaultActivePlugins(pluginsWithIds)); } const state = { ...initialState, plugins: pluginsWithIds, state: { ...initialState.state, ...existingState, layout }, settings: { ...initialState.settings, ...config, ...settings } }; return state; }; var DevtoolsProvider = (props) => { const [store, setStore] = createStore(getExistingStateFromStorage(props.config, props.plugins)); setStorageItem(TANSTACK_DEVTOOLS_STATE, JSON.stringify(store.state)); const updatePlugins = (newPlugins) => { const pluginsWithIds = newPlugins.map((plugin, i) => { const id = generatePluginId(plugin, i); return { ...plugin, id }; }); setStore("plugins", pluginsWithIds); }; createEffect(() => { if (props.onSetPlugins) { props.onSetPlugins(updatePlugins); } }); const value = { store, paneDragBridge: { handler: null }, setStore: (updater) => { const newState = updater(store); const { settings, state: internalState } = newState; setStorageItem(TANSTACK_DEVTOOLS_SETTINGS, JSON.stringify(settings)); setStorageItem(TANSTACK_DEVTOOLS_STATE, JSON.stringify(internalState)); setStore((prev) => ({ ...prev, ...newState })); }, replaceLayout: (next) => { setStore("state", "layout", next === null ? null : reconcile(next, { key: null })); setStorageItem(TANSTACK_DEVTOOLS_STATE, JSON.stringify(store.state)); } }; return createComponent(DevtoolsContext.Provider, { value, get children() { return props.children; } }); }; var PiPContext = createContext(void 0); var PiPProvider = (props) => { const [pipWindow, setPipWindow] = createSignal(null); const closePipWindow = () => { const w = pipWindow(); if (w != null) { w.close(); setPipWindow(null); } }; const requestPipWindow = (settings) => { if (pipWindow() != null) { return; } const pip = window.open("", "TSDT-Devtools-Panel", `${settings},popup`); if (!pip) { throw new Error("Failed to open popup. Please allow popups for this site to view the devtools in picture-in-picture mode."); } if (import.meta.hot && typeof import.meta.hot.on === "function") { import.meta.hot.on("vite:beforeUpdate", () => { localStorage.setItem("pip_open", "false"); closePipWindow(); }); } window.addEventListener("beforeunload", () => { localStorage.setItem("pip_open", "false"); closePipWindow(); }); pip.document.head.innerHTML = ""; pip.document.body.innerHTML = ""; pip.document.title = "TanStack Devtools"; pip.document.body.style.margin = "0"; pip.addEventListener("pagehide", () => { localStorage.setItem("pip_open", "false"); closePipWindow(); }); [...document.styleSheets].forEach((styleSheet) => { try { const cssRules = [...styleSheet.cssRules].map((rule) => rule.cssText).join(""); const style = document.createElement("style"); const style_node = styleSheet.ownerNode; let style_id = ""; if (style_node && "id" in style_node) { style_id = style_node.id; } if (style_id) { style.setAttribute("id", style_id); } style.textContent = cssRules; pip.document.head.appendChild(style); } catch (e) { const link = document.createElement("link"); if (styleSheet.href == null) { return; } link.rel = "stylesheet"; link.type = styleSheet.type; link.media = styleSheet.media.toString(); link.href = styleSheet.href; pip.document.head.appendChild(link); } }); ensureDevtoolsStyles(pip.document); delegateEvents(["focusin", "focusout", "pointermove", "keydown", "pointerdown", "pointerup", "click", "mousedown", "input"], pip.document); setPipWindow(pip); }; createEffect(() => { const gooberStyles = document.querySelector("#_goober"); const w = pipWindow(); if (gooberStyles && w) { const observer = new MutationObserver(() => { const pip_style = w.document.querySelector("#_goober"); if (pip_style) { pip_style.textContent = gooberStyles.textContent; } }); observer.observe(gooberStyles, { childList: true, // observe direct children subtree: true, // and lower descendants too characterDataOldValue: true // pass old data to callback }); onCleanup(() => { observer.disconnect(); }); } }); const value = createMemo(() => ({ pipWindow: pipWindow(), requestPipWindow, closePipWindow, disabled: props.disabled ?? false })); return createComponent$1(PiPContext.Provider, { value, get children() { return props.children; } }); }; var createPiPWindow = () => { const context = createMemo(() => { const ctx = useContext(PiPContext); if (!ctx) { throw new Error("createPiPWindow must be used within a PiPProvider"); } return ctx(); }); return context; }; export { DevtoolsContext, DevtoolsProvider, MAX_ACTIVE_PLUGINS, MIN_PANE_SIZE, PANEL_CLOSE_THRESHOLD, PANEL_MAX_VIEWPORT_RATIO, PANE_DROP_EDGE_RATIO, PLUGINS_STRIP_HEIGHT, PLUGIN_GROUP_TAB_HEIGHT, PLUGIN_SPLITTER_SIZE, PiPProvider, TANSTACK_DEVTOOLS, WORKBENCH_GUTTER, WORKBENCH_GUTTER_NARROW, WORKBENCH_HEADER_HEIGHT, activateTab, allGroups, appendPane, canSplit, closeTab, createPiPWindow, findGroupOfTab, flattenTabs, getAllPermutations, layoutRects, moveTab, resize, setTabs, singleGroup, splitAt, splitterHandles, stackInto, uppercaseFirstLetter, zoneAt };