From a91ce5f36e63da39a1e93131f4037a72239e773c Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Sun, 4 Jan 2026 08:53:42 +0800 Subject: [PATCH 01/26] add seroval json mode --- packages/start/src/config/index.ts | 5 + packages/start/src/server/serialization.ts | 253 ++++++++++++++++++ .../src/server/server-functions-handler.ts | 155 ++++------- packages/start/src/server/server-runtime.ts | 183 +++---------- 4 files changed, 345 insertions(+), 251 deletions(-) create mode 100644 packages/start/src/server/serialization.ts diff --git a/packages/start/src/config/index.ts b/packages/start/src/config/index.ts index 4b1c82179..869aef0cf 100644 --- a/packages/start/src/config/index.ts +++ b/packages/start/src/config/index.ts @@ -21,6 +21,10 @@ export interface SolidStartOptions { routeDir?: string; extensions?: string[]; middleware?: string; + serialization?: { + // This only matters for server function responses + mode?: 'js' | 'json'; + }; } const absolute = (path: string, root: string) => @@ -131,6 +135,7 @@ export function solidStart(options?: SolidStartOptions): Array { "import.meta.env.START_APP_ENTRY": JSON.stringify(appEntryPath), "import.meta.env.START_CLIENT_ENTRY": JSON.stringify(handlers.client), "import.meta.env.START_DEV_OVERLAY": JSON.stringify(start.devOverlay), + "import.meta.env.SEROVAL_MODE": JSON.stringify(start.serialization?.mode || 'json'), }, builder: { sharedPlugins: true, diff --git a/packages/start/src/server/serialization.ts b/packages/start/src/server/serialization.ts new file mode 100644 index 000000000..c157d157e --- /dev/null +++ b/packages/start/src/server/serialization.ts @@ -0,0 +1,253 @@ +import { + crossSerializeStream, + deserialize, + Feature, + fromCrossJSON, + getCrossReferenceHeader, + type SerovalNode, + toCrossJSONStream, +} from "seroval"; +import { + AbortSignalPlugin, + CustomEventPlugin, + DOMExceptionPlugin, + EventPlugin, + FormDataPlugin, + HeadersPlugin, + ReadableStreamPlugin, + RequestPlugin, + ResponsePlugin, + URLPlugin, + URLSearchParamsPlugin, +} from "seroval-plugins/web"; + +// TODO(Alexis): if we can, allow providing an option to extend these. +const DEFAULT_PLUGINS = [ + AbortSignalPlugin, + CustomEventPlugin, + DOMExceptionPlugin, + EventPlugin, + FormDataPlugin, + HeadersPlugin, + ReadableStreamPlugin, + RequestPlugin, + ResponsePlugin, + URLSearchParamsPlugin, + URLPlugin, +]; +const MAX_SERIALIZATION_DEPTH_LIMIT = 64; +const DISABLED_FEATURES = Feature.RegExp; + +/** + * Alexis: + * + * A "chunk" is a piece of data emitted by the streaming serializer. + * Each chunk is represented by a 32-bit value (encoded in hexadecimal), + * followed by the encoded string (8-bit representation). This format + * is important so we know how much of the chunk being streamed we + * are expecting before parsing the entire string data. + * + * This is sort of a bootleg "multipart/form-data" except it's bad at + * handling File/Blob LOL + * + * The format is as follows: + * ;0xFFFFFFFF; + */ +function createChunk(data: string): Uint8Array { + const encodeData = new TextEncoder().encode(data); + const bytes = encodeData.length; + const baseHex = bytes.toString(16); + const totalHex = "00000000".substring(0, 8 - baseHex.length) + baseHex; // 32-bit + const head = new TextEncoder().encode(`;0x${totalHex};`); + + const chunk = new Uint8Array(12 + bytes); + chunk.set(head); + chunk.set(encodeData, 12); + return chunk; +} + +export function serializeToJSStream(id: string, value: any) { + return new ReadableStream({ + start(controller) { + crossSerializeStream(value, { + scopeId: id, + plugins: DEFAULT_PLUGINS, + onSerialize(data: string, initial: boolean) { + controller.enqueue( + createChunk( + initial ? `(${getCrossReferenceHeader(id)},${data})` : data, + ), + ); + }, + onDone() { + controller.close(); + }, + onError(error: any) { + controller.error(error); + }, + }); + }, + }); +} + +export function serializeToJSONStream(value: any) { + return new ReadableStream({ + start(controller) { + toCrossJSONStream(value, { + disabledFeatures: DISABLED_FEATURES, + depthLimit: MAX_SERIALIZATION_DEPTH_LIMIT, + plugins: DEFAULT_PLUGINS, + onParse(node) { + controller.enqueue(createChunk(JSON.stringify(node))); + }, + onDone() { + controller.close(); + }, + onError(error) { + controller.error(error); + }, + }); + }, + }); +} + +class SerovalChunkReader { + reader: ReadableStreamDefaultReader; + buffer: Uint8Array; + done: boolean; + constructor(stream: ReadableStream) { + this.reader = stream.getReader(); + this.buffer = new Uint8Array(0); + this.done = false; + } + + async readChunk() { + // if there's no chunk, read again + const chunk = await this.reader.read(); + if (!chunk.done) { + // repopulate the buffer + const newBuffer = new Uint8Array(this.buffer.length + chunk.value.length); + newBuffer.set(this.buffer); + newBuffer.set(chunk.value, this.buffer.length); + this.buffer = newBuffer; + } else { + this.done = true; + } + } + + async next(): Promise< + { done: true; value: undefined } | { done: false; value: string } + > { + // Check if the buffer is empty + if (this.buffer.length === 0) { + // if we are already done... + if (this.done) { + return { + done: true, + value: undefined, + }; + } + // Otherwise, read a new chunk + await this.readChunk(); + return await this.next(); + } + // Read the "byte header" + // The byte header tells us how big the expected data is + // so we know how much data we should wait before we + // deserialize the data + const head = new TextDecoder().decode(this.buffer.subarray(1, 11)); + const bytes = Number.parseInt(head, 16); // ;0x00000000; + // Check if the buffer has enough bytes to be parsed + while (bytes > this.buffer.length - 12) { + // If it's not enough, and the reader is done + // then the chunk is invalid. + if (this.done) { + throw new Error("Malformed server function stream."); + } + // Otherwise, we read more chunks + await this.readChunk(); + } + // Extract the exact chunk as defined by the byte header + const partial = new TextDecoder().decode( + this.buffer.subarray(12, 12 + bytes), + ); + // The rest goes to the buffer + this.buffer = this.buffer.subarray(12 + bytes); + + // Deserialize the chunk + return { + done: false, + value: partial, + }; + } + + async drain(interpret: (chunk: string) => void) { + while (true) { + const result = await this.next(); + if (result.done) { + break; + } else { + interpret(result.value); + } + } + } +} + +export async function serializeToJSONString(value: any) { + const response = new Response(serializeToJSONStream(value)); + return await response.text(); +} + +export async function deserializeFromJSONString(json: string) { + const blob = new Response(json); + return await deserializeJSONStream(blob); +} + +export async function deserializeJSONStream(response: Response | Request) { + if (!response.body) { + throw new Error("missing body"); + } + const reader = new SerovalChunkReader(response.body); + const result = await reader.next(); + if (!result.done) { + const refs = new Map(); + + function interpretChunk(chunk: string): unknown { + const value = fromCrossJSON(JSON.parse(chunk) as SerovalNode, { + refs, + disabledFeatures: DISABLED_FEATURES, + depthLimit: MAX_SERIALIZATION_DEPTH_LIMIT, + plugins: DEFAULT_PLUGINS, + }); + return value; + } + + void reader.drain(interpretChunk); + + return interpretChunk(result.value); + } + return undefined; +} + +export async function deserializeJSStream(id: string, response: Response) { + if (!response.body) { + throw new Error("missing body"); + } + const reader = new SerovalChunkReader(response.body); + + const result = await reader.next(); + + if (!result.done) { + reader.drain(deserialize).then( + () => { + // @ts-ignore + delete $R[id]; + }, + () => { + // no-op + }, + ); + return deserialize(result.value); + } + return undefined; +} diff --git a/packages/start/src/server/server-functions-handler.ts b/packages/start/src/server/server-functions-handler.ts index 160672684..e5912aaa1 100644 --- a/packages/start/src/server/server-functions-handler.ts +++ b/packages/start/src/server/server-functions-handler.ts @@ -1,74 +1,21 @@ -import { getServerFnById } from "solidstart:server-fn-manifest"; import { parseSetCookie } from "cookie-es"; import { type H3Event, parseCookies } from "h3"; -import { crossSerializeStream, fromJSON, getCrossReferenceHeader } from "seroval"; -import { - CustomEventPlugin, - DOMExceptionPlugin, - EventPlugin, - FormDataPlugin, - HeadersPlugin, - ReadableStreamPlugin, - RequestPlugin, - ResponsePlugin, - URLPlugin, - URLSearchParamsPlugin, -} from "seroval-plugins/web"; import { sharedConfig } from "solid-js"; import { renderToString } from "solid-js/web"; import { provideRequestEvent } from "solid-js/web/storage"; +import { getServerFnById } from "solidstart:server-fn-manifest"; import { getFetchEvent, mergeResponseHeaders } from "./fetchEvent.ts"; import { createPageEvent } from "./handler.ts"; +import { + deserializeFromJSONString, + deserializeJSONStream, + serializeToJSONStream, + serializeToJSStream, +} from "./serialization.ts"; import type { FetchEvent, PageEvent } from "./types.ts"; import { getExpectedRedirectStatus } from "./util.ts"; -function createChunk(data: string) { - const encodeData = new TextEncoder().encode(data); - const bytes = encodeData.length; - const baseHex = bytes.toString(16); - const totalHex = "00000000".substring(0, 8 - baseHex.length) + baseHex; // 32-bit - const head = new TextEncoder().encode(`;0x${totalHex};`); - - const chunk = new Uint8Array(12 + bytes); - chunk.set(head); - chunk.set(encodeData, 12); - return chunk; -} - -function serializeToStream(id: string, value: any) { - return new ReadableStream({ - start(controller) { - crossSerializeStream(value, { - scopeId: id, - plugins: [ - CustomEventPlugin, - DOMExceptionPlugin, - EventPlugin, - FormDataPlugin, - HeadersPlugin, - ReadableStreamPlugin, - RequestPlugin, - ResponsePlugin, - URLSearchParamsPlugin, - URLPlugin, - ], - onSerialize(data: string, initial: boolean) { - controller.enqueue( - createChunk(initial ? `(${getCrossReferenceHeader(id)},${data})` : data), - ); - }, - onDone() { - controller.close(); - }, - onError(error: any) { - controller.error(error); - }, - }); - }, - }); -} - export async function handleServerFunction(h3Event: H3Event) { const event = getFetchEvent(h3Event); const request = event.request; @@ -99,26 +46,10 @@ export async function handleServerFunction(h3Event: H3Event) { if (!instance || h3Event.method === "GET") { const args = url.searchParams.get("args"); if (args) { - const json = JSON.parse(args); - (json.t - ? (fromJSON(json, { - plugins: [ - CustomEventPlugin, - DOMExceptionPlugin, - EventPlugin, - FormDataPlugin, - HeadersPlugin, - ReadableStreamPlugin, - RequestPlugin, - ResponsePlugin, - URLSearchParamsPlugin, - URLPlugin, - ], - }) as any) - : json - ).forEach((arg: any) => { + const result = (await deserializeFromJSONString(args)) as any[]; + for (const arg of result) { parsed.push(arg); - }); + } } } if (h3Event.method === "POST") { @@ -129,21 +60,8 @@ export async function handleServerFunction(h3Event: H3Event) { contentType?.startsWith("application/x-www-form-urlencoded") ) { parsed.push(await event.request.formData()); - } else if (contentType?.startsWith("application/json")) { - parsed = fromJSON(await event.request.json(), { - plugins: [ - CustomEventPlugin, - DOMExceptionPlugin, - EventPlugin, - FormDataPlugin, - HeadersPlugin, - ReadableStreamPlugin, - RequestPlugin, - ResponsePlugin, - URLSearchParamsPlugin, - URLPlugin, - ], - }); + } else { + parsed = (await deserializeJSONStream(event.request.clone())) as any[]; } } try { @@ -178,9 +96,12 @@ export async function handleServerFunction(h3Event: H3Event) { // handle no JS success case if (!instance) return handleNoJS(result, request, parsed); - h3Event.res.headers.set("content-type", "text/javascript"); - - return serializeToStream(instance, result); + if (import.meta.env.SEROVAL_MODE === "js") { + 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) { if (singleFlight && instance) { @@ -189,28 +110,41 @@ 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 = (x as any).customBody(); } else if ((x as any).body === undefined) 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 { x = handleNoJS(x, request, parsed, true); } if (instance) { - h3Event.res.headers.set("content-type", "text/javascript"); - return serializeToStream(instance, x); + if (import.meta.env.SEROVAL_MODE === "js") { + 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; } } -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; @@ -220,7 +154,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); } @@ -237,7 +174,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;`, ); @@ -263,7 +203,7 @@ 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) { @@ -284,7 +224,10 @@ 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) { diff --git a/packages/start/src/server/server-runtime.ts b/packages/start/src/server/server-runtime.ts index 8bbabe40b..d2be79ef9 100644 --- a/packages/start/src/server/server-runtime.ts +++ b/packages/start/src/server/server-runtime.ts @@ -1,122 +1,20 @@ // @ts-ignore - seroval exports issue with NodeNext -import { join } from "pathe"; -import { deserialize, toJSONAsync } from "seroval"; -import { - CustomEventPlugin, - DOMExceptionPlugin, - EventPlugin, - FormDataPlugin, - HeadersPlugin, - ReadableStreamPlugin, - RequestPlugin, - ResponsePlugin, - URLPlugin, - URLSearchParamsPlugin, -} from "seroval-plugins/web"; import { type Component } from "solid-js"; - -class SerovalChunkReader { - reader: ReadableStreamDefaultReader; - buffer: Uint8Array; - done: boolean; - constructor(stream: ReadableStream) { - this.reader = stream.getReader(); - this.buffer = new Uint8Array(0); - this.done = false; - } - - async readChunk() { - // if there's no chunk, read again - const chunk = await this.reader.read(); - if (!chunk.done) { - // repopulate the buffer - let newBuffer = new Uint8Array(this.buffer.length + chunk.value.length); - newBuffer.set(this.buffer); - newBuffer.set(chunk.value, this.buffer.length); - this.buffer = newBuffer; - } else { - this.done = true; - } - } - - async next(): Promise { - // Check if the buffer is empty - if (this.buffer.length === 0) { - // if we are already done... - if (this.done) { - return { - done: true, - value: undefined, - }; - } - // Otherwise, read a new chunk - await this.readChunk(); - return await this.next(); - } - // Read the "byte header" - // The byte header tells us how big the expected data is - // so we know how much data we should wait before we - // deserialize the data - const head = new TextDecoder().decode(this.buffer.subarray(1, 11)); - const bytes = Number.parseInt(head, 16); // ;0x00000000; - // Check if the buffer has enough bytes to be parsed - while (bytes > this.buffer.length - 12) { - // If it's not enough, and the reader is done - // then the chunk is invalid. - if (this.done) { - throw new Error("Malformed server function stream."); - } - // Otherwise, we read more chunks - await this.readChunk(); - } - // Extract the exact chunk as defined by the byte header - const partial = new TextDecoder().decode(this.buffer.subarray(12, 12 + bytes)); - // The rest goes to the buffer - this.buffer = this.buffer.subarray(12 + bytes); - - // Deserialize the chunk - return { - done: false, - value: deserialize(partial), - }; - } - - async drain() { - while (true) { - const result = await this.next(); - if (result.done) { - break; - } - } - } -} - -async function deserializeStream(id: string, response: Response) { - if (!response.body) { - throw new Error("missing body"); - } - const reader = new SerovalChunkReader(response.body); - - const result = await reader.next(); - - if (!result.done) { - reader.drain().then( - () => { - // @ts-ignore - delete $R[id]; - }, - () => { - // no-op - }, - ); - } - - return result.value; -} +import { + deserializeJSONStream, + deserializeJSStream, + serializeToJSONStream, + serializeToJSONString, +} from "./serialization"; 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, @@ -127,20 +25,6 @@ function createRequest(base: string, id: string, instance: string, options: Requ }, }); } - -const plugins = [ - CustomEventPlugin, - DOMExceptionPlugin, - EventPlugin, - FormDataPlugin, - HeadersPlugin, - ReadableStreamPlugin, - RequestPlugin, - ResponsePlugin, - URLSearchParamsPlugin, - URLPlugin, -]; - async function fetchServerFunction( base: string, id: string, @@ -154,15 +38,18 @@ async function fetchServerFunction( ? createRequest(base, id, instance, { ...options, body: args[0] }) : args.length === 1 && args[0] instanceof URLSearchParams ? createRequest(base, id, instance, { - ...options, - body: args[0], - headers: { ...options.headers, "Content-Type": "application/x-www-form-urlencoded" }, - }) + ...options, + body: args[0], + headers: { + ...options.headers, + "Content-Type": "application/x-www-form-urlencoded", + }, + }) : createRequest(base, id, instance, { - ...options, - body: JSON.stringify(await Promise.resolve(toJSONAsync(args, { plugins }))), - headers: { ...options.headers, "Content-Type": "application/json" }, - })); + ...options, + body: serializeToJSONStream(args), + headers: { ...options.headers, "Content-Type": "application/json" }, + })); if ( response.headers.has("Location") || @@ -172,7 +59,8 @@ async function fetchServerFunction( if (response.body) { /* @ts-ignore-next-line */ response.customBody = () => { - return deserializeStream(instance, response); + // TODO check for serialization mode + return deserializeJSStream(instance, response); }; } return response; @@ -184,8 +72,11 @@ async function fetchServerFunction( result = await response.text(); } else if (contentType && contentType.startsWith("application/json")) { result = await response.json(); + } else if (import.meta.env.SEROVAL_MODE === "js") { + // TODO check for serialization mode + result = await deserializeJSStream(instance, response); } else { - result = await deserializeStream(instance, response); + result = await deserializeJSONStream(response); } if (response.headers.has("X-Error")) { throw result; @@ -197,7 +88,8 @@ export function createServerReference(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) { @@ -211,15 +103,16 @@ export function createServerReference(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 + - (args.length - ? `&args=${encodeURIComponent( - JSON.stringify(await Promise.resolve(toJSONAsync(args, { plugins }))), - )}` - : "") + (args.length + ? `&args=${encodeURIComponent( + await serializeToJSONString(args), + )}` + : "") : `${baseURL}_server`, id, options, From 7c5e6e7008a088f181f0ca97523e708bcce9fb06 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Sun, 4 Jan 2026 09:00:33 +0800 Subject: [PATCH 02/26] Update server-runtime.ts --- packages/start/src/server/server-runtime.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/packages/start/src/server/server-runtime.ts b/packages/start/src/server/server-runtime.ts index d2be79ef9..d2a51f2a7 100644 --- a/packages/start/src/server/server-runtime.ts +++ b/packages/start/src/server/server-runtime.ts @@ -59,8 +59,10 @@ async function fetchServerFunction( if (response.body) { /* @ts-ignore-next-line */ response.customBody = () => { - // TODO check for serialization mode - return deserializeJSStream(instance, response); + if (import.meta.env.SEROVAL_MODE === "js") { + return deserializeJSStream(instance, response); + } + return deserializeJSONStream(response); }; } return response; @@ -73,7 +75,6 @@ async function fetchServerFunction( } else if (contentType && contentType.startsWith("application/json")) { result = await response.json(); } else if (import.meta.env.SEROVAL_MODE === "js") { - // TODO check for serialization mode result = await deserializeJSStream(instance, response); } else { result = await deserializeJSONStream(response); From f8fcc2dd1fb4fb42fc178c76823aa5daea5e6b33 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Sun, 4 Jan 2026 09:06:24 +0800 Subject: [PATCH 03/26] Create plenty-geese-enter.md --- .changeset/plenty-geese-enter.md | 5 +++++ 1 file changed, 5 insertions(+) create mode 100644 .changeset/plenty-geese-enter.md diff --git a/.changeset/plenty-geese-enter.md b/.changeset/plenty-geese-enter.md new file mode 100644 index 000000000..27c7c5457 --- /dev/null +++ b/.changeset/plenty-geese-enter.md @@ -0,0 +1,5 @@ +--- +"@solidjs/start": minor +--- + +seroval json mode From 19308b7ca19a6159cf3e9fd43a519a6d44e5feff Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Sun, 4 Jan 2026 09:43:12 +0800 Subject: [PATCH 04/26] Fix client-server comms --- .../src/server/server-functions-handler.ts | 4 ++-- packages/start/src/server/server-runtime.ts | 21 ++++++++++++------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/packages/start/src/server/server-functions-handler.ts b/packages/start/src/server/server-functions-handler.ts index e5912aaa1..c536b940f 100644 --- a/packages/start/src/server/server-functions-handler.ts +++ b/packages/start/src/server/server-functions-handler.ts @@ -60,7 +60,7 @@ export async function handleServerFunction(h3Event: H3Event) { contentType?.startsWith("application/x-www-form-urlencoded") ) { parsed.push(await event.request.formData()); - } else { + } else if (contentType?.startsWith('text/plain')) { parsed = (await deserializeJSONStream(event.request.clone())) as any[]; } } @@ -96,11 +96,11 @@ export async function handleServerFunction(h3Event: H3Event) { // handle no JS success case if (!instance) return handleNoJS(result, request, parsed); + h3Event.res.headers.set("x-serialized", "true"); if (import.meta.env.SEROVAL_MODE === "js") { 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) { diff --git a/packages/start/src/server/server-runtime.ts b/packages/start/src/server/server-runtime.ts index d2a51f2a7..3c0f2a201 100644 --- a/packages/start/src/server/server-runtime.ts +++ b/packages/start/src/server/server-runtime.ts @@ -47,8 +47,11 @@ async function fetchServerFunction( }) : createRequest(base, id, instance, { ...options, - body: serializeToJSONStream(args), - headers: { ...options.headers, "Content-Type": "application/json" }, + // TODO(Alexis): move to serializeToJSONStream + body: await serializeToJSONString(args), + // duplex: 'half', + // body: serializeToJSONStream(args), + headers: { ...options.headers, "Content-Type": "text/plain" }, })); if ( @@ -70,14 +73,16 @@ async function fetchServerFunction( const contentType = response.headers.get("Content-Type"); let result; - if (contentType && contentType.startsWith("text/plain")) { + if (contentType?.startsWith("text/plain")) { result = await response.text(); - } else if (contentType && contentType.startsWith("application/json")) { + } else if (contentType?.startsWith("application/json")) { result = await response.json(); - } else if (import.meta.env.SEROVAL_MODE === "js") { - result = await deserializeJSStream(instance, response); - } else { - result = await deserializeJSONStream(response); + } else if (response.headers.get('x-serialized')) { + if (import.meta.env.SEROVAL_MODE === "js") { + result = await deserializeJSStream(instance, response); + } else { + result = await deserializeJSONStream(response); + } } if (response.headers.has("X-Error")) { throw result; From e291918e0f7cc1caa6ab925fbc92083f7ee5ec58 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Sun, 4 Jan 2026 09:43:17 +0800 Subject: [PATCH 05/26] Add tests --- apps/tests/src/e2e/server-function.test.ts | 5 ++++ .../tests/src/routes/server-function-ping.tsx | 23 +++++++++++++++++++ 2 files changed, 28 insertions(+) create mode 100644 apps/tests/src/routes/server-function-ping.tsx diff --git a/apps/tests/src/e2e/server-function.test.ts b/apps/tests/src/e2e/server-function.test.ts index 7a8131be9..edba86fcc 100644 --- a/apps/tests/src/e2e/server-function.test.ts +++ b/apps/tests/src/e2e/server-function.test.ts @@ -67,4 +67,9 @@ test.describe("server-function", () => { await page.goto("http://localhost:3000/generator-server-function"); await expect(page.locator("#server-fn-test")).toContainText("¡Hola, Mundo!"); }); + + test("should build with a server function ping", async ({ page }) => { + await page.goto("http://localhost:3000/server-function-ping"); + await expect(page.locator("#server-fn-test")).toContainText('{"result":true}'); + }); }); diff --git a/apps/tests/src/routes/server-function-ping.tsx b/apps/tests/src/routes/server-function-ping.tsx new file mode 100644 index 000000000..70f9d2617 --- /dev/null +++ b/apps/tests/src/routes/server-function-ping.tsx @@ -0,0 +1,23 @@ +import { createEffect, createSignal } from "solid-js"; + +async function ping(value: string) { + "use server"; + + return await Promise.resolve(value); +} + +export default function App() { + const [output, setOutput] = createSignal<{ result?: boolean }>({}); + + createEffect(async () => { + const value = `${Math.random() * 1000}`; + const result = await ping(value); + setOutput(prev => ({ ...prev, result: value === result })); + }); + + return ( +
+ {JSON.stringify(output())} +
+ ); +} From 0fa982aa45c4d6573df9f1acf590b28a97b0b627 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Sun, 4 Jan 2026 10:06:47 +0800 Subject: [PATCH 06/26] Add new header --- packages/start/src/server/server-functions-handler.ts | 4 +++- packages/start/src/server/server-runtime.ts | 10 +++++++--- 2 files changed, 10 insertions(+), 4 deletions(-) diff --git a/packages/start/src/server/server-functions-handler.ts b/packages/start/src/server/server-functions-handler.ts index c536b940f..4881e8bbc 100644 --- a/packages/start/src/server/server-functions-handler.ts +++ b/packages/start/src/server/server-functions-handler.ts @@ -60,7 +60,9 @@ export async function handleServerFunction(h3Event: H3Event) { contentType?.startsWith("application/x-www-form-urlencoded") ) { parsed.push(await event.request.formData()); - } else if (contentType?.startsWith('text/plain')) { + } else if (contentType?.startsWith('application/json')) { + parsed = await event.request.json() as any[]; + } else if (request.headers.has('x-serialized')) { parsed = (await deserializeJSONStream(event.request.clone())) as any[]; } } diff --git a/packages/start/src/server/server-runtime.ts b/packages/start/src/server/server-runtime.ts index 3c0f2a201..723542a05 100644 --- a/packages/start/src/server/server-runtime.ts +++ b/packages/start/src/server/server-runtime.ts @@ -1,9 +1,9 @@ -// @ts-ignore - seroval exports issue with NodeNext + import { type Component } from "solid-js"; import { deserializeJSONStream, deserializeJSStream, - serializeToJSONStream, + // serializeToJSONStream, serializeToJSONString, } from "./serialization"; @@ -51,7 +51,11 @@ async function fetchServerFunction( body: await serializeToJSONString(args), // duplex: 'half', // body: serializeToJSONStream(args), - headers: { ...options.headers, "Content-Type": "text/plain" }, + headers: { + ...options.headers, + "x-serialized": "true", + "Content-Type": "text/plain" + }, })); if ( From 9f80e1be117a1376d81395c9112caac226b8bc7d Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Sun, 4 Jan 2026 12:13:06 +0800 Subject: [PATCH 07/26] Update server-functions-handler.ts --- packages/start/src/server/server-functions-handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/start/src/server/server-functions-handler.ts b/packages/start/src/server/server-functions-handler.ts index 4881e8bbc..6152fc8bd 100644 --- a/packages/start/src/server/server-functions-handler.ts +++ b/packages/start/src/server/server-functions-handler.ts @@ -130,11 +130,11 @@ export async function handleServerFunction(h3Event: H3Event) { x = handleNoJS(x, request, parsed, true); } if (instance) { + h3Event.res.headers.set("x-serialized", "true"); if (import.meta.env.SEROVAL_MODE === "js") { 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 2f5839db07e3d7affc607f1ad24150576fdff32d Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Mon, 5 Jan 2026 13:10:35 +0800 Subject: [PATCH 08/26] Clone requests/responses --- .../start/src/server/server-functions-handler.ts | 9 +++++---- packages/start/src/server/server-runtime.ts | 13 +++++++------ 2 files changed, 12 insertions(+), 10 deletions(-) diff --git a/packages/start/src/server/server-functions-handler.ts b/packages/start/src/server/server-functions-handler.ts index 6152fc8bd..62aa27f69 100644 --- a/packages/start/src/server/server-functions-handler.ts +++ b/packages/start/src/server/server-functions-handler.ts @@ -52,18 +52,19 @@ export async function handleServerFunction(h3Event: H3Event) { } } } - if (h3Event.method === "POST") { + if (request.method === "POST") { const contentType = request.headers.get("content-type"); + const clone = request.clone(); if ( contentType?.startsWith("multipart/form-data") || contentType?.startsWith("application/x-www-form-urlencoded") ) { - parsed.push(await event.request.formData()); + parsed.push(await clone.formData()); } else if (contentType?.startsWith('application/json')) { - parsed = await event.request.json() as any[]; + parsed = await clone.json() as any[]; } else if (request.headers.has('x-serialized')) { - parsed = (await deserializeJSONStream(event.request.clone())) as any[]; + parsed = (await deserializeJSONStream(clone)) as any[]; } } try { diff --git a/packages/start/src/server/server-runtime.ts b/packages/start/src/server/server-runtime.ts index 723542a05..3a31cc2f7 100644 --- a/packages/start/src/server/server-runtime.ts +++ b/packages/start/src/server/server-runtime.ts @@ -67,25 +67,26 @@ async function fetchServerFunction( /* @ts-ignore-next-line */ response.customBody = () => { if (import.meta.env.SEROVAL_MODE === "js") { - return deserializeJSStream(instance, response); + return deserializeJSStream(instance, response.clone()); } - return deserializeJSONStream(response); + return deserializeJSONStream(response.clone()); }; } return response; } const contentType = response.headers.get("Content-Type"); + const clone = response.clone(); let result; if (contentType?.startsWith("text/plain")) { - result = await response.text(); + result = await clone.text(); } else if (contentType?.startsWith("application/json")) { - result = await response.json(); + result = await clone.json(); } else if (response.headers.get('x-serialized')) { if (import.meta.env.SEROVAL_MODE === "js") { - result = await deserializeJSStream(instance, response); + result = await deserializeJSStream(instance, clone); } else { - result = await deserializeJSONStream(response); + result = await deserializeJSONStream(clone); } } if (response.headers.has("X-Error")) { From f15a3ac53f1107f2914cf8034b95bb34d110051d Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Thu, 15 Jan 2026 02:15:25 +0800 Subject: [PATCH 09/26] Add more formats --- .../src/server/server-functions-handler.ts | 46 ++++-- .../src/server/server-functions-shared.ts | 15 ++ packages/start/src/server/server-runtime.ts | 138 ++++++++++++++---- 3 files changed, 160 insertions(+), 39 deletions(-) create mode 100644 packages/start/src/server/server-functions-shared.ts diff --git a/packages/start/src/server/server-functions-handler.ts b/packages/start/src/server/server-functions-handler.ts index 62aa27f69..0f7c22ce3 100644 --- a/packages/start/src/server/server-functions-handler.ts +++ b/packages/start/src/server/server-functions-handler.ts @@ -13,6 +13,7 @@ import { serializeToJSONStream, serializeToJSStream, } from "./serialization.ts"; +import { BODY_FORMAL_FILE, BODY_FORMAT_KEY, BodyFormat } from "./server-functions-shared.ts"; import type { FetchEvent, PageEvent } from "./types.ts"; import { getExpectedRedirectStatus } from "./util.ts"; @@ -43,7 +44,7 @@ export async function handleServerFunction(h3Event: H3Event) { let parsed: any[] = []; // grab bound arguments from url when no JS - if (!instance || h3Event.method === "GET") { + if (!instance || request.method === "GET") { const args = url.searchParams.get("args"); if (args) { const result = (await deserializeFromJSONString(args)) as any[]; @@ -54,17 +55,38 @@ export async function handleServerFunction(h3Event: H3Event) { } if (request.method === "POST") { const contentType = request.headers.get("content-type"); + const startType = request.headers.get(BODY_FORMAT_KEY); const clone = request.clone(); - if ( - contentType?.startsWith("multipart/form-data") || - contentType?.startsWith("application/x-www-form-urlencoded") - ) { - parsed.push(await clone.formData()); - } else if (contentType?.startsWith('application/json')) { - parsed = await clone.json() as any[]; - } else if (request.headers.has('x-serialized')) { - parsed = (await deserializeJSONStream(clone)) as any[]; + switch (true) { + case startType === BodyFormat.Seroval: + parsed = (await deserializeJSONStream(clone)) as any[]; + break; + case startType === BodyFormat.String: + parsed.push(await clone.text()); + break; + case startType === BodyFormat.File: { + const formData = await clone.formData(); + parsed.push(formData.get(BODY_FORMAL_FILE)); + break; + } + case startType === BodyFormat.FormData: + case contentType?.startsWith("multipart/form-data"): + parsed.push(await clone.formData()); + break; + case startType === BodyFormat.URLSearchParams: + case contentType?.startsWith("application/x-www-form-urlencoded"): + parsed.push(new URLSearchParams(await clone.text())); + break; + case startType === BodyFormat.Blob: + parsed.push(await clone.blob()); + break; + case startType === BodyFormat.ArrayBuffer: + parsed.push(await clone.arrayBuffer()); + break; + case startType === BodyFormat.Uint8Array: + parsed.push(await clone.bytes()); + break; } } try { @@ -92,14 +114,14 @@ export async function handleServerFunction(h3Event: H3Event) { h3Event.res.status = result.status; if ((result as any).customBody) { result = await (result as any).customBody(); - } else if (result.body == undefined) result = null; + } else if (result.body == null) result = null; } } // handle no JS success case if (!instance) return handleNoJS(result, request, parsed); - h3Event.res.headers.set("x-serialized", "true"); + h3Event.res.headers.set(BODY_FORMAT_KEY, "true"); if (import.meta.env.SEROVAL_MODE === "js") { h3Event.res.headers.set("content-type", "text/javascript"); return serializeToJSStream(instance, result); diff --git a/packages/start/src/server/server-functions-shared.ts b/packages/start/src/server/server-functions-shared.ts new file mode 100644 index 000000000..f7c253dd8 --- /dev/null +++ b/packages/start/src/server/server-functions-shared.ts @@ -0,0 +1,15 @@ + +export const BODY_FORMAT_KEY = "X-Start-Type"; + +export const BODY_FORMAL_FILE = "__START__"; + +export const enum BodyFormat { + Seroval = "0", + String = "1", + FormData = "2", + URLSearchParams = "3", + Blob = "4", + File = "5", + ArrayBuffer = "6", + Uint8Array = "7", +} diff --git a/packages/start/src/server/server-runtime.ts b/packages/start/src/server/server-runtime.ts index 3a31cc2f7..b1496fd31 100644 --- a/packages/start/src/server/server-runtime.ts +++ b/packages/start/src/server/server-runtime.ts @@ -1,4 +1,3 @@ - import { type Component } from "solid-js"; import { deserializeJSONStream, @@ -6,6 +5,7 @@ import { // serializeToJSONStream, serializeToJSONString, } from "./serialization"; +import { BODY_FORMAL_FILE, BODY_FORMAT_KEY, BodyFormat } from "./server-functions-shared"; let INSTANCE = 0; @@ -25,6 +25,113 @@ function createRequest( }, }); } + +function getHeadersAndBody(body: any): { + headers?: HeadersInit; + body: BodyInit; +} | undefined { + switch (true) { + case typeof body === "string": + return { + headers: { + "Content-Type": "text/plain", + [BODY_FORMAT_KEY]: BodyFormat.String, + }, + body, + }; + case body instanceof FormData: + return { + headers: { + "Content-Type": "multipart/form-data", + [BODY_FORMAT_KEY]: BodyFormat.FormData, + }, + body, + }; + case body instanceof URLSearchParams: + return { + headers: { + "Content-Type": "application/x-www-form-urlencoded", + [BODY_FORMAT_KEY]: BodyFormat.URLSearchParams, + }, + body, + }; + case body instanceof Blob: + return { + headers: { + [BODY_FORMAT_KEY]: BodyFormat.Blob, + }, + body, + }; + case body instanceof File: { + const formData = new FormData(); + formData.append(BODY_FORMAL_FILE, body, body.name); + return { + headers: { + [BODY_FORMAT_KEY]: BodyFormat.File, + }, + body: new FormData(), + }; + } + case body instanceof ArrayBuffer: + return { + headers: { + [BODY_FORMAT_KEY]: BodyFormat.ArrayBuffer, + }, + body, + }; + case body instanceof Uint8Array: + return { + headers: { + [BODY_FORMAT_KEY]: BodyFormat.Uint8Array, + }, + body: new Uint8Array(body), + }; + default: + return undefined; + } +} + +async function initializeResponse( + base: string, + id: string, + instance: string, + options: RequestInit, + args: any[], +) { + // No args, skip serialization + if (args.length === 0) { + return createRequest(base, id, instance, options); + } + // For single arguments, we can directly encode as body + if (args.length === 1) { + const body = args[0]; + const result = getHeadersAndBody(body); + if (result) { + return createRequest(base, id, instance, { + ...options, + body: result.body, + headers: { + ...options.headers, + ...result.headers, + }, + }); + } + } + // Fallback to seroval + return createRequest(base, id, instance, { + ...options, + // TODO(Alexis): move to serializeToJSONStream + body: await serializeToJSONString(args), + // duplex: 'half', + // body: serializeToJSONStream(args), + headers: { + ...options.headers, + "Content-Type": "text/plain", + [BODY_FORMAT_KEY]: BodyFormat.Seroval, + }, + }); +} + async function fetchServerFunction( base: string, id: string, @@ -32,31 +139,8 @@ async function fetchServerFunction( args: any[], ) { const instance = `server-fn:${INSTANCE++}`; - const response = await (args.length === 0 - ? createRequest(base, id, instance, options) - : args.length === 1 && args[0] instanceof FormData - ? createRequest(base, id, instance, { ...options, body: args[0] }) - : args.length === 1 && args[0] instanceof URLSearchParams - ? createRequest(base, id, instance, { - ...options, - body: args[0], - headers: { - ...options.headers, - "Content-Type": "application/x-www-form-urlencoded", - }, - }) - : createRequest(base, id, instance, { - ...options, - // TODO(Alexis): move to serializeToJSONStream - body: await serializeToJSONString(args), - // duplex: 'half', - // body: serializeToJSONStream(args), - headers: { - ...options.headers, - "x-serialized": "true", - "Content-Type": "text/plain" - }, - })); + + const response = await initializeResponse(base, id, instance, options, args); if ( response.headers.has("Location") || @@ -82,7 +166,7 @@ async function fetchServerFunction( result = await clone.text(); } else if (contentType?.startsWith("application/json")) { result = await clone.json(); - } else if (response.headers.get('x-serialized')) { + } else if (response.headers.get(BODY_FORMAT_KEY)) { if (import.meta.env.SEROVAL_MODE === "js") { result = await deserializeJSStream(instance, clone); } else { From 25b3f44199171ea341aa8c6177fe8683bb0c0377 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Thu, 15 Jan 2026 02:17:41 +0800 Subject: [PATCH 10/26] Fix `File` encoding --- packages/start/src/server/server-runtime.ts | 16 ++++++++-------- 1 file changed, 8 insertions(+), 8 deletions(-) diff --git a/packages/start/src/server/server-runtime.ts b/packages/start/src/server/server-runtime.ts index b1496fd31..46d07808c 100644 --- a/packages/start/src/server/server-runtime.ts +++ b/packages/start/src/server/server-runtime.ts @@ -55,13 +55,6 @@ function getHeadersAndBody(body: any): { }, body, }; - case body instanceof Blob: - return { - headers: { - [BODY_FORMAT_KEY]: BodyFormat.Blob, - }, - body, - }; case body instanceof File: { const formData = new FormData(); formData.append(BODY_FORMAL_FILE, body, body.name); @@ -69,9 +62,16 @@ function getHeadersAndBody(body: any): { headers: { [BODY_FORMAT_KEY]: BodyFormat.File, }, - body: new FormData(), + body: formData, }; } + case body instanceof Blob: + return { + headers: { + [BODY_FORMAT_KEY]: BodyFormat.Blob, + }, + body, + }; case body instanceof ArrayBuffer: return { headers: { From d0c55e2dcdf00f85ed87b0aadcdffc14bec9c6a1 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Thu, 15 Jan 2026 11:16:00 +0800 Subject: [PATCH 11/26] Update server-functions-handler.ts --- packages/start/src/server/server-functions-handler.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/start/src/server/server-functions-handler.ts b/packages/start/src/server/server-functions-handler.ts index 0f7c22ce3..c03340dae 100644 --- a/packages/start/src/server/server-functions-handler.ts +++ b/packages/start/src/server/server-functions-handler.ts @@ -153,7 +153,7 @@ export async function handleServerFunction(h3Event: H3Event) { x = handleNoJS(x, request, parsed, true); } if (instance) { - h3Event.res.headers.set("x-serialized", "true"); + h3Event.res.headers.set(BODY_FORMAT_KEY, "true"); if (import.meta.env.SEROVAL_MODE === "js") { h3Event.res.headers.set("content-type", "text/javascript"); return serializeToJSStream(instance, x); From a5e55906f9b8a6e70129bc70d79efa5ba61914bc Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Thu, 15 Jan 2026 11:17:48 +0800 Subject: [PATCH 12/26] Update server-runtime.ts --- packages/start/src/server/server-runtime.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/packages/start/src/server/server-runtime.ts b/packages/start/src/server/server-runtime.ts index 46d07808c..c0f4ec4d6 100644 --- a/packages/start/src/server/server-runtime.ts +++ b/packages/start/src/server/server-runtime.ts @@ -4,8 +4,8 @@ import { deserializeJSStream, // serializeToJSONStream, serializeToJSONString, -} from "./serialization"; -import { BODY_FORMAL_FILE, BODY_FORMAT_KEY, BodyFormat } from "./server-functions-shared"; +} from "./serialization.ts"; +import { BODY_FORMAL_FILE, BODY_FORMAT_KEY, BodyFormat } from "./server-functions-shared.ts"; let INSTANCE = 0; From a1f33a64e7bf5f5030e5509b7de9faceb428691d Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Fri, 16 Jan 2026 08:05:01 +0800 Subject: [PATCH 13/26] Fix `FormData` missing boundary --- packages/start/src/server/server-runtime.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/packages/start/src/server/server-runtime.ts b/packages/start/src/server/server-runtime.ts index c0f4ec4d6..cb2b889f2 100644 --- a/packages/start/src/server/server-runtime.ts +++ b/packages/start/src/server/server-runtime.ts @@ -42,7 +42,6 @@ function getHeadersAndBody(body: any): { case body instanceof FormData: return { headers: { - "Content-Type": "multipart/form-data", [BODY_FORMAT_KEY]: BodyFormat.FormData, }, body, From 3cc23ab5e80d054290d8c3976a6c99b20e995381 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Fri, 16 Jan 2026 16:42:23 +0800 Subject: [PATCH 14/26] Add form-data test --- apps/tests/src/e2e/server-function.test.ts | 5 ++++ .../src/routes/server-function-form-data.tsx | 26 +++++++++++++++++++ 2 files changed, 31 insertions(+) create mode 100644 apps/tests/src/routes/server-function-form-data.tsx diff --git a/apps/tests/src/e2e/server-function.test.ts b/apps/tests/src/e2e/server-function.test.ts index edba86fcc..a4dbc3746 100644 --- a/apps/tests/src/e2e/server-function.test.ts +++ b/apps/tests/src/e2e/server-function.test.ts @@ -72,4 +72,9 @@ test.describe("server-function", () => { await page.goto("http://localhost:3000/server-function-ping"); await expect(page.locator("#server-fn-test")).toContainText('{"result":true}'); }); + + test("should build with a server function w/ form data", async ({ page }) => { + await page.goto("http://localhost:3000/server-form-data"); + await expect(page.locator("#server-fn-test")).toContainText('{"result":true}'); + }); }); diff --git a/apps/tests/src/routes/server-function-form-data.tsx b/apps/tests/src/routes/server-function-form-data.tsx new file mode 100644 index 000000000..d54ad095a --- /dev/null +++ b/apps/tests/src/routes/server-function-form-data.tsx @@ -0,0 +1,26 @@ +import { createEffect, createSignal } from "solid-js"; + +async function ping(value: FormData) { + "use server"; + const file = value.get('example') as File; + 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 formData = new FormData(); + formData.append('example', file); + const result = await ping(formData); + const value = await file.text(); + setOutput(prev => ({ ...prev, result: value === result })); + }); + + return ( +
+ {JSON.stringify(output())} +
+ ); +} From 6752f85e44f6c99c825a15cbf3674bf53d35693d Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Fri, 16 Jan 2026 16:46:36 +0800 Subject: [PATCH 15/26] fix tests --- apps/tests/src/e2e/server-function.test.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/tests/src/e2e/server-function.test.ts b/apps/tests/src/e2e/server-function.test.ts index a4dbc3746..861495e3b 100644 --- a/apps/tests/src/e2e/server-function.test.ts +++ b/apps/tests/src/e2e/server-function.test.ts @@ -74,7 +74,7 @@ test.describe("server-function", () => { }); test("should build with a server function w/ form data", async ({ page }) => { - await page.goto("http://localhost:3000/server-form-data"); + await page.goto("http://localhost:3000/server-function-form-data"); await expect(page.locator("#server-fn-test")).toContainText('{"result":true}'); }); }); From fef3a2a7e03b9a1060d5c88b4aa5f4743483a7eb Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Fri, 23 Jan 2026 15:02:13 +0800 Subject: [PATCH 16/26] Bump `seroval` to `1.5.0` --- packages/start/package.json | 2 +- pnpm-lock.yaml | 168 +++++++++++++++++------------------- 2 files changed, 81 insertions(+), 89 deletions(-) diff --git a/packages/start/package.json b/packages/start/package.json index 9170468d3..af2bbca6c 100644 --- a/packages/start/package.json +++ b/packages/start/package.json @@ -56,7 +56,7 @@ "path-to-regexp": "^8.2.0", "pathe": "^2.0.3", "radix3": "^1.1.2", - "seroval": "^1.4.1", + "seroval": "^1.5.0", "seroval-plugins": "^1.4.0", "shiki": "^1.26.1", "solid-js": "^1.9.9", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 25d67e8e2..5d53df2d7 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -15,7 +15,7 @@ importers: devDependencies: '@changesets/cli': specifier: ^2.29.8 - version: 2.29.8(@types/node@25.0.1) + version: 2.29.8(@types/node@25.0.3) citty: specifier: ^0.1.5 version: 0.1.6 @@ -42,7 +42,7 @@ importers: version: 1.9.9 vite: specifier: 7.1.10 - version: 7.1.10(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) + version: 7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) apps/fixtures/basic: dependencies: @@ -60,7 +60,7 @@ importers: version: 1.9.9 vite: specifier: 7.1.10 - version: 7.1.10(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) + version: 7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) apps/fixtures/css: dependencies: @@ -78,11 +78,11 @@ importers: version: 1.9.9 vite: specifier: 7.1.10 - version: 7.1.10(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) + version: 7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) devDependencies: '@tailwindcss/vite': specifier: ^4.1.12 - version: 4.1.17(vite@7.1.10(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1)) + version: 4.1.17(vite@7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1)) tailwindcss: specifier: ^4.1.12 version: 4.1.17 @@ -103,7 +103,7 @@ importers: version: 1.9.9 vite: specifier: 7.1.10 - version: 7.1.10(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) + version: 7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) apps/fixtures/hackernews: dependencies: @@ -115,13 +115,13 @@ importers: version: link:../../../packages/start nitro: specifier: 3.0.1-alpha.0 - version: 3.0.1-alpha.0(@netlify/blobs@10.4.1)(better-sqlite3@11.8.1)(chokidar@4.0.3)(drizzle-orm@0.31.4(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@5.22.0))(@types/better-sqlite3@7.6.12)(better-sqlite3@11.8.1)(prisma@5.22.0))(ioredis@5.6.1)(vite@7.1.10(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1)) + version: 3.0.1-alpha.0(@netlify/blobs@10.4.1)(better-sqlite3@11.8.1)(chokidar@4.0.3)(drizzle-orm@0.31.4(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@5.22.0))(@types/better-sqlite3@7.6.12)(better-sqlite3@11.8.1)(prisma@5.22.0))(ioredis@5.6.1)(vite@7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1)) solid-js: specifier: ^1.9.9 version: 1.9.9 vite: specifier: 7.1.10 - version: 7.1.10(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) + version: 7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) apps/fixtures/nitro-3: dependencies: @@ -136,13 +136,13 @@ importers: version: link:../../../packages/start nitro: specifier: ^3.0.1-alpha.1 - version: 3.0.1-alpha.1(@netlify/blobs@10.4.1)(better-sqlite3@11.8.1)(chokidar@4.0.3)(drizzle-orm@0.31.4(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@5.22.0))(@types/better-sqlite3@7.6.12)(better-sqlite3@11.8.1)(prisma@5.22.0))(ioredis@5.6.1)(rollup@4.52.5)(vite@7.1.10(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1)) + version: 3.0.1-alpha.1(@netlify/blobs@10.4.1)(better-sqlite3@11.8.1)(chokidar@4.0.3)(drizzle-orm@0.31.4(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@5.22.0))(@types/better-sqlite3@7.6.12)(better-sqlite3@11.8.1)(prisma@5.22.0))(ioredis@5.6.1)(rollup@4.52.5)(vite@7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1)) solid-js: specifier: ^1.9.9 version: 1.9.9 vite: specifier: 7.1.10 - version: 7.1.10(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) + version: 7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) apps/fixtures/notes: dependencies: @@ -166,7 +166,7 @@ importers: version: 1.10.2(ioredis@5.6.1) vite: specifier: 7.1.10 - version: 7.1.10(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) + version: 7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) apps/fixtures/todomvc: dependencies: @@ -178,7 +178,7 @@ importers: version: link:../../../packages/start nitro: specifier: 3.0.1-alpha.0 - version: 3.0.1-alpha.0(@netlify/blobs@10.4.1)(better-sqlite3@11.8.1)(chokidar@4.0.3)(drizzle-orm@0.31.4(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@5.22.0))(@types/better-sqlite3@7.6.12)(better-sqlite3@11.8.1)(prisma@5.22.0))(ioredis@5.6.1)(vite@7.1.10(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1)) + version: 3.0.1-alpha.0(@netlify/blobs@10.4.1)(better-sqlite3@11.8.1)(chokidar@4.0.3)(drizzle-orm@0.31.4(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@5.22.0))(@types/better-sqlite3@7.6.12)(better-sqlite3@11.8.1)(prisma@5.22.0))(ioredis@5.6.1)(vite@7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1)) solid-js: specifier: ^1.9.9 version: 1.9.9 @@ -187,7 +187,7 @@ importers: version: 1.10.2(ioredis@5.6.1) vite: specifier: 7.1.10 - version: 7.1.10(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) + version: 7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) apps/landing-page: dependencies: @@ -248,7 +248,7 @@ importers: version: 6.3.7 vite: specifier: ^7.1.10 - version: 7.1.10(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) + version: 7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) apps/tests: dependencies: @@ -284,13 +284,13 @@ importers: version: 1.9.9 vite: specifier: ^7.1.10 - version: 7.1.10(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) + version: 7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) vite-plugin-solid: specifier: ^2.11.9 - version: 2.11.9(patch_hash=71233f1afab9e3ea2dbb03dbda3d84894ef1c6bfbbe69df9f864d03bfe67b6f5)(@testing-library/jest-dom@6.6.2)(solid-js@1.9.9)(vite@7.1.10(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1)) + version: 2.11.9(patch_hash=71233f1afab9e3ea2dbb03dbda3d84894ef1c6bfbbe69df9f864d03bfe67b6f5)(@testing-library/jest-dom@6.6.2)(solid-js@1.9.9)(vite@7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1)) vitest: specifier: ^4.0.10 - version: 4.0.10(@types/debug@4.1.12)(@types/node@25.0.1)(@vitest/browser-playwright@4.0.10)(@vitest/ui@4.0.10)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.30.2)(msw@2.7.0(@types/node@25.0.1)(typescript@5.7.3))(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) + version: 4.0.10(@types/debug@4.1.12)(@types/node@25.0.3)(@vitest/browser-playwright@4.0.10)(@vitest/ui@4.0.10)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.30.2)(msw@2.7.0(@types/node@25.0.3)(typescript@5.7.3))(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) devDependencies: '@playwright/test': specifier: ^1.56.1 @@ -300,10 +300,10 @@ importers: version: 4.17.14 '@vitest/browser': specifier: ^4.0.10 - version: 4.0.10(msw@2.7.0(@types/node@25.0.1)(typescript@5.7.3))(vite@7.1.10(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1))(vitest@4.0.10) + version: 4.0.10(msw@2.7.0(@types/node@25.0.3)(typescript@5.7.3))(vite@7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1))(vitest@4.0.10) '@vitest/browser-playwright': specifier: ^4.0.10 - version: 4.0.10(msw@2.7.0(@types/node@25.0.1)(typescript@5.7.3))(playwright@1.56.1)(vite@7.1.10(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1))(vitest@4.0.10) + version: 4.0.10(msw@2.7.0(@types/node@25.0.3)(typescript@5.7.3))(playwright@1.56.1)(vite@7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1))(vitest@4.0.10) playwright: specifier: ^1.56.1 version: 1.56.1 @@ -324,7 +324,7 @@ importers: version: 0.29.4(solid-js@1.9.9) '@tanstack/server-functions-plugin': specifier: 1.134.5 - version: 1.134.5(vite@7.1.10(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1)) + version: 1.134.5(vite@7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1)) '@types/babel__traverse': specifier: ^7.28.0 version: 7.28.0 @@ -368,11 +368,11 @@ importers: specifier: ^1.1.2 version: 1.1.2 seroval: - specifier: ^1.4.1 - version: 1.4.1 + specifier: ^1.5.0 + version: 1.5.0 seroval-plugins: specifier: ^1.4.0 - version: 1.4.0(seroval@1.4.1) + version: 1.4.0(seroval@1.5.0) shiki: specifier: ^1.26.1 version: 1.26.1 @@ -390,17 +390,17 @@ importers: version: 1.0.6(solid-js@1.9.9) vite-plugin-solid: specifier: ^2.11.9 - version: 2.11.9(patch_hash=71233f1afab9e3ea2dbb03dbda3d84894ef1c6bfbbe69df9f864d03bfe67b6f5)(@testing-library/jest-dom@6.6.2)(solid-js@1.9.9)(vite@7.1.10(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1)) + version: 2.11.9(patch_hash=71233f1afab9e3ea2dbb03dbda3d84894ef1c6bfbbe69df9f864d03bfe67b6f5)(@testing-library/jest-dom@6.6.2)(solid-js@1.9.9)(vite@7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1)) devDependencies: '@types/babel__core': specifier: ^7.20.5 version: 7.20.5 vite: specifier: ^7.1.10 - version: 7.1.10(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) + version: 7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) vitest: specifier: ^4.0.10 - version: 4.0.10(@types/debug@4.1.12)(@types/node@25.0.1)(@vitest/browser-playwright@4.0.10)(@vitest/ui@4.0.10)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.30.2)(msw@2.7.0(@types/node@25.0.1)(typescript@5.7.3))(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) + version: 4.0.10(@types/debug@4.1.12)(@types/node@25.0.3)(@vitest/browser-playwright@4.0.10)(@vitest/ui@4.0.10)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.30.2)(msw@2.7.0(@types/node@25.0.3)(typescript@5.7.3))(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) packages/start-nitro-v2-vite-plugin: dependencies: @@ -410,7 +410,7 @@ importers: devDependencies: vite: specifier: ^7.1.10 - version: 7.1.10(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) + version: 7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) packages: @@ -2221,9 +2221,6 @@ packages: '@types/node@24.9.1': resolution: {integrity: sha512-QoiaXANRkSXK6p0Duvt56W208du4P9Uye9hWLWgGMDTEoKPhuenzNcC4vGUmrNkiOKTlIrBoyNQYNpSwfEZXSg==} - '@types/node@25.0.1': - resolution: {integrity: sha512-czWPzKIAXucn9PtsttxmumiQ9N0ok9FrBwgRWrwmVLlp86BrMExzvXRLFYRJ+Ex3g6yqj+KuaxfX1JTgV2lpfg==} - '@types/node@25.0.3': resolution: {integrity: sha512-W609buLVRVmeW693xKfzHeIV6nJGGz98uCPfeXI1ELMLXVeKYZ9m15fAMSaUPBHYLGFsVRcMmSCksQOrZV9BYA==} @@ -4854,8 +4851,8 @@ packages: resolution: {integrity: sha512-RbcPH1n5cfwKrru7v7+zrZvjLurgHhGyso3HTyGtRivGWgYjbOmGuivCQaORNELjNONoK35nj28EoWul9sb1zQ==} engines: {node: '>=10'} - seroval@1.4.1: - resolution: {integrity: sha512-9GOc+8T6LN4aByLN75uRvMbrwY5RDBW6lSlknsY4LEa9ZmWcxKcRe1G/Q3HZXjltxMHTrStnvrwAICxZrhldtg==} + seroval@1.5.0: + resolution: {integrity: sha512-OE4cvmJ1uSPrKorFIH9/w/Qwuvi/IMcGbv5RKgcJ/zjA/IohDLU6SVaxFN9FwajbP7nsX0dQqMDes1whk3y+yw==} engines: {node: '>=10'} serve-placeholder@2.0.2: @@ -6163,7 +6160,7 @@ snapshots: dependencies: '@changesets/types': 6.1.0 - '@changesets/cli@2.29.8(@types/node@25.0.1)': + '@changesets/cli@2.29.8(@types/node@25.0.3)': dependencies: '@changesets/apply-release-plan': 7.0.14 '@changesets/assemble-release-plan': 6.0.9 @@ -6179,7 +6176,7 @@ snapshots: '@changesets/should-skip-package': 0.1.2 '@changesets/types': 6.1.0 '@changesets/write': 0.4.0 - '@inquirer/external-editor': 1.0.3(@types/node@25.0.1) + '@inquirer/external-editor': 1.0.3(@types/node@25.0.3) '@manypkg/get-packages': 1.1.3 ansi-colors: 4.1.3 ci-info: 3.9.0 @@ -6562,41 +6559,41 @@ snapshots: '@inquirer/ansi@1.0.2': optional: true - '@inquirer/confirm@5.1.21(@types/node@25.0.1)': + '@inquirer/confirm@5.1.21(@types/node@25.0.3)': dependencies: - '@inquirer/core': 10.3.2(@types/node@25.0.1) - '@inquirer/type': 3.0.10(@types/node@25.0.1) + '@inquirer/core': 10.3.2(@types/node@25.0.3) + '@inquirer/type': 3.0.10(@types/node@25.0.3) optionalDependencies: - '@types/node': 25.0.1 + '@types/node': 25.0.3 optional: true - '@inquirer/core@10.3.2(@types/node@25.0.1)': + '@inquirer/core@10.3.2(@types/node@25.0.3)': dependencies: '@inquirer/ansi': 1.0.2 '@inquirer/figures': 1.0.15 - '@inquirer/type': 3.0.10(@types/node@25.0.1) + '@inquirer/type': 3.0.10(@types/node@25.0.3) cli-width: 4.1.0 mute-stream: 2.0.0 signal-exit: 4.1.0 wrap-ansi: 6.2.0 yoctocolors-cjs: 2.1.3 optionalDependencies: - '@types/node': 25.0.1 + '@types/node': 25.0.3 optional: true - '@inquirer/external-editor@1.0.3(@types/node@25.0.1)': + '@inquirer/external-editor@1.0.3(@types/node@25.0.3)': dependencies: chardet: 2.1.1 iconv-lite: 0.7.1 optionalDependencies: - '@types/node': 25.0.1 + '@types/node': 25.0.3 '@inquirer/figures@1.0.15': optional: true - '@inquirer/type@3.0.10(@types/node@25.0.1)': + '@inquirer/type@3.0.10(@types/node@25.0.3)': optionalDependencies: - '@types/node': 25.0.1 + '@types/node': 25.0.3 optional: true '@internationalized/date@3.5.4': @@ -7548,14 +7545,14 @@ snapshots: postcss-selector-parser: 6.0.10 tailwindcss: 3.4.17 - '@tailwindcss/vite@4.1.17(vite@7.1.10(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1))': + '@tailwindcss/vite@4.1.17(vite@7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1))': dependencies: '@tailwindcss/node': 4.1.17 '@tailwindcss/oxide': 4.1.17 tailwindcss: 4.1.17 - vite: 7.1.10(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) + vite: 7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) - '@tanstack/directive-functions-plugin@1.134.5(vite@7.1.10(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1))': + '@tanstack/directive-functions-plugin@1.134.5(vite@7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1))': dependencies: '@babel/code-frame': 7.27.1 '@babel/core': 7.28.3 @@ -7565,7 +7562,7 @@ snapshots: babel-dead-code-elimination: 1.0.10 pathe: 2.0.3 tiny-invariant: 1.3.3 - vite: 7.1.10(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) + vite: 7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) transitivePeerDependencies: - supports-color @@ -7582,7 +7579,7 @@ snapshots: transitivePeerDependencies: - supports-color - '@tanstack/server-functions-plugin@1.134.5(vite@7.1.10(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1))': + '@tanstack/server-functions-plugin@1.134.5(vite@7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1))': dependencies: '@babel/code-frame': 7.27.1 '@babel/core': 7.28.3 @@ -7591,7 +7588,7 @@ snapshots: '@babel/template': 7.27.2 '@babel/traverse': 7.28.5 '@babel/types': 7.28.5 - '@tanstack/directive-functions-plugin': 1.134.5(vite@7.1.10(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1)) + '@tanstack/directive-functions-plugin': 1.134.5(vite@7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1)) babel-dead-code-elimination: 1.0.10 tiny-invariant: 1.3.3 transitivePeerDependencies: @@ -7699,11 +7696,6 @@ snapshots: undici-types: 7.16.0 optional: true - '@types/node@25.0.1': - dependencies: - undici-types: 7.16.0 - optional: true - '@types/node@25.0.3': dependencies: undici-types: 7.16.0 @@ -7789,29 +7781,29 @@ snapshots: - rollup - supports-color - '@vitest/browser-playwright@4.0.10(msw@2.7.0(@types/node@25.0.1)(typescript@5.7.3))(playwright@1.56.1)(vite@7.1.10(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1))(vitest@4.0.10)': + '@vitest/browser-playwright@4.0.10(msw@2.7.0(@types/node@25.0.3)(typescript@5.7.3))(playwright@1.56.1)(vite@7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1))(vitest@4.0.10)': dependencies: - '@vitest/browser': 4.0.10(msw@2.7.0(@types/node@25.0.1)(typescript@5.7.3))(vite@7.1.10(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1))(vitest@4.0.10) - '@vitest/mocker': 4.0.10(msw@2.7.0(@types/node@25.0.1)(typescript@5.7.3))(vite@7.1.10(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1)) + '@vitest/browser': 4.0.10(msw@2.7.0(@types/node@25.0.3)(typescript@5.7.3))(vite@7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1))(vitest@4.0.10) + '@vitest/mocker': 4.0.10(msw@2.7.0(@types/node@25.0.3)(typescript@5.7.3))(vite@7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1)) playwright: 1.56.1 tinyrainbow: 3.0.3 - vitest: 4.0.10(@types/debug@4.1.12)(@types/node@25.0.1)(@vitest/browser-playwright@4.0.10)(@vitest/ui@4.0.10)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.30.2)(msw@2.7.0(@types/node@25.0.1)(typescript@5.7.3))(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) + vitest: 4.0.10(@types/debug@4.1.12)(@types/node@25.0.3)(@vitest/browser-playwright@4.0.10)(@vitest/ui@4.0.10)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.30.2)(msw@2.7.0(@types/node@25.0.3)(typescript@5.7.3))(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) transitivePeerDependencies: - bufferutil - msw - utf-8-validate - vite - '@vitest/browser@4.0.10(msw@2.7.0(@types/node@25.0.1)(typescript@5.7.3))(vite@7.1.10(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1))(vitest@4.0.10)': + '@vitest/browser@4.0.10(msw@2.7.0(@types/node@25.0.3)(typescript@5.7.3))(vite@7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1))(vitest@4.0.10)': dependencies: - '@vitest/mocker': 4.0.10(msw@2.7.0(@types/node@25.0.1)(typescript@5.7.3))(vite@7.1.10(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1)) + '@vitest/mocker': 4.0.10(msw@2.7.0(@types/node@25.0.3)(typescript@5.7.3))(vite@7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1)) '@vitest/utils': 4.0.10 magic-string: 0.30.21 pixelmatch: 7.1.0 pngjs: 7.0.0 sirv: 3.0.2 tinyrainbow: 3.0.3 - vitest: 4.0.10(@types/debug@4.1.12)(@types/node@25.0.1)(@vitest/browser-playwright@4.0.10)(@vitest/ui@4.0.10)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.30.2)(msw@2.7.0(@types/node@25.0.1)(typescript@5.7.3))(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) + vitest: 4.0.10(@types/debug@4.1.12)(@types/node@25.0.3)(@vitest/browser-playwright@4.0.10)(@vitest/ui@4.0.10)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.30.2)(msw@2.7.0(@types/node@25.0.3)(typescript@5.7.3))(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) ws: 8.18.3 transitivePeerDependencies: - bufferutil @@ -7828,14 +7820,14 @@ snapshots: chai: 6.2.1 tinyrainbow: 3.0.3 - '@vitest/mocker@4.0.10(msw@2.7.0(@types/node@25.0.1)(typescript@5.7.3))(vite@7.1.10(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1))': + '@vitest/mocker@4.0.10(msw@2.7.0(@types/node@25.0.3)(typescript@5.7.3))(vite@7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1))': dependencies: '@vitest/spy': 4.0.10 estree-walker: 3.0.3 magic-string: 0.30.21 optionalDependencies: - msw: 2.7.0(@types/node@25.0.1)(typescript@5.7.3) - vite: 7.1.10(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) + msw: 2.7.0(@types/node@25.0.3)(typescript@5.7.3) + vite: 7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) '@vitest/pretty-format@4.0.10': dependencies: @@ -7863,7 +7855,7 @@ snapshots: sirv: 3.0.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vitest: 4.0.10(@types/debug@4.1.12)(@types/node@25.0.1)(@vitest/browser-playwright@4.0.10)(@vitest/ui@4.0.10)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.30.2)(msw@2.7.0(@types/node@25.0.1)(typescript@5.7.3))(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) + vitest: 4.0.10(@types/debug@4.1.12)(@types/node@25.0.3)(@vitest/browser-playwright@4.0.10)(@vitest/ui@4.0.10)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.30.2)(msw@2.7.0(@types/node@25.0.3)(typescript@5.7.3))(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) '@vitest/utils@4.0.10': dependencies: @@ -9589,12 +9581,12 @@ snapshots: ms@2.1.3: {} - msw@2.7.0(@types/node@25.0.1)(typescript@5.7.3): + msw@2.7.0(@types/node@25.0.3)(typescript@5.7.3): dependencies: '@bundled-es-modules/cookie': 2.0.1 '@bundled-es-modules/statuses': 1.0.1 '@bundled-es-modules/tough-cookie': 0.1.6 - '@inquirer/confirm': 5.1.21(@types/node@25.0.1) + '@inquirer/confirm': 5.1.21(@types/node@25.0.3) '@mswjs/interceptors': 0.37.6 '@open-draft/deferred-promise': 2.2.0 '@open-draft/until': 2.1.0 @@ -9642,7 +9634,7 @@ snapshots: nf3@0.1.12: {} - nitro@3.0.1-alpha.0(@netlify/blobs@10.4.1)(better-sqlite3@11.8.1)(chokidar@4.0.3)(drizzle-orm@0.31.4(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@5.22.0))(@types/better-sqlite3@7.6.12)(better-sqlite3@11.8.1)(prisma@5.22.0))(ioredis@5.6.1)(vite@7.1.10(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1)): + nitro@3.0.1-alpha.0(@netlify/blobs@10.4.1)(better-sqlite3@11.8.1)(chokidar@4.0.3)(drizzle-orm@0.31.4(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@5.22.0))(@types/better-sqlite3@7.6.12)(better-sqlite3@11.8.1)(prisma@5.22.0))(ioredis@5.6.1)(vite@7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1)): dependencies: consola: 3.4.2 cookie-es: 2.0.0 @@ -9662,7 +9654,7 @@ snapshots: unenv: 2.0.0-rc.21 unstorage: 2.0.0-alpha.3(@netlify/blobs@10.4.1)(chokidar@4.0.3)(db0@0.3.4(better-sqlite3@11.8.1)(drizzle-orm@0.31.4(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@5.22.0))(@types/better-sqlite3@7.6.12)(better-sqlite3@11.8.1)(prisma@5.22.0)))(ioredis@5.6.1)(ofetch@1.4.1) optionalDependencies: - vite: 7.1.10(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) + vite: 7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -9692,7 +9684,7 @@ snapshots: - sqlite3 - uploadthing - nitro@3.0.1-alpha.1(@netlify/blobs@10.4.1)(better-sqlite3@11.8.1)(chokidar@4.0.3)(drizzle-orm@0.31.4(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@5.22.0))(@types/better-sqlite3@7.6.12)(better-sqlite3@11.8.1)(prisma@5.22.0))(ioredis@5.6.1)(rollup@4.52.5)(vite@7.1.10(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1)): + nitro@3.0.1-alpha.1(@netlify/blobs@10.4.1)(better-sqlite3@11.8.1)(chokidar@4.0.3)(drizzle-orm@0.31.4(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@5.22.0))(@types/better-sqlite3@7.6.12)(better-sqlite3@11.8.1)(prisma@5.22.0))(ioredis@5.6.1)(rollup@4.52.5)(vite@7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1)): dependencies: consola: 3.4.2 crossws: 0.4.1(srvx@0.9.6) @@ -9710,7 +9702,7 @@ snapshots: unstorage: 2.0.0-alpha.4(@netlify/blobs@10.4.1)(chokidar@4.0.3)(db0@0.3.4(better-sqlite3@11.8.1)(drizzle-orm@0.31.4(@opentelemetry/api@1.9.0)(@prisma/client@5.22.0(prisma@5.22.0))(@types/better-sqlite3@7.6.12)(better-sqlite3@11.8.1)(prisma@5.22.0)))(ioredis@5.6.1)(ofetch@2.0.0-alpha.3) optionalDependencies: rollup: 4.52.5 - vite: 7.1.10(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) + vite: 7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) transitivePeerDependencies: - '@azure/app-configuration' - '@azure/cosmos' @@ -10518,13 +10510,13 @@ snapshots: dependencies: seroval: 1.3.2 - seroval-plugins@1.4.0(seroval@1.4.1): + seroval-plugins@1.4.0(seroval@1.5.0): dependencies: - seroval: 1.4.1 + seroval: 1.5.0 seroval@1.3.2: {} - seroval@1.4.1: {} + seroval@1.5.0: {} serve-placeholder@2.0.2: dependencies: @@ -11205,7 +11197,7 @@ snapshots: '@types/unist': 3.0.3 vfile-message: 4.0.2 - vite-plugin-solid@2.11.9(patch_hash=71233f1afab9e3ea2dbb03dbda3d84894ef1c6bfbbe69df9f864d03bfe67b6f5)(@testing-library/jest-dom@6.6.2)(solid-js@1.9.9)(vite@7.1.10(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1)): + vite-plugin-solid@2.11.9(patch_hash=71233f1afab9e3ea2dbb03dbda3d84894ef1c6bfbbe69df9f864d03bfe67b6f5)(@testing-library/jest-dom@6.6.2)(solid-js@1.9.9)(vite@7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1)): dependencies: '@babel/core': 7.28.3 '@types/babel__core': 7.20.5 @@ -11213,14 +11205,14 @@ snapshots: merge-anything: 5.1.7 solid-js: 1.9.9 solid-refresh: 0.6.3(solid-js@1.9.9) - vite: 7.1.10(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) - vitefu: 1.1.1(vite@7.1.10(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1)) + vite: 7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) + vitefu: 1.1.1(vite@7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1)) optionalDependencies: '@testing-library/jest-dom': 6.6.2 transitivePeerDependencies: - supports-color - vite@7.1.10(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1): + vite@7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1): dependencies: esbuild: 0.25.11 fdir: 6.5.0(picomatch@4.0.3) @@ -11229,7 +11221,7 @@ snapshots: rollup: 4.52.5 tinyglobby: 0.2.15 optionalDependencies: - '@types/node': 25.0.1 + '@types/node': 25.0.3 fsevents: 2.3.3 jiti: 2.6.1 lightningcss: 1.30.2 @@ -11237,14 +11229,14 @@ snapshots: tsx: 4.19.2 yaml: 2.8.1 - vitefu@1.1.1(vite@7.1.10(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1)): + vitefu@1.1.1(vite@7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1)): optionalDependencies: - vite: 7.1.10(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) + vite: 7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) - vitest@4.0.10(@types/debug@4.1.12)(@types/node@25.0.1)(@vitest/browser-playwright@4.0.10)(@vitest/ui@4.0.10)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.30.2)(msw@2.7.0(@types/node@25.0.1)(typescript@5.7.3))(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1): + vitest@4.0.10(@types/debug@4.1.12)(@types/node@25.0.3)(@vitest/browser-playwright@4.0.10)(@vitest/ui@4.0.10)(jiti@2.6.1)(jsdom@25.0.1)(lightningcss@1.30.2)(msw@2.7.0(@types/node@25.0.3)(typescript@5.7.3))(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1): dependencies: '@vitest/expect': 4.0.10 - '@vitest/mocker': 4.0.10(msw@2.7.0(@types/node@25.0.1)(typescript@5.7.3))(vite@7.1.10(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1)) + '@vitest/mocker': 4.0.10(msw@2.7.0(@types/node@25.0.3)(typescript@5.7.3))(vite@7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1)) '@vitest/pretty-format': 4.0.10 '@vitest/runner': 4.0.10 '@vitest/snapshot': 4.0.10 @@ -11261,12 +11253,12 @@ snapshots: tinyexec: 0.3.2 tinyglobby: 0.2.15 tinyrainbow: 3.0.3 - vite: 7.1.10(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) + vite: 7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1) why-is-node-running: 2.3.0 optionalDependencies: '@types/debug': 4.1.12 - '@types/node': 25.0.1 - '@vitest/browser-playwright': 4.0.10(msw@2.7.0(@types/node@25.0.1)(typescript@5.7.3))(playwright@1.56.1)(vite@7.1.10(@types/node@25.0.1)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1))(vitest@4.0.10) + '@types/node': 25.0.3 + '@vitest/browser-playwright': 4.0.10(msw@2.7.0(@types/node@25.0.3)(typescript@5.7.3))(playwright@1.56.1)(vite@7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1))(vitest@4.0.10) '@vitest/ui': 4.0.10(vitest@4.0.10) jsdom: 25.0.1 transitivePeerDependencies: From b32d7faba779a65ba10d096baaacb13739b8da19 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Wed, 14 Jan 2026 14:39:21 +0800 Subject: [PATCH 17/26] Add server function inspector --- .../tests/src/routes/server-function-ping.tsx | 28 +- apps/tests/test-results/.last-run.json | 4 - packages/start/package.json | 2 +- packages/start/src/server/serialization.ts | 2 +- packages/start/src/server/server-runtime.ts | 16 +- packages/start/src/shared/ErrorBoundary.tsx | 60 +- .../shared/dev-overlay/DevOverlayDialog.tsx | 115 ++- .../start/src/shared/dev-overlay/styles.css | 92 +- .../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 + pnpm-lock.yaml | 18 +- 38 files changed, 2054 insertions(+), 176 deletions(-) delete mode 100644 apps/tests/test-results/.last-run.json 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/apps/tests/test-results/.last-run.json b/apps/tests/test-results/.last-run.json deleted file mode 100644 index cbcc1fbac..000000000 --- a/apps/tests/test-results/.last-run.json +++ /dev/null @@ -1,4 +0,0 @@ -{ - "status": "passed", - "failedTests": [] -} \ No newline at end of file diff --git a/packages/start/package.json b/packages/start/package.json index af2bbca6c..10a3d0477 100644 --- a/packages/start/package.json +++ b/packages/start/package.json @@ -62,7 +62,7 @@ "solid-js": "^1.9.9", "source-map-js": "^1.2.1", "srvx": "^0.9.1", - "terracotta": "^1.0.6", + "terracotta": "^1.1.0", "vite-plugin-solid": "^2.11.9" }, "engines": { diff --git a/packages/start/src/server/serialization.ts b/packages/start/src/server/serialization.ts index c157d157e..8c0d0c9fb 100644 --- a/packages/start/src/server/serialization.ts +++ b/packages/start/src/server/serialization.ts @@ -111,7 +111,7 @@ export function serializeToJSONStream(value: any) { }); } -class SerovalChunkReader { +export class SerovalChunkReader { reader: ReadableStreamDefaultReader; buffer: Uint8Array; done: boolean; diff --git a/packages/start/src/server/server-runtime.ts b/packages/start/src/server/server-runtime.ts index cb2b889f2..995947236 100644 --- a/packages/start/src/server/server-runtime.ts +++ b/packages/start/src/server/server-runtime.ts @@ -1,4 +1,8 @@ import { type Component } from "solid-js"; +import { + pushRequest, + pushResponse, +} from "../shared/server-function-inspector/server-function-tracker"; import { deserializeJSONStream, deserializeJSStream, @@ -9,13 +13,13 @@ import { BODY_FORMAL_FILE, BODY_FORMAT_KEY, BodyFormat } from "./server-function 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: { @@ -24,6 +28,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; } function getHeadersAndBody(body: any): { 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 e3b836d7a..2ff9a266d 100644 --- a/packages/start/src/shared/dev-overlay/DevOverlayDialog.tsx +++ b/packages/start/src/shared/dev-overlay/DevOverlayDialog.tsx @@ -2,11 +2,19 @@ import ErrorStackParser from "error-stack-parser"; import * as htmlToImage from "html-to-image"; import type { JSX } from "solid-js"; -import { ErrorBoundary, For, Show, Suspense, createMemo, createSignal } 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 +31,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 +71,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,13 +105,18 @@ function StackFramesContent(props: StackFramesContentProps) {
{(() => { - const data = createStackFrame(selectedFrame(), () => props.isCompiled); + const data = createStackFrame( + selectedFrame(), + () => props.isCompiled, + ); return ( }> }> - {source => ( + {(source) => ( <> - {source.source} + + {source.source} +
- {current => ( + {(current) => ( + {current.functionName ?? ""} @@ -137,7 +158,7 @@ function StackFramesContent(props: StackFramesContentProps) { name: current.getFunctionName(), })} -
+ } > {(() => { @@ -145,12 +166,17 @@ function StackFramesContent(props: StackFramesContentProps) { return ( - {source => ( - + {(source) => ( + {source.name ?? ""} - {getFilePath(source)} + + {getFilePath(source)} + )} @@ -173,7 +199,9 @@ interface StackFramesProps { function StackFrames(props: StackFramesProps) { return ( - {current => } + {(current) => ( + + )} ); } @@ -186,7 +214,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 +226,7 @@ export default function DevOverlayDialog(props: DevOverlayDialogProps): JSX.Elem }); function goPrev() { - setCurrentPage(c => { + setCurrentPage((c) => { if (c > 1) { return c - 1; } @@ -205,7 +235,7 @@ export default function DevOverlayDialog(props: DevOverlayDialogProps): JSX.Elem } function goNext() { - setCurrentPage(c => { + setCurrentPage((c) => { if (c < length()) { return c + 1; } @@ -214,7 +244,7 @@ export default function DevOverlayDialog(props: DevOverlayDialogProps): JSX.Elem } function toggleIsCompiled() { - setIsCompiled(c => !c); + setIsCompiled((c) => !c); } const [panel, setPanel] = createSignal(); @@ -228,7 +258,7 @@ export default function DevOverlayDialog(props: DevOverlayDialogProps): JSX.Elem transform: "scale(0.75)", }, }) - .then(url => { + .then((url) => { download(url, "start-screenshot.png"); }); } @@ -239,7 +269,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(); } @@ -249,10 +282,10 @@ export default function DevOverlayDialog(props: DevOverlayDialogProps): JSX.Elem return ( - +
- - + +
@@ -264,43 +297,45 @@ export default function DevOverlayDialog(props: DevOverlayDialogProps): JSX.Elem
1}>
- +
{`${truncated()} of ${props.errors.length}`}
- +
- - - - - +
- {current => ( + {(current) => (
diff --git a/packages/start/src/shared/dev-overlay/styles.css b/packages/start/src/shared/dev-overlay/styles.css index 24a0e416d..ffe2b18b7 100644 --- a/packages/start/src/shared/dev-overlay/styles.css +++ b/packages/start/src/shared/dev-overlay/styles.css @@ -1,28 +1,8 @@ -.dev-overlay { - position: fixed; - inset: 0px; - z-index: 50; - overflow-y: auto; -} - -.dev-overlay>div { - position: relative; - display: flex; - align-items: center; - justify-content: center; - min-height: 100vh; -} - -.dev-overlay-background { - position: absolute; - inset: 0px; - background-color: rgb(17 24 39 / 0.5); -} - .dev-overlay-panel { display: flex; flex-direction: column; width: 75vw; + max-height: 75vh; margin-top: 2rem; margin-bottom: 2rem; gap: 0.5rem; @@ -30,11 +10,6 @@ font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; } -.dev-overlay-panel-container { - margin: 2rem; - z-index: 10; -} - .dev-overlay-navbar { display: flex; flex-direction: row; @@ -54,44 +29,6 @@ padding: 0.25rem; } -.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; -} - -.dev-overlay-button:hover { - background-color: rgb(229 231 235); - color: rgb(55 65 81); -} - -.dev-overlay-button:focus { - outline: 2px solid transparent; - outline-offset: 2px -} - -.dev-overlay-button:focus-visible { - box-shadow: 0 0 0 calc(3px) rgb(17 24 39 / 0.75); -} - -.dev-overlay-button:active { - background-color: rgb(243 244 246); - color: rgb(31 41 55); -} - -.dev-overlay-button>svg { - height: 1.5rem; - width: 1.5rem; -} - .dev-overlay-page-counter { font-size: 0.875rem; line-height: 1.25rem; @@ -174,15 +111,8 @@ .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; } .dev-overlay-stack-frame { @@ -191,23 +121,6 @@ align-items: center; justify-content: space-between; gap: 1rem; - color: rgb(17 24 39); - cursor: default; - user-select: none; - padding-top: 0.5rem; - padding-bottom: 0.5rem; - padding-left: 1rem; - padding-right: 1rem; - font-size: 0.875rem; - line-height: 1.25rem; - outline: none; -} - -.dev-overlay-stack-frame[tc-active], -.dev-overlay-stack-frame[tc-selected], -.dev-overlay-stack-frame:focus { - background-color: rgb(219 234 254); - color: rgb(30 58 138); } .dev-overlay-stack-frame-function { @@ -254,7 +167,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 +176,6 @@ .dev-overlay-code-view { overflow: auto; min-width: 100%; - min-height: 16rem; } .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 diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 5d53df2d7..0d3ec748a 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -386,8 +386,8 @@ importers: specifier: ^0.9.1 version: 0.9.6 terracotta: - specifier: ^1.0.6 - version: 1.0.6(solid-js@1.9.9) + specifier: ^1.1.0 + version: 1.1.0(solid-js@1.9.9) vite-plugin-solid: specifier: ^2.11.9 version: 2.11.9(patch_hash=71233f1afab9e3ea2dbb03dbda3d84894ef1c6bfbbe69df9f864d03bfe67b6f5)(@testing-library/jest-dom@6.6.2)(solid-js@1.9.9)(vite@7.1.10(@types/node@25.0.3)(jiti@2.6.1)(lightningcss@1.30.2)(terser@5.44.0)(tsx@4.19.2)(yaml@2.8.1)) @@ -4958,8 +4958,8 @@ packages: peerDependencies: solid-js: ^1.6.12 - solid-use@0.9.0: - resolution: {integrity: sha512-8TGwB4m3qQ7qKo8Lg0pi/ZyyGVmQIjC4sPyxRCH7VPds0BzSsT734PhP3jhR6zMJxoYHM+uoivjq0XdpzXeOJg==} + solid-use@0.9.1: + resolution: {integrity: sha512-UwvXDVPlrrbj/9ewG9ys5uL2IO4jSiwys2KPzK4zsnAcmEl7iDafZWW1Mo4BSEWOmQCGK6IvpmGHo1aou8iOFw==} engines: {node: '>=10'} peerDependencies: solid-js: ^1.7 @@ -5149,8 +5149,8 @@ packages: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} - terracotta@1.0.6: - resolution: {integrity: sha512-yVrmT/Lg6a3tEbeYEJH8ksb1PYkR5FA9k5gr1TchaSNIiA2ZWs5a+koEbePXwlBP0poaV7xViZ/v50bQFcMgqw==} + terracotta@1.1.0: + resolution: {integrity: sha512-kfQciWUBUBgYkXu7gh3CK3FAJng/iqZslAaY08C+k1Hdx17aVEpcFFb/WPaysxAfcupNH3y53s/pc53xxZauww==} engines: {node: '>=10'} peerDependencies: solid-js: ^1.8 @@ -10653,7 +10653,7 @@ snapshots: '@solid-primitives/transition-group': 1.1.2(solid-js@1.9.9) solid-js: 1.9.9 - solid-use@0.9.0(solid-js@1.9.9): + solid-use@0.9.1(solid-js@1.9.9): dependencies: solid-js: 1.9.9 @@ -10870,10 +10870,10 @@ snapshots: term-size@2.2.1: {} - terracotta@1.0.6(solid-js@1.9.9): + terracotta@1.1.0(solid-js@1.9.9): dependencies: solid-js: 1.9.9 - solid-use: 0.9.0(solid-js@1.9.9) + solid-use: 0.9.1(solid-js@1.9.9) terser@5.44.0: dependencies: From 5eae638ac32b469a71fbea143d34b7b961343c11 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Wed, 14 Jan 2026 18:53:50 +0800 Subject: [PATCH 18/26] 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 9532230a225e81f52f9c73ca12ffc9a996dfc2ab Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Wed, 14 Jan 2026 18:57:35 +0800 Subject: [PATCH 19/26] 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 f265c340c849cecdc41e58cc20e88d45a385baa8 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Thu, 15 Jan 2026 14:51:37 +0800 Subject: [PATCH 20/26] 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 432b8cdcf5210a5e03208c3ce45c5948f7a630d9 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Sun, 18 Jan 2026 23:45:06 +0800 Subject: [PATCH 21/26] 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 25b0ce527c4caa80b2e5bcb66f2819a3954aa324 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Tue, 20 Jan 2026 21:08:36 +0800 Subject: [PATCH 22/26] 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 4585a65ab4e6a72f62e9756c0823d054b1c78bd6 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Fri, 23 Jan 2026 16:14:09 +0800 Subject: [PATCH 23/26] Bump `seroval` to `1.5.0` --- 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 10a3d0477..93cb4fab3 100644 --- a/packages/start/package.json +++ b/packages/start/package.json @@ -57,7 +57,7 @@ "pathe": "^2.0.3", "radix3": "^1.1.2", "seroval": "^1.5.0", - "seroval-plugins": "^1.4.0", + "seroval-plugins": "^1.5.0", "shiki": "^1.26.1", "solid-js": "^1.9.9", "source-map-js": "^1.2.1", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 0d3ec748a..70ed5558e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -371,8 +371,8 @@ importers: specifier: ^1.5.0 version: 1.5.0 seroval-plugins: - specifier: ^1.4.0 - version: 1.4.0(seroval@1.5.0) + specifier: ^1.5.0 + version: 1.5.0(seroval@1.5.0) shiki: specifier: ^1.26.1 version: 1.26.1 @@ -4841,8 +4841,8 @@ packages: peerDependencies: seroval: ^1.0 - seroval-plugins@1.4.0: - resolution: {integrity: sha512-zir1aWzoiax6pbBVjoYVd0O1QQXgIL3eVGBMsBsNmM8Ukq90yGaWlfx0AB9dTS8GPqrOrbXn79vmItCUP9U3BQ==} + seroval-plugins@1.5.0: + resolution: {integrity: sha512-EAHqADIQondwRZIdeW2I636zgsODzoBDwb3PT/+7TLDWyw1Dy/Xv7iGUIEXXav7usHDE9HVhOU61irI3EnyyHA==} engines: {node: '>=10'} peerDependencies: seroval: ^1.0 @@ -10510,7 +10510,7 @@ snapshots: dependencies: seroval: 1.3.2 - seroval-plugins@1.4.0(seroval@1.5.0): + seroval-plugins@1.5.0(seroval@1.5.0): dependencies: seroval: 1.5.0 From f7912672151123953bf1e383ec2b8cf73ce55cb8 Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Fri, 23 Jan 2026 16:14:20 +0800 Subject: [PATCH 24/26] 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 2b682ab4d7d4934a9069bee0b41cfa9f0e29c1ef Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Fri, 23 Jan 2026 16:14:25 +0800 Subject: [PATCH 25/26] 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 860f37b8e9f61ea6a7a98c20c0ce7c94836591ce Mon Sep 17 00:00:00 2001 From: "Alexis H. Munsayac" Date: Fri, 23 Jan 2026 23:20:14 +0800 Subject: [PATCH 26/26] 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; }