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-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 index 70f9d2617..677a35340 100644 --- a/apps/tests/src/routes/server-function-ping.tsx +++ b/apps/tests/src/routes/server-function-ping.tsx @@ -1,18 +1,38 @@ import { createEffect, createSignal } from "solid-js"; -async function ping(value: string) { +async function sleep(value: unknown, ms: number) { + return new Promise((res) => { + setTimeout(res, ms, value); + }) +} + +async function ping(value: Date) { "use server"; - return await Promise.resolve(value); + const current = [ + value, + { + name: 'example', + async *[Symbol.asyncIterator]() { + yield sleep('foo', 5000); + yield sleep('bar', 5000); + yield sleep('baz', 5000); + } + } + ]; + + return current; } export default function App() { const [output, setOutput] = createSignal<{ result?: boolean }>({}); createEffect(async () => { - const value = `${Math.random() * 1000}`; + const value = new Date(); const result = await ping(value); - setOutput(prev => ({ ...prev, result: value === result })); + await ping(value); + console.log(result); + setOutput((prev) => ({ ...prev, result: value.toString() === result[0].toString() })); }); return ( diff --git a/apps/tests/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/src/routes/treeshaking/(no-side-effects).tsx b/apps/tests/src/routes/treeshaking/(no-side-effects).tsx index 40cff9649..be86a00a0 100644 --- a/apps/tests/src/routes/treeshaking/(no-side-effects).tsx +++ b/apps/tests/src/routes/treeshaking/(no-side-effects).tsx @@ -1,4 +1,5 @@ import { createAsync } from "@solidjs/router"; +import { createEffect } from "solid-js"; const a = "myTreeshakingTestUniqueString1"; diff --git a/packages/start/package.json b/packages/start/package.json index babc047d1..70db2203a 100644 --- a/packages/start/package.json +++ b/packages/start/package.json @@ -76,7 +76,7 @@ "solid-js": "^1.9.11", "source-map-js": "^1.2.1", "srvx": "^0.11.22", - "terracotta": "^1.1.0", + "terracotta": "^1.1.1", "vite-plugin-solid": "^2.11.11" }, "engines": { diff --git a/packages/start/src/fns/client.ts b/packages/start/src/fns/client.ts index c50fb21f9..690bc811d 100644 --- a/packages/start/src/fns/client.ts +++ b/packages/start/src/fns/client.ts @@ -1,19 +1,15 @@ import { type Component } from "solid-js"; +import { pushRequest, pushResponse } from "../shared/dev-toolbar/functions/tracker.ts"; import { // serializeToJSONStream, serializeToJSONString, } from "./serialization.ts"; -import { - BODY_FORMAT_KEY, - BodyFormat, - extractBody, - getHeadersAndBody, -} from "./shared.ts"; +import { BODY_FORMAT_KEY, BodyFormat, extractBody, getHeadersAndBody } from "./shared.ts"; let INSTANCE = 0; -function createRequest(base: string, id: string, instance: string, options: RequestInit) { - return fetch(base, { +async function createRequest(base: string, id: string, instance: string, options: RequestInit) { + const request = new Request(base, { method: "POST", ...options, headers: { @@ -22,6 +18,15 @@ function createRequest(base: string, id: string, instance: string, options: Requ "X-Server-Instance": instance, }, }); + if (import.meta.env.DEV) { + console.log(pushRequest); + pushRequest(id, instance, request.clone()); + } + const response = await fetch(request); + if (import.meta.env.DEV) { + pushResponse(id, instance, response.clone()); + } + return response; } async function initializeResponse( diff --git a/packages/start/src/fns/handler.ts b/packages/start/src/fns/handler.ts index 8f21cf740..3e0cb098d 100644 --- a/packages/start/src/fns/handler.ts +++ b/packages/start/src/fns/handler.ts @@ -111,7 +111,6 @@ export async function handleServerFunction(h3Event: H3Event) { h3Event.res.headers.set("content-type", "text/javascript"); return serializeToJSStream(instance, result); } - h3Event.res.headers.set("content-type", "text/plain"); return serializeToJSONStream(result); } catch (x) { if (x instanceof Response) { @@ -121,14 +120,18 @@ export async function handleServerFunction(h3Event: H3Event) { // forward headers if ((x as any).headers) mergeResponseHeaders(h3Event, (x as any).headers); // forward non-redirect statuses - if ((x as any).status && (!instance || (x as any).status < 300 || (x as any).status >= 400)) + if ( + (x as any).status && + (!instance || (x as any).status < 300 || (x as any).status >= 400) + ) h3Event.res.status = (x as any).status; if ((x as any).customBody) { x = await (x as any).customBody(); } else if ((x as any).body == null) x = null; h3Event.res.headers.set("X-Error", "true"); } else if (instance) { - const error = x instanceof Error ? x.message : typeof x === "string" ? x : "true"; + const error = + x instanceof Error ? x.message : typeof x === "string" ? x : "true"; h3Event.res.headers.set("X-Error", error.replace(/[\r\n]+/g, "")); } else { @@ -151,14 +154,18 @@ export async function handleServerFunction(h3Event: H3Event) { h3Event.res.headers.set("content-type", "text/javascript"); return serializeToJSStream(instance, x); } - h3Event.res.headers.set("content-type", "text/plain"); return serializeToJSONStream(x); } return x; } } -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; @@ -168,7 +175,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); } @@ -185,7 +195,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;`, ); @@ -211,7 +224,7 @@ export function createSingleFlightHeaders(sourceEvent: FetchEvent) { // useH3Internals = true; // sourceEvent.nativeEvent.node.req.headers.cookie = ""; // } - SetCookies.forEach(cookie => { + SetCookies.forEach((cookie) => { if (!cookie) return; const { maxAge, expires, name, value } = parseSetCookie(cookie); if (maxAge != null && maxAge <= 0) { @@ -232,7 +245,10 @@ export function createSingleFlightHeaders(sourceEvent: FetchEvent) { return headers; } -async function handleSingleFlight(sourceEvent: FetchEvent, result: any): Promise { +async function handleSingleFlight( + sourceEvent: FetchEvent, + result: any, +): Promise { let revalidate: string[]; let url = new URL(sourceEvent.request.headers.get("referer")!).toString(); if (result instanceof Response) { diff --git a/packages/start/src/fns/serialization.ts b/packages/start/src/fns/serialization.ts index b266eb027..e09e1b004 100644 --- a/packages/start/src/fns/serialization.ts +++ b/packages/start/src/fns/serialization.ts @@ -109,7 +109,7 @@ export function serializeToJSONStream(value: any) { }); } -class SerovalChunkReader { +export class SerovalChunkReader { reader: ReadableStreamDefaultReader; buffer: Uint8Array; done: boolean; diff --git a/packages/start/src/shared/ErrorBoundary.tsx b/packages/start/src/shared/ErrorBoundary.tsx index 6be12348e..7aa8c0df8 100644 --- a/packages/start/src/shared/ErrorBoundary.tsx +++ b/packages/start/src/shared/ErrorBoundary.tsx @@ -1,12 +1,12 @@ // @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 { DevToolbar } from "./dev-toolbar/index.tsx"; import { HttpStatusCode } from "./HttpStatusCode.ts"; -import { DevOverlay } from "./dev-overlay/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" diff --git a/packages/start/src/shared/dev-overlay/index.tsx b/packages/start/src/shared/dev-overlay/index.tsx deleted file mode 100644 index bb4dea670..000000000 --- a/packages/start/src/shared/dev-overlay/index.tsx +++ /dev/null @@ -1,63 +0,0 @@ -// @refresh skip -import { - createEffect, - createSignal, - ErrorBoundary, - type JSX, - onCleanup, - resetErrorBoundaries, - Show, -} from "solid-js"; -import clientOnly from "../clientOnly.ts"; -import { HttpStatusCode } from "../HttpStatusCode.ts"; - -export interface DevOverlayProps { - children?: JSX.Element; -} - -const DevOverlayDialog = import.meta.env.PROD - ? () => <> - : clientOnly(() => import("./DevOverlayDialog"), { lazy: true }); - -export function DevOverlay(props: DevOverlayProps): JSX.Element { - const [errors, setErrors] = createSignal([]); - - function resetError() { - setErrors([]); - resetErrorBoundaries(); - } - - function pushError(error: unknown) { - console.error(error); - setErrors(current => [error, ...current]); - } - - createEffect(() => { - const onErrorEvent = (error: ErrorEvent) => { - pushError(error.error ?? error); - }; - - window.addEventListener("error", onErrorEvent); - - onCleanup(() => { - window.removeEventListener("error", onErrorEvent); - }); - }); - - return ( - <> - { - pushError(error); - return ; - }} - > - {props.children} - - - - - - - ); -} diff --git a/packages/start/src/shared/dev-overlay/styles.css b/packages/start/src/shared/dev-overlay/styles.css deleted file mode 100644 index ce51fdb95..000000000 --- a/packages/start/src/shared/dev-overlay/styles.css +++ /dev/null @@ -1,292 +0,0 @@ -[data-start-dev-overlay] { - position: fixed; - inset: 0px; - z-index: 50; - overflow-y: auto; -} - -[data-start-dev-overlay]>div { - position: relative; - display: flex; - align-items: center; - justify-content: center; - min-height: 100vh; -} - -[data-start-dev-overlay-background] { - position: absolute; - inset: 0px; - background-color: rgb(17 24 39 / 0.5); -} - -[data-start-dev-overlay-panel] { - display: flex; - flex-direction: column; - width: 75vw; - margin-top: 2rem; - margin-bottom: 2rem; - gap: 0.5rem; - color: rgb(17 24 39); - font-family: ui-monospace, SFMono-Regular, Menlo, Monaco, Consolas, "Liberation Mono", "Courier New", monospace; -} - -[data-start-dev-overlay-panel-container] { - margin: 2rem; - z-index: 10; -} - -[data-start-dev-overlay-navbar] { - display: flex; - flex-direction: row; - align-items: center; - justify-content: space-between; - gap: 0.5rem; -} - -[data-start-dev-overlay-pagination] { - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - gap: 0.5rem; - border-radius: 9999px; - background-color: rgb(249 250 251); - padding: 0.25rem; -} - -[data-start-dev-overlay-button] { - display: flex; - padding: 0.125rem; - border-radius: 9999px; - border-width: 0px; - align-items: center; - justify-content: center; - background-color: rgb(249 250 251); - color: rgb(17 24 39); - transition-property: color, background-color, border-color, box-shadow; - transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); - transition-duration: 150ms; -} - -[data-start-dev-overlay-button]:hover { - background-color: rgb(229 231 235); - color: rgb(55 65 81); -} - -[data-start-dev-overlay-button]:focus { - outline: 2px solid transparent; - outline-offset: 2px -} - -[data-start-dev-overlay-button]:focus-visible { - box-shadow: 0 0 0 calc(3px) rgb(17 24 39 / 0.75); -} - -[data-start-dev-overlay-button]:active { - background-color: rgb(243 244 246); - color: rgb(31 41 55); -} - -[data-start-dev-overlay-button]>svg { - height: 1.5rem; - width: 1.5rem; -} - -[data-start-dev-overlay-page-counter] { - font-size: 0.875rem; - line-height: 1.25rem; - font-weight: 600; -} - -[data-start-dev-overlay-navbar-left] { - display: flex; - flex-direction: row; - gap: 0.5rem; -} - - -[data-start-dev-overlay-controls] { - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - gap: 0.25rem; - border-radius: 9999px; - background-color: rgb(249 250 251); - padding: 0.25rem; -} - -[data-start-dev-overlay-version] { - display: flex; - flex-direction: row; - align-items: center; - justify-content: center; - gap: 0.25rem; - border-radius: 9999px; - background-color: rgb(249 250 251); - padding: 0.25rem 0.5rem; - font-size: 0.875rem; - line-height: 1.25rem; - font-weight: 600; -} - -[data-start-dev-overlay-version]>div { - height: 1.5rem; - width: 1.5rem; -} - -[data-start-dev-overlay-version]>div>svg { - height: 1.5rem; - width: 1.5rem; -} - -[data-start-dev-overlay-content] { - display: flex; - flex-direction: column; - gap: 0.75rem; - border-radius: 1.5rem; - background-color: rgb(249 250 251); - padding: 1rem; -} - -[data-start-dev-overlay-error-info] { - display: flex; - flex-direction: column; - gap: 0.25rem; -} - -[data-start-dev-overlay-error-info-name] { - font-weight: 700; -} - -[data-start-dev-overlay-error-info-message] { - font-size: 1.5rem; - line-height: 2rem; - color: rgb(239 68 68); - font-weight: 700; -} - -[data-start-dev-overlay-stack-frames-content] { - display: flex; - flex-direction: column; - gap: 0.75rem; -} - -[data-start-dev-overlay-stack-frames] { - flex: 1 1 0%; - display: flex; - flex-direction: column; - margin: 0; - margin-block: 0; - list-style-type: none; - padding-inline: 0; - max-height: 16rem; - overflow-y: auto; - border-radius: 0.5rem; -} - -[data-start-dev-overlay-stack-frame] { - display: flex; - flex-direction: row; - 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; -} - -[data-start-dev-overlay-stack-frame][tc-active], -[data-start-dev-overlay-stack-frame][tc-selected], -[data-start-dev-overlay-stack-frame]:focus { - background-color: rgb(219 234 254); - color: rgb(30 58 138); -} - -[data-start-dev-overlay-stack-frame-function] { - font-weight: 700; -} - -[data-start-dev-overlay-stack-frame-file] { - white-space: nowrap; - font-size: 0.75rem; - /* 12px */ - line-height: 1rem; - /* 16px */ -} - -[data-start-dev-overlay-stack-frames-code] { - padding: 0.5rem 1rem; - gap: 0.25rem; - flex: 1 1 0%; - display: flex; - flex-direction: column; - align-items: center; - justify-content: center; - border-radius: 0.5rem; - background-color: rgb(17 24 39); -} - -[data-start-dev-overlay-stack-frames-code-fallback] { - display: flex; - align-items: center; - justify-content: center; - color: rgb(249 250 251); - min-height: 16rem; - font-size: 0.875rem; - line-height: 1.25rem; -} - -[data-start-dev-overlay-stack-frames-code-source] { - font-size: 0.875rem; - line-height: 1.25rem; - color: rgb(249 250 251); -} - -[data-start-dev-overlay-stack-frames-code-container] { - width: 100%; - overflow: hidden; - min-width: 100%; - min-height: 16rem; - border-top-width: 1px; - border-top-style: solid; - border-top-color: rgb(249 250 251); -} - -[data-start-dev-overlay-code-view] { - overflow: auto; - min-width: 100%; - min-height: 16rem; -} - -[data-start-dev-overlay-code-view]>.shiki { - float: left; - min-width: 100%; - overflow-x: auto; - font-size: 0.75rem; - line-height: 1rem; - counter-reset: step; - counter-increment: step calc(var(--dev-overlay-code-view-start, 1) - 1); - background-color: transparent !important; -} - -[data-start-dev-overlay-code-view]>.shiki .line::before { - content: counter(step); - counter-increment: step; - width: 1rem; - margin-right: 1.5rem; - display: inline-block; - text-align: right; - color: rgba(115, 138, 148, .4) -} - -[data-start-dev-overlay-error-line] { - background-color: rgba(239, 68, 68, .4); -} diff --git a/packages/start/src/shared/dev-overlay/CodeView.tsx b/packages/start/src/shared/dev-toolbar/error-viewer/CodeView.tsx similarity index 93% rename from packages/start/src/shared/dev-overlay/CodeView.tsx rename to packages/start/src/shared/dev-toolbar/error-viewer/CodeView.tsx index 45950197f..83673540e 100644 --- a/packages/start/src/shared/dev-overlay/CodeView.tsx +++ b/packages/start/src/shared/dev-toolbar/error-viewer/CodeView.tsx @@ -72,7 +72,7 @@ export function CodeView(props: CodeViewProps): JSX.Element | null { for (let i = 0, len = lines.length; i < len; i++) { const el = lines[i] as HTMLElement; if (props.line - minLine() - 1 === i) { - el.dataset.startDevOverlayErrorLine = ''; + el.dataset.startErrorViewerErrorLine = ''; } } } @@ -81,9 +81,9 @@ export function CodeView(props: CodeViewProps): JSX.Element | null { return (
); diff --git a/packages/start/src/shared/dev-overlay/createStackFrame.ts b/packages/start/src/shared/dev-toolbar/error-viewer/create-stack-frame.ts similarity index 100% rename from packages/start/src/shared/dev-overlay/createStackFrame.ts rename to packages/start/src/shared/dev-toolbar/error-viewer/create-stack-frame.ts diff --git a/packages/start/src/shared/dev-overlay/download.ts b/packages/start/src/shared/dev-toolbar/error-viewer/download.ts similarity index 100% rename from packages/start/src/shared/dev-overlay/download.ts rename to packages/start/src/shared/dev-toolbar/error-viewer/download.ts diff --git a/packages/start/src/shared/dev-overlay/env.d.ts b/packages/start/src/shared/dev-toolbar/error-viewer/env.d.ts similarity index 100% rename from packages/start/src/shared/dev-overlay/env.d.ts rename to packages/start/src/shared/dev-toolbar/error-viewer/env.d.ts diff --git a/packages/start/src/shared/dev-overlay/get-source-map.ts b/packages/start/src/shared/dev-toolbar/error-viewer/get-source-map.ts similarity index 100% rename from packages/start/src/shared/dev-overlay/get-source-map.ts rename to packages/start/src/shared/dev-toolbar/error-viewer/get-source-map.ts diff --git a/packages/start/src/shared/dev-overlay/DevOverlayDialog.tsx b/packages/start/src/shared/dev-toolbar/error-viewer/index.tsx similarity index 57% rename from packages/start/src/shared/dev-overlay/DevOverlayDialog.tsx rename to packages/start/src/shared/dev-toolbar/error-viewer/index.tsx index 975e46507..d1d0bfea0 100644 --- a/packages/start/src/shared/dev-overlay/DevOverlayDialog.tsx +++ b/packages/start/src/shared/dev-toolbar/error-viewer/index.tsx @@ -3,13 +3,8 @@ import ErrorStackParser from "error-stack-parser"; import * as htmlToImage from "html-to-image"; import type { JSX } from "solid-js"; import { createMemo, createSignal, ErrorBoundary, For, Show, Suspense } from "solid-js"; -import { Portal } from "solid-js/web"; -// @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 { CodeView } from "./CodeView.tsx"; -import { createStackFrame, type StackFrameSource } from "./createStackFrame.ts"; -import download from "./download.ts"; +import IconButton from "../../ui/IconButton.tsx"; +import { Select, SelectOption } from "../../ui/Select.tsx"; import { ArrowLeftIcon, ArrowRightIcon, @@ -17,10 +12,12 @@ import { DiscordIcon, GithubIcon, RefreshIcon, - SolidStartIcon, ViewCompiledIcon, ViewOriginalIcon, -} from "./icons.tsx"; +} from "../icons.tsx"; +import { CodeView } from "./CodeView.tsx"; +import { createStackFrame, type StackFrameSource } from "./create-stack-frame.ts"; +import download from "./download.ts"; import "./styles.css"; export function classNames(...classes: (string | boolean | undefined)[]): string { @@ -47,9 +44,9 @@ function ErrorInfo(props: ErrorInfoProps): JSX.Element { }); return ( - - {error().name} - {error().message} + + {error().name} + {error().message} ); } @@ -77,7 +74,7 @@ function getFilePath(source: StackFrameSource) { function CodeFallback(): JSX.Element { return ( -
+
Source not available.
); @@ -89,8 +86,8 @@ function StackFramesContent(props: StackFramesContentProps) { const [selectedFrame, setSelectedFrame] = createSignal(stackframes[0]!); return ( -
-
+
+
{(() => { const data = createStackFrame(selectedFrame(), () => props.isCompiled); @@ -99,8 +96,8 @@ function StackFramesContent(props: StackFramesContentProps) { }> {source => ( <> - {source.source} -
+ {source.source} +
- data-start-dev-overlay-stack-frames + data-start-error-viewer-stack-frames value={selectedFrame()} onChange={setSelectedFrame} > @@ -124,11 +121,11 @@ function StackFramesContent(props: StackFramesContentProps) { {current => ( - + + {current.functionName ?? ""} - + {getFilePath({ source: current.getFileName()!, content: "", @@ -137,7 +134,7 @@ function StackFramesContent(props: StackFramesContentProps) { name: current.getFunctionName(), })} -
+ } > {(() => { @@ -146,11 +143,13 @@ function StackFramesContent(props: StackFramesContentProps) { {source => ( - - + + {source.name ?? ""} - {getFilePath(source)} + + {getFilePath(source)} + )} @@ -178,7 +177,8 @@ function StackFrames(props: StackFramesProps) { ); } -interface DevOverlayDialogProps { +interface ErrorViewerProps { + show?: boolean; errors: any[]; resetError: () => void; } @@ -186,7 +186,7 @@ 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 ErrorViewer(props: ErrorViewerProps): JSX.Element { const [currentPage, setCurrentPage] = createSignal(1); const [isCompiled, setIsCompiled] = createSignal(false); const length = createMemo(() => props.errors.length); @@ -248,69 +248,58 @@ export default function DevOverlayDialog(props: DevOverlayDialogProps): JSX.Elem } return ( - - -
- - -
-
-
-
-
- -
- {info.version as string} + +
+
+
+
+ 1}> +
+ + + +
+ {`${truncated()} of ${props.errors.length}`}
- 1}> -
- -
- {`${truncated()} of ${props.errors.length}`} -
- -
-
-
-
- - - - - + + +
-
- - {current => ( -
- - -
- )}
- +
+ + + + + + + + + + + } + > + + + + + + +
+
+ + {current => ( +
+ + +
+ )} +
-
-
+
+ ); } diff --git a/packages/start/src/shared/dev-toolbar/error-viewer/styles.css b/packages/start/src/shared/dev-toolbar/error-viewer/styles.css new file mode 100644 index 000000000..ca63c8aac --- /dev/null +++ b/packages/start/src/shared/dev-toolbar/error-viewer/styles.css @@ -0,0 +1,224 @@ +[data-start-error-viewer] { + display: flex; + flex-direction: column; + + width: 100%; +} + +[data-start-error-viewer-navbar] { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 0.5rem; +} + +[data-start-error-viewer-pagination] { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + gap: 0.5rem; + border-radius: 9999px; + color: rgb(249 250 251); + background-color: rgb(17 24 39); + padding: 0.25rem; +} + +[data-start-error-viewer-button] { + /* empty */ +} + +[data-start-error-viewer-button]:hover { + color: rgb(229 231 235); + background-color: rgb(55 65 81); +} + +[data-start-error-viewer-button]:focus { + outline: 2px solid transparent; + outline-offset: 2px; + outline-color: rgb(229 231 235); +} + +[data-start-error-viewer-button]:focus-visible { + box-shadow: 0 0 0 calc(3px) rgb(17 24 39 / 0.75); +} + +[data-start-error-viewer-button]:active { + color: rgb(243 244 246); + background-color: rgb(31 41 55); +} + +[data-start-error-viewer-button] > svg { + height: 1.5rem; + width: 1.5rem; +} + +[data-start-error-viewer-page-counter] { + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 600; +} + +[data-start-error-viewer-navbar-left] { + display: flex; + flex-direction: row; + gap: 0.5rem; +} + +[data-start-error-viewer-controls] { + display: flex; + flex-direction: row; + align-items: center; + justify-content: center; + gap: 0.25rem; + border-radius: 9999px; + color: rgb(249 250 251); + padding: 0.25rem; +} + +[data-start-error-viewer-content] { + display: flex; + flex-direction: column; + gap: 0.75rem; + padding: 1rem; + + border-top-width: 1px; + border-top-style: solid; + border-top-color: rgb(249 250 251); +} + +[data-start-error-viewer-error-info] { + display: flex; + flex-direction: column; + gap: 0.25rem; +} + +[data-start-error-viewer-error-info-name] { + font-weight: 700; + color: rgb(249 250 251); +} + +[data-start-error-viewer-error-info-message] { + font-size: 1.5rem; + line-height: 2rem; + color: rgb(239 68 68); + font-weight: 700; +} + +[data-start-error-viewer-stack-frames-content] { + display: flex; + flex-direction: column; + gap: 0.75rem; +} + +[data-start-error-viewer-stack-frames] { + flex: 1 1 0%; + max-height: 16rem; + overflow-y: auto; +} + +[data-start-error-viewer-stack-frame] { + display: flex; + flex-direction: row; + align-items: center; + justify-content: space-between; + gap: 1rem; + color: rgb(249 250 251); + 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; +} + +[data-start-error-viewer-stack-frame][tc-active], +[data-start-error-viewer-stack-frame][tc-selected], +[data-start-error-viewer-stack-frame]:focus { + color: rgb(219 234 254); + background-color: rgb(30 58 138); +} + +[data-start-error-viewer-stack-frame-function] { + font-weight: 700; +} + +[data-start-error-viewer-stack-frame-file] { + white-space: nowrap; + font-size: 0.75rem; + /* 12px */ + line-height: 1rem; + /* 16px */ +} + +[data-start-error-viewer-stack-frames-code] { + padding: 0.5rem 1rem; + gap: 0.25rem; + flex: 1 1 0%; + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + border-radius: 0.5rem; + background-color: rgb(17 24 39); +} + +[data-start-error-viewer-stack-frames-code-fallback] { + display: flex; + align-items: center; + justify-content: center; + color: rgb(249 250 251); + min-height: 16rem; + font-size: 0.875rem; + line-height: 1.25rem; +} + +[data-start-error-viewer-stack-frames-code-source] { + font-size: 0.875rem; + line-height: 1.25rem; + color: rgb(249 250 251); +} + +[data-start-error-viewer-stack-frames-code-container] { + width: 100%; + overflow: hidden; + min-width: 100%; + max-height: 16rem; + border-top-width: 1px; + border-top-style: solid; + border-top-color: rgb(249 250 251); +} + +[data-start-error-viewer-code-view] { + overflow: auto; + min-width: 100%; +} + +[data-start-error-viewer-code-view] > .shiki { + float: left; + min-width: 100%; + overflow-x: auto; + font-size: 0.75rem; + line-height: 1rem; + counter-reset: step; + counter-increment: step calc(var(--error-viewer-code-view-start, 1) - 1); + background-color: transparent !important; +} + +[data-start-error-viewer-code-view] > .shiki .line::before { + content: counter(step); + counter-increment: step; + width: 1rem; + margin-right: 1.5rem; + display: inline-block; + text-align: right; + color: rgba(115, 138, 148, 0.4); +} + +[data-start-error-viewer-error-line] { + background-color: rgba(239, 68, 68, 0.4); +} diff --git a/packages/start/src/shared/dev-toolbar/functions/BlobViewer.css b/packages/start/src/shared/dev-toolbar/functions/BlobViewer.css new file mode 100644 index 000000000..b440887f3 --- /dev/null +++ b/packages/start/src/shared/dev-toolbar/functions/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/dev-toolbar/functions/BlobViewer.tsx b/packages/start/src/shared/dev-toolbar/functions/BlobViewer.tsx new file mode 100644 index 000000000..890698a58 --- /dev/null +++ b/packages/start/src/shared/dev-toolbar/functions/BlobViewer.tsx @@ -0,0 +1,78 @@ +import { createMemo, createResource, type JSX, onCleanup, Show, Suspense } from 'solid-js'; + +import { Badge } from "../../ui/Badge.tsx"; +import Button from "../../ui/Button.tsx"; + +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/dev-toolbar/functions/FormDataViewer.tsx b/packages/start/src/shared/dev-toolbar/functions/FormDataViewer.tsx new file mode 100644 index 000000000..a896bbac7 --- /dev/null +++ b/packages/start/src/shared/dev-toolbar/functions/FormDataViewer.tsx @@ -0,0 +1,44 @@ +import { createResource, For, type JSX, Show, Suspense } from 'solid-js'; +import { Section } from '../../ui/Section.tsx'; +import { BlobViewer } from './BlobViewer.tsx'; +import { PropertySeparator, SerovalValue } 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/dev-toolbar/functions/HeadersViewer.css b/packages/start/src/shared/dev-toolbar/functions/HeadersViewer.css new file mode 100644 index 000000000..e6b8c1c4d --- /dev/null +++ b/packages/start/src/shared/dev-toolbar/functions/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/dev-toolbar/functions/HeadersViewer.tsx b/packages/start/src/shared/dev-toolbar/functions/HeadersViewer.tsx new file mode 100644 index 000000000..d0109b65d --- /dev/null +++ b/packages/start/src/shared/dev-toolbar/functions/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/dev-toolbar/functions/HexViewer.css b/packages/start/src/shared/dev-toolbar/functions/HexViewer.css new file mode 100644 index 000000000..8e6d7fb11 --- /dev/null +++ b/packages/start/src/shared/dev-toolbar/functions/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/dev-toolbar/functions/HexViewer.tsx b/packages/start/src/shared/dev-toolbar/functions/HexViewer.tsx new file mode 100644 index 000000000..29bf21f87 --- /dev/null +++ b/packages/start/src/shared/dev-toolbar/functions/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/dev-toolbar/functions/SerovalValue.css b/packages/start/src/shared/dev-toolbar/functions/SerovalValue.css new file mode 100644 index 000000000..b38afc019 --- /dev/null +++ b/packages/start/src/shared/dev-toolbar/functions/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/dev-toolbar/functions/SerovalValue.tsx b/packages/start/src/shared/dev-toolbar/functions/SerovalValue.tsx new file mode 100644 index 000000000..a8b420a92 --- /dev/null +++ b/packages/start/src/shared/dev-toolbar/functions/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/dev-toolbar/functions/SerovalViewer.css b/packages/start/src/shared/dev-toolbar/functions/SerovalViewer.css new file mode 100644 index 000000000..7ed90b0e1 --- /dev/null +++ b/packages/start/src/shared/dev-toolbar/functions/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; + 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/dev-toolbar/functions/SerovalViewer.tsx b/packages/start/src/shared/dev-toolbar/functions/SerovalViewer.tsx new file mode 100644 index 000000000..e6af11736 --- /dev/null +++ b/packages/start/src/shared/dev-toolbar/functions/SerovalViewer.tsx @@ -0,0 +1,997 @@ +import type { SerovalNode } from "seroval"; +import { createEffect, createSignal, For, type JSX, Show, splitProps } from "solid-js"; + +import { Badge } from "../../ui/Badge.tsx"; +import { Cascade, CascadeOption } from "../../ui/Cascade.tsx"; +import { Section } from "../../ui/Section.tsx"; +import { HexViewer } from "./HexViewer.tsx"; +import { PropertySeparator, SerovalValue } from "./SerovalValue.tsx"; + +import { SerovalChunkReader } from "../../../fns/serialization.ts"; +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/dev-toolbar/functions/URLSearchParamsViewer.tsx b/packages/start/src/shared/dev-toolbar/functions/URLSearchParamsViewer.tsx new file mode 100644 index 000000000..6d45ce585 --- /dev/null +++ b/packages/start/src/shared/dev-toolbar/functions/URLSearchParamsViewer.tsx @@ -0,0 +1,41 @@ +import { createResource, For, type JSX, Show, Suspense } from 'solid-js'; +import { Section } from '../../ui/Section.tsx'; +import { PropertySeparator, SerovalValue } from './SerovalValue.tsx'; + +interface URLSearchParamsViewerInnerProps { + source: URLSearchParams; +} + +function URLSearchParamsViewerInner(props: URLSearchParamsViewerInnerProps): JSX.Element { + return ( +
+
+ + {([key, value]) => ( +
+ + + +
+ )} +
+
+
+ ); +} + +export interface URLSearchParamsViewerProps { + source: URLSearchParams | Promise; +} + +export function URLSearchParamsViewer(props: URLSearchParamsViewerProps) { + const [data] = createResource(() => props.source); + + return ( + + + {(current) => } + + + ); +} \ No newline at end of file diff --git a/packages/start/src/shared/dev-toolbar/functions/index.tsx b/packages/start/src/shared/dev-toolbar/functions/index.tsx new file mode 100644 index 000000000..810223cb2 --- /dev/null +++ b/packages/start/src/shared/dev-toolbar/functions/index.tsx @@ -0,0 +1,302 @@ +import { createMemo, createSignal, For, type JSX, Show } from "solid-js"; +import { BODY_FORMAL_FILE, BODY_FORMAT_KEY, BodyFormat } from "../../../fns/shared.ts"; +import { Badge } from "../../ui/Badge.tsx"; +import IconButton from "../../ui/IconButton.tsx"; +import Placeholder from "../../ui/Placeholder.tsx"; +import { Section } from "../../ui/Section.tsx"; +import { Select, SelectOption } from "../../ui/Select.tsx"; +import { Tab, TabGroup, TabList, TabPanel } from "../../ui/Tabs.tsx"; +import { Text } from "../../ui/Text.tsx"; +import { ArrowLeftIcon, TrashIcon } from "../icons.tsx"; +import { BlobViewer } from "./BlobViewer.tsx"; +import { FormDataViewer } from "./FormDataViewer.tsx"; +import { HeadersViewer } from "./HeadersViewer.tsx"; +import { HexViewer } from "./HexViewer.tsx"; +import { PropertySeparator, SerovalValue } from "./SerovalValue.tsx"; +import { SerovalViewer } from "./SerovalViewer.tsx"; +import "./styles.css"; +import { type ServerFunctionRequest, type ServerFunctionResponse } from "./tracker.ts"; +import { URLSearchParamsViewer } from "./URLSearchParamsViewer.tsx"; + +async function getFile(source: Response | Request): Promise { + const formData = await source.formData(); + const file = formData.get(BODY_FORMAL_FILE); + if (!(file && file instanceof File)) { + throw new Error("invalid file input"); + } + 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 + + +
+
+ + + )} +
+
+ ); +} + +export interface ServerFunctionInstance { + request: ServerFunctionRequest; + response?: ServerFunctionResponse; +} + +interface ServerFunctionInstanceDetailProps { + id: string; + value: ServerFunctionInstance; +} + +function ServerFunctionInstanceDetail(props: ServerFunctionInstanceDetailProps) { + return ( + <> + + {props.value.request.source.method} + {props.id} + + + {response => { + if (response().source.ok) { + return {response().source.status}; + } + return {response().source.status}; + }} + + + ); +} + +interface ServerFunctionInstanceViewerProps { + id: string; + instance: ServerFunctionInstance; + onDelete: () => void; + onReturn: () => void; +} + +function ServerFunctionInstanceViewer(props: ServerFunctionInstanceViewerProps): JSX.Element { + const [tab, setTab] = createSignal<"request" | "response">("request"); + return ( +
+
+
+ + + +
+ +
+
+
+ + + +
+
+
+ setTab(value ?? "request")}> + + Request + Response + + + + +
+
+ ); +} + +function EmptyServerFunctions(): JSX.Element { + return ( + + No server function calls detected. + + ); +} + +export interface ServerFunctionViewerProps { + instances: Record; + onDeleteInstance: (value: string) => void; + show?: boolean; +} + +export function ServerFunctionViewer(props: ServerFunctionViewerProps): JSX.Element { + const [currentInstance, setCurrentInstance] = createSignal(); + + const keys = createMemo(() => Object.keys(props.instances)); + + return ( + +
+
+ {/* request/response viewer */} + + {value => ( + + {instance => ( + { + setCurrentInstance(undefined); + }} + onDelete={() => { + props.onDeleteInstance(value()); + }} + /> + )} + + )} + + + {/* list of calls */} +
+ }> + + +
+
+
+
+
+ ); +} diff --git a/packages/start/src/shared/dev-toolbar/functions/styles.css b/packages/start/src/shared/dev-toolbar/functions/styles.css new file mode 100644 index 000000000..bab751c23 --- /dev/null +++ b/packages/start/src/shared/dev-toolbar/functions/styles.css @@ -0,0 +1,104 @@ +[data-start-functions-instance-detail] { + display: flex; + align-items: center; + gap: 0.25rem; +} + +[data-start-functions-viewer] { + color: rgb(249 250 251); + margin: 0.5rem; + + display: flex; + flex-direction: column; + gap: 0.5rem; + + min-height: 100%; +} + +[data-start-functions-instances-container] { + border-radius: 0.5rem; + color: rgb(249 250 251); + width: 100%; + min-height: 100%; + overflow: auto; +} + +[data-start-functions-instances] { + list-style: none; + display: flex; + flex-direction: column; + padding: 0px; +} + +[data-start-functions-instances] > [data-start-select-option] { + display: flex; + align-items: center; + justify-content: space-between; + padding: 0.25rem; + color: rgb(249 250 251); +} + +[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; +} + +[data-start-function-instance-viewer] { + display: flex; + flex-direction: column; + gap: 0.5rem; + + width: 100%; +} + +[data-start-function-instance-viewer-nav] { + display: flex; + align-items: center; + gap: 0.25rem; + + justify-content: space-between; +} + +[data-start-function-instance-viewer-nav-left] { + display: flex; + align-items: center; + gap: 0.25rem; +} + +[data-start-function-instance-viewer-nav-left] > div { + display: flex; + align-items: center; + gap: 0.25rem; +} + +[data-start-function-instance-viewer-content] { + border-top-width: 1px; + border-top-style: solid; + border-top-color: rgb(249 250 251); + + height: 100%; +} + +[data-start-function-instance-viewer-content] > [tc-tab-group] { + height: 100%; +} \ No newline at end of file diff --git a/packages/start/src/shared/dev-toolbar/functions/tracker.ts b/packages/start/src/shared/dev-toolbar/functions/tracker.ts new file mode 100644 index 000000000..2fd56f264 --- /dev/null +++ b/packages/start/src/shared/dev-toolbar/functions/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/dev-overlay/icons.tsx b/packages/start/src/shared/dev-toolbar/icons.tsx similarity index 88% rename from packages/start/src/shared/dev-overlay/icons.tsx rename to packages/start/src/shared/dev-toolbar/icons.tsx index dd5598b3e..f35312170 100644 --- a/packages/start/src/shared/dev-overlay/icons.tsx +++ b/packages/start/src/shared/dev-toolbar/icons.tsx @@ -169,6 +169,25 @@ export function GithubIcon(props: JSX.IntrinsicElements["svg"] & { title: string ); } +export function ErrorIcon(props: JSX.IntrinsicElements["svg"] & { title: string }): JSX.Element { + return ( + + {props.title} + + + ); +} + export function SolidStartIcon( props: JSX.IntrinsicElements["svg"] & { title: string }, ): JSX.Element { @@ -176,7 +195,7 @@ export function SolidStartIcon( {props.title} - + ); } + +export function FunctionIcon(props: JSX.IntrinsicElements["svg"] & { title: string }): JSX.Element { + return ( + + {props.title} + + + ); +} + +export function TrashIcon(props: JSX.IntrinsicElements["svg"] & { title: string }): JSX.Element { + return ( + + {props.title} + + + ); +} diff --git a/packages/start/src/shared/dev-toolbar/index.css b/packages/start/src/shared/dev-toolbar/index.css new file mode 100644 index 000000000..19dc18f0b --- /dev/null +++ b/packages/start/src/shared/dev-toolbar/index.css @@ -0,0 +1,88 @@ +[data-start-dev-toolbar] { + position: fixed; + + bottom: 0px; + left: 0px; + + display: flex; + flex-direction: column-reverse; + align-items: flex-start; + + cursor: grab; +} + +[data-start-dev-toolbar]:active { + cursor: grabbing; +} + +[data-start-dev-toolbar] > [tc-toolbar] { + position: relative; + + background: oklch(25.7% 0.09 281.288); + margin: 1rem; + padding: 0.25rem 0.5rem; + + overflow: hidden; + border-radius: 9999px; + + display: flex; + flex-direction: row; + align-items: center; + + gap: 0.5rem; + + border: rgb(249 250 251) 1px solid; +} + +[data-start-dev-toolbar] > [tc-toolbar] > div { + display: flex; + + flex-direction: row; + align-items: center; + justify-content: center; +} + +[data-start-dev-toolbar] > [tc-toolbar] > div:last-child { + border-left: rgb(249 250 251) 1px solid; + padding-left: 0.5rem; + + gap: 0.5rem; +} + +[data-start-dev-toolbar-version] { + border-radius: 9999px; + color: rgb(249 250 251); + padding: 0.125rem 0.5rem; + + font-size: 0.875rem; + line-height: 1.25rem; + font-weight: 600; + + border: rgb(249 250 251) 1px solid; +} + +[data-start-dev-toolbar-version] > div { + height: 1.5rem; + width: 1.5rem; +} + +[data-start-dev-toolbar] svg { + height: 1.25rem; + width: 1.25rem; +} + +[data-start-dev-toolbar-panel] { + border: rgb(249 250 251) 1px solid; + background: oklch(25.7% 0.09 281.288); + margin: 0rem 1rem; + + overflow: auto; + + width: 50rem; + height: 30rem; + + max-width: calc(100vw - 10rem); + max-height: calc(100vh - 2rem); + + border-radius: 1rem; +} diff --git a/packages/start/src/shared/dev-toolbar/index.tsx b/packages/start/src/shared/dev-toolbar/index.tsx new file mode 100644 index 000000000..7bfa72ef0 --- /dev/null +++ b/packages/start/src/shared/dev-toolbar/index.tsx @@ -0,0 +1,242 @@ +import { + createEffect, + createSignal, + ErrorBoundary, + onCleanup, + resetErrorBoundaries, + Show, + type JSX, +} from "solid-js"; +import { createStore } from "solid-js/store"; +import { Portal } from "solid-js/web"; +import { Toolbar } from "terracotta"; +import info from "../../../package.json" with { type: "json" }; +import clientOnly from "../clientOnly.ts"; +import { HttpStatusCode } from "../HttpStatusCode.ts"; +import IconButton from "../ui/IconButton.tsx"; +import { Text } from "../ui/Text.tsx"; +import { type ServerFunctionInstance, ServerFunctionViewer } from "./functions/index.tsx"; +import { captureServerFunctionCall } from "./functions/tracker.ts"; +import { ErrorIcon, FunctionIcon, SolidStartIcon } from "./icons.tsx"; +import "./index.css"; + +const ErrorViewer = import.meta.env.PROD + ? () => <> + : clientOnly(() => import("./error-viewer/index.tsx"), { lazy: true }); + +export interface DevToolbarProps { + children?: JSX.Element; +} + +export function DevToolbar(props: DevToolbarProps) { + const [ref, setRef] = createSignal(); + + createEffect(() => { + const current = ref(); + + if (current) { + let isDown = false; + + // Offsets of the mouse relatively to the element's position + let offsetX = 0; + let offsetY = 0; + + let currentX = 0; + let currentY = 0; + + let centerX = 0; + let centerY = 0; + + const resetPosition = () => { + current.style.top = "auto"; + current.style.left = "auto"; + current.style.bottom = "auto"; + current.style.right = "auto"; + }; + + let isDirty = false; + + const ac = new AbortController(); + + current.addEventListener( + "mousedown", + e => { + isDown = true; + + const rect = current.getBoundingClientRect(); + + currentX = rect.left; + currentY = rect.top; + + offsetX = e.clientX - currentX; + offsetY = e.clientY - currentY; + + centerX = rect.width / 2; + centerY = rect.height / 2; + + isDirty = true; + }, + { + signal: ac.signal, + }, + ); + + window.addEventListener( + "mouseup", + () => { + if (isDown && !isDirty) { + const preferredAnchorX = currentX + centerX < window.innerWidth / 2 ? "left" : "right"; + const preferredAnchorY = currentY + centerY < window.innerHeight / 2 ? "top" : "bottom"; + + resetPosition(); + + current.style[preferredAnchorX] = "0px"; + current.style[preferredAnchorY] = "0px"; + + current.style.flexDirection = + preferredAnchorY === "bottom" ? "column-reverse" : "column"; + current.style.alignItems = preferredAnchorX === "left" ? "flex-start" : "flex-end"; + } + isDown = false; + }, + { + signal: ac.signal, + }, + ); + + window.addEventListener( + "mousemove", + e => { + if (isDown) { + if (isDirty) { + resetPosition(); + isDirty = false; + } + currentX = e.clientX - offsetX; + currentY = e.clientY - offsetY; + + current.style.left = `${currentX}px`; + current.style.top = `${currentY}px`; + } + }, + { + signal: ac.signal, + passive: true, + }, + ); + + onCleanup(() => { + ac.abort(); + }); + } + }); + + const [content, setContent] = createSignal<"fn" | "err" | undefined>(undefined); + + function toggleContent(value: "fn" | "err") { + if (content() === value) { + setContent(undefined); + } else { + setContent(value); + } + } + + const [errors, setErrors] = createSignal([]); + + function resetError() { + setErrors([]); + resetErrorBoundaries(); + } + + function pushError(error: unknown) { + console.error(error); + setErrors(current => [error, ...current]); + + setContent("err"); + } + + createEffect(() => { + const onErrorEvent = (error: ErrorEvent) => { + pushError(error.error ?? error); + }; + + window.addEventListener("error", onErrorEvent); + + onCleanup(() => { + window.removeEventListener("error", onErrorEvent); + }); + }); + + const [store, setStore] = createStore({ + instances: {} as Record, + }); + + createEffect(() => { + onCleanup( + captureServerFunctionCall(call => { + console.log(call); + if (call.type === "request") { + setStore("instances", call.instance, value => { + return { + ...value, + request: call, + }; + }); + } else { + setStore("instances", call.instance, value => { + return { + ...value, + response: call, + }; + }); + } + }), + ); + }); + + return ( + <> + +
+ +
+ toggleContent("err")} disabled={errors().length === 0}> + + + toggleContent("fn")}> + + +
+
+ +
+ + {info.version as string} + +
+
+
+ + { + setStore("instances", value, undefined); + }} + /> +
+ + { + pushError(error); + return ; + }} + > + {props.children} + + 0}> + + + + ); +} diff --git a/packages/start/src/shared/ui/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..e48c1c75c --- /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; + + color: oklch(97% 0.014 254.604); + + background-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 { + color: rgb(219 234 254); + background-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..3a50ec5b8 --- /dev/null +++ b/packages/start/src/shared/ui/IconButton.css @@ -0,0 +1,39 @@ +[data-start-icon-button] { + display: flex; + padding: 0.25rem; + border-radius: 9999px; + border-width: 0px; + align-items: center; + justify-content: center; + cursor: pointer; + color: rgb(249 250 251); + background: none; + transition-property: color, background-color, border-color, box-shadow; + transition-timing-function: cubic-bezier(0.4, 0, 0.2, 1); + transition-duration: 150ms; +} + +[data-start-icon-button]:hover { + color: rgb(229 231 235); + background-color: rgb(55 65 81); +} + +[data-start-icon-button]:focus { + outline: 2px solid transparent; + outline-color: rgb(249 250 251); + 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 { + color: rgb(243 244 246); + background-color: rgb(31 41 55); +} + +[data-start-icon-button] > svg { + height: 1.25rem; + width: 1.25rem; +} 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..8d8321889 --- /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% - 2rem); + + 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..c00ed704f --- /dev/null +++ b/packages/start/src/shared/ui/Select.css @@ -0,0 +1,37 @@ +[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(249 250 251); + 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 { + color: rgb(229 231 235); + background-color: rgb(55 65 81); +} 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..c9400d374 --- /dev/null +++ b/packages/start/src/shared/ui/Tabs.css @@ -0,0 +1,52 @@ +[data-start-tab] { + display: flex; + align-items: center; + justify-content: center; + + border-radius: 0.5rem; + cursor: pointer; + + 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 { + color: rgb(229 231 235); + background-color: rgb(55 65 81); +} + +[data-start-tab-group] { +} + +[data-start-tab-list] { + display: flex; + gap: 0.25rem; + padding: 0.25rem; +} + +[data-start-tab-panel] { + height: 100%; + + overflow: auto; + + display: flex; + flex-direction: column; + gap: 1rem; + + border-top-width: 1px; + border-top-style: solid; + border-top-color: rgb(249 250 251); +} 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 befb35e93..1b66b838e 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -387,8 +387,8 @@ importers: specifier: ^0.11.22 version: 0.11.22 terracotta: - specifier: ^1.1.0 - version: 1.1.0(solid-js@1.9.11) + specifier: ^1.1.1 + version: 1.1.1(solid-js@1.9.11) vite-plugin-solid: specifier: ^2.11.11 version: 2.11.11(@testing-library/jest-dom@6.9.1)(solid-js@1.9.11)(vite@8.1.4(@types/node@25.5.0)(esbuild@0.27.4)(jiti@2.7.0)(terser@5.46.0)) @@ -3174,8 +3174,8 @@ packages: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} engines: {node: '>=8'} - terracotta@1.1.0: - resolution: {integrity: sha512-kfQciWUBUBgYkXu7gh3CK3FAJng/iqZslAaY08C+k1Hdx17aVEpcFFb/WPaysxAfcupNH3y53s/pc53xxZauww==} + terracotta@1.1.1: + resolution: {integrity: sha512-Sa6wnvkGMFmPq8N/wQb2aKkIW2eXH0LJvUOEk6QZLBEjqy+LsbzAOpDuqEfOmfA/hexssE6eQve2i1IIOeOh4g==} engines: {node: '>=10'} peerDependencies: solid-js: ^1.8 @@ -6235,7 +6235,7 @@ snapshots: term-size@2.2.1: {} - terracotta@1.1.0(solid-js@1.9.11): + terracotta@1.1.1(solid-js@1.9.11): dependencies: solid-js: 1.9.11 solid-use: 0.9.1(solid-js@1.9.11)