@tanstack/devtools
Advanced tools
| import { createComponent, delegateEvents } from 'solid-js/web'; | ||
| import { createContext, createSignal, createEffect, onCleanup, createMemo, useContext } from 'solid-js'; | ||
| import { createStore } from 'solid-js/store'; | ||
| // src/constants.ts | ||
| var PLUGIN_CONTAINER_ID = "plugin-container"; | ||
| var PLUGIN_TITLE_CONTAINER_ID = "plugin-title-container"; | ||
| // 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/utils/storage.ts | ||
| var getStorageItem = (key) => 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/context/devtools-store.ts | ||
| var keyboardModifiers = [ | ||
| "Alt", | ||
| "Control", | ||
| "Meta", | ||
| "Shift" | ||
| ]; | ||
| var initialState = { | ||
| settings: { | ||
| defaultOpen: false, | ||
| hideUntilHover: false, | ||
| position: "bottom-right", | ||
| panelLocation: "bottom", | ||
| openHotkey: ["Shift", "A"], | ||
| requireUrlFlag: false, | ||
| urlFlag: "tanstack-devtools", | ||
| theme: typeof window !== "undefined" && typeof window.matchMedia !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light", | ||
| triggerImage: "", | ||
| triggerHidden: false | ||
| }, | ||
| state: { | ||
| activeTab: "plugins", | ||
| height: 400, | ||
| activePlugins: [], | ||
| persistOpen: false | ||
| } | ||
| }; | ||
| 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."); | ||
| } | ||
| 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); | ||
| } | ||
| }); | ||
| 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(PiPContext.Provider, { | ||
| value, | ||
| get children() { | ||
| return props.children; | ||
| } | ||
| }); | ||
| }; | ||
| var usePiPWindow = () => { | ||
| const context = createMemo(() => { | ||
| const ctx = useContext(PiPContext); | ||
| if (!ctx) { | ||
| throw new Error("usePiPWindow must be used within a PiPProvider"); | ||
| } | ||
| return ctx(); | ||
| }); | ||
| return context; | ||
| }; | ||
| // src/utils/constants.ts | ||
| var MAX_ACTIVE_PLUGINS = 3; | ||
| // 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/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?.activePlugins) { | ||
| const originalLength = existingState.activePlugins.length; | ||
| existingState.activePlugins = existingState.activePlugins.filter((id) => pluginIds.includes(id)); | ||
| if (existingState.activePlugins.length !== originalLength) { | ||
| 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 activePlugins = existingState?.activePlugins || []; | ||
| const shouldFillWithDefaultOpenPlugins = activePlugins.length === 0 && pluginsWithIds.length > 0; | ||
| if (shouldFillWithDefaultOpenPlugins) { | ||
| activePlugins = getDefaultActivePlugins(pluginsWithIds); | ||
| } | ||
| const state = { | ||
| ...initialState, | ||
| plugins: pluginsWithIds, | ||
| state: { | ||
| ...initialState.state, | ||
| ...existingState, | ||
| activePlugins | ||
| }, | ||
| settings: { | ||
| ...initialState.settings, | ||
| ...config, | ||
| ...settings | ||
| } | ||
| }; | ||
| return state; | ||
| }; | ||
| var DevtoolsProvider = (props) => { | ||
| const [store, setStore] = createStore(getExistingStateFromStorage(props.config, props.plugins)); | ||
| 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, | ||
| 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 | ||
| })); | ||
| } | ||
| }; | ||
| return createComponent(DevtoolsContext.Provider, { | ||
| value, | ||
| get children() { | ||
| return props.children; | ||
| } | ||
| }); | ||
| }; | ||
| export { DevtoolsContext, DevtoolsProvider, MAX_ACTIVE_PLUGINS, PLUGIN_CONTAINER_ID, PLUGIN_TITLE_CONTAINER_ID, PiPProvider, TANSTACK_DEVTOOLS, getAllPermutations, initialState, keyboardModifiers, uppercaseFirstLetter, usePiPWindow }; |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
| /** | ||
| * Maximum number of plugins that can be active simultaneously in the devtools | ||
| */ | ||
| export const MAX_ACTIVE_PLUGINS = 3 |
| import { describe, expect, it } from 'vitest' | ||
| import { getDefaultActivePlugins } from './get-default-active-plugins' | ||
| import type { PluginWithId } from './get-default-active-plugins' | ||
| describe('getDefaultActivePlugins', () => { | ||
| it('should return empty array when no plugins provided', () => { | ||
| const result = getDefaultActivePlugins([]) | ||
| expect(result).toEqual([]) | ||
| }) | ||
| it('should automatically activate a single plugin', () => { | ||
| const plugins: Array<PluginWithId> = [ | ||
| { | ||
| id: 'only-plugin', | ||
| }, | ||
| ] | ||
| const result = getDefaultActivePlugins(plugins) | ||
| expect(result).toEqual(['only-plugin']) | ||
| }) | ||
| it('should automatically activate a single plugin even if defaultOpen is false', () => { | ||
| const plugins: Array<PluginWithId> = [ | ||
| { | ||
| id: 'only-plugin', | ||
| defaultOpen: false, | ||
| }, | ||
| ] | ||
| const result = getDefaultActivePlugins(plugins) | ||
| expect(result).toEqual(['only-plugin']) | ||
| }) | ||
| it('should return empty array when multiple plugins without defaultOpen', () => { | ||
| const plugins: Array<PluginWithId> = [ | ||
| { | ||
| id: 'plugin1', | ||
| }, | ||
| { | ||
| id: 'plugin2', | ||
| }, | ||
| { | ||
| id: 'plugin3', | ||
| }, | ||
| ] | ||
| const result = getDefaultActivePlugins(plugins) | ||
| expect(result).toEqual([]) | ||
| }) | ||
| it('should activate plugins with defaultOpen: true', () => { | ||
| const plugins: Array<PluginWithId> = [ | ||
| { | ||
| id: 'plugin1', | ||
| defaultOpen: true, | ||
| }, | ||
| { | ||
| id: 'plugin2', | ||
| defaultOpen: false, | ||
| }, | ||
| { | ||
| id: 'plugin3', | ||
| defaultOpen: true, | ||
| }, | ||
| ] | ||
| const result = getDefaultActivePlugins(plugins) | ||
| expect(result).toEqual(['plugin1', 'plugin3']) | ||
| }) | ||
| it('should limit defaultOpen plugins to MAX_ACTIVE_PLUGINS (3)', () => { | ||
| const plugins: Array<PluginWithId> = [ | ||
| { | ||
| id: 'plugin1', | ||
| defaultOpen: true, | ||
| }, | ||
| { | ||
| id: 'plugin2', | ||
| defaultOpen: true, | ||
| }, | ||
| { | ||
| id: 'plugin3', | ||
| defaultOpen: true, | ||
| }, | ||
| { | ||
| id: 'plugin4', | ||
| defaultOpen: true, | ||
| }, | ||
| { | ||
| id: 'plugin5', | ||
| defaultOpen: true, | ||
| }, | ||
| ] | ||
| const result = getDefaultActivePlugins(plugins) | ||
| // Should only return first 3 | ||
| expect(result).toEqual(['plugin1', 'plugin2', 'plugin3']) | ||
| expect(result.length).toBe(3) | ||
| }) | ||
| it('should activate exactly MAX_ACTIVE_PLUGINS when that many have defaultOpen', () => { | ||
| const plugins: Array<PluginWithId> = [ | ||
| { | ||
| id: 'plugin1', | ||
| defaultOpen: true, | ||
| }, | ||
| { | ||
| id: 'plugin2', | ||
| defaultOpen: true, | ||
| }, | ||
| { | ||
| id: 'plugin3', | ||
| defaultOpen: true, | ||
| }, | ||
| { | ||
| id: 'plugin4', | ||
| defaultOpen: false, | ||
| }, | ||
| ] | ||
| const result = getDefaultActivePlugins(plugins) | ||
| expect(result).toEqual(['plugin1', 'plugin2', 'plugin3']) | ||
| expect(result.length).toBe(3) | ||
| }) | ||
| it('should handle mix of defaultOpen true/false/undefined', () => { | ||
| const plugins: Array<PluginWithId> = [ | ||
| { | ||
| id: 'plugin1', | ||
| defaultOpen: true, | ||
| }, | ||
| { | ||
| id: 'plugin2', | ||
| // undefined defaultOpen | ||
| }, | ||
| { | ||
| id: 'plugin3', | ||
| defaultOpen: false, | ||
| }, | ||
| { | ||
| id: 'plugin4', | ||
| defaultOpen: true, | ||
| }, | ||
| ] | ||
| const result = getDefaultActivePlugins(plugins) | ||
| // Only plugin1 and plugin4 have defaultOpen: true | ||
| expect(result).toEqual(['plugin1', 'plugin4']) | ||
| }) | ||
| it('should return single plugin even if it has defaultOpen: true', () => { | ||
| const plugins: Array<PluginWithId> = [ | ||
| { | ||
| id: 'only-plugin', | ||
| defaultOpen: true, | ||
| }, | ||
| ] | ||
| const result = getDefaultActivePlugins(plugins) | ||
| expect(result).toEqual(['only-plugin']) | ||
| }) | ||
| it('should stop at MAX_ACTIVE_PLUGINS limit when 5 plugins have defaultOpen: true', () => { | ||
| const plugins: Array<PluginWithId> = [ | ||
| { | ||
| id: 'plugin1', | ||
| defaultOpen: true, | ||
| }, | ||
| { | ||
| id: 'plugin2', | ||
| defaultOpen: true, | ||
| }, | ||
| { | ||
| id: 'plugin3', | ||
| defaultOpen: true, | ||
| }, | ||
| { | ||
| id: 'plugin4', | ||
| defaultOpen: true, | ||
| }, | ||
| { | ||
| id: 'plugin5', | ||
| defaultOpen: true, | ||
| }, | ||
| ] | ||
| const result = getDefaultActivePlugins(plugins) | ||
| // Should only activate the first 3, plugin4 and plugin5 should be ignored | ||
| expect(result).toEqual(['plugin1', 'plugin2', 'plugin3']) | ||
| expect(result.length).toBe(3) | ||
| expect(result).not.toContain('plugin4') | ||
| expect(result).not.toContain('plugin5') | ||
| }) | ||
| }) |
| import { MAX_ACTIVE_PLUGINS } from './constants' | ||
| export interface PluginWithId { | ||
| id: string | ||
| defaultOpen?: boolean | ||
| } | ||
| /** | ||
| * Determines which plugins should be active by default when no plugins are currently active. | ||
| * | ||
| * Rules: | ||
| * 1. If there's only 1 plugin, activate it automatically | ||
| * 2. If there are multiple plugins, activate those with defaultOpen: true (up to MAX_ACTIVE_PLUGINS limit) | ||
| * 3. If no plugins have defaultOpen: true, return empty array | ||
| * | ||
| * @param plugins - Array of plugins with IDs | ||
| * @returns Array of plugin IDs that should be active by default | ||
| */ | ||
| export function getDefaultActivePlugins( | ||
| plugins: Array<PluginWithId>, | ||
| ): Array<string> { | ||
| if (plugins.length === 0) { | ||
| return [] | ||
| } | ||
| // If there's only 1 plugin, activate it automatically | ||
| if (plugins.length === 1) { | ||
| return [plugins[0]!.id] | ||
| } | ||
| // Otherwise, activate plugins with defaultOpen: true (up to the limit) | ||
| return plugins | ||
| .filter((plugin) => plugin.defaultOpen === true) | ||
| .slice(0, MAX_ACTIVE_PLUGINS) | ||
| .map((plugin) => plugin.id) | ||
| } |
+3
-3
@@ -1,3 +0,3 @@ | ||
| import { initialState, DevtoolsProvider, PiPProvider } from './chunk/XF4JFOLU.js'; | ||
| export { PLUGIN_CONTAINER_ID, PLUGIN_TITLE_CONTAINER_ID } from './chunk/XF4JFOLU.js'; | ||
| import { initialState, DevtoolsProvider, PiPProvider } from './chunk/VZEY7HNC.js'; | ||
| export { PLUGIN_CONTAINER_ID, PLUGIN_TITLE_CONTAINER_ID } from './chunk/VZEY7HNC.js'; | ||
| import { render, createComponent, Portal } from 'solid-js/web'; | ||
@@ -33,3 +33,3 @@ import { lazy } from 'solid-js'; | ||
| const _self$ = this; | ||
| this.#Component = lazy(() => import('./devtools/UUNAZSBD.js')); | ||
| this.#Component = lazy(() => import('./devtools/7NDEDZB7.js')); | ||
| const Devtools = this.#Component; | ||
@@ -36,0 +36,0 @@ this.#eventBus = new ClientEventBus(this.#eventBusConfig); |
+6
-0
@@ -124,2 +124,8 @@ import { ClientEventBusConfig } from '@tanstack/devtools-event-bus/client'; | ||
| /** | ||
| * Whether the plugin should be open by default when there are no active plugins. | ||
| * If true, this plugin will be added to activePlugins on initial load when activePlugins is empty. | ||
| * @default false | ||
| */ | ||
| defaultOpen?: boolean; | ||
| /** | ||
| * Render the plugin UI by using the provided element. This function will be called | ||
@@ -126,0 +132,0 @@ * when the plugin tab is clicked and expected to be mounted. |
+3
-3
@@ -1,3 +0,3 @@ | ||
| import { initialState, DevtoolsProvider, PiPProvider } from './chunk/XF4JFOLU.js'; | ||
| export { PLUGIN_CONTAINER_ID, PLUGIN_TITLE_CONTAINER_ID } from './chunk/XF4JFOLU.js'; | ||
| import { initialState, DevtoolsProvider, PiPProvider } from './chunk/VZEY7HNC.js'; | ||
| export { PLUGIN_CONTAINER_ID, PLUGIN_TITLE_CONTAINER_ID } from './chunk/VZEY7HNC.js'; | ||
| import { render, createComponent, Portal } from 'solid-js/web'; | ||
@@ -33,3 +33,3 @@ import { lazy } from 'solid-js'; | ||
| const _self$ = this; | ||
| this.#Component = lazy(() => import('./devtools/UUNAZSBD.js')); | ||
| this.#Component = lazy(() => import('./devtools/7NDEDZB7.js')); | ||
| const Devtools = this.#Component; | ||
@@ -36,0 +36,0 @@ this.#eventBus = new ClientEventBus(this.#eventBusConfig); |
+2
-2
@@ -1,3 +0,3 @@ | ||
| import { initialState } from './chunk/XF4JFOLU.js'; | ||
| export { PLUGIN_CONTAINER_ID, PLUGIN_TITLE_CONTAINER_ID } from './chunk/XF4JFOLU.js'; | ||
| import { initialState } from './chunk/VZEY7HNC.js'; | ||
| export { PLUGIN_CONTAINER_ID, PLUGIN_TITLE_CONTAINER_ID } from './chunk/VZEY7HNC.js'; | ||
| import 'solid-js/web'; | ||
@@ -4,0 +4,0 @@ import 'solid-js'; |
+3
-3
| { | ||
| "name": "@tanstack/devtools", | ||
| "version": "0.6.24", | ||
| "version": "0.7.0", | ||
| "description": "TanStack Devtools is a set of tools for building advanced devtools for your application.", | ||
@@ -59,4 +59,4 @@ "author": "Tanner Linsley", | ||
| "@tanstack/devtools-client": "0.0.3", | ||
| "@tanstack/devtools-event-bus": "0.3.3", | ||
| "@tanstack/devtools-ui": "0.4.4" | ||
| "@tanstack/devtools-ui": "0.4.4", | ||
| "@tanstack/devtools-event-bus": "0.3.3" | ||
| }, | ||
@@ -63,0 +63,0 @@ "peerDependencies": { |
| import { beforeEach, describe, expect, it } from 'vitest' | ||
| import { TANSTACK_DEVTOOLS_STATE } from '../utils/storage' | ||
| import { getStateFromLocalStorage } from './devtools-context' | ||
| import { | ||
| getExistingStateFromStorage, | ||
| getStateFromLocalStorage, | ||
| } from './devtools-context' | ||
| import type { TanStackDevtoolsPlugin } from './devtools-context' | ||
@@ -59,2 +63,265 @@ describe('getStateFromLocalStorage', () => { | ||
| }) | ||
| it('should return undefined when no localStorage state exists (allowing defaultOpen to be applied)', () => { | ||
| // No existing state in localStorage - this allows defaultOpen logic to trigger | ||
| const plugins: Array<TanStackDevtoolsPlugin> = [ | ||
| { | ||
| id: 'plugin1', | ||
| render: () => {}, | ||
| name: 'Plugin 1', | ||
| defaultOpen: true, | ||
| }, | ||
| { | ||
| id: 'plugin2', | ||
| render: () => {}, | ||
| name: 'Plugin 2', | ||
| defaultOpen: false, | ||
| }, | ||
| { | ||
| id: 'plugin3', | ||
| render: () => {}, | ||
| name: 'Plugin 3', | ||
| defaultOpen: true, | ||
| }, | ||
| ] | ||
| // When undefined is returned, getExistingStateFromStorage will fill activePlugins with defaultOpen plugins | ||
| const state = getStateFromLocalStorage(plugins) | ||
| expect(state).toEqual(undefined) | ||
| }) | ||
| it('should preserve existing activePlugins from localStorage (defaultOpen should not override)', () => { | ||
| const mockState = { | ||
| activePlugins: ['plugin2'], | ||
| settings: { | ||
| theme: 'dark', | ||
| }, | ||
| } | ||
| localStorage.setItem(TANSTACK_DEVTOOLS_STATE, JSON.stringify(mockState)) | ||
| const plugins: Array<TanStackDevtoolsPlugin> = [ | ||
| { | ||
| id: 'plugin1', | ||
| render: () => {}, | ||
| name: 'Plugin 1', | ||
| defaultOpen: true, | ||
| }, | ||
| { | ||
| id: 'plugin2', | ||
| render: () => {}, | ||
| name: 'Plugin 2', | ||
| defaultOpen: false, | ||
| }, | ||
| ] | ||
| const state = getStateFromLocalStorage(plugins) | ||
| // Should keep existing activePlugins - defaultOpen logic won't override in getExistingStateFromStorage | ||
| expect(state?.activePlugins).toEqual(['plugin2']) | ||
| }) | ||
| it('should automatically activate a single plugin when no active plugins exist', () => { | ||
| // No existing state in localStorage | ||
| const plugins: Array<TanStackDevtoolsPlugin> = [ | ||
| { | ||
| id: 'only-plugin', | ||
| render: () => {}, | ||
| name: 'Only Plugin', | ||
| }, | ||
| ] | ||
| const state = getStateFromLocalStorage(plugins) | ||
| // Should return undefined - the single plugin activation happens in getExistingStateFromStorage | ||
| expect(state).toEqual(undefined) | ||
| }) | ||
| }) | ||
| describe('getExistingStateFromStorage - integration tests', () => { | ||
| beforeEach(() => { | ||
| localStorage.clear() | ||
| }) | ||
| it('should automatically activate a single plugin when no localStorage state exists', () => { | ||
| const plugins: Array<TanStackDevtoolsPlugin> = [ | ||
| { | ||
| id: 'only-plugin', | ||
| render: () => {}, | ||
| name: 'Only Plugin', | ||
| }, | ||
| ] | ||
| const state = getExistingStateFromStorage(undefined, plugins) | ||
| expect(state.state.activePlugins).toEqual(['only-plugin']) | ||
| expect(state.plugins).toHaveLength(1) | ||
| expect(state.plugins![0]?.id).toBe('only-plugin') | ||
| }) | ||
| it('should activate plugins with defaultOpen: true when no localStorage state exists', () => { | ||
| const plugins: Array<TanStackDevtoolsPlugin> = [ | ||
| { | ||
| id: 'plugin1', | ||
| render: () => {}, | ||
| name: 'Plugin 1', | ||
| defaultOpen: true, | ||
| }, | ||
| { | ||
| id: 'plugin2', | ||
| render: () => {}, | ||
| name: 'Plugin 2', | ||
| defaultOpen: false, | ||
| }, | ||
| { | ||
| id: 'plugin3', | ||
| render: () => {}, | ||
| name: 'Plugin 3', | ||
| defaultOpen: true, | ||
| }, | ||
| ] | ||
| const state = getExistingStateFromStorage(undefined, plugins) | ||
| expect(state.state.activePlugins).toEqual(['plugin1', 'plugin3']) | ||
| expect(state.plugins).toHaveLength(3) | ||
| }) | ||
| it('should limit defaultOpen plugins to MAX_ACTIVE_PLUGINS (3) when 5 have defaultOpen: true', () => { | ||
| const plugins: Array<TanStackDevtoolsPlugin> = [ | ||
| { | ||
| id: 'plugin1', | ||
| render: () => {}, | ||
| name: 'Plugin 1', | ||
| defaultOpen: true, | ||
| }, | ||
| { | ||
| id: 'plugin2', | ||
| render: () => {}, | ||
| name: 'Plugin 2', | ||
| defaultOpen: true, | ||
| }, | ||
| { | ||
| id: 'plugin3', | ||
| render: () => {}, | ||
| name: 'Plugin 3', | ||
| defaultOpen: true, | ||
| }, | ||
| { | ||
| id: 'plugin4', | ||
| render: () => {}, | ||
| name: 'Plugin 4', | ||
| defaultOpen: true, | ||
| }, | ||
| { | ||
| id: 'plugin5', | ||
| render: () => {}, | ||
| name: 'Plugin 5', | ||
| defaultOpen: true, | ||
| }, | ||
| ] | ||
| const state = getExistingStateFromStorage(undefined, plugins) | ||
| // Should only activate first 3 plugins | ||
| expect(state.state.activePlugins).toEqual(['plugin1', 'plugin2', 'plugin3']) | ||
| expect(state.state.activePlugins).toHaveLength(3) | ||
| expect(state.state.activePlugins).not.toContain('plugin4') | ||
| expect(state.state.activePlugins).not.toContain('plugin5') | ||
| // All 5 plugins should still be in the plugins array | ||
| expect(state.plugins).toHaveLength(5) | ||
| }) | ||
| it('should preserve existing activePlugins from localStorage even when plugins have defaultOpen', () => { | ||
| const mockState = { | ||
| activePlugins: ['plugin2', 'plugin4'], | ||
| settings: { | ||
| theme: 'dark', | ||
| }, | ||
| } | ||
| localStorage.setItem(TANSTACK_DEVTOOLS_STATE, JSON.stringify(mockState)) | ||
| const plugins: Array<TanStackDevtoolsPlugin> = [ | ||
| { | ||
| id: 'plugin1', | ||
| render: () => {}, | ||
| name: 'Plugin 1', | ||
| defaultOpen: true, | ||
| }, | ||
| { | ||
| id: 'plugin2', | ||
| render: () => {}, | ||
| name: 'Plugin 2', | ||
| defaultOpen: false, | ||
| }, | ||
| { | ||
| id: 'plugin3', | ||
| render: () => {}, | ||
| name: 'Plugin 3', | ||
| defaultOpen: true, | ||
| }, | ||
| { | ||
| id: 'plugin4', | ||
| render: () => {}, | ||
| name: 'Plugin 4', | ||
| defaultOpen: false, | ||
| }, | ||
| ] | ||
| const state = getExistingStateFromStorage(undefined, plugins) | ||
| // Should preserve the localStorage state, not use defaultOpen | ||
| expect(state.state.activePlugins).toEqual(['plugin2', 'plugin4']) | ||
| expect(state.plugins).toHaveLength(4) | ||
| }) | ||
| it('should return empty activePlugins when no defaultOpen and multiple plugins', () => { | ||
| const plugins: Array<TanStackDevtoolsPlugin> = [ | ||
| { | ||
| id: 'plugin1', | ||
| render: () => {}, | ||
| name: 'Plugin 1', | ||
| }, | ||
| { | ||
| id: 'plugin2', | ||
| render: () => {}, | ||
| name: 'Plugin 2', | ||
| }, | ||
| { | ||
| id: 'plugin3', | ||
| render: () => {}, | ||
| name: 'Plugin 3', | ||
| }, | ||
| ] | ||
| const state = getExistingStateFromStorage(undefined, plugins) | ||
| expect(state.state.activePlugins).toEqual([]) | ||
| expect(state.plugins).toHaveLength(3) | ||
| }) | ||
| it('should handle single plugin with defaultOpen: false by activating it anyway', () => { | ||
| const plugins: Array<TanStackDevtoolsPlugin> = [ | ||
| { | ||
| id: 'only-plugin', | ||
| render: () => {}, | ||
| name: 'Only Plugin', | ||
| defaultOpen: false, | ||
| }, | ||
| ] | ||
| const state = getExistingStateFromStorage(undefined, plugins) | ||
| // Single plugin should be activated regardless of defaultOpen flag | ||
| expect(state.state.activePlugins).toEqual(['only-plugin']) | ||
| }) | ||
| it('should merge config settings into the returned state', () => { | ||
| const plugins: Array<TanStackDevtoolsPlugin> = [ | ||
| { | ||
| id: 'plugin1', | ||
| render: () => {}, | ||
| name: 'Plugin 1', | ||
| }, | ||
| ] | ||
| const config = { | ||
| theme: 'light' as const, | ||
| } | ||
| const state = getExistingStateFromStorage(config as any, plugins) | ||
| expect(state.settings.theme).toBe('light') | ||
| expect(state.state.activePlugins).toEqual(['plugin1']) | ||
| }) | ||
| }) |
| import { createContext, createEffect } from 'solid-js' | ||
| import { createStore } from 'solid-js/store' | ||
| import { getDefaultActivePlugins } from '../utils/get-default-active-plugins' | ||
| import { tryParseJson } from '../utils/sanitize' | ||
@@ -53,2 +54,8 @@ import { | ||
| /** | ||
| * Whether the plugin should be open by default when there are no active plugins. | ||
| * If true, this plugin will be added to activePlugins on initial load when activePlugins is empty. | ||
| * @default false | ||
| */ | ||
| defaultOpen?: boolean | ||
| /** | ||
| * Render the plugin UI by using the provided element. This function will be called | ||
@@ -131,3 +138,3 @@ * when the plugin tab is clicked and expected to be mounted. | ||
| const getExistingStateFromStorage = ( | ||
| export const getExistingStateFromStorage = ( | ||
| config?: TanStackDevtoolsConfig, | ||
@@ -139,15 +146,28 @@ plugins?: Array<TanStackDevtoolsPlugin>, | ||
| const pluginsWithIds = | ||
| plugins?.map((plugin, i) => { | ||
| const id = generatePluginId(plugin, i) | ||
| return { | ||
| ...plugin, | ||
| id, | ||
| } | ||
| }) || [] | ||
| // If no active plugins exist, add plugins with defaultOpen: true | ||
| // Or if there's only 1 plugin, activate it automatically | ||
| let activePlugins = existingState?.activePlugins || [] | ||
| const shouldFillWithDefaultOpenPlugins = | ||
| activePlugins.length === 0 && pluginsWithIds.length > 0 | ||
| if (shouldFillWithDefaultOpenPlugins) { | ||
| activePlugins = getDefaultActivePlugins(pluginsWithIds) | ||
| } | ||
| const state: DevtoolsStore = { | ||
| ...initialState, | ||
| plugins: | ||
| plugins?.map((plugin, i) => { | ||
| const id = generatePluginId(plugin, i) | ||
| return { | ||
| ...plugin, | ||
| id, | ||
| } | ||
| }) || [], | ||
| plugins: pluginsWithIds, | ||
| state: { | ||
| ...initialState.state, | ||
| ...existingState, | ||
| activePlugins, | ||
| }, | ||
@@ -154,0 +174,0 @@ settings: { |
| import { createEffect, createMemo, useContext } from 'solid-js' | ||
| import { MAX_ACTIVE_PLUGINS } from '../utils/constants.js' | ||
| import { DevtoolsContext } from './devtools-context.jsx' | ||
@@ -53,3 +54,3 @@ import { useDrawContext } from './draw-context.jsx' | ||
| : [...prev.state.activePlugins, pluginId] | ||
| if (updatedPlugins.length > 3) return prev | ||
| if (updatedPlugins.length > MAX_ACTIVE_PLUGINS) return prev | ||
| return { | ||
@@ -56,0 +57,0 @@ ...prev, |
| import { createComponent, delegateEvents } from 'solid-js/web'; | ||
| import { createContext, createSignal, createEffect, onCleanup, createMemo, useContext } from 'solid-js'; | ||
| import { createStore } from 'solid-js/store'; | ||
| // src/constants.ts | ||
| var PLUGIN_CONTAINER_ID = "plugin-container"; | ||
| var PLUGIN_TITLE_CONTAINER_ID = "plugin-title-container"; | ||
| // 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/utils/storage.ts | ||
| var getStorageItem = (key) => 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/context/devtools-store.ts | ||
| var keyboardModifiers = [ | ||
| "Alt", | ||
| "Control", | ||
| "Meta", | ||
| "Shift" | ||
| ]; | ||
| var initialState = { | ||
| settings: { | ||
| defaultOpen: false, | ||
| hideUntilHover: false, | ||
| position: "bottom-right", | ||
| panelLocation: "bottom", | ||
| openHotkey: ["Shift", "A"], | ||
| requireUrlFlag: false, | ||
| urlFlag: "tanstack-devtools", | ||
| theme: typeof window !== "undefined" && typeof window.matchMedia !== "undefined" && window.matchMedia("(prefers-color-scheme: dark)").matches ? "dark" : "light", | ||
| triggerImage: "", | ||
| triggerHidden: false | ||
| }, | ||
| state: { | ||
| activeTab: "plugins", | ||
| height: 400, | ||
| activePlugins: [], | ||
| persistOpen: false | ||
| } | ||
| }; | ||
| 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."); | ||
| } | ||
| 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); | ||
| } | ||
| }); | ||
| 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(PiPContext.Provider, { | ||
| value, | ||
| get children() { | ||
| return props.children; | ||
| } | ||
| }); | ||
| }; | ||
| var usePiPWindow = () => { | ||
| const context = createMemo(() => { | ||
| const ctx = useContext(PiPContext); | ||
| if (!ctx) { | ||
| throw new Error("usePiPWindow must be used within a PiPProvider"); | ||
| } | ||
| return ctx(); | ||
| }); | ||
| return context; | ||
| }; | ||
| 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?.activePlugins) { | ||
| const originalLength = existingState.activePlugins.length; | ||
| existingState.activePlugins = existingState.activePlugins.filter((id) => pluginIds.includes(id)); | ||
| if (existingState.activePlugins.length !== originalLength) { | ||
| setStorageItem(TANSTACK_DEVTOOLS_STATE, JSON.stringify(existingState)); | ||
| } | ||
| } | ||
| return existingState; | ||
| } | ||
| var getExistingStateFromStorage = (config, plugins) => { | ||
| const existingState = getStateFromLocalStorage(plugins); | ||
| const settings = getSettings(); | ||
| const state = { | ||
| ...initialState, | ||
| plugins: plugins?.map((plugin, i) => { | ||
| const id = generatePluginId(plugin, i); | ||
| return { | ||
| ...plugin, | ||
| id | ||
| }; | ||
| }) || [], | ||
| state: { | ||
| ...initialState.state, | ||
| ...existingState | ||
| }, | ||
| settings: { | ||
| ...initialState.settings, | ||
| ...config, | ||
| ...settings | ||
| } | ||
| }; | ||
| return state; | ||
| }; | ||
| var DevtoolsProvider = (props) => { | ||
| const [store, setStore] = createStore(getExistingStateFromStorage(props.config, props.plugins)); | ||
| 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, | ||
| 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 | ||
| })); | ||
| } | ||
| }; | ||
| return createComponent(DevtoolsContext.Provider, { | ||
| value, | ||
| get children() { | ||
| return props.children; | ||
| } | ||
| }); | ||
| }; | ||
| export { DevtoolsContext, DevtoolsProvider, PLUGIN_CONTAINER_ID, PLUGIN_TITLE_CONTAINER_ID, PiPProvider, TANSTACK_DEVTOOLS, getAllPermutations, initialState, keyboardModifiers, uppercaseFirstLetter, usePiPWindow }; |
Sorry, the diff of this file is too big to display
Sorry, the diff of this file is too big to display
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
Major refactor
Supply chain riskPackage has recently undergone a major refactor. It may be unstable or indicate significant internal changes. Use caution when updating to versions that include significant changes.
Found 1 instance in 1 package
Network access
Supply chain riskThis module accesses the network.
Found 1 instance in 1 package
Long strings
Supply chain riskContains long string literals, which may be a sign of obfuscated or packed code.
Found 1 instance in 1 package
URL strings
Supply chain riskPackage contains fragments of external URLs or IP addresses, which the package may be accessing at runtime.
Found 1 instance in 1 package
538262
2.89%54
5.88%15043
3.42%