From 91e7fcf2f3c611ac8e2ce7bb2a1ed2024072c007 Mon Sep 17 00:00:00 2001 From: Birk Skyum Date: Thu, 20 Aug 2026 01:09:12 +0200 Subject: [PATCH] feat: support SSR development boundaries --- .changeset/quiet-cats-smile.md | 5 ++ README.md | 4 ++ package.json | 4 ++ rollup.config.js | 92 +++++++++++++++++++++------------- src/dev-toolbar/index.tsx | 3 +- src/index.tsx | 24 ++++++--- src/server.tsx | 8 +++ 7 files changed, 96 insertions(+), 44 deletions(-) create mode 100644 .changeset/quiet-cats-smile.md create mode 100644 src/server.tsx diff --git a/.changeset/quiet-cats-smile.md b/.changeset/quiet-cats-smile.md new file mode 100644 index 0000000..bc575f7 --- /dev/null +++ b/.changeset/quiet-cats-smile.md @@ -0,0 +1,5 @@ +--- +"@solidjs/start-devtools": patch +--- + +Support server rendering the development error boundary and avoid duplicate toolbars when authored entries wrap their app. diff --git a/README.md b/README.md index af84163..2f9dbb7 100644 --- a/README.md +++ b/README.md @@ -18,6 +18,10 @@ import { DevToolbar } from "@solidjs/start-devtools"; ; ``` +For authored server and client entries, render `DevToolbar` around the app in a shared +document or root component. The package selects an SSR-safe build on the server, so the +same component provides the development error boundary on both sides. + The package is intended for development and should not be imported into production entries. For component and reactivity inspection, see [Solid Devtools](https://github.com/thetarnav/solid-devtools). diff --git a/package.json b/package.json index 6104925..4017262 100644 --- a/package.json +++ b/package.json @@ -11,6 +11,10 @@ "exports": { ".": { "types": "./dist/types/index.d.ts", + "worker": "./dist/server.js", + "browser": "./dist/index.js", + "deno": "./dist/server.js", + "node": "./dist/server.js", "import": "./dist/index.js" } }, diff --git a/rollup.config.js b/rollup.config.js index 4444f47..c606449 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -6,11 +6,12 @@ import cleaner from 'rollup-plugin-cleaner'; const extensions = ['.js', '.ts', '.json', '.tsx', '.jsx']; -function css() { +function css(server = false) { return { name: 'devtools-css', transform(code, id) { if (!id.endsWith('.css')) return null; + if (server) return { code: '', map: null }; return { code: `const style = document.createElement('style');\nstyle.textContent = ${JSON.stringify(code)};\ndocument.head.append(style);`, map: null, @@ -49,40 +50,59 @@ function packageVersion() { }; } -export default { - input: 'src/index.tsx', - output: { - format: 'esm', - dir: 'dist', +function external(id) { + return ( + id === 'solid-js' || + id.startsWith('solid-js/') || + id === '@solidjs/web' || + id.startsWith('@solidjs/web/') + ); +} + +function config({ input, entryFileNames, chunkFileNames, generate, server = false }) { + return { + input, + output: { + format: 'esm', + dir: 'dist', + entryFileNames, + chunkFileNames, + assetFileNames: 'assets/[name]-[hash][extname]', + sourcemap: true, + }, + external, + plugins: [ + ...(server ? [] : [cleaner({ targets: ['./dist/'] })]), + css(server), + assetUrl(), + packageVersion(), + babel({ + extensions, + babelHelpers: 'bundled', + presets: [ + ['@babel/preset-env', { targets: { esmodules: true } }], + '@babel/preset-typescript', + ['babel-preset-solid', { moduleName: '@solidjs/web', generate }], + ], + }), + nodeResolve({ extensions, browser: !server }), + cjs({ extensions }), + ], + }; +} + +export default [ + config({ + input: 'src/index.tsx', entryFileNames: 'index.js', chunkFileNames: 'chunks/[name]-[hash].js', - assetFileNames: 'assets/[name]-[hash][extname]', - sourcemap: true, - }, - external(id) { - return ( - id === 'solid-js' || - id.startsWith('solid-js/') || - id === '@solidjs/web' || - id.startsWith('@solidjs/web/') - ); - }, - plugins: [ - cleaner({ targets: ['./dist/'] }), - css(), - assetUrl(), - packageVersion(), - babel({ - extensions, - babelHelpers: 'bundled', - presets: [ - ['@babel/preset-env', { targets: { esmodules: true } }], - '@babel/preset-typescript', - ['babel-preset-solid', { moduleName: '@solidjs/web', generate: 'dom' }], - ], - }), - nodeResolve({ extensions, browser: true }), - cjs({ extensions }), - ], -}; - + generate: 'dom', + }), + config({ + input: 'src/server.tsx', + entryFileNames: 'server.js', + chunkFileNames: 'server-chunks/[name]-[hash].js', + generate: 'ssr', + server: true, + }), +]; diff --git a/src/dev-toolbar/index.tsx b/src/dev-toolbar/index.tsx index cea535a..5a27cc2 100644 --- a/src/dev-toolbar/index.tsx +++ b/src/dev-toolbar/index.tsx @@ -1,5 +1,5 @@ import type { JSX } from '@solidjs/web'; -import { clientOnly, Portal } from '@solidjs/web'; +import { clientOnly, httpStatus, isServer, Portal } from '@solidjs/web'; import { createEffect, createSignal, Errored, onSettled } from 'solid-js'; import { Toolbar } from 'terracotta/toolbar'; import version from '../version.js'; @@ -213,6 +213,7 @@ export function DevToolbar(props: DevToolbarProps) { { const err = error(); + if (isServer) httpStatus(500); queueMicrotask(() => pushError(err)); return <>; }} diff --git a/src/index.tsx b/src/index.tsx index b61e12b..46ef092 100644 --- a/src/index.tsx +++ b/src/index.tsx @@ -3,22 +3,32 @@ import { pushServerFunctionCall } from './dev-toolbar/functions/tracker.js'; import { DevToolbar } from './dev-toolbar/index.js'; let dispose: (() => void) | undefined; +let frame: number | undefined; export { DevToolbar, pushServerFunctionCall }; export function mountDevToolbar(): () => void { if (dispose) return dispose; - const host = document.createElement('div'); - host.dataset.solidDevToolbarRoot = ''; - document.body.append(host); - const unmount = render(() => , host); + let unmount: (() => void) | undefined; + let host: HTMLDivElement | undefined; dispose = () => { - unmount(); - host.remove(); + if (frame !== undefined) cancelAnimationFrame(frame); + unmount?.(); + host?.remove(); + frame = undefined; dispose = undefined; }; + + frame = requestAnimationFrame(() => { + frame = undefined; + if (document.querySelector('[data-solid-dev-toolbar]')) return; + host = document.createElement('div'); + host.dataset.solidDevToolbarRoot = ''; + document.body.append(host); + unmount = render(() => , host); + }); + return dispose; } - diff --git a/src/server.tsx b/src/server.tsx new file mode 100644 index 0000000..847a0a9 --- /dev/null +++ b/src/server.tsx @@ -0,0 +1,8 @@ +import { pushServerFunctionCall } from './dev-toolbar/functions/tracker.js'; +import { DevToolbar } from './dev-toolbar/index.js'; + +export { DevToolbar, pushServerFunctionCall }; + +export function mountDevToolbar(): () => void { + return () => {}; +}