From f2341353ce654aa2327e9c71bdb7e38af5f98e42 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Sun, 4 Jan 2026 08:53:42 +0800 Subject: [PATCH 01/25] add seroval json mode --- packages/start/src/fns/client.ts | 13 ++++++++++--- packages/start/src/fns/handler.ts | 32 ++++++++++++++++++++++++------- 2 files changed, 35 insertions(+), 10 deletions(-) diff --git a/packages/start/src/fns/client.ts b/packages/start/src/fns/client.ts index c50fb21f9..2cb5528ac 100644 --- a/packages/start/src/fns/client.ts +++ b/packages/start/src/fns/client.ts @@ -12,7 +12,12 @@ import { let INSTANCE = 0; -function createRequest(base: string, id: string, instance: string, options: RequestInit) { +function createRequest( + base: string, + id: string, + instance: string, + options: RequestInit, +) { return fetch(base, { method: "POST", ...options, @@ -100,7 +105,8 @@ export function cloneServerReference(id: string) { let baseURL = import.meta.env.BASE_URL ?? "/"; if (!baseURL.endsWith("/")) baseURL += "/"; - const fn = (...args: any[]) => fetchServerFunction(`${baseURL}_server`, id, {}, args); + const fn = (...args: any[]) => + fetchServerFunction(`${baseURL}_server`, id, {}, args); return new Proxy(fn, { get(target, prop, receiver) { @@ -114,7 +120,8 @@ export function cloneServerReference(id: string) { const url = `${baseURL}_server?id=${encodeURIComponent(id)}`; return (options: RequestInit) => { const fn = async (...args: any[]) => { - const encodeArgs = options.method && options.method.toUpperCase() === "GET"; + const encodeArgs = + options.method && options.method.toUpperCase() === "GET"; return fetchServerFunction( encodeArgs ? url + diff --git a/packages/start/src/fns/handler.ts b/packages/start/src/fns/handler.ts index dbf1fdcf1..8fa2e97dd 100644 --- a/packages/start/src/fns/handler.ts +++ b/packages/start/src/fns/handler.ts @@ -134,14 +134,18 @@ export async function handleServerFunction(h3Event: H3Event) { // forward headers if ((x as any).headers) mergeResponseHeaders(h3Event, (x as any).headers); // forward non-redirect statuses - if ((x as any).status && (!instance || (x as any).status < 300 || (x as any).status >= 400)) + if ( + (x as any).status && + (!instance || (x as any).status < 300 || (x as any).status >= 400) + ) h3Event.res.status = (x as any).status; if ((x as any).customBody) { x = await (x as any).customBody(); } else if ((x as any).body == null) x = null; h3Event.res.headers.set("X-Error", "true"); } else if (instance) { - const error = x instanceof Error ? x.message : typeof x === "string" ? x : "true"; + const error = + x instanceof Error ? x.message : typeof x === "string" ? x : "true"; h3Event.res.headers.set("X-Error", error.replace(/[\r\n]+/g, "")); } else { @@ -171,7 +175,12 @@ export async function handleServerFunction(h3Event: H3Event) { } } -function handleNoJS(result: any, request: Request, parsed: any[], thrown?: boolean) { +function handleNoJS( + result: any, + request: Request, + parsed: any[], + thrown?: boolean, +) { const url = new URL(request.url); const isError = result instanceof Error; let statusCode = 302; @@ -181,7 +190,10 @@ function handleNoJS(result: any, request: Request, parsed: any[], thrown?: boole if (result.headers.has("Location")) { headers.set( `Location`, - new URL(result.headers.get("Location")!, url.origin + import.meta.env.BASE_URL).toString(), + new URL( + result.headers.get("Location")!, + url.origin + import.meta.env.BASE_URL, + ).toString(), ); statusCode = getExpectedRedirectStatus(result); } @@ -198,7 +210,10 @@ function handleNoJS(result: any, request: Request, parsed: any[], thrown?: boole result: isError ? result.message : result, thrown: thrown, error: isError, - input: [...parsed.slice(0, -1), [...parsed[parsed.length - 1].entries()]], + input: [ + ...parsed.slice(0, -1), + [...parsed[parsed.length - 1].entries()], + ], }), )}; Secure; HttpOnly;`, ); @@ -224,7 +239,7 @@ export function createSingleFlightHeaders(sourceEvent: FetchEvent) { // useH3Internals = true; // sourceEvent.nativeEvent.node.req.headers.cookie = ""; // } - SetCookies.forEach(cookie => { + SetCookies.forEach((cookie) => { if (!cookie) return; const { maxAge, expires, name, value } = parseSetCookie(cookie); if (maxAge != null && maxAge <= 0) { @@ -245,7 +260,10 @@ export function createSingleFlightHeaders(sourceEvent: FetchEvent) { return headers; } -async function handleSingleFlight(sourceEvent: FetchEvent, result: any): Promise { +async function handleSingleFlight( + sourceEvent: FetchEvent, + result: any, +): Promise { let revalidate: string[]; let url = new URL(sourceEvent.request.headers.get("referer")!).toString(); if (result instanceof Response) { From f32ebb2856bc4beed063762f26c9eea8e0fe6449 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Sun, 4 Jan 2026 09:43:12 +0800 Subject: [PATCH 02/25] Fix client-server comms --- packages/start/src/fns/handler.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/start/src/fns/handler.ts b/packages/start/src/fns/handler.ts index 8fa2e97dd..ec4320932 100644 --- a/packages/start/src/fns/handler.ts +++ b/packages/start/src/fns/handler.ts @@ -124,7 +124,6 @@ export async function handleServerFunction(h3Event: H3Event) { h3Event.res.headers.set("content-type", "text/javascript"); return serializeToJSStream(instance, result); } - h3Event.res.headers.set("content-type", "text/plain"); return serializeToJSONStream(result); } catch (x) { if (x instanceof Response) { From 45d24aa6bf72f680a2bde4b96cdcc96f961af5be Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Sun, 4 Jan 2026 12:13:06 +0800 Subject: [PATCH 03/25] Update server-functions-handler.ts --- packages/start/src/fns/handler.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/start/src/fns/handler.ts b/packages/start/src/fns/handler.ts index ec4320932..d2ba034b7 100644 --- a/packages/start/src/fns/handler.ts +++ b/packages/start/src/fns/handler.ts @@ -167,7 +167,6 @@ export async function handleServerFunction(h3Event: H3Event) { h3Event.res.headers.set("content-type", "text/javascript"); return serializeToJSStream(instance, x); } - h3Event.res.headers.set("content-type", "text/plain"); return serializeToJSONStream(x); } return x; From 551455e443ee51279d62bacd08f16ea9b4f441ae Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Wed, 14 Jan 2026 14:39:21 +0800 Subject: [PATCH 04/25] Add server function inspector --- .../tests/src/routes/server-function-ping.tsx | 28 +- packages/start/src/fns/client.ts | 16 +- packages/start/src/fns/serialization.ts | 2 +- packages/start/src/shared/ErrorBoundary.tsx | 60 +- .../shared/dev-overlay/DevOverlayDialog.tsx | 83 +- .../start/src/shared/dev-overlay/styles.css | 11 +- .../HeadersViewer.css | 12 + .../HeadersViewer.tsx | 20 + .../SerovalViewer.css | 88 ++ .../SerovalViewer.tsx | 836 ++++++++++++++++++ .../server-function-inspector/index.tsx | 209 +++++ .../server-function-tracker.ts | 61 ++ .../server-function-inspector/styles.css | 75 ++ packages/start/src/shared/ui/Badge.css | 33 + packages/start/src/shared/ui/Badge.tsx | 17 + packages/start/src/shared/ui/Button.css | 35 + packages/start/src/shared/ui/Button.tsx | 8 + packages/start/src/shared/ui/Cascade.css | 30 + packages/start/src/shared/ui/Cascade.tsx | 13 + packages/start/src/shared/ui/Dialog.css | 26 + packages/start/src/shared/ui/Dialog.tsx | 17 + packages/start/src/shared/ui/IconButton.css | 39 + packages/start/src/shared/ui/IconButton.tsx | 8 + packages/start/src/shared/ui/Properties.css | 15 + packages/start/src/shared/ui/Properties.tsx | 29 + packages/start/src/shared/ui/Section.css | 15 + packages/start/src/shared/ui/Section.tsx | 23 + packages/start/src/shared/ui/Select.css | 38 + packages/start/src/shared/ui/Select.tsx | 13 + packages/start/src/shared/ui/Table.css | 22 + packages/start/src/shared/ui/Table.tsx | 35 + packages/start/src/shared/ui/Tabs.css | 48 + packages/start/src/shared/ui/Tabs.tsx | 22 + packages/start/src/shared/ui/Text.css | 69 ++ packages/start/src/shared/ui/Text.tsx | 37 + 35 files changed, 2021 insertions(+), 72 deletions(-) create mode 100644 packages/start/src/shared/server-function-inspector/HeadersViewer.css create mode 100644 packages/start/src/shared/server-function-inspector/HeadersViewer.tsx create mode 100644 packages/start/src/shared/server-function-inspector/SerovalViewer.css create mode 100644 packages/start/src/shared/server-function-inspector/SerovalViewer.tsx create mode 100644 packages/start/src/shared/server-function-inspector/index.tsx create mode 100644 packages/start/src/shared/server-function-inspector/server-function-tracker.ts create mode 100644 packages/start/src/shared/server-function-inspector/styles.css create mode 100644 packages/start/src/shared/ui/Badge.css create mode 100644 packages/start/src/shared/ui/Badge.tsx create mode 100644 packages/start/src/shared/ui/Button.css create mode 100644 packages/start/src/shared/ui/Button.tsx create mode 100644 packages/start/src/shared/ui/Cascade.css create mode 100644 packages/start/src/shared/ui/Cascade.tsx create mode 100644 packages/start/src/shared/ui/Dialog.css create mode 100644 packages/start/src/shared/ui/Dialog.tsx create mode 100644 packages/start/src/shared/ui/IconButton.css create mode 100644 packages/start/src/shared/ui/IconButton.tsx create mode 100644 packages/start/src/shared/ui/Properties.css create mode 100644 packages/start/src/shared/ui/Properties.tsx create mode 100644 packages/start/src/shared/ui/Section.css create mode 100644 packages/start/src/shared/ui/Section.tsx create mode 100644 packages/start/src/shared/ui/Select.css create mode 100644 packages/start/src/shared/ui/Select.tsx create mode 100644 packages/start/src/shared/ui/Table.css create mode 100644 packages/start/src/shared/ui/Table.tsx create mode 100644 packages/start/src/shared/ui/Tabs.css create mode 100644 packages/start/src/shared/ui/Tabs.tsx create mode 100644 packages/start/src/shared/ui/Text.css create mode 100644 packages/start/src/shared/ui/Text.tsx diff --git a/apps/tests/src/routes/server-function-ping.tsx b/apps/tests/src/routes/server-function-ping.tsx index 70f9d2617..677a35340 100644 --- a/apps/tests/src/routes/server-function-ping.tsx +++ b/apps/tests/src/routes/server-function-ping.tsx @@ -1,18 +1,38 @@ import { createEffect, createSignal } from "solid-js"; -async function ping(value: string) { +async function sleep(value: unknown, ms: number) { + return new Promise((res) => { + setTimeout(res, ms, value); + }) +} + +async function ping(value: Date) { "use server"; - return await Promise.resolve(value); + const current = [ + value, + { + name: 'example', + async *[Symbol.asyncIterator]() { + yield sleep('foo', 5000); + yield sleep('bar', 5000); + yield sleep('baz', 5000); + } + } + ]; + + return current; } export default function App() { const [output, setOutput] = createSignal<{ result?: boolean }>({}); createEffect(async () => { - const value = `${Math.random() * 1000}`; + const value = new Date(); const result = await ping(value); - setOutput(prev => ({ ...prev, result: value === result })); + await ping(value); + console.log(result); + setOutput((prev) => ({ ...prev, result: value.toString() === result[0].toString() })); }); return ( diff --git a/packages/start/src/fns/client.ts b/packages/start/src/fns/client.ts index 2cb5528ac..0e49fc6dc 100644 --- a/packages/start/src/fns/client.ts +++ b/packages/start/src/fns/client.ts @@ -1,4 +1,8 @@ import { type Component } from "solid-js"; +import { + pushRequest, + pushResponse, +} from "../shared/server-function-inspector/server-function-tracker"; import { // serializeToJSONStream, serializeToJSONString, @@ -12,13 +16,13 @@ import { let INSTANCE = 0; -function createRequest( +async function createRequest( base: string, id: string, instance: string, options: RequestInit, ) { - return fetch(base, { + const request = new Request(base, { method: "POST", ...options, headers: { @@ -27,6 +31,14 @@ function createRequest( "X-Server-Instance": instance, }, }); + if (import.meta.env.DEV) { + pushRequest(id, instance, request.clone()); + } + const response = await fetch(request); + if (import.meta.env.DEV) { + pushResponse(id, instance, response.clone()); + } + return response; } async function initializeResponse( diff --git a/packages/start/src/fns/serialization.ts b/packages/start/src/fns/serialization.ts index b266eb027..e09e1b004 100644 --- a/packages/start/src/fns/serialization.ts +++ b/packages/start/src/fns/serialization.ts @@ -109,7 +109,7 @@ export function serializeToJSONStream(value: any) { }); } -class SerovalChunkReader { +export class SerovalChunkReader { reader: ReadableStreamDefaultReader; buffer: Uint8Array; done: boolean; diff --git a/packages/start/src/shared/ErrorBoundary.tsx b/packages/start/src/shared/ErrorBoundary.tsx index 6be12348e..d3dfb783a 100644 --- a/packages/start/src/shared/ErrorBoundary.tsx +++ b/packages/start/src/shared/ErrorBoundary.tsx @@ -1,40 +1,50 @@ // @refresh skip -import { ErrorBoundary as DefaultErrorBoundary, catchError, type ParentProps } from "solid-js"; +import { + catchError, + ErrorBoundary as DefaultErrorBoundary, + type ParentProps, +} from "solid-js"; import { isServer } from "solid-js/web"; -import { HttpStatusCode } from "./HttpStatusCode.ts"; import { DevOverlay } from "./dev-overlay/index.tsx"; +import { HttpStatusCode } from "./HttpStatusCode.ts"; +import { ServerFunctionInspector } from "./server-function-inspector/index.tsx"; export const ErrorBoundary = import.meta.env.DEV && import.meta.env.START_DEV_OVERLAY - ? (props: ParentProps) => {props.children} + ? (props: ParentProps) => ( + + + {props.children} + + ) : (props: ParentProps) => { - const message = isServer - ? "500 | Internal Server Error" - : "Error | Uncaught Client Exception"; - return ( - { - console.error(error); - return ( - <> - - {message} - - - - ); - }} - > - {props.children} - - ); - }; + const message = isServer + ? "500 | Internal Server Error" + : "Error | Uncaught Client Exception"; + return ( + { + console.error(error); + return ( + <> + + {message} + + + + ); + }} + > + {props.children} + + ); + }; export const TopErrorBoundary = (props: ParentProps) => { let isError = false; const res = catchError( () => props.children, - err => { + (err) => { console.error(err); isError = !!err; }, diff --git a/packages/start/src/shared/dev-overlay/DevOverlayDialog.tsx b/packages/start/src/shared/dev-overlay/DevOverlayDialog.tsx index 975e46507..b1c7c80d2 100644 --- a/packages/start/src/shared/dev-overlay/DevOverlayDialog.tsx +++ b/packages/start/src/shared/dev-overlay/DevOverlayDialog.tsx @@ -4,9 +4,10 @@ import * as htmlToImage from "html-to-image"; import type { JSX } from "solid-js"; import { createMemo, createSignal, ErrorBoundary, For, Show, Suspense } from "solid-js"; import { Portal } from "solid-js/web"; -// @ts-ignore - terracotta module resolution issue with NodeNext -import { Dialog, DialogOverlay, DialogPanel, Select, SelectOption } from "terracotta"; import info from "../../../package.json" with { type: "json" }; +import IconButton from "../ui/IconButton.tsx"; +import { Select, SelectOption } from "../ui/Select.tsx"; +import { Dialog, DialogOverlay, DialogPanel } from "../ui/Dialog.tsx"; import { CodeView } from "./CodeView.tsx"; import { createStackFrame, type StackFrameSource } from "./createStackFrame.ts"; import download from "./download.ts"; @@ -23,7 +24,9 @@ import { } from "./icons.tsx"; import "./styles.css"; -export function classNames(...classes: (string | boolean | undefined)[]): string { +export function classNames( + ...classes: (string | boolean | undefined)[] +): string { return classes.filter(Boolean).join(" "); } @@ -61,7 +64,9 @@ interface StackFramesContentProps { function getFileName(source: string): string { try { - const path = source.startsWith("/") ? new URL(source, "file://") : new URL(source); + const path = source.startsWith("/") + ? new URL(source, "file://") + : new URL(source); const paths = path.pathname.split("/"); return paths[paths.length - 1]!; } catch (error) { @@ -93,11 +98,14 @@ function StackFramesContent(props: StackFramesContentProps) {
{(() => { - const data = createStackFrame(selectedFrame(), () => props.isCompiled); + const data = createStackFrame( + selectedFrame(), + () => props.isCompiled, + ); return ( }> }> - {source => ( + {(source) => ( <> {source.source}
@@ -121,10 +129,14 @@ function StackFramesContent(props: StackFramesContentProps) { onChange={setSelectedFrame} > - {current => ( + {(current) => ( + {current.functionName ?? ""} @@ -137,7 +149,7 @@ function StackFramesContent(props: StackFramesContentProps) { name: current.getFunctionName(), })} -
+ } > {(() => { @@ -173,7 +185,9 @@ interface StackFramesProps { function StackFrames(props: StackFramesProps) { return ( - {current => } + {(current) => ( + + )} ); } @@ -186,7 +200,9 @@ interface DevOverlayDialogProps { const ISSUE_THREAD = "https://github.com/solidjs/solid-start/issues/new"; const DISCORD_INVITE = "https://discord.com/invite/solidjs"; -export default function DevOverlayDialog(props: DevOverlayDialogProps): JSX.Element { +export default function DevOverlayDialog( + props: DevOverlayDialogProps, +): JSX.Element { const [currentPage, setCurrentPage] = createSignal(1); const [isCompiled, setIsCompiled] = createSignal(false); const length = createMemo(() => props.errors.length); @@ -196,7 +212,7 @@ export default function DevOverlayDialog(props: DevOverlayDialogProps): JSX.Elem }); function goPrev() { - setCurrentPage(c => { + setCurrentPage((c) => { if (c > 1) { return c - 1; } @@ -205,7 +221,7 @@ export default function DevOverlayDialog(props: DevOverlayDialogProps): JSX.Elem } function goNext() { - setCurrentPage(c => { + setCurrentPage((c) => { if (c < length()) { return c + 1; } @@ -214,7 +230,7 @@ export default function DevOverlayDialog(props: DevOverlayDialogProps): JSX.Elem } function toggleIsCompiled() { - setIsCompiled(c => !c); + setIsCompiled((c) => !c); } const [panel, setPanel] = createSignal(); @@ -228,7 +244,7 @@ export default function DevOverlayDialog(props: DevOverlayDialogProps): JSX.Elem transform: "scale(0.75)", }, }) - .then(url => { + .then((url) => { download(url, "start-screenshot.png"); }); } @@ -239,7 +255,10 @@ export default function DevOverlayDialog(props: DevOverlayDialogProps): JSX.Elem url.searchParams.append("labels", "bug"); url.searchParams.append("labels", "needs+triage"); url.searchParams.append("template", "bug.yml"); - url.searchParams.append("title", `[Bug?]:` + props.errors[truncated() - 1].toString()); + url.searchParams.append( + "title", + `[Bug?]:` + props.errors[truncated() - 1].toString(), + ); window.open(url, "_blank")!.focus(); } @@ -264,39 +283,41 @@ export default function DevOverlayDialog(props: DevOverlayDialogProps): JSX.Elem
1}>
- +
{`${truncated()} of ${props.errors.length}`}
- +
- - - - - +
diff --git a/packages/start/src/shared/dev-overlay/styles.css b/packages/start/src/shared/dev-overlay/styles.css index ce51fdb95..bf9378823 100644 --- a/packages/start/src/shared/dev-overlay/styles.css +++ b/packages/start/src/shared/dev-overlay/styles.css @@ -23,6 +23,7 @@ display: flex; flex-direction: column; width: 75vw; + max-height: 75vh; margin-top: 2rem; margin-bottom: 2rem; gap: 0.5rem; @@ -174,15 +175,8 @@ [data-start-dev-overlay-stack-frames] { flex: 1 1 0%; - display: flex; - flex-direction: column; - margin: 0; - margin-block: 0; - list-style-type: none; - padding-inline: 0; max-height: 16rem; overflow-y: auto; - border-radius: 0.5rem; } [data-start-dev-overlay-stack-frame] { @@ -254,7 +248,7 @@ width: 100%; overflow: hidden; min-width: 100%; - min-height: 16rem; + max-height: 16rem; border-top-width: 1px; border-top-style: solid; border-top-color: rgb(249 250 251); @@ -263,7 +257,6 @@ [data-start-dev-overlay-code-view] { overflow: auto; min-width: 100%; - min-height: 16rem; } [data-start-dev-overlay-code-view]>.shiki { diff --git a/packages/start/src/shared/server-function-inspector/HeadersViewer.css b/packages/start/src/shared/server-function-inspector/HeadersViewer.css new file mode 100644 index 000000000..696eefedd --- /dev/null +++ b/packages/start/src/shared/server-function-inspector/HeadersViewer.css @@ -0,0 +1,12 @@ +[data-start-headers-viewer] { + font-size: 0.75rem; + line-height: 1rem; +} + +[data-start-header-key] { + white-space: nowrap; +} + +[data-start-header-value] { + white-space: nowrap; +} \ No newline at end of file diff --git a/packages/start/src/shared/server-function-inspector/HeadersViewer.tsx b/packages/start/src/shared/server-function-inspector/HeadersViewer.tsx new file mode 100644 index 000000000..9465043c7 --- /dev/null +++ b/packages/start/src/shared/server-function-inspector/HeadersViewer.tsx @@ -0,0 +1,20 @@ +import { Properties } from "../ui/Properties.tsx"; +import { Text } from "../ui/Text.tsx"; + +import './HeadersViewer.css'; + +interface HeadersViewerProps { + headers: Headers; +} + +export function HeadersViewer(props: HeadersViewerProps) { + return ( +
+ {key}:} + renderValue = {(value) => {value}} + /> +
+ ); +} \ No newline at end of file diff --git a/packages/start/src/shared/server-function-inspector/SerovalViewer.css b/packages/start/src/shared/server-function-inspector/SerovalViewer.css new file mode 100644 index 000000000..73bb5e1fd --- /dev/null +++ b/packages/start/src/shared/server-function-inspector/SerovalViewer.css @@ -0,0 +1,88 @@ +[data-start-seroval-viewer] { + display: grid; + grid-template-columns: 1fr 2fr; + border: 1px oklch(70.7% 0.165 254.624) solid; + overflow: auto; + width: 100%; + max-height: 50vh; +} + +[data-start-seroval-value] { + display: flex; + gap: 0.25rem; + align-items: center; +} + +[data-start-seroval-promise] { + display: flex; + gap: 0.25rem; + align-items: center; +} + +[data-start-seroval-renderer] { + display: flex; +} + +[data-start-seroval-renderer] > * + * { + border-left: 1px oklch(70.7% 0.165 254.624) solid; +} + +[data-start-seroval-renderer]:last-child { + border-right: 1px oklch(70.7% 0.165 254.624) solid; +} + +[data-start-seroval-node] { + display: flex; + flex-direction: column; + gap: 0.25rem; + padding: 0.25rem; + + width: 100%; + max-width: 16rem; +} + +[data-start-seroval-node-header] { + position: sticky; + top: 0; + display: flex; + align-items: center; + justify-content: space-between; + background-color: rgb(249 250 251); +} + +[data-start-seroval-node-content] { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +[data-start-seroval-value] { + color: oklch(62.3% 0.214 259.815); +} + +[data-start-seroval-link] { + display: inline-flex; + gap: 0.25rem; + align-items: center; +} + +[data-start-seroval-link] > svg { + width: 1rem; + height: 1rem; +} + +[data-start-seroval-properties] { + display: flex; + flex-direction: column; + gap: 0.25rem; + margin: 0rem 0.25rem; + + overflow: auto; +} + +[data-start-seroval-property] { + display: flex; + flex-direction: row; + align-items: center; + flex-wrap: nowrap; +} diff --git a/packages/start/src/shared/server-function-inspector/SerovalViewer.tsx b/packages/start/src/shared/server-function-inspector/SerovalViewer.tsx new file mode 100644 index 000000000..4d53d5dcc --- /dev/null +++ b/packages/start/src/shared/server-function-inspector/SerovalViewer.tsx @@ -0,0 +1,836 @@ +import type { SerovalNode } from "seroval"; +import { createEffect, createSignal, For, type JSX, Show, splitProps } from "solid-js"; +import { SerovalChunkReader } from "../../server/serialization.ts"; +import { Badge } from "../ui/Badge.tsx"; +import { Cascade, CascadeOption } from "../ui/Cascade.tsx"; +import { Section } from "../ui/Section.tsx"; +import { Text } from "../ui/Text.tsx"; +import './SerovalViewer.css'; + +function LinkIcon( + props: JSX.IntrinsicElements["svg"] & { title: string }, +): JSX.Element { + return ( + + {props.title} + + + ); +} + +export interface SerovalViewerProps { + stream: Request | Response; +} + +function getNodeType(node: SerovalNode) { + switch (node.t) { + // Number = 0, + case 0: return 'number'; + // String = 1, + case 1: return 'string'; + // Constant = 2, + case 2: + switch (node.s) { + case 0: return 'null'; + case 1: return 'undefined'; + case 2: return 'true'; + case 3: return 'false'; + case 4: return '-0'; + case 5: return 'Infinity'; + case 6: return '-Infinity'; + case 7: return 'NaN'; + } + break; + // BigInt = 3, + case 3: return 'bigint' + // Date = 5, + case 5: return 'Date'; + // RegExp = 6, + case 6: return 'RegExp'; + // Set = 7, + case 7: return 'Set'; + // Map = 8, + case 8: return 'Map'; + // Array = 9, + case 9: return 'Array'; + // Object = 10, + case 10: + // NullConstructor = 11, + case 11: return 'Object'; + // Promise = 12, + case 12: return 'Promise'; + // Error = 13, + case 13: + switch (node.s) { + case 0: return 'Error'; + case 1: return 'EvalError'; + case 2: return 'RangeError'; + case 3: return 'ReferenceError'; + case 4: return 'SyntaxError'; + case 5: return 'TypeError'; + case 6: return 'URIError'; + } + return 'Error'; + // AggregateError = 14, + case 14: return 'AggregateError'; + // TypedArray = 15, + case 15: + // BigIntTypedArray = 16, + case 16: return node.c; + // WKSymbol = 17, + case 17: return 'symbol'; + // ArrayBuffer = 19, + case 19: return 'ArrayBuffer' + // DataView = 20, + case 20: return 'DataView' + // Boxed = 21, + case 21: return 'Boxed'; + // PromiseConstructor = 22, + case 22: return 'Promise'; + // Plugin = 25, + case 25: + // due to the nature of this node, we have to traverse it ourselves + return 'Plugin'; + // IteratorFactoryInstance = 28, + case 28: return 'Iterator'; + // AsyncIteratorFactoryInstance = 30, + case 30: return 'AsyncIterator'; + // StreamConstructor = 31, + case 31: return 'Stream'; + } + throw new Error('unsupported node type'); +} + +function traverse( + node: SerovalNode, + handler: (node: SerovalNode) => void, +): void { + handler(node); + switch (node.t) { + // Number = 0, + case 0: + // String = 1, + case 1: + // Constant = 2, + case 2: + // BigInt = 3, + case 3: + // IndexedValue = 4, + case 4: + // Date = 5, + case 5: + // RegExp = 6, + case 6: + break; + // Set = 7, + case 7: + // Traverse items + for (const child of node.a) { + traverse(child, handler); + } + break; + // Map = 8, + case 8: + // Traverse keys + for (const key of node.e.k) { + traverse(key, handler); + } + for (const value of node.e.v) { + traverse(value, handler); + } + break; + // Array = 9, + case 9: + // Traverse items + for (const child of node.a) { + if (child) { + traverse(child, handler); + } + } + break; + // Object = 10, + case 10: + // NullConstructor = 11, + case 11: + for (const child of node.p.k) { + if (typeof child !== "string") { + traverse(child, handler); + } + } + for (const child of node.p.v) { + traverse(child, handler); + } + break; + // Promise = 12, + case 12: + traverse(node.f, handler); + break; + // Error = 13, + case 13: + // AggregateError = 14, + case 14: + if (node.p) { + for (const child of node.p.k) { + if (typeof child !== "string") { + traverse(child, handler); + } + } + for (const child of node.p.v) { + traverse(child, handler); + } + } + break; + // TypedArray = 15, + case 15: + // BigIntTypedArray = 16, + case 16: + traverse(node.f, handler); + break; + // WKSymbol = 17, + case 17: + // Reference = 18, + case 18: + break; + // ArrayBuffer = 19, + case 19: + // DataView = 20, + case 20: + // Boxed = 21, + case 21: + traverse(node.f, handler); + break; + // PromiseConstructor = 22, + case 22: + break; + // PromiseSuccess = 23, + case 23: + // PromiseFailure = 24, + case 24: + traverse(node.a[1], handler); + break; + // Plugin = 25, + case 25: + // due to the nature of this node, we have to traverse it ourselves + break; + // SpecialReference = 26, + case 26: + break; + // IteratorFactory = 27, + case 27: + traverse(node.f, handler); + break; + // IteratorFactoryInstance = 28, + case 28: + traverse(node.a[0], handler); + traverse(node.a[1], handler); + break + // AsyncIteratorFactory = 29, + case 29: + traverse(node.a[1], handler); + break; + // AsyncIteratorFactoryInstance = 30, + case 30: + traverse(node.a[0], handler); + traverse(node.a[1], handler); + break + // StreamConstructor = 31, + case 31: + // Traverse items + for (const child of node.a) { + traverse(child, handler); + } + break; + // StreamNext = 32, + case 32: + // StreamThrow = 33, + case 33: + // StreamReturn = 34 + case 34: + traverse(node.f, handler); + break; + } +} + +interface SerovalValueProps { + value: string | number | boolean | undefined | null; +} + +function SerovalValue(props: SerovalValueProps) { + return ( + + {`${props.value}`} + + ); +} + +function getConstantValue(value: number) { + switch (value) { + case 0: return 'null'; + case 1: return 'undefined'; + case 2: return 'true'; + case 3: return 'false'; + case 4: return '-0'; + case 5: return 'Infinity'; + case 6: return '-Infinity'; + case 7: return 'NaN'; + } + return ''; +} + +function getSymbolValue(value: number) { + switch (value) { + case 0: return 'Symbol.asyncIterator'; + case 1: return 'Symbol.hasInstance'; + case 2: return 'Symbol.isConcatSpreadable'; + case 3: return 'Symbol.iterator'; + case 4: return 'Symbol.match'; + case 5: return 'Symbol.matchAll'; + case 6: return 'Symbol.replace'; + case 7: return 'Symbol.search'; + case 8: return 'Symbol.species'; + case 9: return 'Symbol.toPrimitive'; + case 10: return 'Symbol.toStringTag'; + case 11: return 'Symbol.unscopables'; + } + return ''; +} + +function getObjectFlag(value: number) { + switch (value) { + case 1: return 'non-extensible'; + case 2: return 'sealed'; + case 3: return 'frozen'; + default: return 'none'; + } +} + +function zip(keys: Key[], values: Value[]): [key: Key, value: Value][] { + const zipped: [key: Key, value: Value][] = []; + + for (let i = 0, len = keys.length; i < len; i++) { + zipped[i] = [keys[i]!, values[i]!]; + } + + return zipped; +} + +interface RenderContext { + getNode: (index: number) => SerovalNode | undefined; + getPromise: (index: number) => Extract | undefined; + getStream: (index: number) => Extract[] | undefined; +} + +function getStreamKeyword(t: 32 | 33 | 34): string { + switch (t) { + case 32: return 'next'; + case 33: return 'throw'; + case 34: return 'return'; + } +} + +function PropertySeparator() { + return :; +} + +function renderSerovalNode( + ctx: RenderContext, + node: SerovalNode, + onSelect: (index: number | undefined) => void, + inner?: boolean, +): JSX.Element { + if (node.t >= 4 && (inner || node.t === 4) && node.i != null && !(node.t === 5 || node.t === 6 || node.t === 17)) { + const index = node.i; + const description = `id: ${index}`; + const lookup = ctx.getNode(index)!; + return ( + + + + + + ); + } + switch (node.t) { + // Number = 0, + case 0: + return + // String = 1, + case 1: + return + // Constant = 2, + case 2: + return + // BigInt = 3, + case 3: + return ( + <> + + + + ); + // Date = 5, + case 5: + return + // RegExp = 6, + case 6: + return + // Set = 7, + case 7: + return ( + <> +
+
+ + + +
+
+
+ data-start-seroval-properties defaultValue={undefined} onChange={onSelect}> + [index, node] as const)}> + {([key, value]) => ( +
+ + + {renderSerovalNode(ctx, value, onSelect, true)} +
+ )} +
+ +
+ + ); + // Map = 8, + case 8: + return ( + <> +
+
+ + + +
+
+
+ data-start-seroval-properties defaultValue={undefined} onChange={onSelect}> + + {([key, value]) => ( +
+ {renderSerovalNode(ctx, key, onSelect, true)} + + {renderSerovalNode(ctx, value, onSelect, true)} +
+ )} +
+ +
+ + ); + // Array = 9, + case 9: + return ( + <> +
+
+ + + +
+
+ + + +
+
+
+ data-start-seroval-properties defaultValue={undefined} onChange={onSelect}> + [index, node] as const)}> + {([key, value]) => ( +
+ + + { + value === 0 + ? + : renderSerovalNode(ctx, value, onSelect, true) + } +
+ )} +
+ +
+ + ); + // Object = 10, + case 10: + // NullConstructor = 11, + case 11: + return ( + <> +
+
+ + + +
+
+ + + +
+
+
+ data-start-seroval-properties defaultValue={undefined} onChange={onSelect}> + + {([key, value]) => ( +
+ {( + typeof key === 'string' + ? + : renderSerovalNode(ctx, key, onSelect, true) + )} + + {renderSerovalNode(ctx, value, onSelect, true)} +
+ )} +
+ +
+ + ); + // Promise = 12, + case 12: + return ( + data-start-seroval-properties defaultValue={undefined} onChange={onSelect}> + {renderSerovalNode(ctx, node.f, onSelect, true)} + + ); + // Error = 13, + case 13: + // AggregateError = 14, + case 14: + return ( + <> +
+
+ + + +
+
+ + {current => ( +
+ data-start-seroval-properties defaultValue={undefined} onChange={onSelect}> + + {([key, value]) => ( +
+ {typeof key === 'string' + ? + : renderSerovalNode(ctx, key, onSelect, true)} + + {renderSerovalNode(ctx, value, onSelect, true)} +
+ )} +
+ +
+ )} +
+ + ); + // TypedArray = 15, + case 15: + // BigIntTypedArray = 16, + case 16: + return ( + data-start-seroval-properties defaultValue={undefined} onChange={onSelect}> + {renderSerovalNode(ctx, node.f, onSelect, true)} + + ); + // WKSymbol = 17, + case 17: + return + // Reference = 18, + case 18: + break; + // ArrayBuffer = 19, + case 19: + return ; + // DataView = 20, + case 20: + return ( + <> +
+
+ + + +
+
+ + + +
+
+
+ data-start-seroval-properties defaultValue={undefined} onChange={onSelect}> + {renderSerovalNode(ctx, node.f, onSelect, true)} + +
+ + ); + // Boxed = 21, + case 21: + return ( + data-start-seroval-properties defaultValue={undefined} onChange={onSelect}> + {renderSerovalNode(ctx, node.f, onSelect, true)} + + ); + case 22: + return <> + {(() => { + const result = ctx.getPromise(node.s); + if (result) { + const status = result.t === 23 ? 'success' : 'failure' as const; + return ( + data-start-seroval-properties defaultValue={undefined} onChange={onSelect}> +
+ + + +
+ + + + {renderSerovalNode(ctx, result.a[1], onSelect, true)} + + + ); + } + return + })()} + ; + // Plugin = 25 + case 25: + // due to the nature of this node, we have to traverse it ourselves + break; + // IteratorFactory = 27, + case 27: + break; + // IteratorFactoryInstance = 28, + case 28: + return renderSerovalNode(ctx, node.a[1], onSelect, true); + // AsyncIteratorFactory = 29, + case 29: + break; + // AsyncIteratorFactoryInstance = 30, + case 30: + return renderSerovalNode(ctx, node.a[1], onSelect, true); + // StreamConstructor = 31, + case 31: + return ( + <> + {(() => { + const result = ctx.getStream(node.i) || []; + return ( + data-start-seroval-properties defaultValue={undefined} onChange={onSelect}> + + {(current) => ( +
+ + + {renderSerovalNode(ctx, current.f, onSelect, true)} +
+ )} +
+ + ); + })()} + + ); + } +} + +interface SerovalNodeRendererProps extends RenderContext { + node: SerovalNode; +} + +function SerovalNodeRenderer(props: SerovalNodeRendererProps): JSX.Element { + const [, rest] = splitProps(props, ['node']); + const [next, setNext] = createSignal() + + function onSelect(index: number | undefined) { + if (index == null) { + setNext(undefined); + } else { + setNext(props.getNode(index)); + } + } + + return ( + <> +
+
+ {props.node.i != null && ( + + )} + +
+
+ {renderSerovalNode(props, props.node, onSelect)} +
+
+ + {(current) => ( + + )} + + + ); +} + +interface SerovalRendererProps extends Omit { + node?: SerovalNode; +} + +function SerovalRenderer(props: SerovalRendererProps): JSX.Element { + const [, rest] = splitProps(props, ['node']); + return ( +
+ + {(current) => ( + + )} + +
+ ); +} + +function createSimpleStore>(initial: T) { + const [state, setState] = createSignal(initial); + + return { + get(): T { + return state(); + }, + read(key: K): T[K] { + return state()[key]; + }, + write(key: K, value: T[K]): void { + setState((current) => ({ + ...current, + [key]: value, + })); + }, + update(key: K, value: (current: T[K]) => T[K]): void { + setState((current) => ({ + ...current, + [key]: value(current[key]), + })); + } + } +} + +export function SerovalViewer(props: SerovalViewerProps): JSX.Element { + const [selected, setSelected] = createSignal(); + + const references = createSimpleStore | undefined>>({}); + const streams = createSimpleStore[] | undefined>>({}); + const promises = createSimpleStore | undefined>>({}); + + createEffect(async () => { + setSelected(undefined); + if (!props.stream.body) { + throw new Error("missing body"); + } + const reader = new SerovalChunkReader(props.stream.body); + const result = await reader.next(); + if (!result.done) { + function traverseNode(node: SerovalNode): void { + // Check for promises + switch (node.t) { + case 0: + case 1: + case 2: + case 3: + case 4: + break; + case 23: + case 24: + promises.write(node.i, node); + break; + case 32: + case 33: + case 34: + streams.update(node.i, (current) => { + if (current) { + return [...current, node]; + } + return [node]; + }); + break; + case 5: + case 6: + case 7: + case 8: + case 9: + case 10: + case 11: + case 12: + case 13: + case 14: + case 15: + case 16: + case 17: + case 18: + case 19: + case 20: + case 21: + case 25: + case 26: + case 27: + case 29: + case 31: + references.write(node.i, node); + break; + case 28: { + break; + } + case 30: + break; + } + } + + function interpretChunk(chunk: string): SerovalNode { + const result = JSON.parse(chunk) as SerovalNode; + traverse(result, traverseNode); + return result; + } + + void reader.drain(interpretChunk); + const root = interpretChunk(result.value); + setSelected(root); + } + }); + + return ( +
+ references.read(index)} + getPromise={(index) => promises.read(index)} + getStream={(index) => streams.read(index)} + /> +
+ ); +} diff --git a/packages/start/src/shared/server-function-inspector/index.tsx b/packages/start/src/shared/server-function-inspector/index.tsx new file mode 100644 index 000000000..e3893f5c1 --- /dev/null +++ b/packages/start/src/shared/server-function-inspector/index.tsx @@ -0,0 +1,209 @@ +import { + createEffect, + createSignal, + For, + type JSX, + onCleanup, + Show, +} from "solid-js"; +import { createStore } from "solid-js/store"; +import { Portal } from "solid-js/web"; +import { Badge } from "../ui/Badge.tsx"; +import Button from "../ui/Button.tsx"; +import { Dialog, DialogOverlay, DialogPanel } from "../ui/Dialog.tsx"; +import { Select, SelectOption } from "../ui/Select.tsx"; +import { Tab, TabGroup, TabList, TabPanel } from "../ui/Tabs.tsx"; +import { HeadersViewer } from "./HeadersViewer.tsx"; +import { SerovalViewer } from "./SerovalViewer.tsx"; +import { + captureServerFunctionCall, + type ServerFunctionRequest, + type ServerFunctionResponse, +} from "./server-function-tracker.ts"; +import "./styles.css"; +import { Section } from "../ui/Section.tsx"; + +interface ContentViewerProps { + source: ServerFunctionRequest | ServerFunctionResponse; +} + +function ContentViewer(props: ContentViewerProps): JSX.Element { + return ( + <> +
+ +
+
+ {(() => { + if (props.source.source.headers.has('x-serialized')) { + return + } + })()} +
+ + ); +} + +interface RequestViewerProps { + request: ServerFunctionRequest; +} + +function RequestViewer(props: RequestViewerProps): JSX.Element { + return ( + + + + ); +} + +interface ResponseViewerProps { + response?: ServerFunctionResponse; +} + +function ResponseViewer(props: ResponseViewerProps): JSX.Element { + return ( + + + {(instance) => ( + + )} + + + ); +} + +interface ServerFunctionInstance { + request: ServerFunctionRequest; + response?: ServerFunctionResponse; +} + +interface ServerFunctionInstanceViewerProps { + instance: ServerFunctionInstance; + onDelete: () => void; +} + +function ServerFunctionInstanceViewer( + props: ServerFunctionInstanceViewerProps, +): JSX.Element { + const [tab, setTab] = createSignal<"request" | "response">("request"); + return ( + setTab(value ?? "request")} + > +
+ + Request + Response + +
+ +
+
+ + +
+ ); +} + +export function ServerFunctionInspector(): JSX.Element { + const [currentInstance, setCurrentInstance] = createSignal(); + + const [store, setStore] = createStore({ + instances: {} as Record, + }); + + createEffect(() => { + onCleanup( + captureServerFunctionCall((call) => { + if (call.type === "request") { + setStore("instances", call.instance, { + request: call, + }); + } else { + setStore("instances", call.instance, "response", call); + } + }), + ); + }); + + const [isOpen, setIsOpen] = createSignal(false); + + createEffect(() => { + (window as any).__START__SERVER_FN__ = setIsOpen; + }); + + return ( + + +
+ + +
+ {/* list of calls */} +
+ +
+ {/* request/response viewer */} + + {(value) => ( + + {(instance) => ( + { + setStore("instances", value(), undefined); + }} + /> + )} + + )} + +
+
+
+
+
+ ); +} diff --git a/packages/start/src/shared/server-function-inspector/server-function-tracker.ts b/packages/start/src/shared/server-function-inspector/server-function-tracker.ts new file mode 100644 index 000000000..2fd56f264 --- /dev/null +++ b/packages/start/src/shared/server-function-inspector/server-function-tracker.ts @@ -0,0 +1,61 @@ +export type ServerFunctionRequest = { + type: "request"; + id: string; + instance: string; + source: Request; + time: number; +}; +export type ServerFunctionResponse = { + type: "response"; + id: string; + instance: string; + source: Response; + time: number; +}; + +export type ServerFunctionCall = ServerFunctionRequest | ServerFunctionResponse; + +export type ServerFunctionCallListener = (event: ServerFunctionCall) => void; + +const LISTENERS = new Set(); + +export function captureServerFunctionCall( + listener: ServerFunctionCallListener, +): () => void { + LISTENERS.add(listener); + return () => LISTENERS.delete(listener); +} + +export function pushRequest( + id: string, + instance: string, + source: Request, +): void { + const event: ServerFunctionCall = { + type: "request", + id, + instance, + source, + time: performance.now(), + }; + for (const listener of new Set(LISTENERS)) { + listener(event); + } +} + +export function pushResponse( + id: string, + instance: string, + source: Response, +): void { + const event: ServerFunctionCall = { + type: "response", + id, + instance, + source, + time: performance.now(), + }; + for (const listener of new Set(LISTENERS)) { + listener(event); + } +} diff --git a/packages/start/src/shared/server-function-inspector/styles.css b/packages/start/src/shared/server-function-inspector/styles.css new file mode 100644 index 000000000..437a0c4e7 --- /dev/null +++ b/packages/start/src/shared/server-function-inspector/styles.css @@ -0,0 +1,75 @@ + + +.server-function-instance-detail { + display: flex; + align-items: center; + gap: 0.25rem; +} + + +.server-function-inspector { + display: grid; + grid-template-columns: 1fr 2fr; + width: 75vw; + height: 75vh; + margin-top: 2rem; + margin-bottom: 2rem; + gap: 0.5rem; + color: rgb(17 24 39); +} + +.server-function-instances-container { + border-radius: 0.5rem; + background-color: rgb(249 250 251); + overflow: auto; +} + + +.server-function-instances { + list-style: none; + display: flex; + flex-direction: column; + padding: 0px; +} + +.server-function-instances > [data-start-select-option] { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.25rem; +} + +.server-function-instance-viewer { + display: flex; + flex-direction: column; + gap: 0.25rem; + height: 100%; + overflow: hidden; +} + +.server-function-instance-viewer-toolbar { + display: flex; + align-items: center; + justify-content: space-between; +} + +.server-function-instance-tab-panel { + overflow: auto; +} + +[data-start-headers] { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +[data-start-headers-title] { + font-weight: 500; +} + +.server-function-instance-tab-panel { + display: flex; + flex-direction: column; + gap: 1rem; +} + diff --git a/packages/start/src/shared/ui/Badge.css b/packages/start/src/shared/ui/Badge.css new file mode 100644 index 000000000..1a2276f9f --- /dev/null +++ b/packages/start/src/shared/ui/Badge.css @@ -0,0 +1,33 @@ +[data-start-badge] { + display: inline-flex; + align-items: center; + justify-content: center; + + border-radius: 0.375rem; + padding: 0.125rem 0.25rem; + + background-color: var(--background); + border: 1px var(--foreground) solid; + color: var(--foreground); +} + +[data-start-badge="success"] { + --background: oklch(98.2% 0.018 155.826); + --foreground: oklch(52.7% 0.154 150.069); +} + +[data-start-badge="failure"] { + --background: oklch(97.1% 0.013 17.38); + --foreground: oklch(50.5% 0.213 27.518); +} + +[data-start-badge="warning"] { + --background: oklch(98.7% 0.026 102.212); + --foreground: oklch(47.6% 0.114 61.907); +} + + +[data-start-badge="info"] { + --background: oklch(97% 0.014 254.604); + --foreground: oklch(48.8% 0.243 264.376); +} diff --git a/packages/start/src/shared/ui/Badge.tsx b/packages/start/src/shared/ui/Badge.tsx new file mode 100644 index 000000000..3b3513c94 --- /dev/null +++ b/packages/start/src/shared/ui/Badge.tsx @@ -0,0 +1,17 @@ +import type { JSX } from 'solid-js'; +import './Badge.css'; +import { Text } from './Text.tsx'; + + +export interface BadgeProps { + value: string | number; + type: 'info' | 'success' | 'failure' | 'warning'; +} + +export function Badge(props: BadgeProps): JSX.Element { + return ( + + {props.value} + + ); +} diff --git a/packages/start/src/shared/ui/Button.css b/packages/start/src/shared/ui/Button.css new file mode 100644 index 000000000..985ae4972 --- /dev/null +++ b/packages/start/src/shared/ui/Button.css @@ -0,0 +1,35 @@ +[data-start-button] { + display: flex; + align-items: center; + justify-content: center; + + cursor: pointer; + + border-radius: 0.5rem; + + background-color: oklch(97% 0.014 254.604); + + color: rgb(17 24 39); + padding-top: 0.5rem; + padding-bottom: 0.5rem; + padding-left: 1rem; + padding-right: 1rem; + font-size: 0.875rem; + line-height: 1.25rem; + border: none; + outline: none; + + transition-property: color, background-color, border-color, box-shadow; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; + + white-space: nowrap; +} + +[data-start-button][tc-active], +[data-start-button][tc-selected], +[data-start-button]:hover, +[data-start-button]:focus { + background-color: rgb(219 234 254); + color: rgb(30 58 138); +} diff --git a/packages/start/src/shared/ui/Button.tsx b/packages/start/src/shared/ui/Button.tsx new file mode 100644 index 000000000..a32756439 --- /dev/null +++ b/packages/start/src/shared/ui/Button.tsx @@ -0,0 +1,8 @@ +import { Button as BaseButton } from "terracotta"; +import "./Button.css"; + +const Button: typeof BaseButton = (props) => ( + +); + +export default Button; diff --git a/packages/start/src/shared/ui/Cascade.css b/packages/start/src/shared/ui/Cascade.css new file mode 100644 index 000000000..34057ee1f --- /dev/null +++ b/packages/start/src/shared/ui/Cascade.css @@ -0,0 +1,30 @@ +[data-start-cascade] { + display: flex; + flex-direction: column; + margin: 0; + margin-block: 0; + list-style-type: none; + padding-inline: 0; +} + +[data-start-cascade-option] { + color: rgb(17 24 39); + border-radius: 0.25rem; + font-size: 0.875rem; + line-height: 1.25rem; + outline: none; + cursor: pointer; + + transition-property: color, background-color, border-color, box-shadow; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; + + white-space: nowrap; +} + +[data-start-cascade-option][tc-active], +[data-start-cascade-option][tc-selected], +[data-start-cascade-option]:focus { + background-color: rgb(219 234 254); + color: rgb(30 58 138); +} \ No newline at end of file diff --git a/packages/start/src/shared/ui/Cascade.tsx b/packages/start/src/shared/ui/Cascade.tsx new file mode 100644 index 000000000..459b2af25 --- /dev/null +++ b/packages/start/src/shared/ui/Cascade.tsx @@ -0,0 +1,13 @@ +import { + Select as BaseSelect, + SelectOption as BaseSelectOption, +} from "terracotta"; + +import "./Cascade.css"; + +export const Cascade: typeof BaseSelect = (props) => ( + +); +export const CascadeOption: typeof BaseSelectOption = (props) => ( + +); diff --git a/packages/start/src/shared/ui/Dialog.css b/packages/start/src/shared/ui/Dialog.css new file mode 100644 index 000000000..198f22735 --- /dev/null +++ b/packages/start/src/shared/ui/Dialog.css @@ -0,0 +1,26 @@ +[data-start-dialog] { + position: fixed; + inset: 0px; + z-index: 50; + overflow-y: auto; +} + + +[data-start-dialog]>div { + position: relative; + display: flex; + align-items: center; + justify-content: center; + min-height: 100vh; +} + +[data-start-dialog-overlay] { + position: absolute; + inset: 0px; + background-color: rgb(17 24 39 / 0.5); +} + +[data-start-dialog-panel] { + margin: 2rem; + z-index: 10; +} diff --git a/packages/start/src/shared/ui/Dialog.tsx b/packages/start/src/shared/ui/Dialog.tsx new file mode 100644 index 000000000..1e765a05b --- /dev/null +++ b/packages/start/src/shared/ui/Dialog.tsx @@ -0,0 +1,17 @@ +import { + Dialog as BaseDialog, + DialogOverlay as BaseDialogOverlay, + DialogPanel as BaseDialogPanel, +} from "terracotta"; + +import "./Dialog.css"; + +export const Dialog: typeof BaseDialog = (props) => ( + +); +export const DialogOverlay: typeof BaseDialogOverlay = (props) => ( + +); +export const DialogPanel: typeof BaseDialogPanel = (props) => ( + +); diff --git a/packages/start/src/shared/ui/IconButton.css b/packages/start/src/shared/ui/IconButton.css new file mode 100644 index 000000000..3582bbaec --- /dev/null +++ b/packages/start/src/shared/ui/IconButton.css @@ -0,0 +1,39 @@ +[data-start-icon-button] { + display: flex; + padding: 0.125rem; + border-radius: 9999px; + border-width: 0px; + align-items: center; + justify-content: center; + cursor: pointer; + background-color: rgb(249 250 251); + color: rgb(17 24 39); + transition-property: color, background-color, border-color, box-shadow; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + + +[data-start-icon-button]:hover { + background-color: rgb(229 231 235); + color: rgb(55 65 81); +} + +[data-start-icon-button]:focus { + outline: 2px solid transparent; + outline-offset: 2px +} + +[data-start-icon-button]:focus-visible { + box-shadow: 0 0 0 calc(3px) rgb(17 24 39 / 0.75); +} + +[data-start-icon-button]:active { + background-color: rgb(243 244 246); + color: rgb(31 41 55); +} + +[data-start-icon-button]>svg { + height: 1.5rem; + width: 1.5rem; +} diff --git a/packages/start/src/shared/ui/IconButton.tsx b/packages/start/src/shared/ui/IconButton.tsx new file mode 100644 index 000000000..57417f601 --- /dev/null +++ b/packages/start/src/shared/ui/IconButton.tsx @@ -0,0 +1,8 @@ +import { Button as BaseButton } from "terracotta"; +import "./IconButton.css"; + +const IconButton: typeof BaseButton = (props) => ( + +); + +export default IconButton; diff --git a/packages/start/src/shared/ui/Properties.css b/packages/start/src/shared/ui/Properties.css new file mode 100644 index 000000000..5cb7343fc --- /dev/null +++ b/packages/start/src/shared/ui/Properties.css @@ -0,0 +1,15 @@ +[data-start-properties] { + display: flex; + flex-direction: column; + gap: 0.25rem; + + overflow: auto; +} + +[data-start-property] { + display: flex; + flex-direction: row; + gap: 0.25rem; + + align-items: center; +} diff --git a/packages/start/src/shared/ui/Properties.tsx b/packages/start/src/shared/ui/Properties.tsx new file mode 100644 index 000000000..7127f2a69 --- /dev/null +++ b/packages/start/src/shared/ui/Properties.tsx @@ -0,0 +1,29 @@ +import { For, type JSX } from 'solid-js'; + +import './Properties.css'; + +export type PropertyEntry = [key: unknown, value: unknown]; + +export interface PropertiesProps { + entries: T[]; + + renderKey: (key: T[0]) => JSX.Element; + renderValue: (value: T[1]) => JSX.Element; +} + +export function Properties( + props: PropertiesProps, +): JSX.Element { + return ( +
+ + {(entry) => ( +
+ {props.renderKey(entry[0])} + {props.renderValue(entry[1])} +
+ )} +
+
+ ); +} diff --git a/packages/start/src/shared/ui/Section.css b/packages/start/src/shared/ui/Section.css new file mode 100644 index 000000000..111f59021 --- /dev/null +++ b/packages/start/src/shared/ui/Section.css @@ -0,0 +1,15 @@ +[data-start-section] { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +[data-start-section-title] { + +} + + +[data-start-section-content] { + padding: 0rem 0.5rem; + width: 100%; +} \ No newline at end of file diff --git a/packages/start/src/shared/ui/Section.tsx b/packages/start/src/shared/ui/Section.tsx new file mode 100644 index 000000000..3c532132f --- /dev/null +++ b/packages/start/src/shared/ui/Section.tsx @@ -0,0 +1,23 @@ +import type { JSX } from "solid-js"; +import { Text, type TextProps } from "./Text.tsx"; + +import './Section.css'; + +export interface SectionProps { + title: string; + options?: TextProps<'span'>['options']; + children: JSX.Element; +} + +export function Section(props: SectionProps): JSX.Element { + return ( +
+ + {props.title} + +
+ {props.children} +
+
+ ); +} diff --git a/packages/start/src/shared/ui/Select.css b/packages/start/src/shared/ui/Select.css new file mode 100644 index 000000000..bc95e9770 --- /dev/null +++ b/packages/start/src/shared/ui/Select.css @@ -0,0 +1,38 @@ + +[data-start-select] { + display: flex; + flex-direction: column; + margin: 0; + margin-block: 0; + list-style-type: none; + padding-inline: 0; +} + +[data-start-select] > * + * { + border-top: 1px oklch(70.7% 0.165 254.624) solid; +} + +[data-start-select-option] { + color: rgb(17 24 39); + padding-top: 0.5rem; + padding-bottom: 0.5rem; + padding-left: 1rem; + padding-right: 1rem; + font-size: 0.875rem; + line-height: 1.25rem; + outline: none; + cursor: pointer; + + transition-property: color, background-color, border-color, box-shadow; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; + + white-space: nowrap; +} + +[data-start-select-option][tc-active], +[data-start-select-option][tc-selected], +[data-start-select-option]:focus { + background-color: rgb(219 234 254); + color: rgb(30 58 138); +} diff --git a/packages/start/src/shared/ui/Select.tsx b/packages/start/src/shared/ui/Select.tsx new file mode 100644 index 000000000..2d648832a --- /dev/null +++ b/packages/start/src/shared/ui/Select.tsx @@ -0,0 +1,13 @@ +import { + Select as BaseSelect, + SelectOption as BaseSelectOption, +} from "terracotta"; + +import "./Select.css"; + +export const Select: typeof BaseSelect = (props) => ( + +); +export const SelectOption: typeof BaseSelectOption = (props) => ( + +); diff --git a/packages/start/src/shared/ui/Table.css b/packages/start/src/shared/ui/Table.css new file mode 100644 index 000000000..bf1ed368f --- /dev/null +++ b/packages/start/src/shared/ui/Table.css @@ -0,0 +1,22 @@ +[data-start-table] { + border: 1px oklch(70.7% 0.165 254.624) solid; +} + +[data-start-table-header] { + display: flex; + flex-direction: row; + background-color: oklch(93.2% 0.032 255.585); + font-weight: 600; +} + +[data-start-table-row] { + display: flex; + flex-direction: row; + border-top: 1px oklch(70.7% 0.165 254.624) solid; +} + +[data-start-table-cell] { + flex: 1; + padding: 0.25rem 0.75rem; + overflow: auto; +} diff --git a/packages/start/src/shared/ui/Table.tsx b/packages/start/src/shared/ui/Table.tsx new file mode 100644 index 000000000..87eeb18f0 --- /dev/null +++ b/packages/start/src/shared/ui/Table.tsx @@ -0,0 +1,35 @@ +import type { ComponentProps, JSX } from 'solid-js'; + +import './Table.css'; + +type TableProps = ComponentProps<'div'>; + +export function Table(props: TableProps): JSX.Element { + return ( +
+ ); +} + +type TableHeaderProps = ComponentProps<'div'>; + +export function TableHeader(props: TableHeaderProps): JSX.Element { + return ( +
+ ); +} + +type TableRowProps = ComponentProps<'div'>; + +export function TableRow(props: TableRowProps): JSX.Element { + return ( +
+ ); +} + +type TableCellProps = ComponentProps<'div'>; + +export function TableCell(props: TableCellProps): JSX.Element { + return ( +
+ ); +} diff --git a/packages/start/src/shared/ui/Tabs.css b/packages/start/src/shared/ui/Tabs.css new file mode 100644 index 000000000..9a7c47874 --- /dev/null +++ b/packages/start/src/shared/ui/Tabs.css @@ -0,0 +1,48 @@ +[data-start-tab] { + display: flex; + align-items: center; + justify-content: center; + + border-radius: 0.5rem; + cursor: pointer; + + color: rgb(17 24 39); + padding-top: 0.5rem; + padding-bottom: 0.5rem; + padding-left: 1rem; + padding-right: 1rem; + font-size: 0.875rem; + line-height: 1.25rem; + border: none; + outline: none; + + transition-property: color, background-color, border-color, box-shadow; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +[data-start-tab][tc-active], +[data-start-tab][tc-selected], +[data-start-tab]:hover, +[data-start-tab]:focus { + background-color: rgb(219 234 254); + color: rgb(30 58 138); +} + +[data-start-tab-group] { +} + +[data-start-tab-list] { + display: flex; + gap: 0.25rem; + padding: 0.25rem; + background-color: rgb(249 250 251); + border-radius: 0.5rem; +} + +[data-start-tab-panel] { + padding: 0.5rem; + background-color: rgb(249 250 251); + border-radius: 0.5rem; + height: 100%; +} \ No newline at end of file diff --git a/packages/start/src/shared/ui/Tabs.tsx b/packages/start/src/shared/ui/Tabs.tsx new file mode 100644 index 000000000..1ef39f24e --- /dev/null +++ b/packages/start/src/shared/ui/Tabs.tsx @@ -0,0 +1,22 @@ +import { + Tab as BaseTab, + TabGroup as BaseTabGroup, + TabList as BaseTabList, + TabPanel as BaseTabPanel, +} from "terracotta"; + +import "./Tabs.css"; + +export const Tab: typeof BaseTab = (props) => ( + +); +export const TabGroup: typeof BaseTabGroup = (props) => ( + +); +export const TabPanel: typeof BaseTabPanel = (props) => ( + +); +export const TabList: typeof BaseTabList = (props) => ( + +); + diff --git a/packages/start/src/shared/ui/Text.css b/packages/start/src/shared/ui/Text.css new file mode 100644 index 000000000..a67b817da --- /dev/null +++ b/packages/start/src/shared/ui/Text.css @@ -0,0 +1,69 @@ +[data-start-text-size="xs"] { + font-size: 0.75rem; + line-height: calc(1 / 0.75); +} +[data-start-text-size="sm"] { + font-size: 0.875rem; + line-height: calc(1.25 / 0.875); +} +[data-start-text-size="base"] { + font-size: 1rem; + line-height: 1.5; +} +[data-start-text-size="lg"] { + font-size: 1.125rem; + line-height: calc(1.75 / 1.125); +} +[data-start-text-size="xl"] { + font-size: 1.25rem; + line-height: calc(1.75 / 1.25); +} +[data-start-text-size="2xl"] { + font-size: 1.5rem; + line-height: calc(2 / 1.5); +} + +[data-start-text-font="sans"] { + font-family: system-ui, sans-serif, 'Apple Color Emoji', 'Segoe UI Emoji', 'Segoe UI Symbol', 'Noto Color Emoji'; +} +[data-start-text-font="serif"] { + font-family: ui-serif, Georgia, Cambria, 'Times New Roman', Times, serif; +} +[data-start-text-font="mono"] { + font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, 'Liberation Mono', 'Courier New', monospace; +} + +[data-start-text-weight="thin"] { + font-weight: 100; +} +[data-start-text-weight="extralight"] { + font-weight: 200; +} +[data-start-text-weight="light"] { + font-weight: 300; +} +[data-start-text-weight="normal"] { + font-weight: 400; +} +[data-start-text-weight="medium"] { + font-weight: 500; +} +[data-start-text-weight="semibold"] { + font-weight: 600; +} +[data-start-text-weight="bold"] { + font-weight: 700; +} +[data-start-text-weight="extrabold"] { + font-weight: 800; +} +[data-start-text-weight="black"] { + font-weight: 900; +} + +[data-start-text-wrap="wrap"] { + white-space: wrap; +} +[data-start-text-wrap="nowrap"] { + white-space: nowrap; +} diff --git a/packages/start/src/shared/ui/Text.tsx b/packages/start/src/shared/ui/Text.tsx new file mode 100644 index 000000000..db1c6c146 --- /dev/null +++ b/packages/start/src/shared/ui/Text.tsx @@ -0,0 +1,37 @@ +import { type ComponentProps, createMemo, type JSX, splitProps } from 'solid-js'; +import { Dynamic } from 'solid-js/web'; + +import './Text.css'; + +export type TextProps = ComponentProps & { + options?: { + as?: T; + size?: 'xs' | 'sm' | 'base' | 'lg' | 'xl' | '2xl'; + font?: 'sans' | 'serif' | 'mono'; + weight?: 'thin' | 'extralight' | 'light' | 'normal' | 'medium' | 'semibold' | 'bold' | 'extrabold'; + wrap?: 'wrap' | 'nowrap'; + }; +}; + +export function Text(props: TextProps): JSX.Element { + const [current, rest] = splitProps(props, ['options']); + + const customization = createMemo>(() => { + const options = Object.assign({}, { + size: 'base', + font: 'mono', + weight: 'normal', + wrap: 'wrap', + }, current.options); + const entries = Object.entries(options); + return Object.fromEntries(entries.map(([key, value]) => [`data-start-text-${key}`, value])) as TextProps; + }); + + return ( + + ); +} \ No newline at end of file From 85b7f89f1c102b8c02eb8575bf5baec8023a48d0 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Wed, 14 Jan 2026 18:53:50 +0800 Subject: [PATCH 05/25] Fix styling --- .../src/shared/server-function-inspector/SerovalViewer.css | 2 -- packages/start/src/shared/ui/Section.css | 3 ++- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/packages/start/src/shared/server-function-inspector/SerovalViewer.css b/packages/start/src/shared/server-function-inspector/SerovalViewer.css index 73bb5e1fd..4e7677090 100644 --- a/packages/start/src/shared/server-function-inspector/SerovalViewer.css +++ b/packages/start/src/shared/server-function-inspector/SerovalViewer.css @@ -76,8 +76,6 @@ flex-direction: column; gap: 0.25rem; margin: 0rem 0.25rem; - - overflow: auto; } [data-start-seroval-property] { diff --git a/packages/start/src/shared/ui/Section.css b/packages/start/src/shared/ui/Section.css index 111f59021..1596891c6 100644 --- a/packages/start/src/shared/ui/Section.css +++ b/packages/start/src/shared/ui/Section.css @@ -10,6 +10,7 @@ [data-start-section-content] { - padding: 0rem 0.5rem; width: 100%; + + overflow: auto; } \ No newline at end of file From 5ed6ddbc47bef6e0da1ae81b5334925353284377 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Wed, 14 Jan 2026 18:57:35 +0800 Subject: [PATCH 06/25] Fix new lines --- .../src/shared/server-function-inspector/HeadersViewer.css | 2 +- packages/start/src/shared/ui/Cascade.css | 2 +- packages/start/src/shared/ui/Section.css | 2 +- packages/start/src/shared/ui/Tabs.css | 2 +- packages/start/src/shared/ui/Text.tsx | 2 +- 5 files changed, 5 insertions(+), 5 deletions(-) diff --git a/packages/start/src/shared/server-function-inspector/HeadersViewer.css b/packages/start/src/shared/server-function-inspector/HeadersViewer.css index 696eefedd..fbc235d92 100644 --- a/packages/start/src/shared/server-function-inspector/HeadersViewer.css +++ b/packages/start/src/shared/server-function-inspector/HeadersViewer.css @@ -9,4 +9,4 @@ [data-start-header-value] { white-space: nowrap; -} \ No newline at end of file +} diff --git a/packages/start/src/shared/ui/Cascade.css b/packages/start/src/shared/ui/Cascade.css index 34057ee1f..dbda77829 100644 --- a/packages/start/src/shared/ui/Cascade.css +++ b/packages/start/src/shared/ui/Cascade.css @@ -27,4 +27,4 @@ [data-start-cascade-option]:focus { background-color: rgb(219 234 254); color: rgb(30 58 138); -} \ No newline at end of file +} diff --git a/packages/start/src/shared/ui/Section.css b/packages/start/src/shared/ui/Section.css index 1596891c6..f32df90c6 100644 --- a/packages/start/src/shared/ui/Section.css +++ b/packages/start/src/shared/ui/Section.css @@ -13,4 +13,4 @@ width: 100%; overflow: auto; -} \ No newline at end of file +} diff --git a/packages/start/src/shared/ui/Tabs.css b/packages/start/src/shared/ui/Tabs.css index 9a7c47874..c7ac400fe 100644 --- a/packages/start/src/shared/ui/Tabs.css +++ b/packages/start/src/shared/ui/Tabs.css @@ -45,4 +45,4 @@ background-color: rgb(249 250 251); border-radius: 0.5rem; height: 100%; -} \ No newline at end of file +} diff --git a/packages/start/src/shared/ui/Text.tsx b/packages/start/src/shared/ui/Text.tsx index db1c6c146..44bad1b2d 100644 --- a/packages/start/src/shared/ui/Text.tsx +++ b/packages/start/src/shared/ui/Text.tsx @@ -34,4 +34,4 @@ export function Text(props: Text {...customization()} /> ); -} \ No newline at end of file +} From f486220be1c2b1f1641a4e81e2d279997ecd58cc Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Thu, 15 Jan 2026 14:51:37 +0800 Subject: [PATCH 07/25] Create HexViewer.tsx --- .../server-function-inspector/HexViewer.css | 34 ++++++ .../server-function-inspector/HexViewer.tsx | 111 ++++++++++++++++++ .../SerovalViewer.tsx | 21 ++-- .../server-function-inspector/index.tsx | 29 ++++- 4 files changed, 181 insertions(+), 14 deletions(-) create mode 100644 packages/start/src/shared/server-function-inspector/HexViewer.css create mode 100644 packages/start/src/shared/server-function-inspector/HexViewer.tsx diff --git a/packages/start/src/shared/server-function-inspector/HexViewer.css b/packages/start/src/shared/server-function-inspector/HexViewer.css new file mode 100644 index 000000000..8e6d7fb11 --- /dev/null +++ b/packages/start/src/shared/server-function-inspector/HexViewer.css @@ -0,0 +1,34 @@ +[data-start-hex-viewer] { + display: flex; + border: 1px oklch(70.7% 0.165 254.624) solid; + overflow: auto; +} + +[data-start-hex-viewer-bytes] { + flex: 1; + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; +} + +[data-start-hex-viewer-text] { + flex: 1; + display: flex; + flex-direction: column; + gap: 0.5rem; + padding: 0.5rem; + border-left: 1px oklch(70.7% 0.165 254.624) solid; +} + +[data-start-hex-row] { + display: flex; + align-items: center; + gap: 1rem; +} + +[data-start-hex-text] { + display: flex; + align-items: center; + justify-content: start; +} diff --git a/packages/start/src/shared/server-function-inspector/HexViewer.tsx b/packages/start/src/shared/server-function-inspector/HexViewer.tsx new file mode 100644 index 000000000..29b0a31da --- /dev/null +++ b/packages/start/src/shared/server-function-inspector/HexViewer.tsx @@ -0,0 +1,111 @@ +import { createMemo, createResource, For, type JSX, Show, Suspense } from "solid-js"; +import { Text } from "../ui/Text.tsx"; + +import './HexViewer.css'; + + +function toHex(num: number, digits = 2): string { + return num.toString(16).padStart(digits, '0').toUpperCase(); +} + +function HexChunk(props: HexViewerInnerProps) { + + const content = createMemo(() => { + const byte1 = props.bytes[0] || 0; + const byte2 = props.bytes[1] || 0; + const byte3 = props.bytes[2] || 0; + const byte4 = props.bytes[3] || 0; + + return `${toHex(byte1)} ${toHex(byte2)} ${toHex(byte3)} ${toHex(byte4)}` + }); + return ( + + {content()} + + ); +} + +function HexRow(props: HexViewerInnerProps) { + const chunk1 = createMemo(() => props.bytes.subarray(0, 4)); + const chunk2 = createMemo(() => props.bytes.subarray(4, 8)); + const chunk3 = createMemo(() => props.bytes.subarray(8, 12)); + const chunk4 = createMemo(() => props.bytes.subarray(12, 16)); + + return ( +
+ + + + +
+ ); +} + +function replaceString(string: string): string { + const result = string.codePointAt(0); + if (result == null) { + return string; + } + return String.fromCodePoint(result + 0x2400); +} + +function HexText(props: HexViewerInnerProps) { + const text = createMemo(() => { + const decoder = new TextDecoder(); + const result = decoder.decode(props.bytes).replaceAll(/[\x00-\x1F]/g, replaceString); + return result; + }); + + return ( +
+ + {text()} + +
+ ); +} + +interface HexViewerInnerProps { + bytes: Uint8Array; +} + +export function HexViewerInner(props: HexViewerInnerProps): JSX.Element { + const rows = createMemo(() => { + const arrays: Uint8Array[] = []; + for (let i = 0, len = props.bytes.length; i < len; i += 16) { + arrays.push(props.bytes.subarray(i, i + 16)); + } + return arrays; + }); + + return ( +
+
+ + {(current) => } + +
+
+ + {(current) => } + +
+
+ ); +} + +export interface HexViewerProps { + bytes: Uint8Array | Promise; +} + +export function HexViewer(props: HexViewerProps): JSX.Element { + const [data] = createResource(() => props.bytes); + + return ( + + + {(current) => } + + + ); +} diff --git a/packages/start/src/shared/server-function-inspector/SerovalViewer.tsx b/packages/start/src/shared/server-function-inspector/SerovalViewer.tsx index 4d53d5dcc..005fcc31b 100644 --- a/packages/start/src/shared/server-function-inspector/SerovalViewer.tsx +++ b/packages/start/src/shared/server-function-inspector/SerovalViewer.tsx @@ -6,6 +6,7 @@ import { Cascade, CascadeOption } from "../ui/Cascade.tsx"; import { Section } from "../ui/Section.tsx"; import { Text } from "../ui/Text.tsx"; import './SerovalViewer.css'; +import { HexViewer } from "./HexViewer.tsx"; function LinkIcon( props: JSX.IntrinsicElements["svg"] & { title: string }, @@ -545,15 +546,6 @@ function renderSerovalNode( ); - // TypedArray = 15, - case 15: - // BigIntTypedArray = 16, - case 16: - return ( - data-start-seroval-properties defaultValue={undefined} onChange={onSelect}> - {renderSerovalNode(ctx, node.f, onSelect, true)} - - ); // WKSymbol = 17, case 17: return @@ -561,8 +553,15 @@ function renderSerovalNode( case 18: break; // ArrayBuffer = 19, - case 19: - return ; + case 19: { + const data = atob(node.s); + const result = new TextEncoder().encode(data); + return ; + } + // TypedArray = 15, + case 15: + // BigIntTypedArray = 16, + case 16: // DataView = 20, case 20: return ( diff --git a/packages/start/src/shared/server-function-inspector/index.tsx b/packages/start/src/shared/server-function-inspector/index.tsx index e3893f5c1..55d5fc3e4 100644 --- a/packages/start/src/shared/server-function-inspector/index.tsx +++ b/packages/start/src/shared/server-function-inspector/index.tsx @@ -8,12 +8,15 @@ import { } from "solid-js"; import { createStore } from "solid-js/store"; import { Portal } from "solid-js/web"; +import { BODY_FORMAT_KEY, BodyFormat } from "../../server/server-functions-shared.ts"; import { Badge } from "../ui/Badge.tsx"; import Button from "../ui/Button.tsx"; import { Dialog, DialogOverlay, DialogPanel } from "../ui/Dialog.tsx"; +import { Section } from "../ui/Section.tsx"; import { Select, SelectOption } from "../ui/Select.tsx"; import { Tab, TabGroup, TabList, TabPanel } from "../ui/Tabs.tsx"; import { HeadersViewer } from "./HeadersViewer.tsx"; +import { HexViewer } from "./HexViewer.tsx"; import { SerovalViewer } from "./SerovalViewer.tsx"; import { captureServerFunctionCall, @@ -21,7 +24,6 @@ import { type ServerFunctionResponse, } from "./server-function-tracker.ts"; import "./styles.css"; -import { Section } from "../ui/Section.tsx"; interface ContentViewerProps { source: ServerFunctionRequest | ServerFunctionResponse; @@ -35,8 +37,29 @@ function ContentViewer(props: ContentViewerProps): JSX.Element {
{(() => { - if (props.source.source.headers.has('x-serialized')) { - return + const startType = props.source.source.headers.get(BODY_FORMAT_KEY); + const contentType = props.source.source.headers.get('Content-Type'); + switch (true) { + case startType === "true": + case startType === BodyFormat.Seroval: + return ; + case startType === BodyFormat.String: + return undefined; + case startType === BodyFormat.File: { + return undefined; + } + case startType === BodyFormat.FormData: + case contentType?.startsWith("multipart/form-data"): + return undefined; + case startType === BodyFormat.URLSearchParams: + case contentType?.startsWith("application/x-www-form-urlencoded"): + return undefined; + case startType === BodyFormat.Blob: + return undefined; + case startType === BodyFormat.ArrayBuffer: + return undefined; + case startType === BodyFormat.Uint8Array: + return ; } })()}
From 030cedbf1481987b30e60145f79b5661eb9121f4 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Sun, 18 Jan 2026 23:45:06 +0800 Subject: [PATCH 08/25] Add `FormDataViewer` --- .../FormDataViewer.css | 4 + .../FormDataViewer.tsx | 85 +++ .../SerovalValue.css | 7 + .../SerovalValue.tsx | 14 + .../SerovalViewer.css | 14 +- .../SerovalViewer.tsx | 483 +++++++++++------- .../server-function-inspector/index.tsx | 21 +- packages/start/src/shared/ui/Badge.css | 1 + packages/start/src/shared/ui/Badge.tsx | 4 +- packages/start/src/shared/ui/Button.css | 5 +- packages/start/src/shared/ui/Properties.tsx | 6 +- 11 files changed, 434 insertions(+), 210 deletions(-) create mode 100644 packages/start/src/shared/server-function-inspector/FormDataViewer.css create mode 100644 packages/start/src/shared/server-function-inspector/FormDataViewer.tsx create mode 100644 packages/start/src/shared/server-function-inspector/SerovalValue.css create mode 100644 packages/start/src/shared/server-function-inspector/SerovalValue.tsx diff --git a/packages/start/src/shared/server-function-inspector/FormDataViewer.css b/packages/start/src/shared/server-function-inspector/FormDataViewer.css new file mode 100644 index 000000000..f0a28091f --- /dev/null +++ b/packages/start/src/shared/server-function-inspector/FormDataViewer.css @@ -0,0 +1,4 @@ +[data-start-form-data-viewer] svg { + width: 1rem; + height: 1rem; +} \ No newline at end of file diff --git a/packages/start/src/shared/server-function-inspector/FormDataViewer.tsx b/packages/start/src/shared/server-function-inspector/FormDataViewer.tsx new file mode 100644 index 000000000..ed3bd1092 --- /dev/null +++ b/packages/start/src/shared/server-function-inspector/FormDataViewer.tsx @@ -0,0 +1,85 @@ +import { createResource, For, Show, Suspense, type JSX } from 'solid-js'; + +import { SerovalValue } from './SerovalValue.tsx'; +import { PropertySeparator } from '../ui/Properties.tsx'; +import { Section } from '../ui/Section'; +import Button from '../ui/Button'; +import { Badge } from '../ui/Badge.tsx'; + + +import './FormDataViewer.css'; + +function DocumentIcon( + props: JSX.IntrinsicElements["svg"] & { title: string }, +): JSX.Element { + return ( + + {props.title} + + + ); +} + +interface FormDataViewerInnerProps { + source: FormData; +} + +function FormDataViewerInner(props: FormDataViewerInnerProps): JSX.Element { + function openFileInNewTab(file: File) { + const fileURL = URL.createObjectURL(file); + const link = document.createElement("a"); + link.href = fileURL; + link.target = "_blank"; // Open in a new tab + link.style.display = "none"; // Hide the link + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } + + return ( +
+
+ + {([key, value]) => ( +
+ + + {typeof value === 'string' + ? + : ( + + )} +
+ )} +
+
+
+ ); +} + +export interface FormDataViewerProps { + source: FormData | Promise; +} + +export function FormDataViewer(props: FormDataViewerProps) { + const [data] = createResource(() => props.source); + + return ( + + + {(current) => } + + + ); +} \ No newline at end of file diff --git a/packages/start/src/shared/server-function-inspector/SerovalValue.css b/packages/start/src/shared/server-function-inspector/SerovalValue.css new file mode 100644 index 000000000..b38afc019 --- /dev/null +++ b/packages/start/src/shared/server-function-inspector/SerovalValue.css @@ -0,0 +1,7 @@ + + +[data-start-seroval-value] { + display: flex; + gap: 0.25rem; + align-items: center; +} diff --git a/packages/start/src/shared/server-function-inspector/SerovalValue.tsx b/packages/start/src/shared/server-function-inspector/SerovalValue.tsx new file mode 100644 index 000000000..35d5c3027 --- /dev/null +++ b/packages/start/src/shared/server-function-inspector/SerovalValue.tsx @@ -0,0 +1,14 @@ +import { Text } from "../ui/Text.tsx"; +import './SerovalValue.css'; + +interface SerovalValueProps { + value: string | number | boolean | undefined | null; +} + +export function SerovalValue(props: SerovalValueProps) { + return ( + + {`${props.value}`} + + ); +} diff --git a/packages/start/src/shared/server-function-inspector/SerovalViewer.css b/packages/start/src/shared/server-function-inspector/SerovalViewer.css index 4e7677090..707857c57 100644 --- a/packages/start/src/shared/server-function-inspector/SerovalViewer.css +++ b/packages/start/src/shared/server-function-inspector/SerovalViewer.css @@ -1,16 +1,6 @@ [data-start-seroval-viewer] { display: grid; grid-template-columns: 1fr 2fr; - border: 1px oklch(70.7% 0.165 254.624) solid; - overflow: auto; - width: 100%; - max-height: 50vh; -} - -[data-start-seroval-value] { - display: flex; - gap: 0.25rem; - align-items: center; } [data-start-seroval-promise] { @@ -21,9 +11,11 @@ [data-start-seroval-renderer] { display: flex; + border-top: 1px oklch(70.7% 0.165 254.624) solid; + border-bottom: 1px oklch(70.7% 0.165 254.624) solid; } -[data-start-seroval-renderer] > * + * { +[data-start-seroval-renderer] > * { border-left: 1px oklch(70.7% 0.165 254.624) solid; } diff --git a/packages/start/src/shared/server-function-inspector/SerovalViewer.tsx b/packages/start/src/shared/server-function-inspector/SerovalViewer.tsx index 005fcc31b..ddf1d56a1 100644 --- a/packages/start/src/shared/server-function-inspector/SerovalViewer.tsx +++ b/packages/start/src/shared/server-function-inspector/SerovalViewer.tsx @@ -1,12 +1,22 @@ import type { SerovalNode } from "seroval"; -import { createEffect, createSignal, For, type JSX, Show, splitProps } from "solid-js"; +import { + createEffect, + createSignal, + For, + type JSX, + Show, + splitProps, +} from "solid-js"; + import { SerovalChunkReader } from "../../server/serialization.ts"; import { Badge } from "../ui/Badge.tsx"; import { Cascade, CascadeOption } from "../ui/Cascade.tsx"; import { Section } from "../ui/Section.tsx"; -import { Text } from "../ui/Text.tsx"; -import './SerovalViewer.css'; import { HexViewer } from "./HexViewer.tsx"; +import { SerovalValue } from "./SerovalValue.tsx"; +import { PropertySeparator } from "../ui/Properties.tsx"; + +import "./SerovalViewer.css"; function LinkIcon( props: JSX.IntrinsicElements["svg"] & { title: string }, @@ -20,7 +30,11 @@ function LinkIcon( {...props} > {props.title} - + ); } @@ -32,80 +46,115 @@ export interface SerovalViewerProps { function getNodeType(node: SerovalNode) { switch (node.t) { // Number = 0, - case 0: return 'number'; + case 0: + return "number"; // String = 1, - case 1: return 'string'; + case 1: + return "string"; // Constant = 2, case 2: switch (node.s) { - case 0: return 'null'; - case 1: return 'undefined'; - case 2: return 'true'; - case 3: return 'false'; - case 4: return '-0'; - case 5: return 'Infinity'; - case 6: return '-Infinity'; - case 7: return 'NaN'; + case 0: + return "null"; + case 1: + return "undefined"; + case 2: + return "true"; + case 3: + return "false"; + case 4: + return "-0"; + case 5: + return "Infinity"; + case 6: + return "-Infinity"; + case 7: + return "NaN"; } break; // BigInt = 3, - case 3: return 'bigint' + case 3: + return "bigint"; // Date = 5, - case 5: return 'Date'; + case 5: + return "Date"; // RegExp = 6, - case 6: return 'RegExp'; + case 6: + return "RegExp"; // Set = 7, - case 7: return 'Set'; + case 7: + return "Set"; // Map = 8, - case 8: return 'Map'; + case 8: + return "Map"; // Array = 9, - case 9: return 'Array'; + case 9: + return "Array"; // Object = 10, case 10: // NullConstructor = 11, - case 11: return 'Object'; + case 11: + return "Object"; // Promise = 12, - case 12: return 'Promise'; + case 12: + return "Promise"; // Error = 13, case 13: switch (node.s) { - case 0: return 'Error'; - case 1: return 'EvalError'; - case 2: return 'RangeError'; - case 3: return 'ReferenceError'; - case 4: return 'SyntaxError'; - case 5: return 'TypeError'; - case 6: return 'URIError'; + case 0: + return "Error"; + case 1: + return "EvalError"; + case 2: + return "RangeError"; + case 3: + return "ReferenceError"; + case 4: + return "SyntaxError"; + case 5: + return "TypeError"; + case 6: + return "URIError"; } - return 'Error'; + return "Error"; // AggregateError = 14, - case 14: return 'AggregateError'; + case 14: + return "AggregateError"; // TypedArray = 15, case 15: // BigIntTypedArray = 16, - case 16: return node.c; + case 16: + return node.c; // WKSymbol = 17, - case 17: return 'symbol'; + case 17: + return "symbol"; // ArrayBuffer = 19, - case 19: return 'ArrayBuffer' + case 19: + return "ArrayBuffer"; // DataView = 20, - case 20: return 'DataView' + case 20: + return "DataView"; // Boxed = 21, - case 21: return 'Boxed'; + case 21: + return "Boxed"; // PromiseConstructor = 22, - case 22: return 'Promise'; + case 22: + return "Promise"; // Plugin = 25, case 25: // due to the nature of this node, we have to traverse it ourselves - return 'Plugin'; + return "Plugin"; // IteratorFactoryInstance = 28, - case 28: return 'Iterator'; + case 28: + return "Iterator"; // AsyncIteratorFactoryInstance = 30, - case 30: return 'AsyncIterator'; + case 30: + return "AsyncIterator"; // StreamConstructor = 31, - case 31: return 'Stream'; + case 31: + return "Stream"; } - throw new Error('unsupported node type'); + throw new Error("unsupported node type"); } function traverse( @@ -230,7 +279,7 @@ function traverse( case 28: traverse(node.a[0], handler); traverse(node.a[1], handler); - break + break; // AsyncIteratorFactory = 29, case 29: traverse(node.a[1], handler); @@ -239,7 +288,7 @@ function traverse( case 30: traverse(node.a[0], handler); traverse(node.a[1], handler); - break + break; // StreamConstructor = 31, case 31: // Traverse items @@ -258,60 +307,75 @@ function traverse( } } -interface SerovalValueProps { - value: string | number | boolean | undefined | null; -} - -function SerovalValue(props: SerovalValueProps) { - return ( - - {`${props.value}`} - - ); -} - function getConstantValue(value: number) { switch (value) { - case 0: return 'null'; - case 1: return 'undefined'; - case 2: return 'true'; - case 3: return 'false'; - case 4: return '-0'; - case 5: return 'Infinity'; - case 6: return '-Infinity'; - case 7: return 'NaN'; + case 0: + return "null"; + case 1: + return "undefined"; + case 2: + return "true"; + case 3: + return "false"; + case 4: + return "-0"; + case 5: + return "Infinity"; + case 6: + return "-Infinity"; + case 7: + return "NaN"; } - return ''; + return ""; } function getSymbolValue(value: number) { switch (value) { - case 0: return 'Symbol.asyncIterator'; - case 1: return 'Symbol.hasInstance'; - case 2: return 'Symbol.isConcatSpreadable'; - case 3: return 'Symbol.iterator'; - case 4: return 'Symbol.match'; - case 5: return 'Symbol.matchAll'; - case 6: return 'Symbol.replace'; - case 7: return 'Symbol.search'; - case 8: return 'Symbol.species'; - case 9: return 'Symbol.toPrimitive'; - case 10: return 'Symbol.toStringTag'; - case 11: return 'Symbol.unscopables'; + case 0: + return "Symbol.asyncIterator"; + case 1: + return "Symbol.hasInstance"; + case 2: + return "Symbol.isConcatSpreadable"; + case 3: + return "Symbol.iterator"; + case 4: + return "Symbol.match"; + case 5: + return "Symbol.matchAll"; + case 6: + return "Symbol.replace"; + case 7: + return "Symbol.search"; + case 8: + return "Symbol.species"; + case 9: + return "Symbol.toPrimitive"; + case 10: + return "Symbol.toStringTag"; + case 11: + return "Symbol.unscopables"; } - return ''; + return ""; } function getObjectFlag(value: number) { switch (value) { - case 1: return 'non-extensible'; - case 2: return 'sealed'; - case 3: return 'frozen'; - default: return 'none'; + case 1: + return "non-extensible"; + case 2: + return "sealed"; + case 3: + return "frozen"; + default: + return "none"; } } -function zip(keys: Key[], values: Value[]): [key: Key, value: Value][] { +function zip( + keys: Key[], + values: Value[], +): [key: Key, value: Value][] { const zipped: [key: Key, value: Value][] = []; for (let i = 0, len = keys.length; i < len; i++) { @@ -323,77 +387,89 @@ function zip(keys: Key[], values: Value[]): [key: Key, value: Value] interface RenderContext { getNode: (index: number) => SerovalNode | undefined; - getPromise: (index: number) => Extract | undefined; - getStream: (index: number) => Extract[] | undefined; + getPromise: ( + index: number, + ) => Extract | undefined; + getStream: ( + index: number, + ) => Extract[] | undefined; } function getStreamKeyword(t: 32 | 33 | 34): string { switch (t) { - case 32: return 'next'; - case 33: return 'throw'; - case 34: return 'return'; + case 32: + return "next"; + case 33: + return "throw"; + case 34: + return "return"; } } -function PropertySeparator() { - return :; -} - function renderSerovalNode( ctx: RenderContext, node: SerovalNode, onSelect: (index: number | undefined) => void, inner?: boolean, ): JSX.Element { - if (node.t >= 4 && (inner || node.t === 4) && node.i != null && !(node.t === 5 || node.t === 6 || node.t === 17)) { + if ( + node.t >= 4 && + (inner || node.t === 4) && + node.i != null && + !(node.t === 5 || node.t === 6 || node.t === 17) + ) { const index = node.i; const description = `id: ${index}`; const lookup = ctx.getNode(index)!; return ( - - + {getNodeType(lookup)} + {description} ); } switch (node.t) { // Number = 0, case 0: - return + return ; // String = 1, case 1: - return + return ; // Constant = 2, case 2: - return + return ; // BigInt = 3, case 3: return ( <> - + bigint ); // Date = 5, case 5: - return + return ; // RegExp = 6, case 6: - return + return ; // Set = 7, case 7: return ( <> -
+
-
- data-start-seroval-properties defaultValue={undefined} onChange={onSelect}> +
+ + data-start-seroval-properties + defaultValue={undefined} + onChange={onSelect} + > [index, node] as const)}> {([key, value]) => (
@@ -411,15 +487,19 @@ function renderSerovalNode( case 8: return ( <> -
+
-
- data-start-seroval-properties defaultValue={undefined} onChange={onSelect}> +
+ + data-start-seroval-properties + defaultValue={undefined} + onChange={onSelect} + > {([key, value]) => (
@@ -437,7 +517,7 @@ function renderSerovalNode( case 9: return ( <> -
+
@@ -446,21 +526,25 @@ function renderSerovalNode(
- + {getObjectFlag(node.o)}
-
- data-start-seroval-properties defaultValue={undefined} onChange={onSelect}> +
+ + data-start-seroval-properties + defaultValue={undefined} + onChange={onSelect} + > [index, node] as const)}> {([key, value]) => (
- { - value === 0 - ? - : renderSerovalNode(ctx, value, onSelect, true) - } + {value === 0 ? ( + + ) : ( + renderSerovalNode(ctx, value, onSelect, true) + )}
)}
@@ -474,7 +558,7 @@ function renderSerovalNode( case 11: return ( <> -
+
@@ -483,18 +567,22 @@ function renderSerovalNode(
- + {getObjectFlag(node.o)}
-
- data-start-seroval-properties defaultValue={undefined} onChange={onSelect}> +
+ + data-start-seroval-properties + defaultValue={undefined} + onChange={onSelect} + > {([key, value]) => (
- {( - typeof key === 'string' - ? - : renderSerovalNode(ctx, key, onSelect, true) + {typeof key === "string" ? ( + + ) : ( + renderSerovalNode(ctx, key, onSelect, true) )} {renderSerovalNode(ctx, value, onSelect, true)} @@ -508,7 +596,11 @@ function renderSerovalNode( // Promise = 12, case 12: return ( - data-start-seroval-properties defaultValue={undefined} onChange={onSelect}> + + data-start-seroval-properties + defaultValue={undefined} + onChange={onSelect} + > {renderSerovalNode(ctx, node.f, onSelect, true)} ); @@ -518,7 +610,7 @@ function renderSerovalNode( case 14: return ( <> -
+
@@ -526,15 +618,21 @@ function renderSerovalNode(
- {current => ( -
- data-start-seroval-properties defaultValue={undefined} onChange={onSelect}> + {(current) => ( +
+ + data-start-seroval-properties + defaultValue={undefined} + onChange={onSelect} + > {([key, value]) => (
- {typeof key === 'string' - ? - : renderSerovalNode(ctx, key, onSelect, true)} + {typeof key === "string" ? ( + + ) : ( + renderSerovalNode(ctx, key, onSelect, true) + )} {renderSerovalNode(ctx, value, onSelect, true)}
@@ -543,12 +641,12 @@ function renderSerovalNode(
)} - + ); // WKSymbol = 17, case 17: - return + return ; // Reference = 18, case 18: break; @@ -566,7 +664,7 @@ function renderSerovalNode( case 20: return ( <> -
+
@@ -578,8 +676,12 @@ function renderSerovalNode(
-
- data-start-seroval-properties defaultValue={undefined} onChange={onSelect}> +
+ + data-start-seroval-properties + defaultValue={undefined} + onChange={onSelect} + > {renderSerovalNode(ctx, node.f, onSelect, true)}
@@ -588,34 +690,44 @@ function renderSerovalNode( // Boxed = 21, case 21: return ( - data-start-seroval-properties defaultValue={undefined} onChange={onSelect}> + + data-start-seroval-properties + defaultValue={undefined} + onChange={onSelect} + > {renderSerovalNode(ctx, node.f, onSelect, true)} ); case 22: - return <> - {(() => { - const result = ctx.getPromise(node.s); - if (result) { - const status = result.t === 23 ? 'success' : 'failure' as const; - return ( - data-start-seroval-properties defaultValue={undefined} onChange={onSelect}> -
- - - -
- - - - {renderSerovalNode(ctx, result.a[1], onSelect, true)} - - - ); - } - return - })()} - ; + return ( + <> + {(() => { + const result = ctx.getPromise(node.s); + if (result) { + const status = result.t === 23 ? "success" : ("failure" as const); + return ( + + data-start-seroval-properties + defaultValue={undefined} + onChange={onSelect} + > +
+ + + {status} +
+ + + + {renderSerovalNode(ctx, result.a[1], onSelect, true)} + + + ); + } + return pending; + })()} + + ); // Plugin = 25 case 25: // due to the nature of this node, we have to traverse it ourselves @@ -639,7 +751,11 @@ function renderSerovalNode( {(() => { const result = ctx.getStream(node.i) || []; return ( - data-start-seroval-properties defaultValue={undefined} onChange={onSelect}> + + data-start-seroval-properties + defaultValue={undefined} + onChange={onSelect} + > {(current) => (
@@ -662,8 +778,8 @@ interface SerovalNodeRendererProps extends RenderContext { } function SerovalNodeRenderer(props: SerovalNodeRendererProps): JSX.Element { - const [, rest] = splitProps(props, ['node']); - const [next, setNext] = createSignal() + const [, rest] = splitProps(props, ["node"]); + const [next, setNext] = createSignal(); function onSelect(index: number | undefined) { if (index == null) { @@ -678,44 +794,39 @@ function SerovalNodeRenderer(props: SerovalNodeRendererProps): JSX.Element {
{props.node.i != null && ( - + {`id: ${props.node.i}`} )} - + {getNodeType(props.node)}
{renderSerovalNode(props, props.node, onSelect)}
- {(current) => ( - - )} + {(current) => } ); } -interface SerovalRendererProps extends Omit { +interface SerovalRendererProps extends Omit { node?: SerovalNode; } function SerovalRenderer(props: SerovalRendererProps): JSX.Element { - const [, rest] = splitProps(props, ['node']); + const [, rest] = splitProps(props, ["node"]); return (
- {(current) => ( - - )} + {(current) => }
); } -function createSimpleStore>(initial: T) { +function createSimpleStore>( + initial: T, +) { const [state, setState] = createSignal(initial); return { @@ -736,16 +847,22 @@ function createSimpleStore>(initial: ...current, [key]: value(current[key]), })); - } - } + }, + }; } export function SerovalViewer(props: SerovalViewerProps): JSX.Element { const [selected, setSelected] = createSignal(); - const references = createSimpleStore | undefined>>({}); - const streams = createSimpleStore[] | undefined>>({}); - const promises = createSimpleStore | undefined>>({}); + const references = createSimpleStore< + Record | undefined> + >({}); + const streams = createSimpleStore< + Record[] | undefined> + >({}); + const promises = createSimpleStore< + Record | undefined> + >({}); createEffect(async () => { setSelected(undefined); diff --git a/packages/start/src/shared/server-function-inspector/index.tsx b/packages/start/src/shared/server-function-inspector/index.tsx index 55d5fc3e4..e2378c737 100644 --- a/packages/start/src/shared/server-function-inspector/index.tsx +++ b/packages/start/src/shared/server-function-inspector/index.tsx @@ -15,6 +15,7 @@ import { Dialog, DialogOverlay, DialogPanel } from "../ui/Dialog.tsx"; import { Section } from "../ui/Section.tsx"; import { Select, SelectOption } from "../ui/Select.tsx"; import { Tab, TabGroup, TabList, TabPanel } from "../ui/Tabs.tsx"; +import { FormDataViewer } from "./FormDataViewer.tsx"; import { HeadersViewer } from "./HeadersViewer.tsx"; import { HexViewer } from "./HexViewer.tsx"; import { SerovalViewer } from "./SerovalViewer.tsx"; @@ -37,12 +38,13 @@ function ContentViewer(props: ContentViewerProps): JSX.Element {
{(() => { - const startType = props.source.source.headers.get(BODY_FORMAT_KEY); - const contentType = props.source.source.headers.get('Content-Type'); + const source = props.source.source.clone(); + const startType = source.headers.get(BODY_FORMAT_KEY); + const contentType = source.headers.get('Content-Type'); switch (true) { case startType === "true": case startType === BodyFormat.Seroval: - return ; + return ; case startType === BodyFormat.String: return undefined; case startType === BodyFormat.File: { @@ -50,7 +52,7 @@ function ContentViewer(props: ContentViewerProps): JSX.Element { } case startType === BodyFormat.FormData: case contentType?.startsWith("multipart/form-data"): - return undefined; + return ; case startType === BodyFormat.URLSearchParams: case contentType?.startsWith("application/x-www-form-urlencoded"): return undefined; @@ -59,7 +61,7 @@ function ContentViewer(props: ContentViewerProps): JSX.Element { case startType === BodyFormat.ArrayBuffer: return undefined; case startType === BodyFormat.Uint8Array: - return ; + return ; } })()}
@@ -188,16 +190,17 @@ export function ServerFunctionInspector(): JSX.Element { + > + {current().request.source.method} + {instance} {(response) => { if (response().source.ok) { - return ; + return {response().source.status}; } - return ; + return {response().source.status}; }} diff --git a/packages/start/src/shared/ui/Badge.css b/packages/start/src/shared/ui/Badge.css index 1a2276f9f..cee81bcf5 100644 --- a/packages/start/src/shared/ui/Badge.css +++ b/packages/start/src/shared/ui/Badge.css @@ -2,6 +2,7 @@ display: inline-flex; align-items: center; justify-content: center; + gap: 0.25rem; border-radius: 0.375rem; padding: 0.125rem 0.25rem; diff --git a/packages/start/src/shared/ui/Badge.tsx b/packages/start/src/shared/ui/Badge.tsx index 3b3513c94..5de35361e 100644 --- a/packages/start/src/shared/ui/Badge.tsx +++ b/packages/start/src/shared/ui/Badge.tsx @@ -4,14 +4,14 @@ import { Text } from './Text.tsx'; export interface BadgeProps { - value: string | number; type: 'info' | 'success' | 'failure' | 'warning'; + children: JSX.Element; } export function Badge(props: BadgeProps): JSX.Element { return ( - {props.value} + {props.children} ); } diff --git a/packages/start/src/shared/ui/Button.css b/packages/start/src/shared/ui/Button.css index 985ae4972..fbae0519d 100644 --- a/packages/start/src/shared/ui/Button.css +++ b/packages/start/src/shared/ui/Button.css @@ -10,10 +10,7 @@ background-color: oklch(97% 0.014 254.604); color: rgb(17 24 39); - padding-top: 0.5rem; - padding-bottom: 0.5rem; - padding-left: 1rem; - padding-right: 1rem; + padding: 0.25rem 0.5rem; font-size: 0.875rem; line-height: 1.25rem; border: none; diff --git a/packages/start/src/shared/ui/Properties.tsx b/packages/start/src/shared/ui/Properties.tsx index 7127f2a69..54cd223d4 100644 --- a/packages/start/src/shared/ui/Properties.tsx +++ b/packages/start/src/shared/ui/Properties.tsx @@ -1,5 +1,5 @@ import { For, type JSX } from 'solid-js'; - +import { Text } from './Text.tsx'; import './Properties.css'; export type PropertyEntry = [key: unknown, value: unknown]; @@ -27,3 +27,7 @@ export function Properties(
); } + +export function PropertySeparator() { + return :; +} From 2c0c3da3b5a8f31fd8e0b277a5832a4318ee2ecd Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Tue, 20 Jan 2026 21:08:36 +0800 Subject: [PATCH 09/25] add the remaining viewers --- .../tests/src/routes/server-function-file.tsx | 23 ++++++ .../server-function-inspector/BlobViewer.css | 4 + .../server-function-inspector/BlobViewer.tsx | 78 +++++++++++++++++++ .../FormDataViewer.css | 4 - .../FormDataViewer.tsx | 50 ++---------- .../SerovalViewer.css | 3 +- .../URLSearchParamsViewer.tsx | 42 ++++++++++ .../server-function-inspector/index.tsx | 30 +++++-- packages/start/src/shared/ui/Section.css | 5 +- packages/start/src/shared/ui/Tabs.css | 2 +- 10 files changed, 180 insertions(+), 61 deletions(-) create mode 100644 apps/tests/src/routes/server-function-file.tsx create mode 100644 packages/start/src/shared/server-function-inspector/BlobViewer.css create mode 100644 packages/start/src/shared/server-function-inspector/BlobViewer.tsx delete mode 100644 packages/start/src/shared/server-function-inspector/FormDataViewer.css create mode 100644 packages/start/src/shared/server-function-inspector/URLSearchParamsViewer.tsx diff --git a/apps/tests/src/routes/server-function-file.tsx b/apps/tests/src/routes/server-function-file.tsx new file mode 100644 index 000000000..da0a369f0 --- /dev/null +++ b/apps/tests/src/routes/server-function-file.tsx @@ -0,0 +1,23 @@ +import { createEffect, createSignal } from "solid-js"; + +async function ping(file: File) { + "use server"; + return await file.text(); +} + +export default function App() { + const [output, setOutput] = createSignal<{ result?: boolean }>({}); + + createEffect(async () => { + const file = new File(['Hello, World!'], 'hello-world.txt'); + const result = await ping(file); + const value = await file.text(); + setOutput(prev => ({ ...prev, result: value === result })); + }); + + return ( +
+ {JSON.stringify(output())} +
+ ); +} diff --git a/packages/start/src/shared/server-function-inspector/BlobViewer.css b/packages/start/src/shared/server-function-inspector/BlobViewer.css new file mode 100644 index 000000000..b440887f3 --- /dev/null +++ b/packages/start/src/shared/server-function-inspector/BlobViewer.css @@ -0,0 +1,4 @@ +[data-start-blob-viewer] svg { + width: 1rem; + height: 1rem; +} \ No newline at end of file diff --git a/packages/start/src/shared/server-function-inspector/BlobViewer.tsx b/packages/start/src/shared/server-function-inspector/BlobViewer.tsx new file mode 100644 index 000000000..1f6532ba8 --- /dev/null +++ b/packages/start/src/shared/server-function-inspector/BlobViewer.tsx @@ -0,0 +1,78 @@ +import { createMemo, createResource, type JSX, onCleanup, Show, Suspense } from 'solid-js'; + +import { Badge } from "../ui/Badge"; +import Button from "../ui/Button"; + +import './BlobViewer.css'; + + +function DocumentIcon( + props: JSX.IntrinsicElements["svg"] & { title: string }, +): JSX.Element { + return ( + + {props.title} + + + ); +} + +interface BlobViewerInnerProps { + source: File | Blob; +} + +function BlobViewerInner(props: BlobViewerInnerProps): JSX.Element { + const fileURL = createMemo(() => URL.createObjectURL(props.source)); + + onCleanup(() => { + URL.revokeObjectURL(fileURL()); + }); + + function openFileInNewTab() { + const link = document.createElement("a"); + link.href = fileURL(); + link.target = "_blank"; // Open in a new tab + link.style.display = "none"; // Hide the link + document.body.appendChild(link); + link.click(); + document.body.removeChild(link); + } + + return ( + + ) +} + + +export interface BlobViewerProps { + source: Blob | File | Promise; +} + + +export function BlobViewer(props: BlobViewerProps): JSX.Element { + const [data] = createResource(() => props.source); + + return ( + + + {(current) => } + + + ); +} diff --git a/packages/start/src/shared/server-function-inspector/FormDataViewer.css b/packages/start/src/shared/server-function-inspector/FormDataViewer.css deleted file mode 100644 index f0a28091f..000000000 --- a/packages/start/src/shared/server-function-inspector/FormDataViewer.css +++ /dev/null @@ -1,4 +0,0 @@ -[data-start-form-data-viewer] svg { - width: 1rem; - height: 1rem; -} \ No newline at end of file diff --git a/packages/start/src/shared/server-function-inspector/FormDataViewer.tsx b/packages/start/src/shared/server-function-inspector/FormDataViewer.tsx index ed3bd1092..5bc340680 100644 --- a/packages/start/src/shared/server-function-inspector/FormDataViewer.tsx +++ b/packages/start/src/shared/server-function-inspector/FormDataViewer.tsx @@ -1,47 +1,14 @@ -import { createResource, For, Show, Suspense, type JSX } from 'solid-js'; - -import { SerovalValue } from './SerovalValue.tsx'; +import { createResource, For, type JSX, Show, Suspense } from 'solid-js'; import { PropertySeparator } from '../ui/Properties.tsx'; import { Section } from '../ui/Section'; -import Button from '../ui/Button'; -import { Badge } from '../ui/Badge.tsx'; - - -import './FormDataViewer.css'; - -function DocumentIcon( - props: JSX.IntrinsicElements["svg"] & { title: string }, -): JSX.Element { - return ( - - {props.title} - - - ); -} +import { BlobViewer } from './BlobViewer.tsx'; +import { SerovalValue } from './SerovalValue.tsx'; interface FormDataViewerInnerProps { source: FormData; } function FormDataViewerInner(props: FormDataViewerInnerProps): JSX.Element { - function openFileInNewTab(file: File) { - const fileURL = URL.createObjectURL(file); - const link = document.createElement("a"); - link.href = fileURL; - link.target = "_blank"; // Open in a new tab - link.style.display = "none"; // Hide the link - document.body.appendChild(link); - link.click(); - document.body.removeChild(link); - } - return (
@@ -51,15 +18,8 @@ function FormDataViewerInner(props: FormDataViewerInnerProps): JSX.Element { {typeof value === 'string' - ? - : ( - - )} + ? + : }
)} diff --git a/packages/start/src/shared/server-function-inspector/SerovalViewer.css b/packages/start/src/shared/server-function-inspector/SerovalViewer.css index 707857c57..a3332591e 100644 --- a/packages/start/src/shared/server-function-inspector/SerovalViewer.css +++ b/packages/start/src/shared/server-function-inspector/SerovalViewer.css @@ -27,7 +27,7 @@ display: flex; flex-direction: column; gap: 0.25rem; - padding: 0.25rem; + padding: 0.5rem; width: 100%; max-width: 16rem; @@ -67,7 +67,6 @@ display: flex; flex-direction: column; gap: 0.25rem; - margin: 0rem 0.25rem; } [data-start-seroval-property] { diff --git a/packages/start/src/shared/server-function-inspector/URLSearchParamsViewer.tsx b/packages/start/src/shared/server-function-inspector/URLSearchParamsViewer.tsx new file mode 100644 index 000000000..59f31dce1 --- /dev/null +++ b/packages/start/src/shared/server-function-inspector/URLSearchParamsViewer.tsx @@ -0,0 +1,42 @@ +import { createResource, For, type JSX, Show, Suspense } from 'solid-js'; +import { PropertySeparator } from '../ui/Properties.tsx'; +import { Section } from '../ui/Section'; +import { SerovalValue } from './SerovalValue.tsx'; + +interface URLSearchParamsViewerInnerProps { + source: URLSearchParams; +} + +function URLSearchParamsViewerInner(props: URLSearchParamsViewerInnerProps): JSX.Element { + return ( +
+
+ + {([key, value]) => ( +
+ + + +
+ )} +
+
+
+ ); +} + +export interface URLSearchParamsViewerProps { + source: URLSearchParams | Promise; +} + +export function URLSearchParamsViewer(props: URLSearchParamsViewerProps) { + const [data] = createResource(() => props.source); + + return ( + + + {(current) => } + + + ); +} \ No newline at end of file diff --git a/packages/start/src/shared/server-function-inspector/index.tsx b/packages/start/src/shared/server-function-inspector/index.tsx index e2378c737..42508f6a7 100644 --- a/packages/start/src/shared/server-function-inspector/index.tsx +++ b/packages/start/src/shared/server-function-inspector/index.tsx @@ -8,13 +8,14 @@ import { } from "solid-js"; import { createStore } from "solid-js/store"; import { Portal } from "solid-js/web"; -import { BODY_FORMAT_KEY, BodyFormat } from "../../server/server-functions-shared.ts"; +import { BODY_FORMAL_FILE, BODY_FORMAT_KEY, BodyFormat } from "../../server/server-functions-shared.ts"; import { Badge } from "../ui/Badge.tsx"; import Button from "../ui/Button.tsx"; import { Dialog, DialogOverlay, DialogPanel } from "../ui/Dialog.tsx"; import { Section } from "../ui/Section.tsx"; import { Select, SelectOption } from "../ui/Select.tsx"; import { Tab, TabGroup, TabList, TabPanel } from "../ui/Tabs.tsx"; +import { BlobViewer } from "./BlobViewer.tsx"; import { FormDataViewer } from "./FormDataViewer.tsx"; import { HeadersViewer } from "./HeadersViewer.tsx"; import { HexViewer } from "./HexViewer.tsx"; @@ -25,6 +26,21 @@ import { type ServerFunctionResponse, } from "./server-function-tracker.ts"; import "./styles.css"; +import { URLSearchParamsViewer } from "./URLSearchParamsViewer.tsx"; + +async function getFile(source: Response | Request): Promise { + const formData = await source.formData(); + const file = formData.get(BODY_FORMAL_FILE); + if (!(file && file instanceof File)) { + throw new Error('invalid file input'); + } + return file; +} + +async function getURLSearchParams(source: Response | Request): Promise { + const text = await source.text(); + return new URLSearchParams(text); +} interface ContentViewerProps { source: ServerFunctionRequest | ServerFunctionResponse; @@ -46,20 +62,18 @@ function ContentViewer(props: ContentViewerProps): JSX.Element { case startType === BodyFormat.Seroval: return ; case startType === BodyFormat.String: - return undefined; - case startType === BodyFormat.File: { - return undefined; - } + return ; + case startType === BodyFormat.File: + return ; case startType === BodyFormat.FormData: case contentType?.startsWith("multipart/form-data"): return ; case startType === BodyFormat.URLSearchParams: case contentType?.startsWith("application/x-www-form-urlencoded"): - return undefined; + return ; case startType === BodyFormat.Blob: - return undefined; + return ; case startType === BodyFormat.ArrayBuffer: - return undefined; case startType === BodyFormat.Uint8Array: return ; } diff --git a/packages/start/src/shared/ui/Section.css b/packages/start/src/shared/ui/Section.css index f32df90c6..54ca046d6 100644 --- a/packages/start/src/shared/ui/Section.css +++ b/packages/start/src/shared/ui/Section.css @@ -10,7 +10,10 @@ [data-start-section-content] { - width: 100%; + padding-left: 0.5rem; + border-left: 1px oklch(70.7% 0.165 254.624) solid; + + width: calc(100% - 0.5rem); overflow: auto; } diff --git a/packages/start/src/shared/ui/Tabs.css b/packages/start/src/shared/ui/Tabs.css index c7ac400fe..18afd6906 100644 --- a/packages/start/src/shared/ui/Tabs.css +++ b/packages/start/src/shared/ui/Tabs.css @@ -41,7 +41,7 @@ } [data-start-tab-panel] { - padding: 0.5rem; + padding: 1rem; background-color: rgb(249 250 251); border-radius: 0.5rem; height: 100%; From d42d7c535080b2599a90f0f4c92286f198335a21 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Fri, 23 Jan 2026 16:14:20 +0800 Subject: [PATCH 10/25] Add plugin and sequence renderer --- .../SerovalViewer.css | 6 ++ .../SerovalViewer.tsx | 91 ++++++++++++++++--- 2 files changed, 84 insertions(+), 13 deletions(-) diff --git a/packages/start/src/shared/server-function-inspector/SerovalViewer.css b/packages/start/src/shared/server-function-inspector/SerovalViewer.css index a3332591e..c6d0b2577 100644 --- a/packages/start/src/shared/server-function-inspector/SerovalViewer.css +++ b/packages/start/src/shared/server-function-inspector/SerovalViewer.css @@ -52,6 +52,12 @@ color: oklch(62.3% 0.214 259.815); } +[data-start-seroval-value-wrapper] { + display: inline-flex; + gap: 0.25rem; + align-items: center; +} + [data-start-seroval-link] { display: inline-flex; gap: 0.25rem; diff --git a/packages/start/src/shared/server-function-inspector/SerovalViewer.tsx b/packages/start/src/shared/server-function-inspector/SerovalViewer.tsx index ddf1d56a1..ac6979721 100644 --- a/packages/start/src/shared/server-function-inspector/SerovalViewer.tsx +++ b/packages/start/src/shared/server-function-inspector/SerovalViewer.tsx @@ -11,10 +11,10 @@ import { import { SerovalChunkReader } from "../../server/serialization.ts"; import { Badge } from "../ui/Badge.tsx"; import { Cascade, CascadeOption } from "../ui/Cascade.tsx"; +import { PropertySeparator } from "../ui/Properties.tsx"; import { Section } from "../ui/Section.tsx"; import { HexViewer } from "./HexViewer.tsx"; import { SerovalValue } from "./SerovalValue.tsx"; -import { PropertySeparator } from "../ui/Properties.tsx"; import "./SerovalViewer.css"; @@ -153,6 +153,8 @@ function getNodeType(node: SerovalNode) { // StreamConstructor = 31, case 31: return "Stream"; + case 35: + return "Sequence"; } throw new Error("unsupported node type"); } @@ -266,7 +268,12 @@ function traverse( break; // Plugin = 25, case 25: - // due to the nature of this node, we have to traverse it ourselves + for (const key in node.s) { + const current = node.s[key]; + if (current) { + traverse(current, handler); + } + } break; // SpecialReference = 26, case 26: @@ -304,6 +311,14 @@ function traverse( case 34: traverse(node.f, handler); break; + case 35: + // Traverse items + for (const child of node.a) { + if (child) { + traverse(child, handler); + } + } + break; } } @@ -442,17 +457,27 @@ function renderSerovalNode( // BigInt = 3, case 3: return ( - <> +
bigint - +
); // Date = 5, case 5: - return ; + return ( +
+ Date + +
+ ); // RegExp = 6, case 6: - return ; + return ( +
+ RegExp + +
+ ); // Set = 7, case 7: return ( @@ -730,8 +755,34 @@ function renderSerovalNode( ); // Plugin = 25 case 25: - // due to the nature of this node, we have to traverse it ourselves - break; + return ( + <> +
+
+ + + +
+
+
+ + data-start-seroval-properties + defaultValue={undefined} + onChange={onSelect} + > + + {([key, value]) => ( +
+ + + {renderSerovalNode(ctx, value, onSelect, true)} +
+ )} +
+ +
+ + ); // IteratorFactory = 27, case 27: break; @@ -770,6 +821,24 @@ function renderSerovalNode( })()} ); + case 35: + return ( + + data-start-seroval-properties + defaultValue={undefined} + onChange={onSelect} + > + + {(current, index) => ( +
+ + + {renderSerovalNode(ctx, current, onSelect, true)} +
+ )} +
+ + ); } } @@ -917,13 +986,9 @@ export function SerovalViewer(props: SerovalViewerProps): JSX.Element { case 27: case 29: case 31: + case 35: references.write(node.i, node); break; - case 28: { - break; - } - case 30: - break; } } From 0b779c1f46230f56fc6172e9a9ca602159a58c6c Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Fri, 23 Jan 2026 16:14:25 +0800 Subject: [PATCH 11/25] add more tests --- .../src/routes/server-function-iterator.tsx | 35 +++++++++++++++++ .../src/routes/server-function-plugin.tsx | 38 ++++++++++++++++++ .../server-function-readable-stream.tsx | 39 +++++++++++++++++++ 3 files changed, 112 insertions(+) create mode 100644 apps/tests/src/routes/server-function-iterator.tsx create mode 100644 apps/tests/src/routes/server-function-plugin.tsx create mode 100644 apps/tests/src/routes/server-function-readable-stream.tsx diff --git a/apps/tests/src/routes/server-function-iterator.tsx b/apps/tests/src/routes/server-function-iterator.tsx new file mode 100644 index 000000000..9beb1a34d --- /dev/null +++ b/apps/tests/src/routes/server-function-iterator.tsx @@ -0,0 +1,35 @@ +import { createEffect, createSignal } from "solid-js"; + +async function ping(value: Date) { + "use server"; + + const current = [ + value, + { + name: 'example', + *[Symbol.iterator]() { + yield 'foo'; + yield 'bar'; + yield 'baz'; + } + } + ]; + + return current; +} + +export default function App() { + const [output, setOutput] = createSignal<{ result?: boolean }>({}); + + createEffect(async () => { + const value = new Date(); + const result = await ping(value); + setOutput((prev) => ({ ...prev, result: value.toString() === result[0].toString() })); + }); + + return ( +
+ {JSON.stringify(output())} +
+ ); +} diff --git a/apps/tests/src/routes/server-function-plugin.tsx b/apps/tests/src/routes/server-function-plugin.tsx new file mode 100644 index 000000000..e9b8d4b2b --- /dev/null +++ b/apps/tests/src/routes/server-function-plugin.tsx @@ -0,0 +1,38 @@ +import { createEffect, createSignal } from "solid-js"; + +async function sleep(value: unknown, ms: number) { + return new Promise((res) => { + setTimeout(res, ms, value); + }) +} + +async function ping(value: URLSearchParams, clone: URLSearchParams) { + "use server"; + + const current = [ + value.toString() === clone.toString(), + value, + clone, + ] as const; + + return current; +} + +export default function App() { + const [output, setOutput] = createSignal<{ result?: boolean }>({}); + + createEffect(async () => { + const value = new URLSearchParams([ + ['foo', 'bar'], + ['hello', 'world'], + ]); + const result = await ping(value, value); + setOutput((prev) => ({ ...prev, result: result[0] })); + }); + + return ( +
+ {JSON.stringify(output())} +
+ ); +} diff --git a/apps/tests/src/routes/server-function-readable-stream.tsx b/apps/tests/src/routes/server-function-readable-stream.tsx new file mode 100644 index 000000000..53d39dcf0 --- /dev/null +++ b/apps/tests/src/routes/server-function-readable-stream.tsx @@ -0,0 +1,39 @@ +import { createEffect, createSignal } from "solid-js"; + +async function sleep(value: T, ms: number) { + return new Promise((res) => { + setTimeout(res, ms, value); + }) +} + +async function ping(value: ReadableStream) { + "use server"; + return value; +} + +export default function App() { + const [output, setOutput] = createSignal<{ result?: boolean }>({}); + + createEffect(async () => { + const result = await ping( + new ReadableStream({ + async start(controller) { + controller.enqueue(await sleep('foo', 100)); + controller.enqueue(await sleep('bar', 100)); + controller.enqueue(await sleep('baz', 100)); + controller.close(); + }, + }) + ); + + const reader = result.getReader(); + const first = await reader.read(); + setOutput((prev) => ({ ...prev, result: first.value === 'foo' })); + }); + + return ( +
+ {JSON.stringify(output())} +
+ ); +} From a2358648cb2d3a78de5575868a57897dd19f3dd4 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Fri, 23 Jan 2026 23:20:14 +0800 Subject: [PATCH 12/25] final output --- .../FormDataViewer.tsx | 7 +- .../HeadersViewer.css | 8 +- .../HeadersViewer.tsx | 23 +-- .../SerovalValue.tsx | 4 + .../SerovalViewer.css | 12 -- .../SerovalViewer.tsx | 83 +++++----- .../URLSearchParamsViewer.tsx | 7 +- .../server-function-inspector/index.tsx | 152 +++++++++++++----- .../server-function-inspector/styles.css | 16 +- packages/start/src/shared/ui/Placeholder.css | 12 ++ packages/start/src/shared/ui/Placeholder.tsx | 14 ++ packages/start/src/shared/ui/Properties.css | 15 -- packages/start/src/shared/ui/Properties.tsx | 33 ---- packages/start/src/shared/ui/Section.css | 6 +- packages/start/src/shared/ui/Section.tsx | 2 +- packages/start/src/shared/ui/Tabs.css | 6 + 16 files changed, 226 insertions(+), 174 deletions(-) create mode 100644 packages/start/src/shared/ui/Placeholder.css create mode 100644 packages/start/src/shared/ui/Placeholder.tsx delete mode 100644 packages/start/src/shared/ui/Properties.css delete mode 100644 packages/start/src/shared/ui/Properties.tsx diff --git a/packages/start/src/shared/server-function-inspector/FormDataViewer.tsx b/packages/start/src/shared/server-function-inspector/FormDataViewer.tsx index 5bc340680..1efd3f83c 100644 --- a/packages/start/src/shared/server-function-inspector/FormDataViewer.tsx +++ b/packages/start/src/shared/server-function-inspector/FormDataViewer.tsx @@ -1,8 +1,7 @@ import { createResource, For, type JSX, Show, Suspense } from 'solid-js'; -import { PropertySeparator } from '../ui/Properties.tsx'; import { Section } from '../ui/Section'; import { BlobViewer } from './BlobViewer.tsx'; -import { SerovalValue } from './SerovalValue.tsx'; +import { SerovalValue, PropertySeparator } from './SerovalValue.tsx'; interface FormDataViewerInnerProps { source: FormData; @@ -11,10 +10,10 @@ interface FormDataViewerInnerProps { function FormDataViewerInner(props: FormDataViewerInnerProps): JSX.Element { return (
-
+
{([key, value]) => ( -
+
{typeof value === 'string' diff --git a/packages/start/src/shared/server-function-inspector/HeadersViewer.css b/packages/start/src/shared/server-function-inspector/HeadersViewer.css index fbc235d92..e6b8c1c4d 100644 --- a/packages/start/src/shared/server-function-inspector/HeadersViewer.css +++ b/packages/start/src/shared/server-function-inspector/HeadersViewer.css @@ -3,10 +3,6 @@ line-height: 1rem; } -[data-start-header-key] { - white-space: nowrap; -} - -[data-start-header-value] { - white-space: nowrap; +[data-start-headers-viewer] > [data-start-property] > *:first-child { + text-transform: capitalize; } diff --git a/packages/start/src/shared/server-function-inspector/HeadersViewer.tsx b/packages/start/src/shared/server-function-inspector/HeadersViewer.tsx index 9465043c7..bff549c7d 100644 --- a/packages/start/src/shared/server-function-inspector/HeadersViewer.tsx +++ b/packages/start/src/shared/server-function-inspector/HeadersViewer.tsx @@ -1,7 +1,8 @@ -import { Properties } from "../ui/Properties.tsx"; -import { Text } from "../ui/Text.tsx"; +import { For } from "solid-js"; +import { PropertySeparator, SerovalValue } from "./SerovalValue.tsx"; import './HeadersViewer.css'; +import { Text } from "../ui/Text.tsx"; interface HeadersViewerProps { headers: Headers; @@ -9,12 +10,16 @@ interface HeadersViewerProps { export function HeadersViewer(props: HeadersViewerProps) { return ( -
- {key}:} - renderValue = {(value) => {value}} - /> +
+ + {([key, value]) => ( +
+ {key} + + +
+ )} +
); -} \ No newline at end of file +} diff --git a/packages/start/src/shared/server-function-inspector/SerovalValue.tsx b/packages/start/src/shared/server-function-inspector/SerovalValue.tsx index 35d5c3027..ed5a2429a 100644 --- a/packages/start/src/shared/server-function-inspector/SerovalValue.tsx +++ b/packages/start/src/shared/server-function-inspector/SerovalValue.tsx @@ -12,3 +12,7 @@ export function SerovalValue(props: SerovalValueProps) { ); } + +export function PropertySeparator() { + return :; +} diff --git a/packages/start/src/shared/server-function-inspector/SerovalViewer.css b/packages/start/src/shared/server-function-inspector/SerovalViewer.css index c6d0b2577..5bfe25533 100644 --- a/packages/start/src/shared/server-function-inspector/SerovalViewer.css +++ b/packages/start/src/shared/server-function-inspector/SerovalViewer.css @@ -69,15 +69,3 @@ height: 1rem; } -[data-start-seroval-properties] { - display: flex; - flex-direction: column; - gap: 0.25rem; -} - -[data-start-seroval-property] { - display: flex; - flex-direction: row; - align-items: center; - flex-wrap: nowrap; -} diff --git a/packages/start/src/shared/server-function-inspector/SerovalViewer.tsx b/packages/start/src/shared/server-function-inspector/SerovalViewer.tsx index ac6979721..038d2f483 100644 --- a/packages/start/src/shared/server-function-inspector/SerovalViewer.tsx +++ b/packages/start/src/shared/server-function-inspector/SerovalViewer.tsx @@ -11,10 +11,9 @@ import { import { SerovalChunkReader } from "../../server/serialization.ts"; import { Badge } from "../ui/Badge.tsx"; import { Cascade, CascadeOption } from "../ui/Cascade.tsx"; -import { PropertySeparator } from "../ui/Properties.tsx"; import { Section } from "../ui/Section.tsx"; import { HexViewer } from "./HexViewer.tsx"; -import { SerovalValue } from "./SerovalValue.tsx"; +import { SerovalValue, PropertySeparator } from "./SerovalValue.tsx"; import "./SerovalViewer.css"; @@ -483,7 +482,7 @@ function renderSerovalNode( return ( <>
-
+
@@ -491,13 +490,13 @@ function renderSerovalNode(
- data-start-seroval-properties + data-start-properties defaultValue={undefined} onChange={onSelect} > [index, node] as const)}> {([key, value]) => ( -
+
{renderSerovalNode(ctx, value, onSelect, true)} @@ -513,7 +512,7 @@ function renderSerovalNode( return ( <>
-
+
@@ -521,13 +520,13 @@ function renderSerovalNode(
- data-start-seroval-properties + data-start-properties defaultValue={undefined} onChange={onSelect} > - + }> {([key, value]) => ( -
+
{renderSerovalNode(ctx, key, onSelect, true)} {renderSerovalNode(ctx, value, onSelect, true)} @@ -543,12 +542,12 @@ function renderSerovalNode( return ( <>
-
+
-
+
{getObjectFlag(node.o)} @@ -556,13 +555,13 @@ function renderSerovalNode(
- data-start-seroval-properties + data-start-properties defaultValue={undefined} onChange={onSelect} > - [index, node] as const)}> + [index, node] as const)} fallback={}> {([key, value]) => ( -
+
{value === 0 ? ( @@ -584,12 +583,12 @@ function renderSerovalNode( return ( <>
-
+
-
+
{getObjectFlag(node.o)} @@ -597,13 +596,13 @@ function renderSerovalNode(
- data-start-seroval-properties + data-start-properties defaultValue={undefined} onChange={onSelect} > - + }> {([key, value]) => ( -
+
{typeof key === "string" ? ( ) : ( @@ -622,7 +621,7 @@ function renderSerovalNode( case 12: return ( - data-start-seroval-properties + data-start-properties defaultValue={undefined} onChange={onSelect} > @@ -636,7 +635,7 @@ function renderSerovalNode( return ( <>
-
+
@@ -646,13 +645,13 @@ function renderSerovalNode( {(current) => (
- data-start-seroval-properties + data-start-properties defaultValue={undefined} onChange={onSelect} > - + }> {([key, value]) => ( -
+
{typeof key === "string" ? ( ) : ( @@ -690,12 +689,12 @@ function renderSerovalNode( return ( <>
-
+
-
+
@@ -703,7 +702,7 @@ function renderSerovalNode(
- data-start-seroval-properties + data-start-properties defaultValue={undefined} onChange={onSelect} > @@ -716,7 +715,7 @@ function renderSerovalNode( case 21: return ( - data-start-seroval-properties + data-start-properties defaultValue={undefined} onChange={onSelect} > @@ -732,16 +731,16 @@ function renderSerovalNode( const status = result.t === 23 ? "success" : ("failure" as const); return ( - data-start-seroval-properties + data-start-properties defaultValue={undefined} onChange={onSelect} > -
+
{status}
- + {renderSerovalNode(ctx, result.a[1], onSelect, true)} @@ -758,7 +757,7 @@ function renderSerovalNode( return ( <>
-
+
@@ -766,13 +765,13 @@ function renderSerovalNode(
- data-start-seroval-properties + data-start-properties defaultValue={undefined} onChange={onSelect} > - + }> {([key, value]) => ( -
+
{renderSerovalNode(ctx, value, onSelect, true)} @@ -803,13 +802,13 @@ function renderSerovalNode( const result = ctx.getStream(node.i) || []; return ( - data-start-seroval-properties + data-start-properties defaultValue={undefined} onChange={onSelect} > - + }> {(current) => ( -
+
{renderSerovalNode(ctx, current.f, onSelect, true)} @@ -824,13 +823,13 @@ function renderSerovalNode( case 35: return ( - data-start-seroval-properties + data-start-properties defaultValue={undefined} onChange={onSelect} > - + }> {(current, index) => ( -
+
{renderSerovalNode(ctx, current, onSelect, true)} @@ -862,10 +861,10 @@ function SerovalNodeRenderer(props: SerovalNodeRendererProps): JSX.Element { <>
+ {getNodeType(props.node)} {props.node.i != null && ( {`id: ${props.node.i}`} )} - {getNodeType(props.node)}
{renderSerovalNode(props, props.node, onSelect)} diff --git a/packages/start/src/shared/server-function-inspector/URLSearchParamsViewer.tsx b/packages/start/src/shared/server-function-inspector/URLSearchParamsViewer.tsx index 59f31dce1..80e7dd509 100644 --- a/packages/start/src/shared/server-function-inspector/URLSearchParamsViewer.tsx +++ b/packages/start/src/shared/server-function-inspector/URLSearchParamsViewer.tsx @@ -1,7 +1,6 @@ import { createResource, For, type JSX, Show, Suspense } from 'solid-js'; -import { PropertySeparator } from '../ui/Properties.tsx'; import { Section } from '../ui/Section'; -import { SerovalValue } from './SerovalValue.tsx'; +import { SerovalValue, PropertySeparator } from './SerovalValue.tsx'; interface URLSearchParamsViewerInnerProps { source: URLSearchParams; @@ -10,10 +9,10 @@ interface URLSearchParamsViewerInnerProps { function URLSearchParamsViewerInner(props: URLSearchParamsViewerInnerProps): JSX.Element { return (
-
+
{([key, value]) => ( -
+
diff --git a/packages/start/src/shared/server-function-inspector/index.tsx b/packages/start/src/shared/server-function-inspector/index.tsx index 42508f6a7..eca6591d2 100644 --- a/packages/start/src/shared/server-function-inspector/index.tsx +++ b/packages/start/src/shared/server-function-inspector/index.tsx @@ -1,5 +1,6 @@ import { createEffect, + createMemo, createSignal, For, type JSX, @@ -27,6 +28,9 @@ import { } from "./server-function-tracker.ts"; import "./styles.css"; import { URLSearchParamsViewer } from "./URLSearchParamsViewer.tsx"; +import { Text } from "../ui/Text.tsx"; +import Placeholder from "../ui/Placeholder.tsx"; +import { PropertySeparator, SerovalValue } from "./SerovalValue.tsx"; async function getFile(source: Response | Request): Promise { const formData = await source.formData(); @@ -87,24 +91,80 @@ interface RequestViewerProps { request: ServerFunctionRequest; } +function convertRequestToEntries(request: Request) { + return [ + ['Cache', request.cache], + ['Credentials', request.credentials], + ['Destination', request.destination], + ['Integrity', request.integrity], + ['Keep Alive', request.keepalive], + ['Mode', request.mode], + ['Redirect', request.redirect], + ['Referrer', request.referrer], + ['Referrer Policy', request.referrerPolicy], + ['URL', request.url], + ]; +} + function RequestViewer(props: RequestViewerProps): JSX.Element { return ( - + +
+ + {([key, value]) => ( +
+ {key} + + +
+ )} +
+
); } interface ResponseViewerProps { + request: ServerFunctionRequest; response?: ServerFunctionResponse; } +function convertResponseToEntries(response: Response) { + return [ + ['OK', response.ok], + ['Redirected', response.redirected], + ['Status', response.status], + ['Status Text', response.statusText], + ['Type', response.type], + ['URL', response.url], + ]; +} + function ResponseViewer(props: ResponseViewerProps): JSX.Element { return ( - + {(instance) => ( - + <> +
+ + {([key, value]) => ( +
+ {key} + + +
+ )} +
+
+ Timing + + +
+
+ + )}
@@ -148,11 +208,21 @@ function ServerFunctionInstanceViewer(
- + ); } +function EmptyServerFunctions(): JSX.Element { + return ( + + + No server function calls detected. + + + ); +} + export function ServerFunctionInspector(): JSX.Element { const [currentInstance, setCurrentInstance] = createSignal(); @@ -180,6 +250,8 @@ export function ServerFunctionInspector(): JSX.Element { (window as any).__START__SERVER_FN__ = setIsOpen; }); + const keys = createMemo(() => Object.keys(store.instances)); + return ( @@ -189,41 +261,43 @@ export function ServerFunctionInspector(): JSX.Element {
{/* list of calls */}
- + }> + +
{/* request/response viewer */} diff --git a/packages/start/src/shared/server-function-inspector/styles.css b/packages/start/src/shared/server-function-inspector/styles.css index 437a0c4e7..b1769566e 100644 --- a/packages/start/src/shared/server-function-inspector/styles.css +++ b/packages/start/src/shared/server-function-inspector/styles.css @@ -53,10 +53,6 @@ justify-content: space-between; } -.server-function-instance-tab-panel { - overflow: auto; -} - [data-start-headers] { display: flex; flex-direction: column; @@ -67,9 +63,17 @@ font-weight: 500; } -.server-function-instance-tab-panel { +[data-start-properties] { display: flex; flex-direction: column; - gap: 1rem; + gap: 0.25rem; } +[data-start-property] { + display: flex; + flex-direction: row; + align-items: center; + flex-wrap: nowrap; + + gap: 0.25rem; +} \ No newline at end of file diff --git a/packages/start/src/shared/ui/Placeholder.css b/packages/start/src/shared/ui/Placeholder.css new file mode 100644 index 000000000..178046658 --- /dev/null +++ b/packages/start/src/shared/ui/Placeholder.css @@ -0,0 +1,12 @@ +[data-start-placeholder] { + display: flex; + align-items: center; + justify-content: center; + + width: 100%; + height: 100%; +} + +[data-start-placeholder] > span { + text-align: center; +} \ No newline at end of file diff --git a/packages/start/src/shared/ui/Placeholder.tsx b/packages/start/src/shared/ui/Placeholder.tsx new file mode 100644 index 000000000..b2c28be66 --- /dev/null +++ b/packages/start/src/shared/ui/Placeholder.tsx @@ -0,0 +1,14 @@ +import type { JSX } from 'solid-js'; +import './Placeholder.css'; + +export interface PlaceholderProps { + children?: JSX.Element; +} + +export default function Placeholder(props: PlaceholderProps): JSX.Element { + return ( +
+ {props.children} +
+ ); +} \ No newline at end of file diff --git a/packages/start/src/shared/ui/Properties.css b/packages/start/src/shared/ui/Properties.css deleted file mode 100644 index 5cb7343fc..000000000 --- a/packages/start/src/shared/ui/Properties.css +++ /dev/null @@ -1,15 +0,0 @@ -[data-start-properties] { - display: flex; - flex-direction: column; - gap: 0.25rem; - - overflow: auto; -} - -[data-start-property] { - display: flex; - flex-direction: row; - gap: 0.25rem; - - align-items: center; -} diff --git a/packages/start/src/shared/ui/Properties.tsx b/packages/start/src/shared/ui/Properties.tsx deleted file mode 100644 index 54cd223d4..000000000 --- a/packages/start/src/shared/ui/Properties.tsx +++ /dev/null @@ -1,33 +0,0 @@ -import { For, type JSX } from 'solid-js'; -import { Text } from './Text.tsx'; -import './Properties.css'; - -export type PropertyEntry = [key: unknown, value: unknown]; - -export interface PropertiesProps { - entries: T[]; - - renderKey: (key: T[0]) => JSX.Element; - renderValue: (value: T[1]) => JSX.Element; -} - -export function Properties( - props: PropertiesProps, -): JSX.Element { - return ( -
- - {(entry) => ( -
- {props.renderKey(entry[0])} - {props.renderValue(entry[1])} -
- )} -
-
- ); -} - -export function PropertySeparator() { - return :; -} diff --git a/packages/start/src/shared/ui/Section.css b/packages/start/src/shared/ui/Section.css index 54ca046d6..27e368b15 100644 --- a/packages/start/src/shared/ui/Section.css +++ b/packages/start/src/shared/ui/Section.css @@ -1,19 +1,19 @@ [data-start-section] { display: flex; flex-direction: column; - gap: 0.25rem; } [data-start-section-title] { - + text-transform: uppercase; } [data-start-section-content] { + margin-left: 0.5rem; padding-left: 0.5rem; border-left: 1px oklch(70.7% 0.165 254.624) solid; - width: calc(100% - 0.5rem); + width: calc(100% - 1rem); overflow: auto; } diff --git a/packages/start/src/shared/ui/Section.tsx b/packages/start/src/shared/ui/Section.tsx index 3c532132f..0524ed4f0 100644 --- a/packages/start/src/shared/ui/Section.tsx +++ b/packages/start/src/shared/ui/Section.tsx @@ -12,7 +12,7 @@ export interface SectionProps { export function Section(props: SectionProps): JSX.Element { return (
- + {props.title}
diff --git a/packages/start/src/shared/ui/Tabs.css b/packages/start/src/shared/ui/Tabs.css index 18afd6906..36ed6bb25 100644 --- a/packages/start/src/shared/ui/Tabs.css +++ b/packages/start/src/shared/ui/Tabs.css @@ -45,4 +45,10 @@ background-color: rgb(249 250 251); border-radius: 0.5rem; height: 100%; + + overflow: auto; + + display: flex; + flex-direction: column; + gap: 1rem; } From b2a40edb58dfa23865e675cfbb858fe9199f7a81 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Thu, 26 Feb 2026 11:23:34 +0800 Subject: [PATCH 13/25] fix paths --- .../start/src/shared/server-function-inspector/BlobViewer.tsx | 4 ++-- .../src/shared/server-function-inspector/FormDataViewer.tsx | 4 ++-- .../server-function-inspector/URLSearchParamsViewer.tsx | 4 ++-- 3 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/start/src/shared/server-function-inspector/BlobViewer.tsx b/packages/start/src/shared/server-function-inspector/BlobViewer.tsx index 1f6532ba8..68c8c4973 100644 --- a/packages/start/src/shared/server-function-inspector/BlobViewer.tsx +++ b/packages/start/src/shared/server-function-inspector/BlobViewer.tsx @@ -1,7 +1,7 @@ import { createMemo, createResource, type JSX, onCleanup, Show, Suspense } from 'solid-js'; -import { Badge } from "../ui/Badge"; -import Button from "../ui/Button"; +import { Badge } from "../ui/Badge.tsx"; +import Button from "../ui/Button.tsx"; import './BlobViewer.css'; diff --git a/packages/start/src/shared/server-function-inspector/FormDataViewer.tsx b/packages/start/src/shared/server-function-inspector/FormDataViewer.tsx index 1efd3f83c..6840f5132 100644 --- a/packages/start/src/shared/server-function-inspector/FormDataViewer.tsx +++ b/packages/start/src/shared/server-function-inspector/FormDataViewer.tsx @@ -1,7 +1,7 @@ import { createResource, For, type JSX, Show, Suspense } from 'solid-js'; -import { Section } from '../ui/Section'; +import { Section } from '../ui/Section.tsx'; import { BlobViewer } from './BlobViewer.tsx'; -import { SerovalValue, PropertySeparator } from './SerovalValue.tsx'; +import { PropertySeparator, SerovalValue } from './SerovalValue.tsx'; interface FormDataViewerInnerProps { source: FormData; diff --git a/packages/start/src/shared/server-function-inspector/URLSearchParamsViewer.tsx b/packages/start/src/shared/server-function-inspector/URLSearchParamsViewer.tsx index 80e7dd509..b0d6a3f9d 100644 --- a/packages/start/src/shared/server-function-inspector/URLSearchParamsViewer.tsx +++ b/packages/start/src/shared/server-function-inspector/URLSearchParamsViewer.tsx @@ -1,6 +1,6 @@ import { createResource, For, type JSX, Show, Suspense } from 'solid-js'; -import { Section } from '../ui/Section'; -import { SerovalValue, PropertySeparator } from './SerovalValue.tsx'; +import { Section } from '../ui/Section.tsx'; +import { PropertySeparator, SerovalValue } from './SerovalValue.tsx'; interface URLSearchParamsViewerInnerProps { source: URLSearchParams; From c456cbe998f5be0e83de3aebabfd191aada72573 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Tue, 7 Jul 2026 10:43:12 +0800 Subject: [PATCH 14/25] Fix paths --- .../src/shared/server-function-inspector/SerovalViewer.tsx | 2 +- .../start/src/shared/server-function-inspector/index.tsx | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/packages/start/src/shared/server-function-inspector/SerovalViewer.tsx b/packages/start/src/shared/server-function-inspector/SerovalViewer.tsx index 038d2f483..2b72cd6eb 100644 --- a/packages/start/src/shared/server-function-inspector/SerovalViewer.tsx +++ b/packages/start/src/shared/server-function-inspector/SerovalViewer.tsx @@ -8,7 +8,6 @@ import { splitProps, } from "solid-js"; -import { SerovalChunkReader } from "../../server/serialization.ts"; import { Badge } from "../ui/Badge.tsx"; import { Cascade, CascadeOption } from "../ui/Cascade.tsx"; import { Section } from "../ui/Section.tsx"; @@ -16,6 +15,7 @@ import { HexViewer } from "./HexViewer.tsx"; import { SerovalValue, PropertySeparator } from "./SerovalValue.tsx"; import "./SerovalViewer.css"; +import { SerovalChunkReader } from "../../fns/serialization.ts"; function LinkIcon( props: JSX.IntrinsicElements["svg"] & { title: string }, diff --git a/packages/start/src/shared/server-function-inspector/index.tsx b/packages/start/src/shared/server-function-inspector/index.tsx index eca6591d2..c760af55c 100644 --- a/packages/start/src/shared/server-function-inspector/index.tsx +++ b/packages/start/src/shared/server-function-inspector/index.tsx @@ -9,7 +9,7 @@ import { } from "solid-js"; import { createStore } from "solid-js/store"; import { Portal } from "solid-js/web"; -import { BODY_FORMAL_FILE, BODY_FORMAT_KEY, BodyFormat } from "../../server/server-functions-shared.ts"; +import { BODY_FORMAL_FILE, BODY_FORMAT_KEY, BodyFormat } from "../../fns/shared.ts"; import { Badge } from "../ui/Badge.tsx"; import Button from "../ui/Button.tsx"; import { Dialog, DialogOverlay, DialogPanel } from "../ui/Dialog.tsx"; @@ -27,10 +27,10 @@ import { type ServerFunctionResponse, } from "./server-function-tracker.ts"; import "./styles.css"; -import { URLSearchParamsViewer } from "./URLSearchParamsViewer.tsx"; -import { Text } from "../ui/Text.tsx"; import Placeholder from "../ui/Placeholder.tsx"; +import { Text } from "../ui/Text.tsx"; import { PropertySeparator, SerovalValue } from "./SerovalValue.tsx"; +import { URLSearchParamsViewer } from "./URLSearchParamsViewer.tsx"; async function getFile(source: Response | Request): Promise { const formData = await source.formData(); From 1f602ecce7318f7bb65e907416d7e97b034ec4f7 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Tue, 7 Jul 2026 14:54:18 +0800 Subject: [PATCH 15/25] move dev overlay --- .../{dev-overlay => dev-toolbar/error-viewer}/CodeView.tsx | 0 .../error-viewer}/DevOverlayDialog.tsx | 0 .../error-viewer/create-stack-frame.ts} | 0 .../{dev-overlay => dev-toolbar/error-viewer}/download.ts | 0 .../shared/{dev-overlay => dev-toolbar/error-viewer}/env.d.ts | 0 .../{dev-overlay => dev-toolbar/error-viewer}/get-source-map.ts | 0 .../shared/{dev-overlay => dev-toolbar/error-viewer}/icons.tsx | 2 +- .../shared/{dev-overlay => dev-toolbar/error-viewer}/index.tsx | 0 .../shared/{dev-overlay => dev-toolbar/error-viewer}/styles.css | 0 9 files changed, 1 insertion(+), 1 deletion(-) rename packages/start/src/shared/{dev-overlay => dev-toolbar/error-viewer}/CodeView.tsx (100%) rename packages/start/src/shared/{dev-overlay => dev-toolbar/error-viewer}/DevOverlayDialog.tsx (100%) rename packages/start/src/shared/{dev-overlay/createStackFrame.ts => dev-toolbar/error-viewer/create-stack-frame.ts} (100%) rename packages/start/src/shared/{dev-overlay => dev-toolbar/error-viewer}/download.ts (100%) rename packages/start/src/shared/{dev-overlay => dev-toolbar/error-viewer}/env.d.ts (100%) rename packages/start/src/shared/{dev-overlay => dev-toolbar/error-viewer}/get-source-map.ts (100%) rename packages/start/src/shared/{dev-overlay => dev-toolbar/error-viewer}/icons.tsx (99%) rename packages/start/src/shared/{dev-overlay => dev-toolbar/error-viewer}/index.tsx (100%) rename packages/start/src/shared/{dev-overlay => dev-toolbar/error-viewer}/styles.css (100%) diff --git a/packages/start/src/shared/dev-overlay/CodeView.tsx b/packages/start/src/shared/dev-toolbar/error-viewer/CodeView.tsx similarity index 100% rename from packages/start/src/shared/dev-overlay/CodeView.tsx rename to packages/start/src/shared/dev-toolbar/error-viewer/CodeView.tsx diff --git a/packages/start/src/shared/dev-overlay/DevOverlayDialog.tsx b/packages/start/src/shared/dev-toolbar/error-viewer/DevOverlayDialog.tsx similarity index 100% rename from packages/start/src/shared/dev-overlay/DevOverlayDialog.tsx rename to packages/start/src/shared/dev-toolbar/error-viewer/DevOverlayDialog.tsx diff --git a/packages/start/src/shared/dev-overlay/createStackFrame.ts b/packages/start/src/shared/dev-toolbar/error-viewer/create-stack-frame.ts similarity index 100% rename from packages/start/src/shared/dev-overlay/createStackFrame.ts rename to packages/start/src/shared/dev-toolbar/error-viewer/create-stack-frame.ts diff --git a/packages/start/src/shared/dev-overlay/download.ts b/packages/start/src/shared/dev-toolbar/error-viewer/download.ts similarity index 100% rename from packages/start/src/shared/dev-overlay/download.ts rename to packages/start/src/shared/dev-toolbar/error-viewer/download.ts diff --git a/packages/start/src/shared/dev-overlay/env.d.ts b/packages/start/src/shared/dev-toolbar/error-viewer/env.d.ts similarity index 100% rename from packages/start/src/shared/dev-overlay/env.d.ts rename to packages/start/src/shared/dev-toolbar/error-viewer/env.d.ts diff --git a/packages/start/src/shared/dev-overlay/get-source-map.ts b/packages/start/src/shared/dev-toolbar/error-viewer/get-source-map.ts similarity index 100% rename from packages/start/src/shared/dev-overlay/get-source-map.ts rename to packages/start/src/shared/dev-toolbar/error-viewer/get-source-map.ts diff --git a/packages/start/src/shared/dev-overlay/icons.tsx b/packages/start/src/shared/dev-toolbar/error-viewer/icons.tsx similarity index 99% rename from packages/start/src/shared/dev-overlay/icons.tsx rename to packages/start/src/shared/dev-toolbar/error-viewer/icons.tsx index dd5598b3e..b1b9a9caa 100644 --- a/packages/start/src/shared/dev-overlay/icons.tsx +++ b/packages/start/src/shared/dev-toolbar/error-viewer/icons.tsx @@ -176,7 +176,7 @@ export function SolidStartIcon( {props.title} - + Date: Tue, 7 Jul 2026 19:42:21 +0800 Subject: [PATCH 16/25] Update icons.tsx --- .../start/src/shared/dev-toolbar/{error-viewer => }/icons.tsx | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/start/src/shared/dev-toolbar/{error-viewer => }/icons.tsx (100%) diff --git a/packages/start/src/shared/dev-toolbar/error-viewer/icons.tsx b/packages/start/src/shared/dev-toolbar/icons.tsx similarity index 100% rename from packages/start/src/shared/dev-toolbar/error-viewer/icons.tsx rename to packages/start/src/shared/dev-toolbar/icons.tsx From bd9171675a80e4a28f2942eb91905bfd19513ca6 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Wed, 8 Jul 2026 03:10:57 +0800 Subject: [PATCH 17/25] Update terracotta --- packages/start/package.json | 2 +- pnpm-lock.yaml | 10 +++++----- 2 files changed, 6 insertions(+), 6 deletions(-) diff --git a/packages/start/package.json b/packages/start/package.json index 353a1d1d9..8c0f6ae3f 100644 --- a/packages/start/package.json +++ b/packages/start/package.json @@ -76,7 +76,7 @@ "solid-js": "^1.9.11", "source-map-js": "^1.2.1", "srvx": "^0.11.22", - "terracotta": "^1.1.0", + "terracotta": "^1.1.1", "vite-plugin-solid": "^2.11.11" }, "engines": { diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index befb35e93..1b66b838e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -387,8 +387,8 @@ importers: specifier: ^0.11.22 version: 0.11.22 terracotta: - specifier: ^1.1.0 - version: 1.1.0(solid-js@1.9.11) + specifier: ^1.1.1 + version: 1.1.1(solid-js@1.9.11) vite-plugin-solid: specifier: ^2.11.11 version: 2.11.11(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@8.1.4(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)) @@ -3174,8 +3174,8 @@ packages: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} - terracotta@1.1.0: - resolution: {integrity: sha512-kfQciWUBUBgYkXu7gh3CK3FAJng/iqZslAaY08C+k1Hdx17aVEpcFFb/WPaysxAfcupNH3y53s/pc53xxZauww==} + terracotta@1.1.1: + resolution: {integrity: sha512-Sa6wnvkGMFmPq8N/wQb2aKkIW2eXH0LJvUOEk6QZLBEjqy+LsbzAOpDuqEfOmfA/hexssE6eQve2i1IIOeOh4g==} engines: {node: '>=10'} peerDependencies: solid-js: ^1.8 @@ -6235,7 +6235,7 @@ snapshots: term-size@2.2.1: {} - terracotta@1.1.0(solid-js@1.9.11): + terracotta@1.1.1(solid-js@1.9.11): dependencies: solid-js: 1.9.11 solid-use: 0.9.1(solid-js@1.9.11) From ae98bfdcb7b34f50a1536996bb1456e2634c8d01 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Wed, 8 Jul 2026 14:37:56 +0800 Subject: [PATCH 18/25] Move server-function-inspector --- .../functions}/BlobViewer.css | 0 .../functions}/BlobViewer.tsx | 0 .../functions}/FormDataViewer.tsx | 0 .../functions}/HeadersViewer.css | 0 .../functions}/HeadersViewer.tsx | 0 .../functions}/HexViewer.css | 0 .../functions}/HexViewer.tsx | 0 .../functions}/SerovalValue.css | 0 .../functions}/SerovalValue.tsx | 0 .../functions}/SerovalViewer.css | 0 .../functions}/SerovalViewer.tsx | 0 .../functions}/URLSearchParamsViewer.tsx | 0 .../functions}/index.tsx | 0 .../functions}/server-function-tracker.ts | 0 .../functions}/styles.css | 0 15 files changed, 0 insertions(+), 0 deletions(-) rename packages/start/src/shared/{server-function-inspector => dev-toolbar/functions}/BlobViewer.css (100%) rename packages/start/src/shared/{server-function-inspector => dev-toolbar/functions}/BlobViewer.tsx (100%) rename packages/start/src/shared/{server-function-inspector => dev-toolbar/functions}/FormDataViewer.tsx (100%) rename packages/start/src/shared/{server-function-inspector => dev-toolbar/functions}/HeadersViewer.css (100%) rename packages/start/src/shared/{server-function-inspector => dev-toolbar/functions}/HeadersViewer.tsx (100%) rename packages/start/src/shared/{server-function-inspector => dev-toolbar/functions}/HexViewer.css (100%) rename packages/start/src/shared/{server-function-inspector => dev-toolbar/functions}/HexViewer.tsx (100%) rename packages/start/src/shared/{server-function-inspector => dev-toolbar/functions}/SerovalValue.css (100%) rename packages/start/src/shared/{server-function-inspector => dev-toolbar/functions}/SerovalValue.tsx (100%) rename packages/start/src/shared/{server-function-inspector => dev-toolbar/functions}/SerovalViewer.css (100%) rename packages/start/src/shared/{server-function-inspector => dev-toolbar/functions}/SerovalViewer.tsx (100%) rename packages/start/src/shared/{server-function-inspector => dev-toolbar/functions}/URLSearchParamsViewer.tsx (100%) rename packages/start/src/shared/{server-function-inspector => dev-toolbar/functions}/index.tsx (100%) rename packages/start/src/shared/{server-function-inspector => dev-toolbar/functions}/server-function-tracker.ts (100%) rename packages/start/src/shared/{server-function-inspector => dev-toolbar/functions}/styles.css (100%) diff --git a/packages/start/src/shared/server-function-inspector/BlobViewer.css b/packages/start/src/shared/dev-toolbar/functions/BlobViewer.css similarity index 100% rename from packages/start/src/shared/server-function-inspector/BlobViewer.css rename to packages/start/src/shared/dev-toolbar/functions/BlobViewer.css diff --git a/packages/start/src/shared/server-function-inspector/BlobViewer.tsx b/packages/start/src/shared/dev-toolbar/functions/BlobViewer.tsx similarity index 100% rename from packages/start/src/shared/server-function-inspector/BlobViewer.tsx rename to packages/start/src/shared/dev-toolbar/functions/BlobViewer.tsx diff --git a/packages/start/src/shared/server-function-inspector/FormDataViewer.tsx b/packages/start/src/shared/dev-toolbar/functions/FormDataViewer.tsx similarity index 100% rename from packages/start/src/shared/server-function-inspector/FormDataViewer.tsx rename to packages/start/src/shared/dev-toolbar/functions/FormDataViewer.tsx diff --git a/packages/start/src/shared/server-function-inspector/HeadersViewer.css b/packages/start/src/shared/dev-toolbar/functions/HeadersViewer.css similarity index 100% rename from packages/start/src/shared/server-function-inspector/HeadersViewer.css rename to packages/start/src/shared/dev-toolbar/functions/HeadersViewer.css diff --git a/packages/start/src/shared/server-function-inspector/HeadersViewer.tsx b/packages/start/src/shared/dev-toolbar/functions/HeadersViewer.tsx similarity index 100% rename from packages/start/src/shared/server-function-inspector/HeadersViewer.tsx rename to packages/start/src/shared/dev-toolbar/functions/HeadersViewer.tsx diff --git a/packages/start/src/shared/server-function-inspector/HexViewer.css b/packages/start/src/shared/dev-toolbar/functions/HexViewer.css similarity index 100% rename from packages/start/src/shared/server-function-inspector/HexViewer.css rename to packages/start/src/shared/dev-toolbar/functions/HexViewer.css diff --git a/packages/start/src/shared/server-function-inspector/HexViewer.tsx b/packages/start/src/shared/dev-toolbar/functions/HexViewer.tsx similarity index 100% rename from packages/start/src/shared/server-function-inspector/HexViewer.tsx rename to packages/start/src/shared/dev-toolbar/functions/HexViewer.tsx diff --git a/packages/start/src/shared/server-function-inspector/SerovalValue.css b/packages/start/src/shared/dev-toolbar/functions/SerovalValue.css similarity index 100% rename from packages/start/src/shared/server-function-inspector/SerovalValue.css rename to packages/start/src/shared/dev-toolbar/functions/SerovalValue.css diff --git a/packages/start/src/shared/server-function-inspector/SerovalValue.tsx b/packages/start/src/shared/dev-toolbar/functions/SerovalValue.tsx similarity index 100% rename from packages/start/src/shared/server-function-inspector/SerovalValue.tsx rename to packages/start/src/shared/dev-toolbar/functions/SerovalValue.tsx diff --git a/packages/start/src/shared/server-function-inspector/SerovalViewer.css b/packages/start/src/shared/dev-toolbar/functions/SerovalViewer.css similarity index 100% rename from packages/start/src/shared/server-function-inspector/SerovalViewer.css rename to packages/start/src/shared/dev-toolbar/functions/SerovalViewer.css diff --git a/packages/start/src/shared/server-function-inspector/SerovalViewer.tsx b/packages/start/src/shared/dev-toolbar/functions/SerovalViewer.tsx similarity index 100% rename from packages/start/src/shared/server-function-inspector/SerovalViewer.tsx rename to packages/start/src/shared/dev-toolbar/functions/SerovalViewer.tsx diff --git a/packages/start/src/shared/server-function-inspector/URLSearchParamsViewer.tsx b/packages/start/src/shared/dev-toolbar/functions/URLSearchParamsViewer.tsx similarity index 100% rename from packages/start/src/shared/server-function-inspector/URLSearchParamsViewer.tsx rename to packages/start/src/shared/dev-toolbar/functions/URLSearchParamsViewer.tsx diff --git a/packages/start/src/shared/server-function-inspector/index.tsx b/packages/start/src/shared/dev-toolbar/functions/index.tsx similarity index 100% rename from packages/start/src/shared/server-function-inspector/index.tsx rename to packages/start/src/shared/dev-toolbar/functions/index.tsx diff --git a/packages/start/src/shared/server-function-inspector/server-function-tracker.ts b/packages/start/src/shared/dev-toolbar/functions/server-function-tracker.ts similarity index 100% rename from packages/start/src/shared/server-function-inspector/server-function-tracker.ts rename to packages/start/src/shared/dev-toolbar/functions/server-function-tracker.ts diff --git a/packages/start/src/shared/server-function-inspector/styles.css b/packages/start/src/shared/dev-toolbar/functions/styles.css similarity index 100% rename from packages/start/src/shared/server-function-inspector/styles.css rename to packages/start/src/shared/dev-toolbar/functions/styles.css From 3167556c2aa884ec194763ef0e66fcf70bc253ec Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Wed, 8 Jul 2026 15:06:00 +0800 Subject: [PATCH 19/25] Update tracker.ts --- .../functions/{server-function-tracker.ts => tracker.ts} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename packages/start/src/shared/dev-toolbar/functions/{server-function-tracker.ts => tracker.ts} (100%) diff --git a/packages/start/src/shared/dev-toolbar/functions/server-function-tracker.ts b/packages/start/src/shared/dev-toolbar/functions/tracker.ts similarity index 100% rename from packages/start/src/shared/dev-toolbar/functions/server-function-tracker.ts rename to packages/start/src/shared/dev-toolbar/functions/tracker.ts From 14d8fbc431825231ef4b90646dc253e81c4bf0d5 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Fri, 17 Jul 2026 04:14:40 +0800 Subject: [PATCH 20/25] initial rework for the dev toolbar --- .../routes/treeshaking/(no-side-effects).tsx | 4 + packages/start/src/fns/client.ts | 26 +- packages/start/src/shared/ErrorBoundary.tsx | 60 ++- .../dev-toolbar/error-viewer/CodeView.tsx | 6 +- .../error-viewer/DevOverlayDialog.tsx | 337 ----------------- .../shared/dev-toolbar/error-viewer/index.tsx | 344 +++++++++++++++--- .../dev-toolbar/error-viewer/styles.css | 169 +++------ .../dev-toolbar/functions/BlobViewer.tsx | 4 +- .../dev-toolbar/functions/FormDataViewer.tsx | 2 +- .../dev-toolbar/functions/HeadersViewer.tsx | 2 +- .../dev-toolbar/functions/HexViewer.tsx | 2 +- .../dev-toolbar/functions/SerovalValue.tsx | 2 +- .../dev-toolbar/functions/SerovalViewer.css | 2 +- .../dev-toolbar/functions/SerovalViewer.tsx | 89 ++--- .../functions/URLSearchParamsViewer.tsx | 2 +- .../shared/dev-toolbar/functions/index.tsx | 311 ++++++++-------- .../shared/dev-toolbar/functions/styles.css | 85 +++-- .../start/src/shared/dev-toolbar/icons.tsx | 53 +++ .../start/src/shared/dev-toolbar/index.css | 87 +++++ .../start/src/shared/dev-toolbar/index.tsx | 242 ++++++++++++ packages/start/src/shared/ui/Button.css | 8 +- packages/start/src/shared/ui/IconButton.css | 24 +- packages/start/src/shared/ui/Select.css | 7 +- packages/start/src/shared/ui/Tabs.css | 14 +- 24 files changed, 1035 insertions(+), 847 deletions(-) delete mode 100644 packages/start/src/shared/dev-toolbar/error-viewer/DevOverlayDialog.tsx create mode 100644 packages/start/src/shared/dev-toolbar/index.css create mode 100644 packages/start/src/shared/dev-toolbar/index.tsx diff --git a/apps/tests/src/routes/treeshaking/(no-side-effects).tsx b/apps/tests/src/routes/treeshaking/(no-side-effects).tsx index 40cff9649..1bbeaf9ff 100644 --- a/apps/tests/src/routes/treeshaking/(no-side-effects).tsx +++ b/apps/tests/src/routes/treeshaking/(no-side-effects).tsx @@ -1,4 +1,5 @@ import { createAsync } from "@solidjs/router"; +import { createEffect } from "solid-js"; const a = "myTreeshakingTestUniqueString1"; @@ -14,5 +15,8 @@ async function getGetA() { export default function Treeshaking() { const s = createAsync(() => getGetA()); + createEffect(() => { + throw new Error('something') + }) return

hello: {s()}

; } diff --git a/packages/start/src/fns/client.ts b/packages/start/src/fns/client.ts index 0e49fc6dc..690bc811d 100644 --- a/packages/start/src/fns/client.ts +++ b/packages/start/src/fns/client.ts @@ -1,27 +1,14 @@ import { type Component } from "solid-js"; -import { - pushRequest, - pushResponse, -} from "../shared/server-function-inspector/server-function-tracker"; +import { pushRequest, pushResponse } from "../shared/dev-toolbar/functions/tracker.ts"; import { // serializeToJSONStream, serializeToJSONString, } from "./serialization.ts"; -import { - BODY_FORMAT_KEY, - BodyFormat, - extractBody, - getHeadersAndBody, -} from "./shared.ts"; +import { BODY_FORMAT_KEY, BodyFormat, extractBody, getHeadersAndBody } from "./shared.ts"; let INSTANCE = 0; -async function createRequest( - base: string, - id: string, - instance: string, - options: RequestInit, -) { +async function createRequest(base: string, id: string, instance: string, options: RequestInit) { const request = new Request(base, { method: "POST", ...options, @@ -32,6 +19,7 @@ async function createRequest( }, }); if (import.meta.env.DEV) { + console.log(pushRequest); pushRequest(id, instance, request.clone()); } const response = await fetch(request); @@ -117,8 +105,7 @@ export function cloneServerReference(id: string) { let baseURL = import.meta.env.BASE_URL ?? "/"; if (!baseURL.endsWith("/")) baseURL += "/"; - const fn = (...args: any[]) => - fetchServerFunction(`${baseURL}_server`, id, {}, args); + const fn = (...args: any[]) => fetchServerFunction(`${baseURL}_server`, id, {}, args); return new Proxy(fn, { get(target, prop, receiver) { @@ -132,8 +119,7 @@ export function cloneServerReference(id: string) { const url = `${baseURL}_server?id=${encodeURIComponent(id)}`; return (options: RequestInit) => { const fn = async (...args: any[]) => { - const encodeArgs = - options.method && options.method.toUpperCase() === "GET"; + const encodeArgs = options.method && options.method.toUpperCase() === "GET"; return fetchServerFunction( encodeArgs ? url + diff --git a/packages/start/src/shared/ErrorBoundary.tsx b/packages/start/src/shared/ErrorBoundary.tsx index d3dfb783a..7aa8c0df8 100644 --- a/packages/start/src/shared/ErrorBoundary.tsx +++ b/packages/start/src/shared/ErrorBoundary.tsx @@ -1,50 +1,40 @@ // @refresh skip -import { - catchError, - ErrorBoundary as DefaultErrorBoundary, - type ParentProps, -} from "solid-js"; +import { catchError, ErrorBoundary as DefaultErrorBoundary, type ParentProps } from "solid-js"; import { isServer } from "solid-js/web"; -import { DevOverlay } from "./dev-overlay/index.tsx"; +import { DevToolbar } from "./dev-toolbar/index.tsx"; import { HttpStatusCode } from "./HttpStatusCode.ts"; -import { ServerFunctionInspector } from "./server-function-inspector/index.tsx"; export const ErrorBoundary = import.meta.env.DEV && import.meta.env.START_DEV_OVERLAY - ? (props: ParentProps) => ( - - - {props.children} - - ) + ? (props: ParentProps) => {props.children} : (props: ParentProps) => { - const message = isServer - ? "500 | Internal Server Error" - : "Error | Uncaught Client Exception"; - return ( - { - console.error(error); - return ( - <> - - {message} - - - - ); - }} - > - {props.children} - - ); - }; + const message = isServer + ? "500 | Internal Server Error" + : "Error | Uncaught Client Exception"; + return ( + { + console.error(error); + return ( + <> + + {message} + + + + ); + }} + > + {props.children} + + ); + }; export const TopErrorBoundary = (props: ParentProps) => { let isError = false; const res = catchError( () => props.children, - (err) => { + err => { console.error(err); isError = !!err; }, diff --git a/packages/start/src/shared/dev-toolbar/error-viewer/CodeView.tsx b/packages/start/src/shared/dev-toolbar/error-viewer/CodeView.tsx index 45950197f..83673540e 100644 --- a/packages/start/src/shared/dev-toolbar/error-viewer/CodeView.tsx +++ b/packages/start/src/shared/dev-toolbar/error-viewer/CodeView.tsx @@ -72,7 +72,7 @@ export function CodeView(props: CodeViewProps): JSX.Element | null { for (let i = 0, len = lines.length; i < len; i++) { const el = lines[i] as HTMLElement; if (props.line - minLine() - 1 === i) { - el.dataset.startDevOverlayErrorLine = ''; + el.dataset.startErrorViewerErrorLine = ''; } } } @@ -81,9 +81,9 @@ export function CodeView(props: CodeViewProps): JSX.Element | null { return (
); diff --git a/packages/start/src/shared/dev-toolbar/error-viewer/DevOverlayDialog.tsx b/packages/start/src/shared/dev-toolbar/error-viewer/DevOverlayDialog.tsx deleted file mode 100644 index b1c7c80d2..000000000 --- a/packages/start/src/shared/dev-toolbar/error-viewer/DevOverlayDialog.tsx +++ /dev/null @@ -1,337 +0,0 @@ -// @refresh skip -import ErrorStackParser from "error-stack-parser"; -import * as htmlToImage from "html-to-image"; -import type { JSX } from "solid-js"; -import { createMemo, createSignal, ErrorBoundary, For, Show, Suspense } from "solid-js"; -import { Portal } from "solid-js/web"; -import info from "../../../package.json" with { type: "json" }; -import IconButton from "../ui/IconButton.tsx"; -import { Select, SelectOption } from "../ui/Select.tsx"; -import { Dialog, DialogOverlay, DialogPanel } from "../ui/Dialog.tsx"; -import { CodeView } from "./CodeView.tsx"; -import { createStackFrame, type StackFrameSource } from "./createStackFrame.ts"; -import download from "./download.ts"; -import { - ArrowLeftIcon, - ArrowRightIcon, - CameraIcon, - DiscordIcon, - GithubIcon, - RefreshIcon, - SolidStartIcon, - ViewCompiledIcon, - ViewOriginalIcon, -} from "./icons.tsx"; -import "./styles.css"; - -export function classNames( - ...classes: (string | boolean | undefined)[] -): string { - return classes.filter(Boolean).join(" "); -} - -interface ErrorInfoProps { - error: unknown; -} - -function ErrorInfo(props: ErrorInfoProps): JSX.Element { - const error = createMemo(() => { - const e = props.error; - - if (e instanceof Error) { - return { name: e.name, message: e.message }; - } - - if (e instanceof ErrorEvent) { - return { message: e.message }; - } - - return { message: (e as Error).toString() }; - }); - - return ( - - {error().name} - {error().message} - - ); -} - -interface StackFramesContentProps { - error: Error; - isCompiled: boolean; -} - -function getFileName(source: string): string { - try { - const path = source.startsWith("/") - ? new URL(source, "file://") - : new URL(source); - const paths = path.pathname.split("/"); - return paths[paths.length - 1]!; - } catch (error) { - return getFileName(`/${source}`); - } -} - -function getFilePath(source: StackFrameSource) { - const line = source.line ? `:${source.line}` : ""; - const column = source.column ? `:${source.column}` : ""; - return `${getFileName(source.source)}${line}${column}`; -} - -function CodeFallback(): JSX.Element { - return ( -
- Source not available. -
- ); -} - -function StackFramesContent(props: StackFramesContentProps) { - const stackframes = ErrorStackParser.parse(props.error); - - const [selectedFrame, setSelectedFrame] = createSignal(stackframes[0]!); - - return ( -
-
- - {(() => { - const data = createStackFrame( - selectedFrame(), - () => props.isCompiled, - ); - return ( - }> - }> - {(source) => ( - <> - {source.source} -
- -
- - )} -
-
- ); - })()} -
-
- - data-start-dev-overlay-stack-frames - value={selectedFrame()} - onChange={setSelectedFrame} - > - - {(current) => ( - - - {current.functionName ?? ""} - - - {getFilePath({ - source: current.getFileName()!, - content: "", - line: current.getLineNumber()!, - column: current.getColumnNumber()!, - name: current.getFunctionName(), - })} - - - } - > - {(() => { - const data = createStackFrame(current, () => props.isCompiled); - return ( - - - {source => ( - - - {source.name ?? ""} - - {getFilePath(source)} - - )} - - - ); - })()} - - )} - - -
- ); -} - -interface StackFramesProps { - error: unknown; - isCompiled: boolean; -} - -function StackFrames(props: StackFramesProps) { - return ( - - {(current) => ( - - )} - - ); -} - -interface DevOverlayDialogProps { - errors: any[]; - resetError: () => void; -} - -const ISSUE_THREAD = "https://github.com/solidjs/solid-start/issues/new"; -const DISCORD_INVITE = "https://discord.com/invite/solidjs"; - -export default function DevOverlayDialog( - props: DevOverlayDialogProps, -): JSX.Element { - const [currentPage, setCurrentPage] = createSignal(1); - const [isCompiled, setIsCompiled] = createSignal(false); - const length = createMemo(() => props.errors.length); - - const truncated = createMemo(() => { - return Math.min(currentPage(), length()); - }); - - function goPrev() { - setCurrentPage((c) => { - if (c > 1) { - return c - 1; - } - return length(); - }); - } - - function goNext() { - setCurrentPage((c) => { - if (c < length()) { - return c + 1; - } - return 1; - }); - } - - function toggleIsCompiled() { - setIsCompiled((c) => !c); - } - - const [panel, setPanel] = createSignal(); - - function downloadScreenshot() { - const current = panel(); - if (current) { - htmlToImage - .toPng(current, { - style: { - transform: "scale(0.75)", - }, - }) - .then((url) => { - download(url, "start-screenshot.png"); - }); - } - } - - function redirectToGithub() { - const url = new URL(ISSUE_THREAD); - url.searchParams.append("labels", "bug"); - url.searchParams.append("labels", "needs+triage"); - url.searchParams.append("template", "bug.yml"); - url.searchParams.append( - "title", - `[Bug?]:` + props.errors[truncated() - 1].toString(), - ); - window.open(url, "_blank")!.focus(); - } - - function redirectToDiscord() { - window.open(DISCORD_INVITE, "_blank")!.focus(); - } - - return ( - - -
- - -
-
-
-
-
- -
- {info.version as string} -
- 1}> -
- - - -
- {`${truncated()} of ${props.errors.length}`} -
- - - -
-
-
-
- - - - - - - - - - - - } - > - - - - - - -
-
- - {current => ( -
- - -
- )} -
-
-
-
-
-
- ); -} diff --git a/packages/start/src/shared/dev-toolbar/error-viewer/index.tsx b/packages/start/src/shared/dev-toolbar/error-viewer/index.tsx index bb4dea670..d1d0bfea0 100644 --- a/packages/start/src/shared/dev-toolbar/error-viewer/index.tsx +++ b/packages/start/src/shared/dev-toolbar/error-viewer/index.tsx @@ -1,63 +1,305 @@ // @refresh skip +import ErrorStackParser from "error-stack-parser"; +import * as htmlToImage from "html-to-image"; +import type { JSX } from "solid-js"; +import { createMemo, createSignal, ErrorBoundary, For, Show, Suspense } from "solid-js"; +import IconButton from "../../ui/IconButton.tsx"; +import { Select, SelectOption } from "../../ui/Select.tsx"; import { - createEffect, - createSignal, - ErrorBoundary, - type JSX, - onCleanup, - resetErrorBoundaries, - Show, -} from "solid-js"; -import clientOnly from "../clientOnly.ts"; -import { HttpStatusCode } from "../HttpStatusCode.ts"; - -export interface DevOverlayProps { - children?: JSX.Element; -} - -const DevOverlayDialog = import.meta.env.PROD - ? () => <> - : clientOnly(() => import("./DevOverlayDialog"), { lazy: true }); - -export function DevOverlay(props: DevOverlayProps): JSX.Element { - const [errors, setErrors] = createSignal([]); - - function resetError() { - setErrors([]); - resetErrorBoundaries(); - } + ArrowLeftIcon, + ArrowRightIcon, + CameraIcon, + DiscordIcon, + GithubIcon, + RefreshIcon, + ViewCompiledIcon, + ViewOriginalIcon, +} from "../icons.tsx"; +import { CodeView } from "./CodeView.tsx"; +import { createStackFrame, type StackFrameSource } from "./create-stack-frame.ts"; +import download from "./download.ts"; +import "./styles.css"; - function pushError(error: unknown) { - console.error(error); - setErrors(current => [error, ...current]); - } +export function classNames(...classes: (string | boolean | undefined)[]): string { + return classes.filter(Boolean).join(" "); +} - createEffect(() => { - const onErrorEvent = (error: ErrorEvent) => { - pushError(error.error ?? error); - }; +interface ErrorInfoProps { + error: unknown; +} - window.addEventListener("error", onErrorEvent); +function ErrorInfo(props: ErrorInfoProps): JSX.Element { + const error = createMemo(() => { + const e = props.error; - onCleanup(() => { - window.removeEventListener("error", onErrorEvent); - }); + if (e instanceof Error) { + return { name: e.name, message: e.message }; + } + + if (e instanceof ErrorEvent) { + return { message: e.message }; + } + + return { message: (e as Error).toString() }; }); return ( - <> - { - pushError(error); - return ; - }} + + {error().name} + {error().message} + + ); +} + +interface StackFramesContentProps { + error: Error; + isCompiled: boolean; +} + +function getFileName(source: string): string { + try { + const path = source.startsWith("/") ? new URL(source, "file://") : new URL(source); + const paths = path.pathname.split("/"); + return paths[paths.length - 1]!; + } catch (error) { + return getFileName(`/${source}`); + } +} + +function getFilePath(source: StackFrameSource) { + const line = source.line ? `:${source.line}` : ""; + const column = source.column ? `:${source.column}` : ""; + return `${getFileName(source.source)}${line}${column}`; +} + +function CodeFallback(): JSX.Element { + return ( +
+ Source not available. +
+ ); +} + +function StackFramesContent(props: StackFramesContentProps) { + const stackframes = ErrorStackParser.parse(props.error); + + const [selectedFrame, setSelectedFrame] = createSignal(stackframes[0]!); + + return ( +
+
+ + {(() => { + const data = createStackFrame(selectedFrame(), () => props.isCompiled); + return ( + }> + }> + {source => ( + <> + {source.source} +
+ +
+ + )} +
+
+ ); + })()} +
+
+ + data-start-error-viewer-stack-frames + value={selectedFrame()} + onChange={setSelectedFrame} > - {props.children} - - - - - - + + {current => ( + + + {current.functionName ?? ""} + + + {getFilePath({ + source: current.getFileName()!, + content: "", + line: current.getLineNumber()!, + column: current.getColumnNumber()!, + name: current.getFunctionName(), + })} + + + } + > + {(() => { + const data = createStackFrame(current, () => props.isCompiled); + return ( + + + {source => ( + + + {source.name ?? ""} + + + {getFilePath(source)} + + + )} + + + ); + })()} + + )} + + +
+ ); +} + +interface StackFramesProps { + error: unknown; + isCompiled: boolean; +} + +function StackFrames(props: StackFramesProps) { + return ( + + {current => } + + ); +} + +interface ErrorViewerProps { + show?: boolean; + errors: any[]; + resetError: () => void; +} + +const ISSUE_THREAD = "https://github.com/solidjs/solid-start/issues/new"; +const DISCORD_INVITE = "https://discord.com/invite/solidjs"; + +export default function ErrorViewer(props: ErrorViewerProps): JSX.Element { + const [currentPage, setCurrentPage] = createSignal(1); + const [isCompiled, setIsCompiled] = createSignal(false); + const length = createMemo(() => props.errors.length); + + const truncated = createMemo(() => { + return Math.min(currentPage(), length()); + }); + + function goPrev() { + setCurrentPage(c => { + if (c > 1) { + return c - 1; + } + return length(); + }); + } + + function goNext() { + setCurrentPage(c => { + if (c < length()) { + return c + 1; + } + return 1; + }); + } + + function toggleIsCompiled() { + setIsCompiled(c => !c); + } + + const [panel, setPanel] = createSignal(); + + function downloadScreenshot() { + const current = panel(); + if (current) { + htmlToImage + .toPng(current, { + style: { + transform: "scale(0.75)", + }, + }) + .then(url => { + download(url, "start-screenshot.png"); + }); + } + } + + function redirectToGithub() { + const url = new URL(ISSUE_THREAD); + url.searchParams.append("labels", "bug"); + url.searchParams.append("labels", "needs+triage"); + url.searchParams.append("template", "bug.yml"); + url.searchParams.append("title", `[Bug?]:` + props.errors[truncated() - 1].toString()); + window.open(url, "_blank")!.focus(); + } + + function redirectToDiscord() { + window.open(DISCORD_INVITE, "_blank")!.focus(); + } + + return ( + +
+
+
+
+ 1}> +
+ + + +
+ {`${truncated()} of ${props.errors.length}`} +
+ + + +
+
+
+
+ + + + + + + + + + + } + > + + + + + + +
+
+ + {current => ( +
+ + +
+ )} +
+
+
+
); } diff --git a/packages/start/src/shared/dev-toolbar/error-viewer/styles.css b/packages/start/src/shared/dev-toolbar/error-viewer/styles.css index bf9378823..ca63c8aac 100644 --- a/packages/start/src/shared/dev-toolbar/error-viewer/styles.css +++ b/packages/start/src/shared/dev-toolbar/error-viewer/styles.css @@ -1,42 +1,11 @@ -[data-start-dev-overlay] { - position: fixed; - inset: 0px; - z-index: 50; - overflow-y: auto; -} - -[data-start-dev-overlay]>div { - position: relative; - display: flex; - align-items: center; - justify-content: center; - min-height: 100vh; -} - -[data-start-dev-overlay-background] { - position: absolute; - inset: 0px; - background-color: rgb(17 24 39 / 0.5); -} - -[data-start-dev-overlay-panel] { +[data-start-error-viewer] { display: flex; flex-direction: column; - width: 75vw; - max-height: 75vh; - margin-top: 2rem; - margin-bottom: 2rem; - gap: 0.5rem; - color: rgb(17 24 39); - font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; -} -[data-start-dev-overlay-panel-container] { - margin: 2rem; - z-index: 10; + width: 100%; } -[data-start-dev-overlay-navbar] { +[data-start-error-viewer-navbar] { display: flex; flex-direction: row; align-items: center; @@ -44,148 +13,118 @@ gap: 0.5rem; } -[data-start-dev-overlay-pagination] { +[data-start-error-viewer-pagination] { display: flex; flex-direction: row; align-items: center; justify-content: center; gap: 0.5rem; border-radius: 9999px; - background-color: rgb(249 250 251); + color: rgb(249 250 251); + background-color: rgb(17 24 39); padding: 0.25rem; } -[data-start-dev-overlay-button] { - display: flex; - padding: 0.125rem; - border-radius: 9999px; - border-width: 0px; - align-items: center; - justify-content: center; - background-color: rgb(249 250 251); - color: rgb(17 24 39); - transition-property: color, background-color, border-color, box-shadow; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 150ms; +[data-start-error-viewer-button] { + /* empty */ } -[data-start-dev-overlay-button]:hover { - background-color: rgb(229 231 235); - color: rgb(55 65 81); +[data-start-error-viewer-button]:hover { + color: rgb(229 231 235); + background-color: rgb(55 65 81); } -[data-start-dev-overlay-button]:focus { +[data-start-error-viewer-button]:focus { outline: 2px solid transparent; - outline-offset: 2px + outline-offset: 2px; + outline-color: rgb(229 231 235); } -[data-start-dev-overlay-button]:focus-visible { +[data-start-error-viewer-button]:focus-visible { box-shadow: 0 0 0 calc(3px) rgb(17 24 39 / 0.75); } -[data-start-dev-overlay-button]:active { - background-color: rgb(243 244 246); - color: rgb(31 41 55); +[data-start-error-viewer-button]:active { + color: rgb(243 244 246); + background-color: rgb(31 41 55); } -[data-start-dev-overlay-button]>svg { +[data-start-error-viewer-button] > svg { height: 1.5rem; width: 1.5rem; } -[data-start-dev-overlay-page-counter] { +[data-start-error-viewer-page-counter] { font-size: 0.875rem; line-height: 1.25rem; font-weight: 600; } -[data-start-dev-overlay-navbar-left] { +[data-start-error-viewer-navbar-left] { display: flex; flex-direction: row; gap: 0.5rem; } - -[data-start-dev-overlay-controls] { +[data-start-error-viewer-controls] { display: flex; flex-direction: row; align-items: center; justify-content: center; gap: 0.25rem; border-radius: 9999px; - background-color: rgb(249 250 251); + color: rgb(249 250 251); padding: 0.25rem; } -[data-start-dev-overlay-version] { - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - gap: 0.25rem; - border-radius: 9999px; - background-color: rgb(249 250 251); - padding: 0.25rem 0.5rem; - font-size: 0.875rem; - line-height: 1.25rem; - font-weight: 600; -} - -[data-start-dev-overlay-version]>div { - height: 1.5rem; - width: 1.5rem; -} - -[data-start-dev-overlay-version]>div>svg { - height: 1.5rem; - width: 1.5rem; -} - -[data-start-dev-overlay-content] { +[data-start-error-viewer-content] { display: flex; flex-direction: column; gap: 0.75rem; - border-radius: 1.5rem; - background-color: rgb(249 250 251); padding: 1rem; + + border-top-width: 1px; + border-top-style: solid; + border-top-color: rgb(249 250 251); } -[data-start-dev-overlay-error-info] { +[data-start-error-viewer-error-info] { display: flex; flex-direction: column; gap: 0.25rem; } -[data-start-dev-overlay-error-info-name] { +[data-start-error-viewer-error-info-name] { font-weight: 700; + color: rgb(249 250 251); } -[data-start-dev-overlay-error-info-message] { +[data-start-error-viewer-error-info-message] { font-size: 1.5rem; line-height: 2rem; color: rgb(239 68 68); font-weight: 700; } -[data-start-dev-overlay-stack-frames-content] { +[data-start-error-viewer-stack-frames-content] { display: flex; flex-direction: column; gap: 0.75rem; } -[data-start-dev-overlay-stack-frames] { +[data-start-error-viewer-stack-frames] { flex: 1 1 0%; max-height: 16rem; overflow-y: auto; } -[data-start-dev-overlay-stack-frame] { +[data-start-error-viewer-stack-frame] { display: flex; flex-direction: row; align-items: center; justify-content: space-between; gap: 1rem; - color: rgb(17 24 39); + color: rgb(249 250 251); cursor: default; user-select: none; padding-top: 0.5rem; @@ -197,18 +136,18 @@ outline: none; } -[data-start-dev-overlay-stack-frame][tc-active], -[data-start-dev-overlay-stack-frame][tc-selected], -[data-start-dev-overlay-stack-frame]:focus { - background-color: rgb(219 234 254); - color: rgb(30 58 138); +[data-start-error-viewer-stack-frame][tc-active], +[data-start-error-viewer-stack-frame][tc-selected], +[data-start-error-viewer-stack-frame]:focus { + color: rgb(219 234 254); + background-color: rgb(30 58 138); } -[data-start-dev-overlay-stack-frame-function] { +[data-start-error-viewer-stack-frame-function] { font-weight: 700; } -[data-start-dev-overlay-stack-frame-file] { +[data-start-error-viewer-stack-frame-file] { white-space: nowrap; font-size: 0.75rem; /* 12px */ @@ -216,7 +155,7 @@ /* 16px */ } -[data-start-dev-overlay-stack-frames-code] { +[data-start-error-viewer-stack-frames-code] { padding: 0.5rem 1rem; gap: 0.25rem; flex: 1 1 0%; @@ -228,7 +167,7 @@ background-color: rgb(17 24 39); } -[data-start-dev-overlay-stack-frames-code-fallback] { +[data-start-error-viewer-stack-frames-code-fallback] { display: flex; align-items: center; justify-content: center; @@ -238,13 +177,13 @@ line-height: 1.25rem; } -[data-start-dev-overlay-stack-frames-code-source] { +[data-start-error-viewer-stack-frames-code-source] { font-size: 0.875rem; line-height: 1.25rem; color: rgb(249 250 251); } -[data-start-dev-overlay-stack-frames-code-container] { +[data-start-error-viewer-stack-frames-code-container] { width: 100%; overflow: hidden; min-width: 100%; @@ -254,32 +193,32 @@ border-top-color: rgb(249 250 251); } -[data-start-dev-overlay-code-view] { +[data-start-error-viewer-code-view] { overflow: auto; min-width: 100%; } -[data-start-dev-overlay-code-view]>.shiki { +[data-start-error-viewer-code-view] > .shiki { float: left; min-width: 100%; overflow-x: auto; font-size: 0.75rem; line-height: 1rem; counter-reset: step; - counter-increment: step calc(var(--dev-overlay-code-view-start, 1) - 1); + counter-increment: step calc(var(--error-viewer-code-view-start, 1) - 1); background-color: transparent !important; } -[data-start-dev-overlay-code-view]>.shiki .line::before { +[data-start-error-viewer-code-view] > .shiki .line::before { content: counter(step); counter-increment: step; width: 1rem; margin-right: 1.5rem; display: inline-block; text-align: right; - color: rgba(115, 138, 148, .4) + color: rgba(115, 138, 148, 0.4); } -[data-start-dev-overlay-error-line] { - background-color: rgba(239, 68, 68, .4); +[data-start-error-viewer-error-line] { + background-color: rgba(239, 68, 68, 0.4); } diff --git a/packages/start/src/shared/dev-toolbar/functions/BlobViewer.tsx b/packages/start/src/shared/dev-toolbar/functions/BlobViewer.tsx index 68c8c4973..890698a58 100644 --- a/packages/start/src/shared/dev-toolbar/functions/BlobViewer.tsx +++ b/packages/start/src/shared/dev-toolbar/functions/BlobViewer.tsx @@ -1,7 +1,7 @@ import { createMemo, createResource, type JSX, onCleanup, Show, Suspense } from 'solid-js'; -import { Badge } from "../ui/Badge.tsx"; -import Button from "../ui/Button.tsx"; +import { Badge } from "../../ui/Badge.tsx"; +import Button from "../../ui/Button.tsx"; import './BlobViewer.css'; diff --git a/packages/start/src/shared/dev-toolbar/functions/FormDataViewer.tsx b/packages/start/src/shared/dev-toolbar/functions/FormDataViewer.tsx index 6840f5132..a896bbac7 100644 --- a/packages/start/src/shared/dev-toolbar/functions/FormDataViewer.tsx +++ b/packages/start/src/shared/dev-toolbar/functions/FormDataViewer.tsx @@ -1,5 +1,5 @@ import { createResource, For, type JSX, Show, Suspense } from 'solid-js'; -import { Section } from '../ui/Section.tsx'; +import { Section } from '../../ui/Section.tsx'; import { BlobViewer } from './BlobViewer.tsx'; import { PropertySeparator, SerovalValue } from './SerovalValue.tsx'; diff --git a/packages/start/src/shared/dev-toolbar/functions/HeadersViewer.tsx b/packages/start/src/shared/dev-toolbar/functions/HeadersViewer.tsx index bff549c7d..d0109b65d 100644 --- a/packages/start/src/shared/dev-toolbar/functions/HeadersViewer.tsx +++ b/packages/start/src/shared/dev-toolbar/functions/HeadersViewer.tsx @@ -2,7 +2,7 @@ import { For } from "solid-js"; import { PropertySeparator, SerovalValue } from "./SerovalValue.tsx"; import './HeadersViewer.css'; -import { Text } from "../ui/Text.tsx"; +import { Text } from "../../ui/Text.tsx"; interface HeadersViewerProps { headers: Headers; diff --git a/packages/start/src/shared/dev-toolbar/functions/HexViewer.tsx b/packages/start/src/shared/dev-toolbar/functions/HexViewer.tsx index 29b0a31da..29bf21f87 100644 --- a/packages/start/src/shared/dev-toolbar/functions/HexViewer.tsx +++ b/packages/start/src/shared/dev-toolbar/functions/HexViewer.tsx @@ -1,5 +1,5 @@ import { createMemo, createResource, For, type JSX, Show, Suspense } from "solid-js"; -import { Text } from "../ui/Text.tsx"; +import { Text } from "../../ui/Text.tsx"; import './HexViewer.css'; diff --git a/packages/start/src/shared/dev-toolbar/functions/SerovalValue.tsx b/packages/start/src/shared/dev-toolbar/functions/SerovalValue.tsx index ed5a2429a..a8b420a92 100644 --- a/packages/start/src/shared/dev-toolbar/functions/SerovalValue.tsx +++ b/packages/start/src/shared/dev-toolbar/functions/SerovalValue.tsx @@ -1,4 +1,4 @@ -import { Text } from "../ui/Text.tsx"; +import { Text } from "../../ui/Text.tsx"; import './SerovalValue.css'; interface SerovalValueProps { diff --git a/packages/start/src/shared/dev-toolbar/functions/SerovalViewer.css b/packages/start/src/shared/dev-toolbar/functions/SerovalViewer.css index 5bfe25533..7ed90b0e1 100644 --- a/packages/start/src/shared/dev-toolbar/functions/SerovalViewer.css +++ b/packages/start/src/shared/dev-toolbar/functions/SerovalViewer.css @@ -39,7 +39,7 @@ display: flex; align-items: center; justify-content: space-between; - background-color: rgb(249 250 251); + color: rgb(249 250 251); } [data-start-seroval-node-content] { diff --git a/packages/start/src/shared/dev-toolbar/functions/SerovalViewer.tsx b/packages/start/src/shared/dev-toolbar/functions/SerovalViewer.tsx index 2b72cd6eb..e6af11736 100644 --- a/packages/start/src/shared/dev-toolbar/functions/SerovalViewer.tsx +++ b/packages/start/src/shared/dev-toolbar/functions/SerovalViewer.tsx @@ -1,25 +1,16 @@ import type { SerovalNode } from "seroval"; -import { - createEffect, - createSignal, - For, - type JSX, - Show, - splitProps, -} from "solid-js"; +import { createEffect, createSignal, For, type JSX, Show, splitProps } from "solid-js"; -import { Badge } from "../ui/Badge.tsx"; -import { Cascade, CascadeOption } from "../ui/Cascade.tsx"; -import { Section } from "../ui/Section.tsx"; +import { Badge } from "../../ui/Badge.tsx"; +import { Cascade, CascadeOption } from "../../ui/Cascade.tsx"; +import { Section } from "../../ui/Section.tsx"; import { HexViewer } from "./HexViewer.tsx"; -import { SerovalValue, PropertySeparator } from "./SerovalValue.tsx"; +import { PropertySeparator, SerovalValue } from "./SerovalValue.tsx"; +import { SerovalChunkReader } from "../../../fns/serialization.ts"; import "./SerovalViewer.css"; -import { SerovalChunkReader } from "../../fns/serialization.ts"; -function LinkIcon( - props: JSX.IntrinsicElements["svg"] & { title: string }, -): JSX.Element { +function LinkIcon(props: JSX.IntrinsicElements["svg"] & { title: string }): JSX.Element { return ( void, -): void { +function traverse(node: SerovalNode, handler: (node: SerovalNode) => void): void { handler(node); switch (node.t) { // Number = 0, @@ -386,10 +374,7 @@ function getObjectFlag(value: number) { } } -function zip( - keys: Key[], - values: Value[], -): [key: Key, value: Value][] { +function zip(keys: Key[], values: Value[]): [key: Key, value: Value][] { const zipped: [key: Key, value: Value][] = []; for (let i = 0, len = keys.length; i < len; i++) { @@ -401,12 +386,8 @@ function zip( interface RenderContext { getNode: (index: number) => SerovalNode | undefined; - getPromise: ( - index: number, - ) => Extract | undefined; - getStream: ( - index: number, - ) => Extract[] | undefined; + getPromise: (index: number) => Extract | undefined; + getStream: (index: number) => Extract[] | undefined; } function getStreamKeyword(t: 32 | 33 | 34): string { @@ -559,7 +540,10 @@ function renderSerovalNode( defaultValue={undefined} onChange={onSelect} > - [index, node] as const)} fallback={}> + [index, node] as const)} + fallback={} + > {([key, value]) => (
@@ -642,14 +626,17 @@ function renderSerovalNode(
- {(current) => ( + {current => (
data-start-properties defaultValue={undefined} onChange={onSelect} > - }> + } + > {([key, value]) => (
{typeof key === "string" ? ( @@ -807,7 +794,7 @@ function renderSerovalNode( onChange={onSelect} > }> - {(current) => ( + {current => (
@@ -830,7 +817,9 @@ function renderSerovalNode( }> {(current, index) => (
- + {renderSerovalNode(ctx, current, onSelect, true)}
@@ -862,16 +851,12 @@ function SerovalNodeRenderer(props: SerovalNodeRendererProps): JSX.Element {
{getNodeType(props.node)} - {props.node.i != null && ( - {`id: ${props.node.i}`} - )} -
-
- {renderSerovalNode(props, props.node, onSelect)} + {props.node.i != null && {`id: ${props.node.i}`}}
+
{renderSerovalNode(props, props.node, onSelect)}
- {(current) => } + {current => } ); @@ -885,16 +870,12 @@ function SerovalRenderer(props: SerovalRendererProps): JSX.Element { const [, rest] = splitProps(props, ["node"]); return (
- - {(current) => } - + {current => }
); } -function createSimpleStore>( - initial: T, -) { +function createSimpleStore>(initial: T) { const [state, setState] = createSignal(initial); return { @@ -905,13 +886,13 @@ function createSimpleStore>( return state()[key]; }, write(key: K, value: T[K]): void { - setState((current) => ({ + setState(current => ({ ...current, [key]: value, })); }, update(key: K, value: (current: T[K]) => T[K]): void { - setState((current) => ({ + setState(current => ({ ...current, [key]: value(current[key]), })); @@ -956,7 +937,7 @@ export function SerovalViewer(props: SerovalViewerProps): JSX.Element { case 32: case 33: case 34: - streams.update(node.i, (current) => { + streams.update(node.i, current => { if (current) { return [...current, node]; } @@ -1007,9 +988,9 @@ export function SerovalViewer(props: SerovalViewerProps): JSX.Element {
references.read(index)} - getPromise={(index) => promises.read(index)} - getStream={(index) => streams.read(index)} + getNode={index => references.read(index)} + getPromise={index => promises.read(index)} + getStream={index => streams.read(index)} />
); diff --git a/packages/start/src/shared/dev-toolbar/functions/URLSearchParamsViewer.tsx b/packages/start/src/shared/dev-toolbar/functions/URLSearchParamsViewer.tsx index b0d6a3f9d..6d45ce585 100644 --- a/packages/start/src/shared/dev-toolbar/functions/URLSearchParamsViewer.tsx +++ b/packages/start/src/shared/dev-toolbar/functions/URLSearchParamsViewer.tsx @@ -1,5 +1,5 @@ import { createResource, For, type JSX, Show, Suspense } from 'solid-js'; -import { Section } from '../ui/Section.tsx'; +import { Section } from '../../ui/Section.tsx'; import { PropertySeparator, SerovalValue } from './SerovalValue.tsx'; interface URLSearchParamsViewerInnerProps { diff --git a/packages/start/src/shared/dev-toolbar/functions/index.tsx b/packages/start/src/shared/dev-toolbar/functions/index.tsx index c760af55c..810223cb2 100644 --- a/packages/start/src/shared/dev-toolbar/functions/index.tsx +++ b/packages/start/src/shared/dev-toolbar/functions/index.tsx @@ -1,42 +1,28 @@ -import { - createEffect, - createMemo, - createSignal, - For, - type JSX, - onCleanup, - Show, -} from "solid-js"; -import { createStore } from "solid-js/store"; -import { Portal } from "solid-js/web"; -import { BODY_FORMAL_FILE, BODY_FORMAT_KEY, BodyFormat } from "../../fns/shared.ts"; -import { Badge } from "../ui/Badge.tsx"; -import Button from "../ui/Button.tsx"; -import { Dialog, DialogOverlay, DialogPanel } from "../ui/Dialog.tsx"; -import { Section } from "../ui/Section.tsx"; -import { Select, SelectOption } from "../ui/Select.tsx"; -import { Tab, TabGroup, TabList, TabPanel } from "../ui/Tabs.tsx"; +import { createMemo, createSignal, For, type JSX, Show } from "solid-js"; +import { BODY_FORMAL_FILE, BODY_FORMAT_KEY, BodyFormat } from "../../../fns/shared.ts"; +import { Badge } from "../../ui/Badge.tsx"; +import IconButton from "../../ui/IconButton.tsx"; +import Placeholder from "../../ui/Placeholder.tsx"; +import { Section } from "../../ui/Section.tsx"; +import { Select, SelectOption } from "../../ui/Select.tsx"; +import { Tab, TabGroup, TabList, TabPanel } from "../../ui/Tabs.tsx"; +import { Text } from "../../ui/Text.tsx"; +import { ArrowLeftIcon, TrashIcon } from "../icons.tsx"; import { BlobViewer } from "./BlobViewer.tsx"; import { FormDataViewer } from "./FormDataViewer.tsx"; import { HeadersViewer } from "./HeadersViewer.tsx"; import { HexViewer } from "./HexViewer.tsx"; +import { PropertySeparator, SerovalValue } from "./SerovalValue.tsx"; import { SerovalViewer } from "./SerovalViewer.tsx"; -import { - captureServerFunctionCall, - type ServerFunctionRequest, - type ServerFunctionResponse, -} from "./server-function-tracker.ts"; import "./styles.css"; -import Placeholder from "../ui/Placeholder.tsx"; -import { Text } from "../ui/Text.tsx"; -import { PropertySeparator, SerovalValue } from "./SerovalValue.tsx"; +import { type ServerFunctionRequest, type ServerFunctionResponse } from "./tracker.ts"; import { URLSearchParamsViewer } from "./URLSearchParamsViewer.tsx"; async function getFile(source: Response | Request): Promise { const formData = await source.formData(); const file = formData.get(BODY_FORMAL_FILE); if (!(file && file instanceof File)) { - throw new Error('invalid file input'); + throw new Error("invalid file input"); } return file; } @@ -60,7 +46,7 @@ function ContentViewer(props: ContentViewerProps): JSX.Element { {(() => { const source = props.source.source.clone(); const startType = source.headers.get(BODY_FORMAT_KEY); - const contentType = source.headers.get('Content-Type'); + const contentType = source.headers.get("Content-Type"); switch (true) { case startType === "true": case startType === BodyFormat.Seroval: @@ -93,16 +79,16 @@ interface RequestViewerProps { function convertRequestToEntries(request: Request) { return [ - ['Cache', request.cache], - ['Credentials', request.credentials], - ['Destination', request.destination], - ['Integrity', request.integrity], - ['Keep Alive', request.keepalive], - ['Mode', request.mode], - ['Redirect', request.redirect], - ['Referrer', request.referrer], - ['Referrer Policy', request.referrerPolicy], - ['URL', request.url], + ["Cache", request.cache], + ["Credentials", request.credentials], + ["Destination", request.destination], + ["Integrity", request.integrity], + ["Keep Alive", request.keepalive], + ["Mode", request.mode], + ["Redirect", request.redirect], + ["Referrer", request.referrer], + ["Referrer Policy", request.referrerPolicy], + ["URL", request.url], ]; } @@ -113,7 +99,7 @@ function RequestViewer(props: RequestViewerProps): JSX.Element { {([key, value]) => (
- {key} + {key}
@@ -132,12 +118,12 @@ interface ResponseViewerProps { function convertResponseToEntries(response: Response) { return [ - ['OK', response.ok], - ['Redirected', response.redirected], - ['Status', response.status], - ['Status Text', response.statusText], - ['Type', response.type], - ['URL', response.url], + ["OK", response.ok], + ["Redirected", response.redirected], + ["Status", response.status], + ["Status Text", response.statusText], + ["Type", response.type], + ["URL", response.url], ]; } @@ -145,22 +131,24 @@ function ResponseViewer(props: ResponseViewerProps): JSX.Element { return ( - {(instance) => ( + {instance => ( <>
{([key, value]) => (
- {key} + {key}
)}
- Timing + Timing - +
@@ -171,153 +159,144 @@ function ResponseViewer(props: ResponseViewerProps): JSX.Element { ); } -interface ServerFunctionInstance { +export interface ServerFunctionInstance { request: ServerFunctionRequest; response?: ServerFunctionResponse; } +interface ServerFunctionInstanceDetailProps { + id: string; + value: ServerFunctionInstance; +} + +function ServerFunctionInstanceDetail(props: ServerFunctionInstanceDetailProps) { + return ( + <> + + {props.value.request.source.method} + {props.id} + + + {response => { + if (response().source.ok) { + return {response().source.status}; + } + return {response().source.status}; + }} + + + ); +} + interface ServerFunctionInstanceViewerProps { + id: string; instance: ServerFunctionInstance; onDelete: () => void; + onReturn: () => void; } -function ServerFunctionInstanceViewer( - props: ServerFunctionInstanceViewerProps, -): JSX.Element { +function ServerFunctionInstanceViewer(props: ServerFunctionInstanceViewerProps): JSX.Element { const [tab, setTab] = createSignal<"request" | "response">("request"); return ( - setTab(value ?? "request")} - > -
- - Request - Response - -
- +
+
+
+ + + +
+ +
+
+
+ + +
- - - +
+ setTab(value ?? "request")}> + + Request + Response + + + + +
+
); } function EmptyServerFunctions(): JSX.Element { return ( - - No server function calls detected. - + No server function calls detected. ); } -export function ServerFunctionInspector(): JSX.Element { - const [currentInstance, setCurrentInstance] = createSignal(); - - const [store, setStore] = createStore({ - instances: {} as Record, - }); - - createEffect(() => { - onCleanup( - captureServerFunctionCall((call) => { - if (call.type === "request") { - setStore("instances", call.instance, { - request: call, - }); - } else { - setStore("instances", call.instance, "response", call); - } - }), - ); - }); - - const [isOpen, setIsOpen] = createSignal(false); +export interface ServerFunctionViewerProps { + instances: Record; + onDeleteInstance: (value: string) => void; + show?: boolean; +} - createEffect(() => { - (window as any).__START__SERVER_FN__ = setIsOpen; - }); +export function ServerFunctionViewer(props: ServerFunctionViewerProps): JSX.Element { + const [currentInstance, setCurrentInstance] = createSignal(); - const keys = createMemo(() => Object.keys(store.instances)); + const keys = createMemo(() => Object.keys(props.instances)); return ( - - -
- - -
- {/* list of calls */} -
- }> - - -
- {/* request/response viewer */} - - {(value) => ( - - {(instance) => ( - { - setStore("instances", value(), undefined); - }} - /> - )} - + +
+
+ {/* request/response viewer */} + + {value => ( + + {instance => ( + { + setCurrentInstance(undefined); + }} + onDelete={() => { + props.onDeleteInstance(value()); + }} + /> )} + )} + + + {/* list of calls */} +
+ }> + +
- +
-
-
+
+ ); } diff --git a/packages/start/src/shared/dev-toolbar/functions/styles.css b/packages/start/src/shared/dev-toolbar/functions/styles.css index b1769566e..bab751c23 100644 --- a/packages/start/src/shared/dev-toolbar/functions/styles.css +++ b/packages/start/src/shared/dev-toolbar/functions/styles.css @@ -1,56 +1,41 @@ - - -.server-function-instance-detail { +[data-start-functions-instance-detail] { display: flex; align-items: center; gap: 0.25rem; } +[data-start-functions-viewer] { + color: rgb(249 250 251); + margin: 0.5rem; -.server-function-inspector { - display: grid; - grid-template-columns: 1fr 2fr; - width: 75vw; - height: 75vh; - margin-top: 2rem; - margin-bottom: 2rem; + display: flex; + flex-direction: column; gap: 0.5rem; - color: rgb(17 24 39); + + min-height: 100%; } -.server-function-instances-container { +[data-start-functions-instances-container] { border-radius: 0.5rem; - background-color: rgb(249 250 251); + color: rgb(249 250 251); + width: 100%; + min-height: 100%; overflow: auto; } - -.server-function-instances { +[data-start-functions-instances] { list-style: none; display: flex; flex-direction: column; padding: 0px; } -.server-function-instances > [data-start-select-option] { +[data-start-functions-instances] > [data-start-select-option] { display: flex; align-items: center; justify-content: space-between; padding: 0.25rem; -} - -.server-function-instance-viewer { - display: flex; - flex-direction: column; - gap: 0.25rem; - height: 100%; - overflow: hidden; -} - -.server-function-instance-viewer-toolbar { - display: flex; - align-items: center; - justify-content: space-between; + color: rgb(249 250 251); } [data-start-headers] { @@ -76,4 +61,44 @@ flex-wrap: nowrap; gap: 0.25rem; +} + +[data-start-function-instance-viewer] { + display: flex; + flex-direction: column; + gap: 0.5rem; + + width: 100%; +} + +[data-start-function-instance-viewer-nav] { + display: flex; + align-items: center; + gap: 0.25rem; + + justify-content: space-between; +} + +[data-start-function-instance-viewer-nav-left] { + display: flex; + align-items: center; + gap: 0.25rem; +} + +[data-start-function-instance-viewer-nav-left] > div { + display: flex; + align-items: center; + gap: 0.25rem; +} + +[data-start-function-instance-viewer-content] { + border-top-width: 1px; + border-top-style: solid; + border-top-color: rgb(249 250 251); + + height: 100%; +} + +[data-start-function-instance-viewer-content] > [tc-tab-group] { + height: 100%; } \ No newline at end of file diff --git a/packages/start/src/shared/dev-toolbar/icons.tsx b/packages/start/src/shared/dev-toolbar/icons.tsx index b1b9a9caa..f35312170 100644 --- a/packages/start/src/shared/dev-toolbar/icons.tsx +++ b/packages/start/src/shared/dev-toolbar/icons.tsx @@ -169,6 +169,25 @@ export function GithubIcon(props: JSX.IntrinsicElements["svg"] & { title: string ); } +export function ErrorIcon(props: JSX.IntrinsicElements["svg"] & { title: string }): JSX.Element { + return ( + + {props.title} + + + ); +} + export function SolidStartIcon( props: JSX.IntrinsicElements["svg"] & { title: string }, ): JSX.Element { @@ -454,3 +473,37 @@ export function SolidStartIcon( ); } + +export function FunctionIcon(props: JSX.IntrinsicElements["svg"] & { title: string }): JSX.Element { + return ( + + {props.title} + + + ); +} + +export function TrashIcon(props: JSX.IntrinsicElements["svg"] & { title: string }): JSX.Element { + return ( + + {props.title} + + + ); +} diff --git a/packages/start/src/shared/dev-toolbar/index.css b/packages/start/src/shared/dev-toolbar/index.css new file mode 100644 index 000000000..070517bb1 --- /dev/null +++ b/packages/start/src/shared/dev-toolbar/index.css @@ -0,0 +1,87 @@ +[data-start-dev-toolbar] { + position: fixed; + + bottom: 0px; + left: 0px; + + display: flex; + flex-direction: column-reverse; + align-items: flex-start; + + cursor: grab; +} + +[data-start-dev-toolbar]:active { + cursor: grabbing; +} + +[data-start-dev-toolbar] > [tc-toolbar] { + position: relative; + + background: oklch(25.7% 0.09 281.288); + margin: 1rem; + padding: 0.25rem 0.5rem; + + overflow: hidden; + border-radius: 9999px; + + display: flex; + flex-direction: row; + align-items: center; + + gap: 0.5rem; + + border: rgb(249 250 251) 1px solid; +} + +[data-start-dev-toolbar] > [tc-toolbar] > div { + display: flex; + + flex-direction: row; + align-items: center; + justify-content: center; +} + +[data-start-dev-toolbar] > [tc-toolbar] > div:last-child { + border-left: rgb(249 250 251) 1px solid; + padding-left: 0.5rem; + + gap: 0.5rem; +} + +[data-start-dev-toolbar-version] { + border-radius: 9999px; + color: rgb(249 250 251); + padding: 0.125rem 0.5rem; + + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 600; + + border: rgb(249 250 251) 1px solid; +} + +[data-start-dev-toolbar-version] > div { + height: 1.5rem; + width: 1.5rem; +} + +[data-start-dev-toolbar] svg { + height: 1.25rem; + width: 1.25rem; +} + +[data-start-dev-toolbar-panel] { + background: oklch(25.7% 0.09 281.288); + margin: 0rem 1rem; + + overflow: auto; + + width: 50rem; + height: 30rem; + + max-width: calc(100vw - 1rem); + max-height: calc(100vh - 1rem); + + border-radius: 1rem; +} diff --git a/packages/start/src/shared/dev-toolbar/index.tsx b/packages/start/src/shared/dev-toolbar/index.tsx new file mode 100644 index 000000000..19f34bf3a --- /dev/null +++ b/packages/start/src/shared/dev-toolbar/index.tsx @@ -0,0 +1,242 @@ +import { + createEffect, + createSignal, + ErrorBoundary, + onCleanup, + resetErrorBoundaries, + Show, + type JSX, +} from "solid-js"; +import { createStore } from "solid-js/store"; +import { Portal } from "solid-js/web"; +import { Toolbar } from "terracotta"; +import info from "../../../package.json" with { type: "json" }; +import clientOnly from "../clientOnly.ts"; +import { HttpStatusCode } from "../HttpStatusCode.ts"; +import IconButton from "../ui/IconButton.tsx"; +import { Text } from "../ui/Text.tsx"; +import { ServerFunctionInstance, ServerFunctionViewer } from "./functions/index.tsx"; +import { captureServerFunctionCall } from "./functions/tracker.ts"; +import { ErrorIcon, FunctionIcon, SolidStartIcon } from "./icons"; +import "./index.css"; + +const ErrorViewer = import.meta.env.PROD + ? () => <> + : clientOnly(() => import("./error-viewer/index.tsx"), { lazy: true }); + +export interface DevToolbarProps { + children?: JSX.Element; +} + +export function DevToolbar(props: DevToolbarProps) { + const [ref, setRef] = createSignal(); + + createEffect(() => { + const current = ref(); + + if (current) { + let isDown = false; + + // Offsets of the mouse relatively to the element's position + let offsetX = 0; + let offsetY = 0; + + let currentX = 0; + let currentY = 0; + + let centerX = 0; + let centerY = 0; + + const resetPosition = () => { + current.style.top = "auto"; + current.style.left = "auto"; + current.style.bottom = "auto"; + current.style.right = "auto"; + }; + + let isDirty = false; + + const ac = new AbortController(); + + current.addEventListener( + "mousedown", + e => { + isDown = true; + + const rect = current.getBoundingClientRect(); + + currentX = rect.left; + currentY = rect.top; + + offsetX = e.clientX - currentX; + offsetY = e.clientY - currentY; + + centerX = rect.width / 2; + centerY = rect.height / 2; + + isDirty = true; + }, + { + signal: ac.signal, + }, + ); + + window.addEventListener( + "mouseup", + () => { + if (isDown && !isDirty) { + const preferredAnchorX = currentX + centerX < window.innerWidth / 2 ? "left" : "right"; + const preferredAnchorY = currentY + centerY < window.innerHeight / 2 ? "top" : "bottom"; + + resetPosition(); + + current.style[preferredAnchorX] = "0px"; + current.style[preferredAnchorY] = "0px"; + + current.style.flexDirection = + preferredAnchorY === "bottom" ? "column-reverse" : "column"; + current.style.alignItems = preferredAnchorX === "left" ? "flex-start" : "flex-end"; + } + isDown = false; + }, + { + signal: ac.signal, + }, + ); + + window.addEventListener( + "mousemove", + e => { + if (isDown) { + if (isDirty) { + resetPosition(); + isDirty = false; + } + currentX = e.clientX - offsetX; + currentY = e.clientY - offsetY; + + current.style.left = `${currentX}px`; + current.style.top = `${currentY}px`; + } + }, + { + signal: ac.signal, + passive: true, + }, + ); + + onCleanup(() => { + ac.abort(); + }); + } + }); + + const [content, setContent] = createSignal<"fn" | "err" | undefined>(undefined); + + function toggleContent(value: "fn" | "err") { + if (content() === value) { + setContent(undefined); + } else { + setContent(value); + } + } + + const [errors, setErrors] = createSignal([]); + + function resetError() { + setErrors([]); + resetErrorBoundaries(); + } + + function pushError(error: unknown) { + console.error(error); + setErrors(current => [error, ...current]); + + setContent("err"); + } + + createEffect(() => { + const onErrorEvent = (error: ErrorEvent) => { + pushError(error.error ?? error); + }; + + window.addEventListener("error", onErrorEvent); + + onCleanup(() => { + window.removeEventListener("error", onErrorEvent); + }); + }); + + const [store, setStore] = createStore({ + instances: {} as Record, + }); + + createEffect(() => { + onCleanup( + captureServerFunctionCall(call => { + console.log(call); + if (call.type === "request") { + setStore("instances", call.instance, value => { + return { + ...value, + request: call, + }; + }); + } else { + setStore("instances", call.instance, value => { + return { + ...value, + response: call, + }; + }); + } + }), + ); + }); + + return ( + <> + +
+ +
+ toggleContent("err")} disabled={errors().length === 0}> + + + toggleContent("fn")}> + + +
+
+ +
+ + {info.version as string} + +
+
+
+ + { + setStore("instances", value, undefined); + }} + /> +
+
+ { + pushError(error); + return ; + }} + > + {props.children} + + 0}> + + + + ); +} diff --git a/packages/start/src/shared/ui/Button.css b/packages/start/src/shared/ui/Button.css index fbae0519d..e48c1c75c 100644 --- a/packages/start/src/shared/ui/Button.css +++ b/packages/start/src/shared/ui/Button.css @@ -7,9 +7,9 @@ border-radius: 0.5rem; - background-color: oklch(97% 0.014 254.604); + color: oklch(97% 0.014 254.604); - color: rgb(17 24 39); + background-color: rgb(17 24 39); padding: 0.25rem 0.5rem; font-size: 0.875rem; line-height: 1.25rem; @@ -27,6 +27,6 @@ [data-start-button][tc-selected], [data-start-button]:hover, [data-start-button]:focus { - background-color: rgb(219 234 254); - color: rgb(30 58 138); + color: rgb(219 234 254); + background-color: rgb(30 58 138); } diff --git a/packages/start/src/shared/ui/IconButton.css b/packages/start/src/shared/ui/IconButton.css index 3582bbaec..3a50ec5b8 100644 --- a/packages/start/src/shared/ui/IconButton.css +++ b/packages/start/src/shared/ui/IconButton.css @@ -1,27 +1,27 @@ [data-start-icon-button] { display: flex; - padding: 0.125rem; + padding: 0.25rem; border-radius: 9999px; border-width: 0px; align-items: center; justify-content: center; cursor: pointer; - background-color: rgb(249 250 251); - color: rgb(17 24 39); + color: rgb(249 250 251); + background: none; transition-property: color, background-color, border-color, box-shadow; transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); transition-duration: 150ms; } - [data-start-icon-button]:hover { - background-color: rgb(229 231 235); - color: rgb(55 65 81); + color: rgb(229 231 235); + background-color: rgb(55 65 81); } [data-start-icon-button]:focus { outline: 2px solid transparent; - outline-offset: 2px + outline-color: rgb(249 250 251); + outline-offset: 2px; } [data-start-icon-button]:focus-visible { @@ -29,11 +29,11 @@ } [data-start-icon-button]:active { - background-color: rgb(243 244 246); - color: rgb(31 41 55); + color: rgb(243 244 246); + background-color: rgb(31 41 55); } -[data-start-icon-button]>svg { - height: 1.5rem; - width: 1.5rem; +[data-start-icon-button] > svg { + height: 1.25rem; + width: 1.25rem; } diff --git a/packages/start/src/shared/ui/Select.css b/packages/start/src/shared/ui/Select.css index bc95e9770..c00ed704f 100644 --- a/packages/start/src/shared/ui/Select.css +++ b/packages/start/src/shared/ui/Select.css @@ -1,4 +1,3 @@ - [data-start-select] { display: flex; flex-direction: column; @@ -13,7 +12,7 @@ } [data-start-select-option] { - color: rgb(17 24 39); + color: rgb(249 250 251); padding-top: 0.5rem; padding-bottom: 0.5rem; padding-left: 1rem; @@ -33,6 +32,6 @@ [data-start-select-option][tc-active], [data-start-select-option][tc-selected], [data-start-select-option]:focus { - background-color: rgb(219 234 254); - color: rgb(30 58 138); + color: rgb(229 231 235); + background-color: rgb(55 65 81); } diff --git a/packages/start/src/shared/ui/Tabs.css b/packages/start/src/shared/ui/Tabs.css index 36ed6bb25..c9400d374 100644 --- a/packages/start/src/shared/ui/Tabs.css +++ b/packages/start/src/shared/ui/Tabs.css @@ -6,7 +6,6 @@ border-radius: 0.5rem; cursor: pointer; - color: rgb(17 24 39); padding-top: 0.5rem; padding-bottom: 0.5rem; padding-left: 1rem; @@ -25,8 +24,8 @@ [data-start-tab][tc-selected], [data-start-tab]:hover, [data-start-tab]:focus { - background-color: rgb(219 234 254); - color: rgb(30 58 138); + color: rgb(229 231 235); + background-color: rgb(55 65 81); } [data-start-tab-group] { @@ -36,14 +35,9 @@ display: flex; gap: 0.25rem; padding: 0.25rem; - background-color: rgb(249 250 251); - border-radius: 0.5rem; } [data-start-tab-panel] { - padding: 1rem; - background-color: rgb(249 250 251); - border-radius: 0.5rem; height: 100%; overflow: auto; @@ -51,4 +45,8 @@ display: flex; flex-direction: column; gap: 1rem; + + border-top-width: 1px; + border-top-style: solid; + border-top-color: rgb(249 250 251); } From 8c5b3ef8e32c6eaacba4a86cde19f0bde9cd6c25 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Fri, 17 Jul 2026 04:15:21 +0800 Subject: [PATCH 21/25] Update (no-side-effects).tsx --- apps/tests/src/routes/treeshaking/(no-side-effects).tsx | 3 --- 1 file changed, 3 deletions(-) diff --git a/apps/tests/src/routes/treeshaking/(no-side-effects).tsx b/apps/tests/src/routes/treeshaking/(no-side-effects).tsx index 1bbeaf9ff..be86a00a0 100644 --- a/apps/tests/src/routes/treeshaking/(no-side-effects).tsx +++ b/apps/tests/src/routes/treeshaking/(no-side-effects).tsx @@ -15,8 +15,5 @@ async function getGetA() { export default function Treeshaking() { const s = createAsync(() => getGetA()); - createEffect(() => { - throw new Error('something') - }) return

hello: {s()}

; } From 54c706c7eccc0cfe9d78a70f19bb0c4729099d8e Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Fri, 17 Jul 2026 10:44:27 +0800 Subject: [PATCH 22/25] fix build --- packages/start/src/shared/dev-toolbar/index.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/start/src/shared/dev-toolbar/index.tsx b/packages/start/src/shared/dev-toolbar/index.tsx index 19f34bf3a..7bfa72ef0 100644 --- a/packages/start/src/shared/dev-toolbar/index.tsx +++ b/packages/start/src/shared/dev-toolbar/index.tsx @@ -15,9 +15,9 @@ import clientOnly from "../clientOnly.ts"; import { HttpStatusCode } from "../HttpStatusCode.ts"; import IconButton from "../ui/IconButton.tsx"; import { Text } from "../ui/Text.tsx"; -import { ServerFunctionInstance, ServerFunctionViewer } from "./functions/index.tsx"; +import { type ServerFunctionInstance, ServerFunctionViewer } from "./functions/index.tsx"; import { captureServerFunctionCall } from "./functions/tracker.ts"; -import { ErrorIcon, FunctionIcon, SolidStartIcon } from "./icons"; +import { ErrorIcon, FunctionIcon, SolidStartIcon } from "./icons.tsx"; import "./index.css"; const ErrorViewer = import.meta.env.PROD From 7ced71ca0c508ba3eb300d4426c6045487ad236b Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Fri, 17 Jul 2026 11:22:39 +0800 Subject: [PATCH 23/25] fix styles --- packages/start/src/shared/dev-toolbar/index.css | 5 +++-- packages/start/src/shared/ui/Section.css | 2 +- 2 files changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/start/src/shared/dev-toolbar/index.css b/packages/start/src/shared/dev-toolbar/index.css index 070517bb1..19dc18f0b 100644 --- a/packages/start/src/shared/dev-toolbar/index.css +++ b/packages/start/src/shared/dev-toolbar/index.css @@ -72,6 +72,7 @@ } [data-start-dev-toolbar-panel] { + border: rgb(249 250 251) 1px solid; background: oklch(25.7% 0.09 281.288); margin: 0rem 1rem; @@ -80,8 +81,8 @@ width: 50rem; height: 30rem; - max-width: calc(100vw - 1rem); - max-height: calc(100vh - 1rem); + max-width: calc(100vw - 10rem); + max-height: calc(100vh - 2rem); border-radius: 1rem; } diff --git a/packages/start/src/shared/ui/Section.css b/packages/start/src/shared/ui/Section.css index 27e368b15..8d8321889 100644 --- a/packages/start/src/shared/ui/Section.css +++ b/packages/start/src/shared/ui/Section.css @@ -13,7 +13,7 @@ padding-left: 0.5rem; border-left: 1px oklch(70.7% 0.165 254.624) solid; - width: calc(100% - 1rem); + width: calc(100% - 2rem); overflow: auto; } From fca04005b2e0adf374885f495f922fff88f1f93f Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Sat, 18 Jul 2026 00:58:45 +0800 Subject: [PATCH 24/25] fix border colors --- .../start/src/shared/dev-toolbar/error-viewer/styles.css | 4 ++-- .../start/src/shared/dev-toolbar/functions/HexViewer.css | 4 ++-- .../src/shared/dev-toolbar/functions/SerovalViewer.css | 8 ++++---- .../start/src/shared/dev-toolbar/functions/styles.css | 2 +- packages/start/src/shared/dev-toolbar/index.css | 6 +++--- packages/start/src/shared/ui/Section.css | 2 +- packages/start/src/shared/ui/Select.css | 2 +- 7 files changed, 14 insertions(+), 14 deletions(-) diff --git a/packages/start/src/shared/dev-toolbar/error-viewer/styles.css b/packages/start/src/shared/dev-toolbar/error-viewer/styles.css index ca63c8aac..5a422cd8f 100644 --- a/packages/start/src/shared/dev-toolbar/error-viewer/styles.css +++ b/packages/start/src/shared/dev-toolbar/error-viewer/styles.css @@ -85,7 +85,7 @@ border-top-width: 1px; border-top-style: solid; - border-top-color: rgb(249 250 251); + border-top-color: oklch(87% 0.065 274.039); } [data-start-error-viewer-error-info] { @@ -190,7 +190,7 @@ max-height: 16rem; border-top-width: 1px; border-top-style: solid; - border-top-color: rgb(249 250 251); + border-top-color: oklch(87% 0.065 274.039); } [data-start-error-viewer-code-view] { diff --git a/packages/start/src/shared/dev-toolbar/functions/HexViewer.css b/packages/start/src/shared/dev-toolbar/functions/HexViewer.css index 8e6d7fb11..9e2a9f996 100644 --- a/packages/start/src/shared/dev-toolbar/functions/HexViewer.css +++ b/packages/start/src/shared/dev-toolbar/functions/HexViewer.css @@ -1,6 +1,6 @@ [data-start-hex-viewer] { display: flex; - border: 1px oklch(70.7% 0.165 254.624) solid; + border: 1px oklch(87% 0.065 274.039) solid; overflow: auto; } @@ -18,7 +18,7 @@ flex-direction: column; gap: 0.5rem; padding: 0.5rem; - border-left: 1px oklch(70.7% 0.165 254.624) solid; + border-left: 1px oklch(87% 0.065 274.039) solid; } [data-start-hex-row] { diff --git a/packages/start/src/shared/dev-toolbar/functions/SerovalViewer.css b/packages/start/src/shared/dev-toolbar/functions/SerovalViewer.css index 7ed90b0e1..6c930d8bd 100644 --- a/packages/start/src/shared/dev-toolbar/functions/SerovalViewer.css +++ b/packages/start/src/shared/dev-toolbar/functions/SerovalViewer.css @@ -11,16 +11,16 @@ [data-start-seroval-renderer] { display: flex; - border-top: 1px oklch(70.7% 0.165 254.624) solid; - border-bottom: 1px oklch(70.7% 0.165 254.624) solid; + border-top: 1px oklch(87% 0.065 274.039) solid; + border-bottom: 1px oklch(87% 0.065 274.039) solid; } [data-start-seroval-renderer] > * { - border-left: 1px oklch(70.7% 0.165 254.624) solid; + border-left: 1px oklch(87% 0.065 274.039) solid; } [data-start-seroval-renderer]:last-child { - border-right: 1px oklch(70.7% 0.165 254.624) solid; + border-right: 1px oklch(87% 0.065 274.039) solid; } [data-start-seroval-node] { diff --git a/packages/start/src/shared/dev-toolbar/functions/styles.css b/packages/start/src/shared/dev-toolbar/functions/styles.css index bab751c23..b0f2cc136 100644 --- a/packages/start/src/shared/dev-toolbar/functions/styles.css +++ b/packages/start/src/shared/dev-toolbar/functions/styles.css @@ -94,7 +94,7 @@ [data-start-function-instance-viewer-content] { border-top-width: 1px; border-top-style: solid; - border-top-color: rgb(249 250 251); + border-top-color: oklch(87% 0.065 274.039); height: 100%; } diff --git a/packages/start/src/shared/dev-toolbar/index.css b/packages/start/src/shared/dev-toolbar/index.css index 19dc18f0b..5b69569dd 100644 --- a/packages/start/src/shared/dev-toolbar/index.css +++ b/packages/start/src/shared/dev-toolbar/index.css @@ -31,7 +31,7 @@ gap: 0.5rem; - border: rgb(249 250 251) 1px solid; + border: oklch(87% 0.065 274.039) 1px solid; } [data-start-dev-toolbar] > [tc-toolbar] > div { @@ -58,7 +58,7 @@ line-height: 1.25rem; font-weight: 600; - border: rgb(249 250 251) 1px solid; + border: oklch(87% 0.065 274.039) 1px solid; } [data-start-dev-toolbar-version] > div { @@ -72,7 +72,7 @@ } [data-start-dev-toolbar-panel] { - border: rgb(249 250 251) 1px solid; + border: oklch(87% 0.065 274.039) 1px solid; background: oklch(25.7% 0.09 281.288); margin: 0rem 1rem; diff --git a/packages/start/src/shared/ui/Section.css b/packages/start/src/shared/ui/Section.css index 8d8321889..67b6878b0 100644 --- a/packages/start/src/shared/ui/Section.css +++ b/packages/start/src/shared/ui/Section.css @@ -11,7 +11,7 @@ [data-start-section-content] { margin-left: 0.5rem; padding-left: 0.5rem; - border-left: 1px oklch(70.7% 0.165 254.624) solid; + border-left: 1px oklch(87% 0.065 274.039) solid; width: calc(100% - 2rem); diff --git a/packages/start/src/shared/ui/Select.css b/packages/start/src/shared/ui/Select.css index c00ed704f..1cc2a3131 100644 --- a/packages/start/src/shared/ui/Select.css +++ b/packages/start/src/shared/ui/Select.css @@ -8,7 +8,7 @@ } [data-start-select] > * + * { - border-top: 1px oklch(70.7% 0.165 254.624) solid; + border-top: 1px oklch(87% 0.065 274.039) solid; } [data-start-select-option] { From 640ad607ba137d22ee129e7a3a6c9a2ec9298823 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Sun, 19 Jul 2026 08:34:09 +0800 Subject: [PATCH 25/25] fix styling --- .../dev-toolbar/functions/SerovalViewer.css | 8 ++--- .../shared/dev-toolbar/functions/index.tsx | 6 +++- .../shared/dev-toolbar/functions/styles.css | 34 +++++++++++-------- packages/start/src/shared/ui/Cascade.css | 1 - packages/start/src/shared/ui/Table.css | 2 +- packages/start/src/shared/ui/Tabs.css | 11 +++--- 6 files changed, 35 insertions(+), 27 deletions(-) diff --git a/packages/start/src/shared/dev-toolbar/functions/SerovalViewer.css b/packages/start/src/shared/dev-toolbar/functions/SerovalViewer.css index 6c930d8bd..2bcb7df04 100644 --- a/packages/start/src/shared/dev-toolbar/functions/SerovalViewer.css +++ b/packages/start/src/shared/dev-toolbar/functions/SerovalViewer.css @@ -5,7 +5,7 @@ [data-start-seroval-promise] { display: flex; - gap: 0.25rem; + gap: 0.5rem; align-items: center; } @@ -27,7 +27,7 @@ display: flex; flex-direction: column; gap: 0.25rem; - padding: 0.5rem; + padding: 0.25rem; width: 100%; max-width: 16rem; @@ -45,11 +45,11 @@ [data-start-seroval-node-content] { display: flex; flex-direction: column; - gap: 0.5rem; + gap: 0.25rem; } [data-start-seroval-value] { - color: oklch(62.3% 0.214 259.815); + color: rgb(249 250 251); } [data-start-seroval-value-wrapper] { diff --git a/packages/start/src/shared/dev-toolbar/functions/index.tsx b/packages/start/src/shared/dev-toolbar/functions/index.tsx index 810223cb2..4249f94fc 100644 --- a/packages/start/src/shared/dev-toolbar/functions/index.tsx +++ b/packages/start/src/shared/dev-toolbar/functions/index.tsx @@ -7,7 +7,7 @@ import { Section } from "../../ui/Section.tsx"; import { Select, SelectOption } from "../../ui/Select.tsx"; import { Tab, TabGroup, TabList, TabPanel } from "../../ui/Tabs.tsx"; import { Text } from "../../ui/Text.tsx"; -import { ArrowLeftIcon, TrashIcon } from "../icons.tsx"; +import { ArrowLeftIcon, FunctionIcon, TrashIcon } from "../icons.tsx"; import { BlobViewer } from "./BlobViewer.tsx"; import { FormDataViewer } from "./FormDataViewer.tsx"; import { HeadersViewer } from "./HeadersViewer.tsx"; @@ -272,6 +272,10 @@ export function ServerFunctionViewer(props: ServerFunctionViewerProps): JSX.Elem {/* list of calls */} +
+ + Server functions +
}>