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 diff --git a/apps/tests/src/e2e/server-function.test.ts b/apps/tests/src/e2e/server-function.test.ts index 7a8131be9..861495e3b 100644 --- a/apps/tests/src/e2e/server-function.test.ts +++ b/apps/tests/src/e2e/server-function.test.ts @@ -67,4 +67,14 @@ 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}'); + }); + + test("should build with a server function w/ form data", async ({ page }) => { + await page.goto("http://localhost:3000/server-function-form-data"); + await expect(page.locator("#server-fn-test")).toContainText('{"result":true}'); + }); }); 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/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())} +
+ ); +} 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-ping.tsx b/apps/tests/src/routes/server-function-ping.tsx new file mode 100644 index 000000000..677a35340 --- /dev/null +++ b/apps/tests/src/routes/server-function-ping.tsx @@ -0,0 +1,43 @@ +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: Date) { + "use server"; + + 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 = new Date(); + const result = await ping(value); + await ping(value); + console.log(result); + 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())} +
+ ); +} 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 9170468d3..93cb4fab3 100644 --- a/packages/start/package.json +++ b/packages/start/package.json @@ -56,13 +56,13 @@ "path-to-regexp": "^8.2.0", "pathe": "^2.0.3", "radix3": "^1.1.2", - "seroval": "^1.4.1", - "seroval-plugins": "^1.4.0", + "seroval": "^1.5.0", + "seroval-plugins": "^1.5.0", "shiki": "^1.26.1", "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/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..8c0d0c9fb --- /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); + }, + }); + }, + }); +} + +export 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..c03340dae 100644 --- a/packages/start/src/server/server-functions-handler.ts +++ b/packages/start/src/server/server-functions-handler.ts @@ -1,74 +1,22 @@ -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 { BODY_FORMAL_FILE, BODY_FORMAT_KEY, BodyFormat } from "./server-functions-shared.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; @@ -96,54 +44,49 @@ 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 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") { + 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 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, - ], - }); + 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 { @@ -171,16 +114,19 @@ 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("content-type", "text/javascript"); - - return serializeToStream(instance, result); + 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); + } + return serializeToJSONStream(result); } catch (x) { if (x instanceof Response) { if (singleFlight && instance) { @@ -189,28 +135,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); + 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); + } + 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 +179,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 +199,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 +228,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 +249,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-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 8bbabe40b..995947236 100644 --- a/packages/start/src/server/server-runtime.ts +++ b/packages/start/src/server/server-runtime.ts @@ -1,146 +1,148 @@ -// @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"; +import { + pushRequest, + pushResponse, +} from "../shared/server-function-inspector/server-function-tracker"; +import { + deserializeJSONStream, + deserializeJSStream, + // serializeToJSONStream, + serializeToJSONString, +} from "./serialization.ts"; +import { BODY_FORMAL_FILE, BODY_FORMAT_KEY, BodyFormat } from "./server-functions-shared.ts"; -class SerovalChunkReader { - reader: ReadableStreamDefaultReader; - buffer: Uint8Array; - done: boolean; - constructor(stream: ReadableStream) { - this.reader = stream.getReader(); - this.buffer = new Uint8Array(0); - this.done = false; - } +let INSTANCE = 0; - 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 function createRequest( + base: string, + id: string, + instance: string, + options: RequestInit, +) { + const request = new Request(base, { + method: "POST", + ...options, + headers: { + ...options.headers, + "X-Server-Id": id, + "X-Server-Instance": instance, + }, + }); + if (import.meta.env.DEV) { + pushRequest(id, instance, request.clone()); } - - 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), - }; + const response = await fetch(request); + if (import.meta.env.DEV) { + pushResponse(id, instance, response.clone()); } + return response; +} - async drain() { - while (true) { - const result = await this.next(); - if (result.done) { - break; - } +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: { + [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 File: { + const formData = new FormData(); + formData.append(BODY_FORMAL_FILE, body, body.name); + return { + headers: { + [BODY_FORMAT_KEY]: BodyFormat.File, + }, + body: formData, + }; } + case body instanceof Blob: + return { + headers: { + [BODY_FORMAT_KEY]: BodyFormat.Blob, + }, + body, + }; + 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 deserializeStream(id: string, response: Response) { - if (!response.body) { - throw new Error("missing body"); +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); } - const reader = new SerovalChunkReader(response.body); - - const result = await reader.next(); - - if (!result.done) { - reader.drain().then( - () => { - // @ts-ignore - delete $R[id]; - }, - () => { - // no-op - }, - ); + // 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, + }, + }); + } } - - return result.value; -} - -let INSTANCE = 0; - -function createRequest(base: string, id: string, instance: string, options: RequestInit) { - return fetch(base, { - method: "POST", + // 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, - "X-Server-Id": id, - "X-Server-Instance": instance, + "Content-Type": "text/plain", + [BODY_FORMAT_KEY]: BodyFormat.Seroval, }, }); } -const plugins = [ - CustomEventPlugin, - DOMExceptionPlugin, - EventPlugin, - FormDataPlugin, - HeadersPlugin, - ReadableStreamPlugin, - RequestPlugin, - ResponsePlugin, - URLSearchParamsPlugin, - URLPlugin, -]; - async function fetchServerFunction( base: string, id: string, @@ -148,21 +150,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, - body: JSON.stringify(await Promise.resolve(toJSONAsync(args, { plugins }))), - headers: { ...options.headers, "Content-Type": "application/json" }, - })); + + const response = await initializeResponse(base, id, instance, options, args); if ( response.headers.has("Location") || @@ -172,20 +161,28 @@ async function fetchServerFunction( if (response.body) { /* @ts-ignore-next-line */ response.customBody = () => { - return deserializeStream(instance, response); + if (import.meta.env.SEROVAL_MODE === "js") { + return deserializeJSStream(instance, response.clone()); + } + return deserializeJSONStream(response.clone()); }; } return response; } const contentType = response.headers.get("Content-Type"); + const clone = response.clone(); let result; - if (contentType && contentType.startsWith("text/plain")) { - result = await response.text(); - } else if (contentType && contentType.startsWith("application/json")) { - result = await response.json(); - } else { - result = await deserializeStream(instance, response); + if (contentType?.startsWith("text/plain")) { + result = await clone.text(); + } else if (contentType?.startsWith("application/json")) { + result = await clone.json(); + } else if (response.headers.get(BODY_FORMAT_KEY)) { + if (import.meta.env.SEROVAL_MODE === "js") { + result = await deserializeJSStream(instance, clone); + } else { + result = await deserializeJSONStream(clone); + } } if (response.headers.has("X-Error")) { throw result; @@ -197,7 +194,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 +209,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, 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/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.tsx b/packages/start/src/shared/server-function-inspector/FormDataViewer.tsx new file mode 100644 index 000000000..1efd3f83c --- /dev/null +++ b/packages/start/src/shared/server-function-inspector/FormDataViewer.tsx @@ -0,0 +1,44 @@ +import { createResource, For, type JSX, Show, Suspense } from 'solid-js'; +import { Section } from '../ui/Section'; +import { BlobViewer } from './BlobViewer.tsx'; +import { SerovalValue, PropertySeparator } from './SerovalValue.tsx'; + +interface FormDataViewerInnerProps { + source: FormData; +} + +function FormDataViewerInner(props: FormDataViewerInnerProps): JSX.Element { + 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/HeadersViewer.css b/packages/start/src/shared/server-function-inspector/HeadersViewer.css new file mode 100644 index 000000000..e6b8c1c4d --- /dev/null +++ b/packages/start/src/shared/server-function-inspector/HeadersViewer.css @@ -0,0 +1,8 @@ +[data-start-headers-viewer] { + font-size: 0.75rem; + line-height: 1rem; +} + +[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 new file mode 100644 index 000000000..bff549c7d --- /dev/null +++ b/packages/start/src/shared/server-function-inspector/HeadersViewer.tsx @@ -0,0 +1,25 @@ +import { For } from "solid-js"; +import { PropertySeparator, SerovalValue } from "./SerovalValue.tsx"; + +import './HeadersViewer.css'; +import { Text } from "../ui/Text.tsx"; + +interface HeadersViewerProps { + headers: Headers; +} + +export function HeadersViewer(props: HeadersViewerProps) { + return ( +
+ + {([key, value]) => ( +
+ {key} + + +
+ )} +
+
+ ); +} 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/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..ed5a2429a --- /dev/null +++ b/packages/start/src/shared/server-function-inspector/SerovalValue.tsx @@ -0,0 +1,18 @@ +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}`} + + ); +} + +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 new file mode 100644 index 000000000..5bfe25533 --- /dev/null +++ b/packages/start/src/shared/server-function-inspector/SerovalViewer.css @@ -0,0 +1,71 @@ +[data-start-seroval-viewer] { + display: grid; + grid-template-columns: 1fr 2fr; +} + +[data-start-seroval-promise] { + display: flex; + gap: 0.25rem; + align-items: center; +} + +[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] > * { + 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.5rem; + + 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-value-wrapper] { + display: inline-flex; + gap: 0.25rem; + align-items: center; +} + +[data-start-seroval-link] { + display: inline-flex; + gap: 0.25rem; + align-items: center; +} + +[data-start-seroval-link] > svg { + width: 1rem; + height: 1rem; +} + 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..038d2f483 --- /dev/null +++ b/packages/start/src/shared/server-function-inspector/SerovalViewer.tsx @@ -0,0 +1,1016 @@ +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 { HexViewer } from "./HexViewer.tsx"; +import { SerovalValue, PropertySeparator } from "./SerovalValue.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"; + case 35: + return "Sequence"; + } + 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: + for (const key in node.s) { + const current = node.s[key]; + if (current) { + traverse(current, handler); + } + } + 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; + case 35: + // Traverse items + for (const child of node.a) { + if (child) { + traverse(child, handler); + } + } + break; + } +} + +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 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 ( + + + {getNodeType(lookup)} + {description} + + ); + } + switch (node.t) { + // Number = 0, + case 0: + return ; + // String = 1, + case 1: + return ; + // Constant = 2, + case 2: + return ; + // BigInt = 3, + case 3: + return ( +
+ bigint + +
+ ); + // Date = 5, + case 5: + return ( +
+ Date + +
+ ); + // RegExp = 6, + case 6: + return ( +
+ RegExp + +
+ ); + // Set = 7, + case 7: + return ( + <> +
+
+ + + +
+
+
+ + data-start-properties + defaultValue={undefined} + onChange={onSelect} + > + [index, node] as const)}> + {([key, value]) => ( +
+ + + {renderSerovalNode(ctx, value, onSelect, true)} +
+ )} +
+ +
+ + ); + // Map = 8, + case 8: + return ( + <> +
+
+ + + +
+
+
+ + data-start-properties + defaultValue={undefined} + onChange={onSelect} + > + }> + {([key, value]) => ( +
+ {renderSerovalNode(ctx, key, onSelect, true)} + + {renderSerovalNode(ctx, value, onSelect, true)} +
+ )} +
+ +
+ + ); + // Array = 9, + case 9: + return ( + <> +
+
+ + + +
+
+ + + {getObjectFlag(node.o)} +
+
+
+ + data-start-properties + defaultValue={undefined} + onChange={onSelect} + > + [index, node] as const)} fallback={}> + {([key, value]) => ( +
+ + + {value === 0 ? ( + + ) : ( + renderSerovalNode(ctx, value, onSelect, true) + )} +
+ )} +
+ +
+ + ); + // Object = 10, + case 10: + // NullConstructor = 11, + case 11: + return ( + <> +
+
+ + + +
+
+ + + {getObjectFlag(node.o)} +
+
+
+ + data-start-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-properties + defaultValue={undefined} + onChange={onSelect} + > + {renderSerovalNode(ctx, node.f, onSelect, true)} + + ); + // Error = 13, + case 13: + // AggregateError = 14, + case 14: + return ( + <> +
+
+ + + +
+
+ + {(current) => ( +
+ + data-start-properties + defaultValue={undefined} + onChange={onSelect} + > + }> + {([key, value]) => ( +
+ {typeof key === "string" ? ( + + ) : ( + renderSerovalNode(ctx, key, onSelect, true) + )} + + {renderSerovalNode(ctx, value, onSelect, true)} +
+ )} +
+ +
+ )} +
+ + ); + // WKSymbol = 17, + case 17: + return ; + // Reference = 18, + case 18: + break; + // ArrayBuffer = 19, + 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 ( + <> +
+
+ + + +
+
+ + + +
+
+
+ + data-start-properties + defaultValue={undefined} + onChange={onSelect} + > + {renderSerovalNode(ctx, node.f, onSelect, true)} + +
+ + ); + // Boxed = 21, + case 21: + return ( + + data-start-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-properties + defaultValue={undefined} + onChange={onSelect} + > +
+ + + {status} +
+ + + + {renderSerovalNode(ctx, result.a[1], onSelect, true)} + + + ); + } + return pending; + })()} + + ); + // Plugin = 25 + case 25: + return ( + <> +
+
+ + + +
+
+
+ + data-start-properties + defaultValue={undefined} + onChange={onSelect} + > + }> + {([key, value]) => ( +
+ + + {renderSerovalNode(ctx, value, onSelect, true)} +
+ )} +
+ +
+ + ); + // 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-properties + defaultValue={undefined} + onChange={onSelect} + > + }> + {(current) => ( +
+ + + {renderSerovalNode(ctx, current.f, onSelect, true)} +
+ )} +
+ + ); + })()} + + ); + case 35: + return ( + + data-start-properties + defaultValue={undefined} + onChange={onSelect} + > + }> + {(current, index) => ( +
+ + + {renderSerovalNode(ctx, current, 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 ( + <> +
+
+ {getNodeType(props.node)} + {props.node.i != null && ( + {`id: ${props.node.i}`} + )} +
+
+ {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< + Record | undefined> + >({}); + const streams = createSimpleStore< + Record[] | undefined> + >({}); + const promises = createSimpleStore< + Record | 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: + case 35: + references.write(node.i, node); + 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/URLSearchParamsViewer.tsx b/packages/start/src/shared/server-function-inspector/URLSearchParamsViewer.tsx new file mode 100644 index 000000000..80e7dd509 --- /dev/null +++ b/packages/start/src/shared/server-function-inspector/URLSearchParamsViewer.tsx @@ -0,0 +1,41 @@ +import { createResource, For, type JSX, Show, Suspense } from 'solid-js'; +import { Section } from '../ui/Section'; +import { SerovalValue, PropertySeparator } 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 new file mode 100644 index 000000000..eca6591d2 --- /dev/null +++ b/packages/start/src/shared/server-function-inspector/index.tsx @@ -0,0 +1,323 @@ +import { + createEffect, + createMemo, + createSignal, + For, + type JSX, + onCleanup, + Show, +} from "solid-js"; +import { createStore } from "solid-js/store"; +import { Portal } from "solid-js/web"; +import { BODY_FORMAL_FILE, BODY_FORMAT_KEY, BodyFormat } from "../../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"; +import { SerovalViewer } from "./SerovalViewer.tsx"; +import { + captureServerFunctionCall, + type ServerFunctionRequest, + type ServerFunctionResponse, +} from "./server-function-tracker.ts"; +import "./styles.css"; +import { URLSearchParamsViewer } from "./URLSearchParamsViewer.tsx"; +import { Text } from "../ui/Text.tsx"; +import Placeholder from "../ui/Placeholder.tsx"; +import { PropertySeparator, SerovalValue } from "./SerovalValue.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; +} + +function ContentViewer(props: ContentViewerProps): JSX.Element { + return ( + <> +
+ +
+
+ {(() => { + 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 ; + case startType === BodyFormat.String: + 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 ; + case startType === BodyFormat.Blob: + return ; + case startType === BodyFormat.ArrayBuffer: + case startType === BodyFormat.Uint8Array: + return ; + } + })()} +
+ + ); +} + +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 + + +
+
+ + + )} +
+
+ ); +} + +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 + +
+ +
+
+ + +
+ ); +} + +function EmptyServerFunctions(): JSX.Element { + return ( + + + No server function calls detected. + + + ); +} + +export function ServerFunctionInspector(): JSX.Element { + const [currentInstance, setCurrentInstance] = createSignal(); + + const [store, setStore] = createStore({ + instances: {} as Record, + }); + + createEffect(() => { + onCleanup( + captureServerFunctionCall((call) => { + if (call.type === "request") { + setStore("instances", call.instance, { + request: call, + }); + } else { + setStore("instances", call.instance, "response", call); + } + }), + ); + }); + + const [isOpen, setIsOpen] = createSignal(false); + + createEffect(() => { + (window as any).__START__SERVER_FN__ = setIsOpen; + }); + + const keys = createMemo(() => Object.keys(store.instances)); + + 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..b1769566e --- /dev/null +++ b/packages/start/src/shared/server-function-inspector/styles.css @@ -0,0 +1,79 @@ + + +.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; +} + +[data-start-headers] { + display: flex; + flex-direction: column; + gap: 0.5rem; +} + +[data-start-headers-title] { + font-weight: 500; +} + +[data-start-properties] { + display: flex; + flex-direction: column; + 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/Badge.css b/packages/start/src/shared/ui/Badge.css new file mode 100644 index 000000000..cee81bcf5 --- /dev/null +++ b/packages/start/src/shared/ui/Badge.css @@ -0,0 +1,34 @@ +[data-start-badge] { + display: inline-flex; + align-items: center; + justify-content: center; + gap: 0.25rem; + + 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..5de35361e --- /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 { + type: 'info' | 'success' | 'failure' | 'warning'; + children: JSX.Element; +} + +export function Badge(props: BadgeProps): JSX.Element { + return ( + + {props.children} + + ); +} diff --git a/packages/start/src/shared/ui/Button.css b/packages/start/src/shared/ui/Button.css new file mode 100644 index 000000000..fbae0519d --- /dev/null +++ b/packages/start/src/shared/ui/Button.css @@ -0,0 +1,32 @@ +[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: 0.25rem 0.5rem; + 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..dbda77829 --- /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); +} 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/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/Section.css b/packages/start/src/shared/ui/Section.css new file mode 100644 index 000000000..27e368b15 --- /dev/null +++ b/packages/start/src/shared/ui/Section.css @@ -0,0 +1,19 @@ +[data-start-section] { + display: flex; + flex-direction: column; +} + +[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% - 1rem); + + overflow: auto; +} diff --git a/packages/start/src/shared/ui/Section.tsx b/packages/start/src/shared/ui/Section.tsx new file mode 100644 index 000000000..0524ed4f0 --- /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..36ed6bb25 --- /dev/null +++ b/packages/start/src/shared/ui/Tabs.css @@ -0,0 +1,54 @@ +[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: 1rem; + background-color: rgb(249 250 251); + border-radius: 0.5rem; + height: 100%; + + overflow: auto; + + display: flex; + flex-direction: column; + gap: 1rem; +} 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..44bad1b2d --- /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 ( + + ); +} diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 25d67e8e2..70ed5558e 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) + specifier: ^1.5.0 + version: 1.5.0(seroval@1.5.0) shiki: specifier: ^1.26.1 version: 1.26.1 @@ -386,21 +386,21 @@ 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.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==} @@ -4844,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 @@ -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: @@ -4961,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 @@ -5152,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 @@ -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.5.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: @@ -10661,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 @@ -10878,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: @@ -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: