@tanstack/devtools
Version:
TanStack Devtools is a set of tools for building advanced devtools for your application.
666 lines (658 loc) • 23 kB
JavaScript
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 };