-
-
-
-
-
{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 (
+
+ );
+}
+
+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]) => (
+
+ )}
+
+
+ );
+}
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 (
+
+ );
+}
+
+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}
+ >
+
+
+
+
+ {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]) => (
+
+ )}
+
+
+
+
+ );
+}
+
+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]) => (
+
+ )}
+
+
+
+
+ >
+ )}
+
+
+ );
+}
+
+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 (
+
+ );
+}
+
export function SolidStartIcon(
props: JSX.IntrinsicElements["svg"] & { title: string },
): JSX.Element {
@@ -176,7 +195,7 @@ export function SolidStartIcon(