diff --git a/packages/scan/package.json b/packages/scan/package.json index 499d663e..06cf6392 100644 --- a/packages/scan/package.json +++ b/packages/scan/package.json @@ -94,6 +94,11 @@ "import": "./dist/lite/index.mjs", "require": "./dist/lite/index.js" }, + "./tanstack-plugin": { + "types": "./dist/tanstack-plugin/index.d.ts", + "import": "./dist/tanstack-plugin/index.mjs", + "require": "./dist/tanstack-plugin/index.js" + }, "./auto": { "production": { "import": { @@ -193,6 +198,9 @@ ], "react-component-name/loader": [ "./dist/react-component-name/loader.d.ts" + ], + "tanstack-plugin": [ + "./dist/tanstack-plugin/index.d.ts" ] } }, diff --git a/packages/scan/src/tanstack-plugin/index.tsx b/packages/scan/src/tanstack-plugin/index.tsx new file mode 100644 index 00000000..b0ef8006 --- /dev/null +++ b/packages/scan/src/tanstack-plugin/index.tsx @@ -0,0 +1,105 @@ +import { render } from 'preact'; +import { useEffect, useRef, useState } from 'preact/hooks'; +import { scan } from '~core/index'; +import type { Options } from '~core/index'; +import styles from '~web/assets/css/styles.css'; +import { SvgSprite } from '~web/components/svg-sprite'; +import { ToolbarElementContext } from '~web/toolbar-context'; +import { Content } from '~web/views'; +import { ScanOverlay } from '~web/views/inspector/overlay'; + +// Structural type matching TanStackDevtoolsPlugin — no runtime @tanstack/devtools dependency. +export interface ReactScanTanStackPlugin { + id?: string; + name: string; + defaultOpen?: boolean; + render: (el: HTMLDivElement, props: Record) => void; + destroy?: (pluginId: string) => void; +} + +const TanStackPanel = () => { + const containerRef = useRef(null); + // Force a re-render after mount so ToolbarElementContext receives the live DOM ref, + // enabling portal targets in popovers and other-visualization. + const [, forceUpdate] = useState(false); + useEffect(() => { + forceUpdate(true); + }, []); + + return ( + + +
+ +
+
+ ); +}; + +/** + * Creates a TanStack DevTools plugin that embeds React Scan's inspector, + * notifications, and scan controls inside the TanStack DevTools panel. + * + * @example + * ```tsx + * import { TanStackDevtools } from '@tanstack/react-devtools'; + * import { createReactScanPlugin } from 'react-scan/tanstack-plugin'; + * + * + * ``` + * + * React Scan's render outline overlays continue to appear on the page as usual. + * The floating toolbar is suppressed — all controls live inside the TanStack panel. + */ +export function createReactScanPlugin( + options: Omit = {}, +): ReactScanTanStackPlugin { + let shadowRoot: ShadowRoot | null = null; + let container: HTMLDivElement | null = null; + + return { + id: 'react-scan', + name: 'React Scan', + render(el) { + // Start scanning without the floating widget. + scan({ ...options, showToolbar: false }); + + // Create an isolated shadow root — same pattern as initRootContainer in core/index.ts. + shadowRoot = el.attachShadow({ mode: 'open' }); + + const styleEl = document.createElement('style'); + styleEl.textContent = styles; + shadowRoot.appendChild(styleEl); + + container = document.createElement('div'); + container.style.cssText = 'height:100%;display:flex;flex-direction:column;'; + shadowRoot.appendChild(container); + + render( + <> + + + , + container, + ); + }, + destroy() { + if (container) { + // Double render(null) is required to fully unmount Preact components + // and flush internal VNode references and event listeners. + render(null, container); + render(null, container); + container = null; + shadowRoot = null; + } + }, + }; +} diff --git a/packages/scan/src/web/toolbar-context.ts b/packages/scan/src/web/toolbar-context.ts new file mode 100644 index 00000000..d9056131 --- /dev/null +++ b/packages/scan/src/web/toolbar-context.ts @@ -0,0 +1,3 @@ +import { createContext } from 'preact'; + +export const ToolbarElementContext = createContext(null); diff --git a/packages/scan/src/web/widget/index.tsx b/packages/scan/src/web/widget/index.tsx index 7025eb03..023f29f6 100644 --- a/packages/scan/src/web/widget/index.tsx +++ b/packages/scan/src/web/widget/index.tsx @@ -1,4 +1,5 @@ -import { createContext, type JSX } from "preact"; +import type { JSX } from "preact"; +import { ToolbarElementContext } from '~web/toolbar-context'; import { useCallback, useEffect, useRef, useState } from "preact/hooks"; import { Store, ReactScanInternals } from "~core/index"; import { @@ -757,4 +758,4 @@ export const Widget = () => { ); }; -export const ToolbarElementContext = createContext(null); +export { ToolbarElementContext } from '~web/toolbar-context'; diff --git a/packages/scan/tsup.config.ts b/packages/scan/tsup.config.ts index 113c591b..47ded252 100644 --- a/packages/scan/tsup.config.ts +++ b/packages/scan/tsup.config.ts @@ -124,6 +124,7 @@ export default defineConfig([ './src/install-hook.ts', './src/core/all-environments.ts', './src/lite/index.ts', + './src/tanstack-plugin/index.tsx', ], banner: { js: banner, @@ -144,6 +145,7 @@ export default defineConfig([ await addDirectivesToChunkFiles(DIST_PATH); await addDirectivesToChunkFiles(path.join(DIST_PATH, 'lite')); await addDirectivesToChunkFiles(path.join(DIST_PATH, 'core')); + await addDirectivesToChunkFiles(path.join(DIST_PATH, 'tanstack-plugin')); }, minify: false, env: {