Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
5 changes: 5 additions & 0 deletions .changeset/devtools-upload-viewer.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"@solidjs/start": patch
---

Show server function calls that send files in the dev toolbar as a Seroval tree. The arguments used to show as one raw string, with the files listed apart. Each file now shows in its place in the arguments, with its name and type.
2 changes: 1 addition & 1 deletion packages/start/src/fns/uploads.ts
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,7 @@ import { createPlugin, type Plugin, type SerovalNode } from "seroval";
export const ARGS_PART = "args";
export const FILE_PART = "file";

const UPLOAD_TAG = "solid-start/upload";
export const UPLOAD_TAG = "solid-start/upload";

/*
* Migration note: seroval's binary mode (`binary.serialize` and
Expand Down
51 changes: 49 additions & 2 deletions packages/start/src/shared/dev-toolbar/functions/SerovalViewer.tsx
Original file line number Diff line number Diff line change
@@ -1,11 +1,13 @@
import type { SerovalNode } from "seroval";
import { Feature, fromCrossJSON, type SerovalNode } from "seroval";
import { createEffect, createSignal, For, type JSX, Show } from "solid-js";

import { Badge } from "../../ui/Badge.tsx";
import { BlobViewer } from "./BlobViewer.tsx";
import { HexViewer } from "./HexViewer.tsx";
import { PropertySeparator, SerovalValue } from "./SerovalValue.tsx";

import { SerovalChunkReader } from "../../../fns/serialization.ts";
import { FILE_PART, UPLOAD_TAG } from "../../../fns/uploads.ts";
import "./SerovalViewer.css";

function LinkIcon(props: JSX.IntrinsicElements["svg"] & { title: string }): JSX.Element {
Expand All @@ -29,6 +31,8 @@ function LinkIcon(props: JSX.IntrinsicElements["svg"] & { title: string }): JSX.

export interface SerovalViewerProps {
stream: Request | Response;
/** The form a multipart call sent, so each upload shows its file part. */
uploads?: FormData;
}

function getNodeType(node: SerovalNode) {
Expand Down Expand Up @@ -386,6 +390,7 @@ interface RenderContext {
getNode: (index: number) => SerovalNode | undefined;
getPromise: (index: number) => Extract<SerovalNode, { t: 23 | 24 }> | undefined;
getStream: (index: number) => Extract<SerovalNode, { t: 32 | 33 | 34 }>[] | undefined;
getUpload: (node: SerovalNode) => Blob | undefined;
}

function getStreamKeyword(t: 32 | 33 | 34): string {
Expand Down Expand Up @@ -908,7 +913,15 @@ function TreeValue(props: TreeValueProps): JSX.Element {
</ExpandableRow>
);
// Plugin = 25,
case 25:
case 25: {
const upload = ctx.getUpload(node);
if (upload) {
return (
<LeafRow label={props.label}>
<BlobViewer source={upload} />
</LeafRow>
);
}
return (
<ExpandableRow
label={props.label}
Expand All @@ -923,6 +936,7 @@ function TreeValue(props: TreeValueProps): JSX.Element {
</For>
</ExpandableRow>
);
}
// IteratorFactoryInstance = 28,
case 28:
// AsyncIteratorFactoryInstance = 30,
Expand Down Expand Up @@ -1035,6 +1049,38 @@ function createSimpleStore<T extends Record<string | number, unknown>>(initial:
};
}

/**
* Reads a primitive node, such as a string or a number, in an upload node.
*/
function readPrimitive(node: SerovalNode | undefined): unknown {
if (!node) return undefined;
return fromCrossJSON(node, { refs: new Map(), disabledFeatures: Feature.RegExp });
}

/**
* Finds the file part of an upload node written by `createUploadSerializer`,
* and gives it the type, name and last modified time the node holds, as the
* server does.
*/
function getUpload(uploads: FormData | undefined, node: SerovalNode): Blob | undefined {
if (!uploads || node.t !== 25 || node.c !== UPLOAD_TAG) return undefined;
const fields = node.s as { i?: SerovalNode; t?: SerovalNode; n?: SerovalNode; l?: SerovalNode };
const index = readPrimitive(fields.i);
if (typeof index !== "number") return undefined;
const part = uploads.get(`${FILE_PART}${index}`);
if (!(part instanceof Blob)) return undefined;
// A client before the metadata fields sends only the index.
const type = readPrimitive(fields.t);
if (typeof type !== "string") return part;
const name = readPrimitive(fields.n);
if (typeof name !== "string") return new Blob([part], { type });
const lastModified = readPrimitive(fields.l);
return new File([part], name, {
type,
lastModified: typeof lastModified === "number" ? lastModified : undefined,
});
}

export function SerovalViewer(props: SerovalViewerProps): JSX.Element {
const [selected, setSelected] = createSignal<SerovalNode>();

Expand Down Expand Up @@ -1128,6 +1174,7 @@ export function SerovalViewer(props: SerovalViewerProps): JSX.Element {
getNode: index => references.read(index),
getPromise: index => promises.read(index),
getStream: index => streams.read(index),
getUpload: node => getUpload(props.uploads, node),
}}
node={root}
seen={[]}
Expand Down
31 changes: 30 additions & 1 deletion packages/start/src/shared/dev-toolbar/functions/index.tsx
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
import { createMemo, createSignal, For, type JSX, Show } from "solid-js";
import { createMemo, createResource, createSignal, For, type JSX, Show, Suspense } from "solid-js";
import { BODY_FORMAL_FILE, BODY_FORMAT_KEY, BodyFormat } from "../../../fns/shared.ts";
import { ARGS_PART } from "../../../fns/uploads.ts";
import { Badge } from "../../ui/Badge.tsx";
import IconButton from "../../ui/IconButton.tsx";
import Placeholder from "../../ui/Placeholder.tsx";
Expand Down Expand Up @@ -31,6 +32,32 @@ async function getURLSearchParams(source: Response | Request): Promise<URLSearch
return new URLSearchParams(text);
}

interface SerovalFormDataViewerProps {
source: Request | Response;
}

/**
* Shows a call that sent files: the arguments from the `ARGS_PART` field as a
* Seroval tree, with each upload in the tree replaced by its file part.
*/

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

does solid start have its own dev-toolbar. I kinda forget if the other package is just 2.0 or not

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

yes. Solid 2.0 have it separated now.

function SerovalFormDataViewer(props: SerovalFormDataViewerProps): JSX.Element {
const [form] = createResource(() => props.source.formData());
return (
<Suspense>
<Show when={form()} keyed>
{current => {
const args = current.get(ARGS_PART);
return (
<Show when={typeof args === "string"} fallback={<FormDataViewer source={current} />}>
<SerovalViewer stream={new Response(args as string)} uploads={current} />
</Show>
);
}}
</Show>
</Suspense>
);
}

interface ContentViewerProps {
source: ServerFunctionRequest | ServerFunctionResponse;
}
Expand All @@ -48,6 +75,8 @@ function ContentViewer(props: ContentViewerProps): JSX.Element {
case startType === "true":
case startType === BodyFormat.Seroval:
return <SerovalViewer stream={source} />;
case startType === BodyFormat.SerovalFormData:
return <SerovalFormDataViewer source={source} />;
case startType === BodyFormat.String:
return <HexViewer bytes={source.bytes()} />;
case startType === BodyFormat.File:
Expand Down
Loading