@octanejs/tanstack-devtools 0.0.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/LICENSE ADDED
@@ -0,0 +1,21 @@
1
+ MIT License
2
+
3
+ Copyright (c) 2026 Dominic Gannaway
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining a copy
6
+ of this software and associated documentation files (the "Software"), to deal
7
+ in the Software without restriction, including without limitation the rights
8
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
9
+ copies of the Software, and to permit persons to whom the Software is
10
+ furnished to do so, subject to the following conditions:
11
+
12
+ The above copyright notice and this permission notice shall be included in all
13
+ copies or substantial portions of the Software.
14
+
15
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
16
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
17
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
18
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
19
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
20
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
21
+ SOFTWARE.
package/README.md ADDED
@@ -0,0 +1,54 @@
1
+ # @octanejs/tanstack-devtools
2
+
3
+ [TanStack Devtools](https://tanstack.com/devtools) bindings for
4
+ [Octane](https://github.com/octanejs/octane).
5
+
6
+ This is an Octane port of `@tanstack/react-devtools`. It reuses the
7
+ framework-agnostic `@tanstack/devtools` core **unchanged** and ports the thin
8
+ adapter layer — the `TanStackDevtools` component — so Octane elements can be used
9
+ as plugin panels, titles, and custom triggers.
10
+
11
+ ## Installation
12
+
13
+ ```bash
14
+ pnpm add @octanejs/tanstack-devtools
15
+ ```
16
+
17
+ ## Usage
18
+
19
+ ```tsx
20
+ import { TanStackDevtools } from '@octanejs/tanstack-devtools';
21
+
22
+ function App() @{
23
+ <>
24
+ <TanStackDevtools
25
+ plugins={[
26
+ {
27
+ id: 'my-plugin',
28
+ name: 'My Plugin',
29
+ render: () => <MyPluginPanel />,
30
+ },
31
+ ]}
32
+ />
33
+ <MyApp />
34
+ </>
35
+ }
36
+ ```
37
+
38
+ `plugins`, `config`, and `eventBusConfig` mirror `@tanstack/react-devtools`.
39
+ `config.customTrigger` accepts an Octane element (or a function returning one) for
40
+ a custom launcher.
41
+
42
+ ## Notes / divergences
43
+
44
+ - Public adapter types are Octane-prefixed: `TanStackDevtoolsOctanePlugin` and
45
+ `TanStackDevtoolsOctaneInit`.
46
+ - `ref` is the normal React-19-style ref prop and events are native — there is no
47
+ synthetic event layer.
48
+ - The main entry also re-exports the `@tanstack/devtools` core surface
49
+ (`TanStackDevtoolsCore`, `PLUGIN_CONTAINER_ID`, `PLUGIN_TITLE_CONTAINER_ID`, and
50
+ the plugin authoring types) so you don't need a direct dependency on
51
+ `@tanstack/devtools` to type plugins.
52
+
53
+ Only include the devtools in development, e.g. behind a `import.meta.env.DEV`
54
+ check or via `lazy()`.
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@octanejs/tanstack-devtools",
3
+ "version": "0.0.1",
4
+ "license": "MIT",
5
+ "type": "module",
6
+ "engines": {
7
+ "node": ">=22"
8
+ },
9
+ "description": "TanStack Devtools bindings for Octane — reuses @tanstack/devtools and ports the React adapter's TanStackDevtools component.",
10
+ "author": {
11
+ "name": "Dominic Gannaway",
12
+ "email": "dg@domgan.com"
13
+ },
14
+ "publishConfig": {
15
+ "access": "public"
16
+ },
17
+ "repository": {
18
+ "type": "git",
19
+ "url": "git+https://github.com/octanejs/octane.git",
20
+ "directory": "packages/tanstack-devtools"
21
+ },
22
+ "main": "src/index.ts",
23
+ "module": "src/index.ts",
24
+ "types": "src/index.ts",
25
+ "files": [
26
+ "src",
27
+ "README.md"
28
+ ],
29
+ "exports": {
30
+ ".": "./src/index.ts"
31
+ },
32
+ "dependencies": {
33
+ "@tanstack/devtools": "0.12.5"
34
+ },
35
+ "peerDependencies": {
36
+ "octane": "0.1.7"
37
+ },
38
+ "devDependencies": {
39
+ "@testing-library/jest-dom": "^6.9.1",
40
+ "vitest": "^4.1.9",
41
+ "@octanejs/testing-library": "0.1.4",
42
+ "octane": "0.1.7"
43
+ },
44
+ "scripts": {
45
+ "test": "vitest run"
46
+ }
47
+ }
@@ -0,0 +1,240 @@
1
+ // Ported from @tanstack/react-devtools 0.10.7
2
+ // (https://github.com/TanStack/devtools/tree/main/packages/react-devtools/src/devtools.tsx).
3
+ //
4
+ // Octane adaptations:
5
+ // - React hooks (useEffect/useMemo/useRef/useState) come directly from octane.
6
+ // - `createPortal` comes from octane instead of react-dom. Octane renders a portal
7
+ // VALUE returned from a component at any position, so each plugin / title / trigger
8
+ // is portaled into the container the framework-agnostic core hands us via a tiny
9
+ // `DevtoolsPortal` component (matching the pattern in @octanejs/lexical's Decorators).
10
+ // - The framework-agnostic `@tanstack/devtools` core (TanStackDevtoolsCore, Solid) is
11
+ // reused UNCHANGED, exactly as the React binding does.
12
+ // - `ref` is the normal React-19-style ref prop; native events; no synthetic layer.
13
+ import { createPortal, useEffect, useMemo, useRef, useState } from 'octane';
14
+ import { TanStackDevtoolsCore } from '@tanstack/devtools';
15
+ import type {
16
+ ClientEventBusConfig,
17
+ TanStackDevtoolsConfig,
18
+ TanStackDevtoolsPlugin,
19
+ TanStackDevtoolsPluginProps,
20
+ TanStackDevtoolsTheme,
21
+ } from '@tanstack/devtools';
22
+
23
+ // An octane renderable (JSX element descriptor, component, primitive, array, …).
24
+ // `{} | null | undefined` rather than `unknown` so the `(el, props) => Renderable`
25
+ // callback member of the render/name/trigger unions keeps its parameter inference
26
+ // (a `unknown` union member collapses the whole union to `unknown`).
27
+ type Renderable = {} | null | undefined;
28
+
29
+ type PluginRender = Renderable | ((el: HTMLElement, props: TanStackDevtoolsPluginProps) => Renderable);
30
+
31
+ type TriggerProps = {
32
+ theme: TanStackDevtoolsTheme;
33
+ };
34
+
35
+ type TriggerRender = Renderable | ((el: HTMLElement, props: TriggerProps) => Renderable);
36
+
37
+ export type TanStackDevtoolsOctanePlugin = Omit<TanStackDevtoolsPlugin, 'render' | 'name'> & {
38
+ /**
39
+ * The render can be an octane element or a function that returns one. If it's a
40
+ * function, it's called to render the plugin; otherwise it's rendered directly.
41
+ *
42
+ * ```jsx
43
+ * { render: () => <CustomPluginComponent /> }
44
+ * // or
45
+ * { render: <CustomPluginComponent /> }
46
+ * ```
47
+ */
48
+ render: PluginRender;
49
+ /**
50
+ * Name displayed in the devtools UI. A string is used verbatim; an element (or a
51
+ * function returning one) is portaled into the plugin's title slot.
52
+ *
53
+ * ```jsx
54
+ * { name: 'Your Plugin', render: () => <CustomPluginComponent /> }
55
+ * // or
56
+ * { name: <h1>Your Plugin title</h1>, render: () => <CustomPluginComponent /> }
57
+ * ```
58
+ */
59
+ name: string | PluginRender;
60
+ };
61
+
62
+ type TanStackDevtoolsOctaneConfig = Omit<Partial<TanStackDevtoolsConfig>, 'customTrigger'> & {
63
+ /**
64
+ * Optional custom trigger component. An octane element or a function returning one.
65
+ *
66
+ * ```jsx
67
+ * { customTrigger: <CustomTriggerComponent /> }
68
+ * ```
69
+ */
70
+ customTrigger?: TriggerRender;
71
+ };
72
+
73
+ export interface TanStackDevtoolsOctaneInit {
74
+ /**
75
+ * Plugins to render in the devtools. Each plugin needs a `render` that returns an
76
+ * octane element (or a function that does).
77
+ *
78
+ * ```jsx
79
+ * <TanStackDevtools
80
+ * plugins={[{ id: 'your-plugin-id', name: 'Your Plugin', render: <CustomPluginComponent /> }]}
81
+ * />
82
+ * ```
83
+ */
84
+ plugins?: Array<TanStackDevtoolsOctanePlugin>;
85
+ /**
86
+ * Configuration for the devtools shell. Used to seed the initial state the first
87
+ * time the devtools start; afterwards settings persist in local storage and change
88
+ * through the settings panel.
89
+ */
90
+ config?: TanStackDevtoolsOctaneConfig;
91
+ /**
92
+ * Configuration for the TanStack Devtools client event bus.
93
+ */
94
+ eventBusConfig?: ClientEventBusConfig;
95
+ }
96
+
97
+ type ComponentMap = Record<string, Renderable>;
98
+ type ContainerMap = Record<string, HTMLElement>;
99
+ type SetComponents = (updater: (prev: ComponentMap) => ComponentMap) => void;
100
+
101
+ const convertRender = (
102
+ Component: PluginRender,
103
+ setComponents: SetComponents,
104
+ e: HTMLElement,
105
+ props: TanStackDevtoolsPluginProps,
106
+ ) => {
107
+ const element = typeof Component === 'function' ? Component(e, props) : Component;
108
+
109
+ setComponents((prev) => ({
110
+ ...prev,
111
+ [e.getAttribute('id') as string]: element,
112
+ }));
113
+ };
114
+
115
+ const convertTrigger = (
116
+ Component: TriggerRender,
117
+ setComponent: (element: Renderable) => void,
118
+ e: HTMLElement,
119
+ props: TriggerProps,
120
+ ) => {
121
+ const element = typeof Component === 'function' ? Component(e, props) : Component;
122
+ setComponent(element);
123
+ };
124
+
125
+ // A createPortal VALUE returned from a component renders at any position in octane,
126
+ // so each container's content lives in its own tiny portal component.
127
+ function DevtoolsPortal(props: { target: HTMLElement; content: Renderable }) {
128
+ return createPortal(props.content, props.target);
129
+ }
130
+
131
+ export function TanStackDevtools(props: TanStackDevtoolsOctaneInit) {
132
+ const { plugins, config, eventBusConfig } = props;
133
+
134
+ const devToolRef = useRef<HTMLDivElement>(null);
135
+
136
+ const [pluginContainers, setPluginContainers] = useState<ContainerMap>({});
137
+ const [titleContainers, setTitleContainers] = useState<ContainerMap>({});
138
+ const [triggerContainer, setTriggerContainer] = useState<HTMLElement | null>(null);
139
+
140
+ const [PluginComponents, setPluginComponents] = useState<ComponentMap>({});
141
+ const [TitleComponents, setTitleComponents] = useState<ComponentMap>({});
142
+ const [TriggerComponent, setTriggerComponent] = useState<Renderable>(null);
143
+
144
+ const pluginsMap = useMemo<Array<TanStackDevtoolsPlugin>>(
145
+ () =>
146
+ plugins?.map((plugin) => {
147
+ return {
148
+ ...plugin,
149
+ name:
150
+ typeof plugin.name === 'string'
151
+ ? plugin.name
152
+ : (e: HTMLElement, pluginProps: TanStackDevtoolsPluginProps) => {
153
+ const id = e.getAttribute('id')!;
154
+ const target = e.ownerDocument.getElementById(id);
155
+
156
+ if (target) {
157
+ setTitleContainers((prev) => ({
158
+ ...prev,
159
+ [id]: e,
160
+ }));
161
+ }
162
+
163
+ convertRender(plugin.name as PluginRender, setTitleComponents, e, pluginProps);
164
+ },
165
+ render: (e: HTMLElement, pluginProps: TanStackDevtoolsPluginProps) => {
166
+ const id = e.getAttribute('id')!;
167
+ const target = e.ownerDocument.getElementById(id);
168
+
169
+ if (target) {
170
+ setPluginContainers((prev) => ({
171
+ ...prev,
172
+ [id]: e,
173
+ }));
174
+ }
175
+
176
+ convertRender(plugin.render, setPluginComponents, e, pluginProps);
177
+ },
178
+ } as TanStackDevtoolsPlugin;
179
+ }) ?? [],
180
+ [plugins],
181
+ );
182
+
183
+ const [devtools] = useState(() => {
184
+ const { customTrigger, ...coreConfig } = config || {};
185
+ return new TanStackDevtoolsCore({
186
+ config: {
187
+ ...coreConfig,
188
+ customTrigger: customTrigger
189
+ ? (el: HTMLElement, triggerProps: TriggerProps) => {
190
+ setTriggerContainer(el);
191
+ convertTrigger(customTrigger, setTriggerComponent, el, triggerProps);
192
+ }
193
+ : undefined,
194
+ },
195
+ eventBusConfig,
196
+ plugins: pluginsMap,
197
+ });
198
+ });
199
+
200
+ useEffect(() => {
201
+ devtools.setConfig({
202
+ plugins: pluginsMap,
203
+ });
204
+ }, [devtools, pluginsMap]);
205
+
206
+ useEffect(() => {
207
+ if (devToolRef.current) {
208
+ devtools.mount(devToolRef.current);
209
+ }
210
+
211
+ return () => devtools.unmount();
212
+ }, [devtools]);
213
+
214
+ const hasPlugins =
215
+ Object.values(pluginContainers).length > 0 && Object.values(PluginComponents).length > 0;
216
+ const hasTitles =
217
+ Object.values(titleContainers).length > 0 && Object.values(TitleComponents).length > 0;
218
+
219
+ return (
220
+ <>
221
+ <div style={{ position: 'absolute' }} ref={devToolRef} />
222
+
223
+ @if (hasPlugins) {
224
+ @for (const [id, pluginContainer] of Object.entries(pluginContainers); key id) {
225
+ <DevtoolsPortal target={pluginContainer} content={PluginComponents[id]} />
226
+ }
227
+ }
228
+
229
+ @if (hasTitles) {
230
+ @for (const [id, titleContainer] of Object.entries(titleContainers); key id) {
231
+ <DevtoolsPortal target={titleContainer} content={TitleComponents[id]} />
232
+ }
233
+ }
234
+
235
+ @if (triggerContainer && TriggerComponent) {
236
+ <DevtoolsPortal target={triggerContainer} content={TriggerComponent} />
237
+ }
238
+ </>
239
+ );
240
+ }
@@ -0,0 +1,41 @@
1
+ // Declaration companion for devtools.tsrx.
2
+ import type {
3
+ ClientEventBusConfig,
4
+ TanStackDevtoolsConfig,
5
+ TanStackDevtoolsPlugin,
6
+ TanStackDevtoolsPluginProps,
7
+ TanStackDevtoolsTheme,
8
+ } from '@tanstack/devtools';
9
+
10
+ // `{} | null | undefined` rather than `unknown` so the `(el, props) => Renderable`
11
+ // callback member of the render/name/trigger unions keeps its parameter inference.
12
+ type Renderable = {} | null | undefined;
13
+
14
+ type PluginRender =
15
+ | Renderable
16
+ | ((el: HTMLElement, props: TanStackDevtoolsPluginProps) => Renderable);
17
+
18
+ type TriggerProps = {
19
+ theme: TanStackDevtoolsTheme;
20
+ };
21
+
22
+ type TriggerRender = Renderable | ((el: HTMLElement, props: TriggerProps) => Renderable);
23
+
24
+ export type TanStackDevtoolsOctanePlugin = Omit<TanStackDevtoolsPlugin, 'render' | 'name'> & {
25
+ render: PluginRender;
26
+ name: string | PluginRender;
27
+ };
28
+
29
+ type TanStackDevtoolsOctaneConfig = Omit<Partial<TanStackDevtoolsConfig>, 'customTrigger'> & {
30
+ customTrigger?: TriggerRender;
31
+ };
32
+
33
+ export interface TanStackDevtoolsOctaneInit {
34
+ plugins?: Array<TanStackDevtoolsOctanePlugin>;
35
+ config?: TanStackDevtoolsOctaneConfig;
36
+ eventBusConfig?: ClientEventBusConfig;
37
+ }
38
+
39
+ export declare function TanStackDevtools(props: TanStackDevtoolsOctaneInit): unknown;
40
+
41
+ export {};
package/src/index.ts ADDED
@@ -0,0 +1,20 @@
1
+ 'use client';
2
+
3
+ export { TanStackDevtools } from './devtools.tsrx';
4
+ export type { TanStackDevtoolsOctanePlugin, TanStackDevtoolsOctaneInit } from './devtools.tsrx';
5
+
6
+ // Re-export the framework-agnostic core surface so consumers don't need a direct
7
+ // dependency on @tanstack/devtools for plugin authoring types.
8
+ export {
9
+ PLUGIN_CONTAINER_ID,
10
+ PLUGIN_TITLE_CONTAINER_ID,
11
+ TanStackDevtoolsCore,
12
+ } from '@tanstack/devtools';
13
+ export type {
14
+ ClientEventBusConfig,
15
+ TanStackDevtoolsConfig,
16
+ TanStackDevtoolsInit,
17
+ TanStackDevtoolsPlugin,
18
+ TanStackDevtoolsPluginProps,
19
+ TanStackDevtoolsTheme,
20
+ } from '@tanstack/devtools';