diff --git a/.changeset/icy-rings-exist.md b/.changeset/icy-rings-exist.md new file mode 100644 index 000000000..27c7c5457 --- /dev/null +++ b/.changeset/icy-rings-exist.md @@ -0,0 +1,5 @@ +--- +"@solidjs/start": minor +--- + +seroval json mode diff --git a/apps/fixtures/serialization-modes/README.md b/apps/fixtures/serialization-modes/README.md new file mode 100644 index 000000000..a117e56c4 --- /dev/null +++ b/apps/fixtures/serialization-modes/README.md @@ -0,0 +1,19 @@ +# Serialization checks + +This fixture is designed to point out the differences between Seroval 2 modes. + +```ts +export default defineConfig({ + middleware: "./src/middleware.ts", + serialization: { + mode: "js" // "json" + } +}); +``` + +On JS mode, seroval will use a custom serializer, while this improves performance and reduces payload size, it runs an `eval()` on client-side, +so a strict CSP will block deserialization. On JSON mode, the payload will be slightly larger, but deserialization happens via `JSON.parse` and thus CSP will not block it. + +> [!IMPORTANT] +> For backwards compatibility, `v1` has "js" as the default. +> On `v2`, "json" is the new default. diff --git a/apps/fixtures/serialization-modes/app.config.ts b/apps/fixtures/serialization-modes/app.config.ts new file mode 100644 index 000000000..313ebbb2d --- /dev/null +++ b/apps/fixtures/serialization-modes/app.config.ts @@ -0,0 +1,8 @@ +import { defineConfig } from "@solidjs/start/config"; + +export default defineConfig({ + middleware: "./src/middleware.ts", + serialization: { + mode: "js" + } +}); diff --git a/apps/fixtures/serialization-modes/package.json b/apps/fixtures/serialization-modes/package.json new file mode 100644 index 000000000..9ce41c5d5 --- /dev/null +++ b/apps/fixtures/serialization-modes/package.json @@ -0,0 +1,21 @@ +{ + "name": "fixture-serialization-modes", + "type": "module", + "scripts": { + "dev": "vinxi dev", + "build": "vinxi build", + "start": "vinxi start", + "version": "vinxi version" + }, + "dependencies": { + "@solidjs/meta": "^0.29.4", + "@solidjs/router": "^0.15.0", + "@solidjs/start": "workspace:*", + "shieldwall": "^0.4.0", + "solid-js": "^1.9.5", + "vinxi": "^0.5.7" + }, + "engines": { + "node": ">=22" + } +} diff --git a/apps/fixtures/serialization-modes/public/favicon.ico b/apps/fixtures/serialization-modes/public/favicon.ico new file mode 100644 index 000000000..fb282da07 Binary files /dev/null and b/apps/fixtures/serialization-modes/public/favicon.ico differ diff --git a/apps/fixtures/serialization-modes/src/app.css b/apps/fixtures/serialization-modes/src/app.css new file mode 100644 index 000000000..8596998a4 --- /dev/null +++ b/apps/fixtures/serialization-modes/src/app.css @@ -0,0 +1,39 @@ +body { + font-family: Gordita, Roboto, Oxygen, Ubuntu, Cantarell, "Open Sans", "Helvetica Neue", sans-serif; +} + +a { + margin-right: 1rem; +} + +main { + text-align: center; + padding: 1em; + margin: 0 auto; +} + +h1 { + color: #335d92; + text-transform: uppercase; + font-size: 4rem; + font-weight: 100; + line-height: 1.1; + margin: 4rem auto; + max-width: 14rem; +} + +p { + max-width: 14rem; + margin: 2rem auto; + line-height: 1.35; +} + +@media (min-width: 480px) { + h1 { + max-width: none; + } + + p { + max-width: none; + } +} diff --git a/apps/fixtures/serialization-modes/src/app.tsx b/apps/fixtures/serialization-modes/src/app.tsx new file mode 100644 index 000000000..d1359c8d8 --- /dev/null +++ b/apps/fixtures/serialization-modes/src/app.tsx @@ -0,0 +1,22 @@ +import { MetaProvider, Title } from "@solidjs/meta"; +import { Router } from "@solidjs/router"; +import { FileRoutes } from "@solidjs/start/router"; +import { Suspense } from "solid-js"; +import "./app.css"; + +export default function App() { + return ( + ( + + SolidStart - Basic + Index + About + {props.children} + + )} + > + + + ); +} diff --git a/apps/fixtures/serialization-modes/src/components/Counter.css b/apps/fixtures/serialization-modes/src/components/Counter.css new file mode 100644 index 000000000..220e17946 --- /dev/null +++ b/apps/fixtures/serialization-modes/src/components/Counter.css @@ -0,0 +1,21 @@ +.increment { + font-family: inherit; + font-size: inherit; + padding: 1em 2em; + color: #335d92; + background-color: rgba(68, 107, 158, 0.1); + border-radius: 2em; + border: 2px solid rgba(68, 107, 158, 0); + outline: none; + width: 200px; + font-variant-numeric: tabular-nums; + cursor: pointer; +} + +.increment:focus { + border: 2px solid #335d92; +} + +.increment:active { + background-color: rgba(68, 107, 158, 0.2); +} \ No newline at end of file diff --git a/apps/fixtures/serialization-modes/src/components/Counter.tsx b/apps/fixtures/serialization-modes/src/components/Counter.tsx new file mode 100644 index 000000000..091fc5d0b --- /dev/null +++ b/apps/fixtures/serialization-modes/src/components/Counter.tsx @@ -0,0 +1,11 @@ +import { createSignal } from "solid-js"; +import "./Counter.css"; + +export default function Counter() { + const [count, setCount] = createSignal(0); + return ( + + ); +} diff --git a/apps/fixtures/serialization-modes/src/entry-client.tsx b/apps/fixtures/serialization-modes/src/entry-client.tsx new file mode 100644 index 000000000..0ca4e3c30 --- /dev/null +++ b/apps/fixtures/serialization-modes/src/entry-client.tsx @@ -0,0 +1,4 @@ +// @refresh reload +import { mount, StartClient } from "@solidjs/start/client"; + +mount(() => , document.getElementById("app")!); diff --git a/apps/fixtures/serialization-modes/src/entry-server.tsx b/apps/fixtures/serialization-modes/src/entry-server.tsx new file mode 100644 index 000000000..401eff83f --- /dev/null +++ b/apps/fixtures/serialization-modes/src/entry-server.tsx @@ -0,0 +1,21 @@ +// @refresh reload +import { createHandler, StartServer } from "@solidjs/start/server"; + +export default createHandler(() => ( + ( + + + + + + {assets} + + +
{children}
+ {scripts} + + + )} + /> +)); diff --git a/apps/fixtures/serialization-modes/src/global.d.ts b/apps/fixtures/serialization-modes/src/global.d.ts new file mode 100644 index 000000000..dc6f10c22 --- /dev/null +++ b/apps/fixtures/serialization-modes/src/global.d.ts @@ -0,0 +1 @@ +/// diff --git a/apps/fixtures/serialization-modes/src/middleware.ts b/apps/fixtures/serialization-modes/src/middleware.ts new file mode 100644 index 000000000..b00d186a8 --- /dev/null +++ b/apps/fixtures/serialization-modes/src/middleware.ts @@ -0,0 +1,26 @@ +import { createMiddleware } from "@solidjs/start/middleware"; +import { csp } from "shieldwall/start"; +import { UNSAFE_INLINE } from "shieldwall/start/csp"; + +export default createMiddleware({ + onRequest: [ + csp({ + extend: "production_basic", + config: { + withNonce: false, + reportOnly: false, + value: { + "default-src": ["self"], + "script-src": ["self", UNSAFE_INLINE, "http:"], + "style-src": ["self", UNSAFE_INLINE], + "img-src": ["self", "data:", "https:", "http:"], + "font-src": ["self"], + "connect-src": ["self", "ws://localhost:*", "http://localhost:*"], + "frame-src": ["self"], + "base-uri": ["self"] + // "form-action": ["self"] + } + } + }) + ] +}); diff --git a/apps/fixtures/serialization-modes/src/routes/[...404].tsx b/apps/fixtures/serialization-modes/src/routes/[...404].tsx new file mode 100644 index 000000000..4ea71ec7f --- /dev/null +++ b/apps/fixtures/serialization-modes/src/routes/[...404].tsx @@ -0,0 +1,19 @@ +import { Title } from "@solidjs/meta"; +import { HttpStatusCode } from "@solidjs/start"; + +export default function NotFound() { + return ( +
+ Not Found + +

Page Not Found

+

+ Visit{" "} + + start.solidjs.com + {" "} + to learn how to build SolidStart apps. +

+
+ ); +} diff --git a/apps/fixtures/serialization-modes/src/routes/index.tsx b/apps/fixtures/serialization-modes/src/routes/index.tsx new file mode 100644 index 000000000..a08ccf229 --- /dev/null +++ b/apps/fixtures/serialization-modes/src/routes/index.tsx @@ -0,0 +1,30 @@ +import { Title } from "@solidjs/meta"; +import { createEffect } from "solid-js"; +import Counter from "~/components/Counter"; + +const breakval = () => { + "use server"; + + return new Date(); +}; + +export default function Home() { + createEffect(() => { + console.log(breakval()); + }); + + return ( +
+ Hello World +

Hello world!

+ +

+ Visit{" "} + + start.solidjs.com + {" "} + to learn how to build SolidStart apps. +

+
+ ); +} diff --git a/apps/fixtures/serialization-modes/tsconfig.json b/apps/fixtures/serialization-modes/tsconfig.json new file mode 100644 index 000000000..7d5871a07 --- /dev/null +++ b/apps/fixtures/serialization-modes/tsconfig.json @@ -0,0 +1,19 @@ +{ + "compilerOptions": { + "target": "ESNext", + "module": "ESNext", + "moduleResolution": "bundler", + "allowSyntheticDefaultImports": true, + "esModuleInterop": true, + "jsx": "preserve", + "jsxImportSource": "solid-js", + "allowJs": true, + "strict": true, + "noEmit": true, + "types": ["vinxi/types/client"], + "isolatedModules": true, + "paths": { + "~/*": ["./src/*"] + } + } +} diff --git a/apps/tests/src/middleware.ts b/apps/tests/src/middleware.ts new file mode 100644 index 000000000..ad8bab262 --- /dev/null +++ b/apps/tests/src/middleware.ts @@ -0,0 +1,5 @@ +import { createMiddleware } from "@solidjs/start/middleware"; + +export default createMiddleware({ + onRequest: [] +}); diff --git a/apps/tests/src/routes/actions/use-submission.tsx b/apps/tests/src/routes/actions/use-submission.tsx new file mode 100644 index 000000000..e1cb69d3b --- /dev/null +++ b/apps/tests/src/routes/actions/use-submission.tsx @@ -0,0 +1,23 @@ +import { Title } from "@solidjs/meta"; +import { action, useSubmission } from "@solidjs/router"; +import { Show } from "solid-js"; + +const actionStuff = action(async () => { + "use server"; + + return "Hello world!"; +}, "actionStuff"); + +export default function Home() { + const actionData = useSubmission(actionStuff); + return ( +
+ Hello World +
+ +
+ + {result =>

{result().result}

}
+
+ ); +} diff --git a/packages/start/config/index.d.ts b/packages/start/config/index.d.ts index 513b935fa..bb1613f64 100644 --- a/packages/start/config/index.d.ts +++ b/packages/start/config/index.d.ts @@ -26,6 +26,19 @@ type SolidStartInlineConfig = { experimental?: { islands?: boolean; }; + serialization?: { + /** + * The serialization mode to use for server functions. + * The "js" mode uses a custom binary format that is more efficient than JSON, but requires a custom deserializer (with `eval()`) on the client. + * A strong CSP should block `eval()` executions, which would prevent the "js" mode from working. + * The "json" mode uses JSON for serialization, which is less efficient but can be deserialized with `JSON.parse` on the client. + * + * @default "js" + * @ + * @warning on v2, "json" will be the default. + */ + mode?: "js" | "json"; + }; vite?: | ViteCustomizableConfig | ((options: { router: "server" | "client" | "server-function" }) => ViteCustomizableConfig); diff --git a/packages/start/config/index.js b/packages/start/config/index.js index ba7bebc1f..afc40039e 100644 --- a/packages/start/config/index.js +++ b/packages/start/config/index.js @@ -89,6 +89,7 @@ export function defineConfig(baseConfig = {}) { } } }); + const routeDir = join(start.appRoot, start.routeDir); let server = start.server; if (!start.ssr) { @@ -99,6 +100,8 @@ export function defineConfig(baseConfig = {}) { entryExtension = ".jsx"; } + const serializationMode = start.serialization?.mode || 'js' + return createApp({ server: { compressPublicAssets: { @@ -167,6 +170,7 @@ export function defineConfig(baseConfig = {}) { "import.meta.env.SSR": JSON.stringify(true), "import.meta.env.START_SSR": JSON.stringify(start.ssr), "import.meta.env.START_DEV_OVERLAY": JSON.stringify(start.devOverlay), + "import.meta.env.SEROVAL_MODE": JSON.stringify(serializationMode), ...userConfig.define } }) @@ -234,6 +238,7 @@ export function defineConfig(baseConfig = {}) { "import.meta.env.START_SSR": JSON.stringify(start.ssr), "import.meta.env.START_DEV_OVERLAY": JSON.stringify(start.devOverlay), "import.meta.env.SERVER_BASE_URL": JSON.stringify(server?.baseURL ?? ""), + "import.meta.env.SEROVAL_MODE": JSON.stringify(serializationMode), ...userConfig.define } }) @@ -293,6 +298,7 @@ export function defineConfig(baseConfig = {}) { "import.meta.env.SSR": JSON.stringify(true), "import.meta.env.START_SSR": JSON.stringify(start.ssr), "import.meta.env.START_DEV_OVERLAY": JSON.stringify(start.devOverlay), + "import.meta.env.SEROVAL_MODE": JSON.stringify(serializationMode), ...userConfig.define } }) diff --git a/packages/start/src/runtime/serialization.ts b/packages/start/src/runtime/serialization.ts new file mode 100644 index 000000000..b206a6830 --- /dev/null +++ b/packages/start/src/runtime/serialization.ts @@ -0,0 +1,262 @@ +import { + crossSerializeStream, + deserialize, + Feature, + fromCrossJSON, + fromJSON, + getCrossReferenceHeader, + type SerovalNode, + toCrossJSONStream, + toJSONAsync, +} from "seroval"; +import { + AbortSignalPlugin, + CustomEventPlugin, + DOMExceptionPlugin, + EventPlugin, + FormDataPlugin, + HeadersPlugin, + ReadableStreamPlugin, + RequestPlugin, + ResponsePlugin, + URLPlugin, + URLSearchParamsPlugin, +} from "seroval-plugins/web"; + +// TODO(Alexis): if we can, allow providing an option to extend these. +const DEFAULT_PLUGINS = [ + AbortSignalPlugin, + CustomEventPlugin, + DOMExceptionPlugin, + EventPlugin, + FormDataPlugin, + HeadersPlugin, + ReadableStreamPlugin, + RequestPlugin, + ResponsePlugin, + URLSearchParamsPlugin, + URLPlugin, +]; +const MAX_SERIALIZATION_DEPTH_LIMIT = 64; +const DISABLED_FEATURES = Feature.RegExp; + +/** + * Alexis: + * + * A "chunk" is a piece of data emitted by the streaming serializer. + * Each chunk is represented by a 32-bit value (encoded in hexadecimal), + * followed by the encoded string (8-bit representation). This format + * is important so we know how much of the chunk being streamed we + * are expecting before parsing the entire string data. + * + * This is sort of a bootleg "multipart/form-data" except it's bad at + * handling File/Blob LOL + * + * The format is as follows: + * ;0xFFFFFFFF; + */ +function createChunk(data: string): Uint8Array { + const encodeData = new TextEncoder().encode(data); + const bytes = encodeData.length; + const baseHex = bytes.toString(16); + const totalHex = "00000000".substring(0, 8 - baseHex.length) + baseHex; // 32-bit + const head = new TextEncoder().encode(`;0x${totalHex};`); + + const chunk = new Uint8Array(12 + bytes); + chunk.set(head); + chunk.set(encodeData, 12); + return chunk; +} + +export function serializeToJSStream(id: string, value: any) { + return new ReadableStream({ + start(controller) { + crossSerializeStream(value, { + scopeId: id, + plugins: DEFAULT_PLUGINS, + onSerialize(data: string, initial: boolean) { + controller.enqueue( + createChunk( + initial ? `(${getCrossReferenceHeader(id)},${data})` : data, + ), + ); + }, + onDone() { + controller.close(); + }, + onError(error: any) { + controller.error(error); + }, + }); + }, + }); +} + +export function serializeToJSONStream(value: any) { + return new ReadableStream({ + start(controller) { + toCrossJSONStream(value, { + disabledFeatures: DISABLED_FEATURES, + depthLimit: MAX_SERIALIZATION_DEPTH_LIMIT, + plugins: DEFAULT_PLUGINS, + onParse(node) { + controller.enqueue(createChunk(JSON.stringify(node))); + }, + onDone() { + controller.close(); + }, + onError(error) { + controller.error(error); + }, + }); + }, + }); +} + +class SerovalChunkReader { + reader: ReadableStreamDefaultReader; + buffer: Uint8Array; + done: boolean; + constructor(stream: ReadableStream) { + this.reader = stream.getReader(); + this.buffer = new Uint8Array(0); + this.done = false; + } + + async readChunk() { + // if there's no chunk, read again + const chunk = await this.reader.read(); + if (!chunk.done) { + // repopulate the buffer + const newBuffer = new Uint8Array(this.buffer.length + chunk.value.length); + newBuffer.set(this.buffer); + newBuffer.set(chunk.value, this.buffer.length); + this.buffer = newBuffer; + } else { + this.done = true; + } + } + + async next(): Promise< + { done: true; value: undefined } | { done: false; value: string } + > { + // Check if the buffer is empty + if (this.buffer.length === 0) { + // if we are already done... + if (this.done) { + return { + done: true, + value: undefined, + }; + } + // Otherwise, read a new chunk + await this.readChunk(); + return await this.next(); + } + // Read the "byte header" + // The byte header tells us how big the expected data is + // so we know how much data we should wait before we + // deserialize the data + const head = new TextDecoder().decode(this.buffer.subarray(1, 11)); + const bytes = Number.parseInt(head, 16); // ;0x00000000; + // Check if the buffer has enough bytes to be parsed + while (bytes > this.buffer.length - 12) { + // If it's not enough, and the reader is done + // then the chunk is invalid. + if (this.done) { + throw new Error("Malformed server function stream."); + } + // Otherwise, we read more chunks + await this.readChunk(); + } + // Extract the exact chunk as defined by the byte header + const partial = new TextDecoder().decode( + this.buffer.subarray(12, 12 + bytes), + ); + // The rest goes to the buffer + this.buffer = this.buffer.subarray(12 + bytes); + + // Deserialize the chunk + return { + done: false, + value: partial, + }; + } + + async drain(interpret: (chunk: string) => void) { + while (true) { + const result = await this.next(); + if (result.done) { + break; + } else { + interpret(result.value); + } + } + } +} + +export async function serializeToJSONString(value: any) { + // const response = new Response(serializeToJSONStream(value)); + // return await response.text(); + return JSON.stringify(toJSONAsync(value, { + plugins: DEFAULT_PLUGINS, + depthLimit: MAX_SERIALIZATION_DEPTH_LIMIT, + disabledFeatures: DISABLED_FEATURES, + })); +} + +export async function deserializeFromJSONString(json: string) { + return fromJSON(JSON.parse(json), { + plugins: DEFAULT_PLUGINS, + disabledFeatures: DISABLED_FEATURES, + }); +} + +export async function deserializeJSONStream(response: Response | Request) { + if (!response.body) { + throw new Error("missing body"); + } + const reader = new SerovalChunkReader(response.body); + const result = await reader.next(); + if (!result.done) { + const refs = new Map(); + + function interpretChunk(chunk: string): unknown { + const value = fromCrossJSON(JSON.parse(chunk) as SerovalNode, { + refs, + disabledFeatures: DISABLED_FEATURES, + depthLimit: MAX_SERIALIZATION_DEPTH_LIMIT, + plugins: DEFAULT_PLUGINS, + }); + return value; + } + + void reader.drain(interpretChunk); + + return interpretChunk(result.value); + } + return undefined; +} + +export async function deserializeJSStream(id: string, response: Response) { + if (!response.body) { + throw new Error("missing body"); + } + const reader = new SerovalChunkReader(response.body); + + const result = await reader.next(); + + if (!result.done) { + reader.drain(deserialize).then( + () => { + // @ts-ignore + delete $R[id]; + }, + () => { + // no-op + }, + ); + return deserialize(result.value); + } + return undefined; +} diff --git a/packages/start/src/runtime/server-handler.ts b/packages/start/src/runtime/server-handler.ts index 062487adc..a7007fee0 100644 --- a/packages/start/src/runtime/server-handler.ts +++ b/packages/start/src/runtime/server-handler.ts @@ -1,19 +1,5 @@ /// import { parseSetCookie } from "cookie-es"; -import { crossSerializeStream, fromJSON, getCrossReferenceHeader } from "seroval"; -// @ts-ignore -import { - CustomEventPlugin, - DOMExceptionPlugin, - EventPlugin, - FormDataPlugin, - HeadersPlugin, - ReadableStreamPlugin, - RequestPlugin, - ResponsePlugin, - URLPlugin, - URLSearchParamsPlugin -} from "seroval-plugins/web"; import { sharedConfig } from "solid-js"; import { renderToString } from "solid-js/web"; import { provideRequestEvent } from "solid-js/web/storage"; @@ -32,52 +18,7 @@ import { createPageEvent } from "../server/pageEvent"; import { FetchEvent, PageEvent } from "../server"; // @ts-ignore import serverFnManifest from "solidstart:server-fn-manifest"; - -function createChunk(data: string) { - const encodeData = new TextEncoder().encode(data); - const bytes = encodeData.length; - const baseHex = bytes.toString(16); - const totalHex = "00000000".substring(0, 8 - baseHex.length) + baseHex; // 32-bit - const head = new TextEncoder().encode(`;0x${totalHex};`); - - const chunk = new Uint8Array(12 + bytes); - chunk.set(head); - chunk.set(encodeData, 12); - return chunk; -} - -function serializeToStream(id: string, value: any) { - return new ReadableStream({ - start(controller) { - crossSerializeStream(value, { - scopeId: id, - plugins: [ - CustomEventPlugin, - DOMExceptionPlugin, - EventPlugin, - FormDataPlugin, - HeadersPlugin, - ReadableStreamPlugin, - RequestPlugin, - ResponsePlugin, - URLSearchParamsPlugin, - URLPlugin - ], - onSerialize(data, initial) { - controller.enqueue( - createChunk(initial ? `(${getCrossReferenceHeader(id)},${data})` : data) - ); - }, - onDone() { - controller.close(); - }, - onError(error) { - controller.error(error); - } - }); - } - }); -} +import { deserializeFromJSONString, serializeToJSONStream, serializeToJSStream } from "./serialization"; async function handleServerFunction(h3Event: HTTPEvent) { const event = getFetchEvent(h3Event); @@ -131,24 +72,10 @@ async function handleServerFunction(h3Event: HTTPEvent) { if (!instance || h3Event.method === "GET") { const args = url.searchParams.get("args"); if (args) { - const json = JSON.parse(args); - (json.t - ? (fromJSON(json, { - plugins: [ - CustomEventPlugin, - DOMExceptionPlugin, - EventPlugin, - FormDataPlugin, - HeadersPlugin, - ReadableStreamPlugin, - RequestPlugin, - ResponsePlugin, - URLSearchParamsPlugin, - URLPlugin - ] - }) as any) - : json - ).forEach((arg: any) => parsed.push(arg)); + const result = (await deserializeFromJSONString(args)) as any[]; + for (const arg of result) { + parsed.push(arg); + } } } if (h3Event.method === "POST") { @@ -168,43 +95,27 @@ async function handleServerFunction(h3Event: HTTPEvent) { (hasReadableStream && ((h3Request as EdgeIncomingMessage).body as ReadableStream).locked); const requestBody = isReadableStream ? h3Request : h3Request.body; - if ( + // workaround for https://github.com/unjs/nitro/issues/1721 + // (issue only in edge runtimes and netlify preset) + const tmpReq = isH3EventBodyStreamLocked + ? request + : new Request(request, { ...request, body: requestBody }); + if (request.headers.get('x-serialized')) { + parsed = await deserializeFromJSONString(await tmpReq.text()) as any[]; + } else if ( contentType?.startsWith("multipart/form-data") || contentType?.startsWith("application/x-www-form-urlencoded") ) { // workaround for https://github.com/unjs/nitro/issues/1721 // (issue only in edge runtimes and netlify preset) - parsed.push( - await (isH3EventBodyStreamLocked - ? request - : new Request(request, { ...request, body: requestBody }) - ).formData() - ); + parsed.push(await tmpReq.formData()); // what should work when #1721 is fixed // parsed.push(await request.formData); - } else if (contentType?.startsWith("application/json")) { - // workaround for https://github.com/unjs/nitro/issues/1721 - // (issue only in edge runtimes and netlify preset) - const tmpReq = isH3EventBodyStreamLocked - ? request - : new Request(request, { ...request, body: requestBody }); + } else if (contentType?.startsWith('application/json')) { // what should work when #1721 is fixed // just use request.json() here - parsed = fromJSON(await tmpReq.json(), { - plugins: [ - CustomEventPlugin, - DOMExceptionPlugin, - EventPlugin, - FormDataPlugin, - HeadersPlugin, - ReadableStreamPlugin, - RequestPlugin, - ResponsePlugin, - URLSearchParamsPlugin, - URLPlugin - ] - }); - } + parsed = await tmpReq.json() as any[]; + } } try { let result = await provideRequestEvent(event, async () => { @@ -238,8 +149,12 @@ async function handleServerFunction(h3Event: HTTPEvent) { // handle no JS success case if (!instance) return handleNoJS(result, request, parsed); - setHeader(h3Event, "content-type", "text/javascript"); - return serializeToStream(instance, result); + setHeader(h3Event, 'x-serialized', 'true'); + if (import.meta.env.SEROVAL_MODE === 'js') { + setHeader(h3Event, "content-type", "text/javascript"); + return serializeToJSStream(instance, result); + } + return serializeToJSONStream(result); } catch (x) { if (x instanceof Response) { if (singleFlight && instance) { @@ -261,8 +176,12 @@ async function handleServerFunction(h3Event: HTTPEvent) { x = handleNoJS(x, request, parsed, true); } if (instance) { - setHeader(h3Event, "content-type", "text/javascript"); - return serializeToStream(instance, x); + setHeader(h3Event, 'x-serialized', 'true'); + if (import.meta.env.SEROVAL_MODE === 'js') { + setHeader(h3Event, "content-type", "text/javascript"); + return serializeToJSStream(instance, x); + } + return serializeToJSONStream(x); } return x; } diff --git a/packages/start/src/runtime/server-runtime.ts b/packages/start/src/runtime/server-runtime.ts index 18075d763..baec39ea5 100644 --- a/packages/start/src/runtime/server-runtime.ts +++ b/packages/start/src/runtime/server-runtime.ts @@ -1,125 +1,19 @@ -import { deserialize, toJSONAsync } from "seroval"; -import { - CustomEventPlugin, - DOMExceptionPlugin, - EventPlugin, - FormDataPlugin, - HeadersPlugin, - ReadableStreamPlugin, - RequestPlugin, - ResponsePlugin, - URLPlugin, - URLSearchParamsPlugin -} from "seroval-plugins/web"; import { type Component } from "solid-js"; import { createIslandReference } from "../server/islands/index"; - -class SerovalChunkReader { - reader: ReadableStreamDefaultReader; - buffer: Uint8Array; - done: boolean; - constructor(stream: ReadableStream) { - this.reader = stream.getReader(); - this.buffer = new Uint8Array(0); - this.done = false; - } - - async readChunk() { - // if there's no chunk, read again - const chunk = await this.reader.read(); - if (!chunk.done) { - // repopulate the buffer - let newBuffer = new Uint8Array(this.buffer.length + chunk.value.length); - newBuffer.set(this.buffer); - newBuffer.set(chunk.value, this.buffer.length); - this.buffer = newBuffer; - } else { - this.done = true; - } - } - - async next(): Promise { - // Check if the buffer is empty - if (this.buffer.length === 0) { - // if we are already done... - if (this.done) { - return { - done: true, - value: undefined - }; - } - // Otherwise, read a new chunk - await this.readChunk(); - return await this.next(); - } - // Read the "byte header" - // The byte header tells us how big the expected data is - // so we know how much data we should wait before we - // deserialize the data - const head = new TextDecoder().decode(this.buffer.subarray(1, 11)); - const bytes = Number.parseInt(head, 16); // ;0x00000000; - if (Number.isNaN(bytes)) { - throw new Error(`Malformed server function stream header: ${head}`); - } - - // Check if the buffer has enough bytes to be parsed - while (bytes > this.buffer.length - 12) { - // If it's not enough, and the reader is done - // then the chunk is invalid. - if (this.done) { - throw new Error("Malformed server function stream."); - } - // Otherwise, we read more chunks - await this.readChunk(); - } - // Extract the exact chunk as defined by the byte header - const partial = new TextDecoder().decode(this.buffer.subarray(12, 12 + bytes)); - // The rest goes to the buffer - this.buffer = this.buffer.subarray(12 + bytes); - - // Deserialize the chunk - return { - done: false, - value: deserialize(partial) - }; - } - - async drain() { - while (true) { - const result = await this.next(); - if (result.done) { - break; - } - } - } -} - -async function deserializeStream(id: string, response: Response) { - if (!response.body) { - throw new Error("missing body"); - } - const reader = new SerovalChunkReader(response.body); - - const result = await reader.next(); - - if (!result.done) { - reader.drain().then( - () => { - // @ts-ignore - delete $R[id]; - }, - () => { - // no-op - } - ); - } - - return result.value; -} +import { + deserializeJSONStream, + deserializeJSStream, + serializeToJSONString, +} from "./serialization"; let INSTANCE = 0; -function createRequest(base: string, id: string, instance: string, options: RequestInit) { +function createRequest( + base: string, + id: string, + instance: string, + options: RequestInit, +) { return fetch(base, { method: "POST", ...options, @@ -131,41 +25,37 @@ function createRequest(base: string, id: string, instance: string, options: Requ }); } -const plugins = [ - CustomEventPlugin, - DOMExceptionPlugin, - EventPlugin, - FormDataPlugin, - HeadersPlugin, - ReadableStreamPlugin, - RequestPlugin, - ResponsePlugin, - URLSearchParamsPlugin, - URLPlugin -]; - async function fetchServerFunction( base: string, id: string, options: Omit, - args: any[] + args: any[], ) { const instance = `server-fn:${INSTANCE++}`; const response = await (args.length === 0 ? createRequest(base, id, instance, options) : args.length === 1 && args[0] instanceof FormData - ? createRequest(base, id, instance, { ...options, body: args[0] }) - : args.length === 1 && args[0] instanceof URLSearchParams - ? createRequest(base, id, instance, { - ...options, - body: args[0], - headers: { ...options.headers, "Content-Type": "application/x-www-form-urlencoded" } - }) - : createRequest(base, id, instance, { - ...options, - body: JSON.stringify(await Promise.resolve(toJSONAsync(args, { plugins }))), - headers: { ...options.headers, "Content-Type": "application/json" } - })); + ? createRequest(base, id, instance, { ...options, body: args[0] }) + : args.length === 1 && args[0] instanceof URLSearchParams + ? createRequest(base, id, instance, { + ...options, + body: args[0], + headers: { + ...options.headers, + "Content-Type": "application/x-www-form-urlencoded", + }, + }) + : createRequest(base, id, instance, { + ...options, + body: await serializeToJSONString(args), + // duplex: 'half', + // body: serializeToJSONStream(args), + headers: { + ...options.headers, + "x-serialized": "true", + "Content-Type": "text/plain" + }, + })); if ( response.headers.has("Location") || @@ -175,20 +65,28 @@ async function fetchServerFunction( if (response.body) { /* @ts-ignore-next-line */ response.customBody = () => { - return deserializeStream(instance, response); + if (import.meta.env.SEROVAL_MODE === "js") { + return deserializeJSStream(instance, response.clone()); + } + return deserializeJSONStream(response.clone()); }; } return response; } const contentType = response.headers.get("Content-Type"); + const cloned = response.clone(); let result; if (contentType && contentType.startsWith("text/plain")) { - result = await response.text(); + result = await cloned.text(); } else if (contentType && contentType.startsWith("application/json")) { - result = await response.json(); - } else { - result = await deserializeStream(instance, response); + result = await cloned.json(); + } else if (response.headers.get("x-serialized")) { + if (import.meta.env.SEROVAL_MODE === "js") { + result = await deserializeJSStream(instance, cloned); + } else { + result = await deserializeJSONStream(cloned); + } } if (response.headers.has("X-Error")) { throw result; @@ -208,23 +106,24 @@ export function createServerReference(fn: Function, id: string, name: string) { } if (prop === "withOptions") { const url = `${baseURL}/_server/?id=${encodeURIComponent(id)}&name=${encodeURIComponent( - name + name, )}`; return (options: RequestInit) => { const fn = async (...args: any[]) => { - const encodeArgs = options.method && options.method.toUpperCase() === "GET"; + const encodeArgs = + options.method && options.method.toUpperCase() === "GET"; return fetchServerFunction( encodeArgs ? url + - (args.length - ? `&args=${encodeURIComponent( - JSON.stringify(await Promise.resolve(toJSONAsync(args, { plugins }))) - )}` - : "") + (args.length + ? `&args=${encodeURIComponent( + await serializeToJSONString(args), + )}` + : "") : `${baseURL}/_server`, `${id}#${name}`, options, - encodeArgs ? [] : args + encodeArgs ? [] : args, ); }; fn.url = url; @@ -234,12 +133,21 @@ export function createServerReference(fn: Function, id: string, name: string) { return (target as any)[prop]; }, apply(target, thisArg, args) { - return fetchServerFunction(`${baseURL}/_server`, `${id}#${name}`, {}, args); - } + return fetchServerFunction( + `${baseURL}/_server`, + `${id}#${name}`, + {}, + args, + ); + }, }); } -export function createClientReference(Component: Component, id: string, name: string) { +export function createClientReference( + Component: Component, + id: string, + name: string, +) { if (typeof Component === "function") { return createIslandReference(Component, id, name); } diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index f17d8a90b..8e93bea21 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -29,6 +29,132 @@ importers: apps/fixtures: {} + apps/fixtures/bare: + dependencies: + '@solidjs/start': + specifier: ^1.1.0 + version: 1.2.1(@testing-library/jest-dom@6.8.0)(solid-js@1.9.9)(vinxi@0.5.8(@types/node@25.0.3)(db0@0.3.2)(ioredis@5.7.0)(jiti@2.6.0)(terser@5.44.1)(yaml@2.8.1))(vite@6.3.6(@types/node@25.0.3)(jiti@2.6.0)(terser@5.44.1)(yaml@2.8.1)) + solid-js: + specifier: ^1.9.5 + version: 1.9.9 + vinxi: + specifier: ^0.5.7 + version: 0.5.8(@types/node@25.0.3)(db0@0.3.2)(ioredis@5.7.0)(jiti@2.6.0)(terser@5.44.1)(yaml@2.8.1) + + apps/fixtures/basic: + dependencies: + '@solidjs/meta': + specifier: ^0.29.4 + version: 0.29.4(solid-js@1.9.9) + '@solidjs/router': + specifier: ^0.15.0 + version: 0.15.3(solid-js@1.9.9) + '@solidjs/start': + specifier: ^1.1.0 + version: 1.2.1(@testing-library/jest-dom@6.8.0)(solid-js@1.9.9)(vinxi@0.5.8(@types/node@25.0.3)(db0@0.3.2)(ioredis@5.7.0)(jiti@2.6.0)(terser@5.44.1)(yaml@2.8.1))(vite@6.3.6(@types/node@25.0.3)(jiti@2.6.0)(terser@5.44.1)(yaml@2.8.1)) + solid-js: + specifier: ^1.9.5 + version: 1.9.9 + vinxi: + specifier: ^0.5.7 + version: 0.5.8(@types/node@25.0.3)(db0@0.3.2)(ioredis@5.7.0)(jiti@2.6.0)(terser@5.44.1)(yaml@2.8.1) + + apps/fixtures/experiments: + dependencies: + '@solidjs/meta': + specifier: ^0.29.4 + version: 0.29.4(solid-js@1.9.9) + '@solidjs/router': + specifier: ^0.15.0 + version: 0.15.3(solid-js@1.9.9) + '@solidjs/start': + specifier: ^1.1.0 + version: 1.2.1(@testing-library/jest-dom@6.8.0)(solid-js@1.9.9)(vinxi@0.5.8(@types/node@25.0.3)(db0@0.3.2)(ioredis@5.7.0)(jiti@2.6.0)(terser@5.44.1)(yaml@2.8.1))(vite@6.3.6(@types/node@25.0.3)(jiti@2.6.0)(terser@5.44.1)(yaml@2.8.1)) + solid-js: + specifier: ^1.9.5 + version: 1.9.9 + vinxi: + specifier: ^0.5.7 + version: 0.5.8(@types/node@25.0.3)(db0@0.3.2)(ioredis@5.7.0)(jiti@2.6.0)(terser@5.44.1)(yaml@2.8.1) + + apps/fixtures/hackernews: + dependencies: + '@solidjs/router': + specifier: ^0.15.0 + version: 0.15.3(solid-js@1.9.9) + '@solidjs/start': + specifier: ^1.1.0 + version: 1.2.1(@testing-library/jest-dom@6.8.0)(solid-js@1.9.9)(vinxi@0.5.8(@types/node@25.0.3)(db0@0.3.2)(ioredis@5.7.0)(jiti@2.6.0)(terser@5.44.1)(yaml@2.8.1))(vite@6.3.6(@types/node@25.0.3)(jiti@2.6.0)(terser@5.44.1)(yaml@2.8.1)) + solid-js: + specifier: ^1.9.5 + version: 1.9.9 + vinxi: + specifier: ^0.5.7 + version: 0.5.8(@types/node@25.0.3)(db0@0.3.2)(ioredis@5.7.0)(jiti@2.6.0)(terser@5.44.1)(yaml@2.8.1) + + apps/fixtures/notes: + dependencies: + '@solidjs/router': + specifier: ^0.15.0 + version: 0.15.3(solid-js@1.9.9) + '@solidjs/start': + specifier: ^1.1.0 + version: 1.2.1(@testing-library/jest-dom@6.8.0)(solid-js@1.9.9)(vinxi@0.5.8(@types/node@25.0.3)(db0@0.3.2)(ioredis@5.7.0)(jiti@2.6.0)(terser@5.44.1)(yaml@2.8.1))(vite@6.3.6(@types/node@25.0.3)(jiti@2.6.0)(terser@5.44.1)(yaml@2.8.1)) + date-fns: + specifier: ^3.6.0 + version: 3.6.0 + marked: + specifier: ^12.0.1 + version: 12.0.2 + solid-js: + specifier: ^1.9.5 + version: 1.9.9 + unstorage: + specifier: 1.10.2 + version: 1.10.2(ioredis@5.7.0) + vinxi: + specifier: ^0.5.7 + version: 0.5.8(@types/node@25.0.3)(db0@0.3.2)(ioredis@5.7.0)(jiti@2.6.0)(terser@5.44.1)(yaml@2.8.1) + + apps/fixtures/serialization-modes: + dependencies: + '@solidjs/meta': + specifier: ^0.29.4 + version: 0.29.4(solid-js@1.9.9) + '@solidjs/router': + specifier: ^0.15.0 + version: 0.15.3(solid-js@1.9.9) + '@solidjs/start': + specifier: workspace:* + version: link:../../../packages/start + shieldwall: + specifier: ^0.4.0 + version: 0.4.0(@solidjs/start@packages+start) + solid-js: + specifier: ^1.9.5 + version: 1.9.9 + vinxi: + specifier: ^0.5.7 + version: 0.5.8(@types/node@25.0.3)(db0@0.3.2)(ioredis@5.7.0)(jiti@2.6.0)(terser@5.44.1)(yaml@2.8.1) + + apps/fixtures/todomvc: + dependencies: + '@solidjs/router': + specifier: ^0.15.0 + version: 0.15.3(solid-js@1.9.9) + '@solidjs/start': + specifier: ^1.1.0 + version: 1.2.1(@testing-library/jest-dom@6.8.0)(solid-js@1.9.9)(vinxi@0.5.8(@types/node@25.0.3)(db0@0.3.2)(ioredis@5.7.0)(jiti@2.6.0)(terser@5.44.1)(yaml@2.8.1))(vite@6.3.6(@types/node@25.0.3)(jiti@2.6.0)(terser@5.44.1)(yaml@2.8.1)) + solid-js: + specifier: ^1.9.5 + version: 1.9.9 + unstorage: + specifier: 1.10.2 + version: 1.10.2(ioredis@5.7.0) + vinxi: + specifier: ^0.5.7 + version: 0.5.8(@types/node@25.0.3)(db0@0.3.2)(ioredis@5.7.0)(jiti@2.6.0)(terser@5.44.1)(yaml@2.8.1) + apps/landing-page: dependencies: '@solidjs/meta': @@ -1568,6 +1694,11 @@ packages: peerDependencies: solid-js: ^1.8.6 + '@solidjs/start@1.2.1': + resolution: {integrity: sha512-O5E7rcCwm2f8GlXKgS2xnU37Ld5vMVXJgo/qR7UI5iR5uFo9V2Ac+SSVNXkM98CeHKHt55h1UjbpxxTANEsHmA==} + peerDependencies: + vinxi: ^0.5.7 + '@solidjs/testing-library@0.8.10': resolution: {integrity: sha512-qdeuIerwyq7oQTIrrKvV0aL9aFeuwTd86VYD3afdq5HYEwoox1OBTJy4y8A3TFZr8oAR0nujYgCzY/8wgHGfeQ==} engines: {node: '>= 14'} @@ -2400,6 +2531,10 @@ packages: crossws@0.3.5: resolution: {integrity: sha512-ojKiDvcmByhwa8YYqbQI/hg7MEU0NC03+pSdEq4ZUnZR9xXpwk7E43SMNGkn+JxJGPFtNvQ48+vV2p+P1ml5PA==} + csp-header@5.2.1: + resolution: {integrity: sha512-qOJNu39JZkPrbrAM40a1tQCePEPYVIoI6nMDhX4RA07QjU8efS+zyd/zE83XJu85KKazH9NjKlvvlswFMteMgg==} + engines: {node: '>=10'} + css.escape@1.5.1: resolution: {integrity: sha512-YUifsXXuknHlUsmlgyY0PKzgPOr7/FjCePfHNt0jxm83wHZi44VDMQ7/fGNkjY3/jV1MC+1CmZbaHzugyeRtpg==} @@ -2436,8 +2571,12 @@ packages: resolution: {integrity: sha512-ZYP5VBHshaDAiVZxjbRVcFJpc+4xGgT0bK3vzy1HLN8jTO975HEbuYzZJcHoQEY5K1a0z8YayJkyVETa08eNTg==} engines: {node: '>=18'} + date-fns@3.6.0: + resolution: {integrity: sha512-fRHTG8g/Gif+kSh50gaGEdToemgfj74aRX3swtiouboip5JDLAyDE9F11nHMIcvOaXeOC6D7SpNhi7uFyB7Uww==} + dax-sh@0.43.2: resolution: {integrity: sha512-uULa1sSIHgXKGCqJ/pA0zsnzbHlVnuq7g8O2fkHokWFNwEGIhh5lAJlxZa1POG5En5ba7AU4KcBAvGQWMMf8rg==} + deprecated: This package has moved to simply be 'dax' instead of 'dax-sh' dayjs@1.11.13: resolution: {integrity: sha512-oaMBel6gjolK862uaPQOVTA7q3TZhuSvuMQAAglQDOWYO9A91IrAOUJEyKVlqJlHE0vq5p5UXxzdPfMH/x6xNg==} @@ -2962,11 +3101,12 @@ packages: glob@10.4.5: resolution: {integrity: sha512-7Bv8RF0k6xjo7d4A/PxYLbUCfb6c+Vpd2/mB2yRDlew7Jb5hEXiCD9ibfO7wpk8i4sevK6DFny9h7EYbM3/sHg==} + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me hasBin: true glob@7.2.3: resolution: {integrity: sha512-nFR0zLpU2YCaRxwoCJvL6UvCH2JFyFVIvwTLsIf21AuHlMskA1hhTdk+LlYJtOlYt9v6dvszD2BGRqBL+iQK9Q==} - deprecated: Glob versions prior to v9 are no longer supported + deprecated: Old versions of glob are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me global-dirs@3.0.1: resolution: {integrity: sha512-NBcGGFbBA9s1VzD41QXDG+3++t9Mn5t1FpLdhESY6oKY4gYTFpX4wO3sqGUa0Srjtbfj3szX0RnemmrVRUdULA==} @@ -3452,6 +3592,11 @@ packages: resolution: {integrity: sha512-hXdUTZYIVOt1Ex//jAQi+wTZZpUpwBj/0QsOzqegb3rGMMeJiSEu5xLHnYfBrRV4RH2+OCSOO95Is/7x1WJ4bw==} engines: {node: '>=10'} + marked@12.0.2: + resolution: {integrity: sha512-qXUm7e/YKFoqFPYPa3Ukg9xlI5cyAtGmyEIzMfW//m6kXwCy2Ps9DYf5ioijFKQ8qyuscrHoY04iJGctu2Kg0Q==} + engines: {node: '>= 18'} + hasBin: true + math-intrinsics@1.1.0: resolution: {integrity: sha512-/IXtbwEk5HTPyEwyKX6hGkYXxM9nbj64B+ilVJnC/R6B0pH5G4V3b0pVbL7DBj4tkhBAppbQUlf6F6Xl9LHu1g==} engines: {node: '>= 0.4'} @@ -4196,6 +4341,11 @@ packages: resolution: {integrity: sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==} engines: {node: '>=8'} + shieldwall@0.4.0: + resolution: {integrity: sha512-3IO6SxrRUlL7BGWnfhdACnuG8bqa+kKzSgtuW0daZpdF9iGoEW9EroY2djuP1AjY/R3SCbql6xCJ1CtX+GDMRw==} + peerDependencies: + '@solidjs/start': ^1.1.1 + shiki@1.29.2: resolution: {integrity: sha512-njXuliz/cP+67jU2hukkxCNuH1yUi4QfdZZY+sMr5PPrIyXSu5iTb/qYC4BiWWB0vZ+7TbdvYUCeL23zpwCfbg==} @@ -4442,6 +4592,7 @@ packages: tar@7.4.4: resolution: {integrity: sha512-O1z7ajPkjTgEgmTGz0v9X4eqeEXTDREPTO77pVC1Nbs86feBU1Zhdg+edzavPmYW1olxkwsqA2v4uOw6E8LeDg==} engines: {node: '>=18'} + deprecated: Old versions of tar are not supported, and contain widely publicized security vulnerabilities, which have been fixed in the current version. Please update. Support for old versions may be purchased (at exorbitant rates) by contacting i@izs.me term-size@2.2.1: resolution: {integrity: sha512-wK0Ri4fOGjv/XPy8SBHZChl8CM7uMc5VML7SqiQ0zG7+J5Vr+RMQDoHa2CNT6KHUnTGIXH34UDMkPzAUyapBZg==} @@ -4689,6 +4840,50 @@ packages: resolution: {integrity: sha512-6NCPkv1ClwH+/BGE9QeoTIl09nuiAt0gS28nn1PvYXsGKRwM2TCbFA2QiilmehPDTXIe684k4rZI1yl3A1PCUw==} engines: {node: '>=18.12.0'} + unstorage@1.10.2: + resolution: {integrity: sha512-cULBcwDqrS8UhlIysUJs2Dk0Mmt8h7B0E6mtR+relW9nZvsf/u4SkAYyNliPiPW7XtFNb5u3IUMkxGxFTTRTgQ==} + peerDependencies: + '@azure/app-configuration': ^1.5.0 + '@azure/cosmos': ^4.0.0 + '@azure/data-tables': ^13.2.2 + '@azure/identity': ^4.0.1 + '@azure/keyvault-secrets': ^4.8.0 + '@azure/storage-blob': ^12.17.0 + '@capacitor/preferences': ^5.0.7 + '@netlify/blobs': ^6.5.0 || ^7.0.0 + '@planetscale/database': ^1.16.0 + '@upstash/redis': ^1.28.4 + '@vercel/kv': ^1.0.1 + idb-keyval: ^6.2.1 + ioredis: ^5.3.2 + peerDependenciesMeta: + '@azure/app-configuration': + optional: true + '@azure/cosmos': + optional: true + '@azure/data-tables': + optional: true + '@azure/identity': + optional: true + '@azure/keyvault-secrets': + optional: true + '@azure/storage-blob': + optional: true + '@capacitor/preferences': + optional: true + '@netlify/blobs': + optional: true + '@planetscale/database': + optional: true + '@upstash/redis': + optional: true + '@vercel/kv': + optional: true + idb-keyval: + optional: true + ioredis: + optional: true + unstorage@1.17.1: resolution: {integrity: sha512-KKGwRTT0iVBCErKemkJCLs7JdxNVfqTPc/85ae1XES0+bsHbc/sFBfVi5kJp156cc51BHinIH2l3k0EZ24vOBQ==} peerDependencies: @@ -4931,6 +5126,7 @@ packages: whatwg-encoding@3.1.1: resolution: {integrity: sha512-6qN4hJdMwfYBtE3YBTTHhoeuUrDBPZmbQaxWAqSALV/MeEnR5z1xd8UKud2RAkFoPkmB+hli1TZSnyi84xz1vQ==} engines: {node: '>=18'} + deprecated: Use @exodus/bytes instead for a more spec-conformant and faster implementation whatwg-mimetype@4.0.0: resolution: {integrity: sha512-QaKxh0eNIi2mE9p2vEdzfagOKHCcj1pJ56EEHGQOVxp8r9/iszLUUV7v89x9O1p/T+NlTM5W7jW6+cz4Fq1YVg==} @@ -6647,6 +6843,30 @@ snapshots: dependencies: solid-js: 1.9.9 + '@solidjs/start@1.2.1(@testing-library/jest-dom@6.8.0)(solid-js@1.9.9)(vinxi@0.5.8(@types/node@25.0.3)(db0@0.3.2)(ioredis@5.7.0)(jiti@2.6.0)(terser@5.44.1)(yaml@2.8.1))(vite@6.3.6(@types/node@25.0.3)(jiti@2.6.0)(terser@5.44.1)(yaml@2.8.1))': + dependencies: + '@tanstack/server-functions-plugin': 1.121.21(vite@6.3.6(@types/node@25.0.3)(jiti@2.6.0)(terser@5.44.1)(yaml@2.8.1)) + '@vinxi/plugin-directives': 0.5.1(vinxi@0.5.8(@types/node@25.0.3)(db0@0.3.2)(ioredis@5.7.0)(jiti@2.6.0)(terser@5.44.1)(yaml@2.8.1)) + '@vinxi/server-components': 0.5.1(vinxi@0.5.8(@types/node@25.0.3)(db0@0.3.2)(ioredis@5.7.0)(jiti@2.6.0)(terser@5.44.1)(yaml@2.8.1)) + cookie-es: 2.0.0 + defu: 6.1.4 + error-stack-parser: 2.1.4 + html-to-image: 1.11.13 + radix3: 1.1.2 + seroval: 1.4.1 + seroval-plugins: 1.4.0(seroval@1.4.1) + shiki: 1.29.2 + source-map-js: 1.2.1 + terracotta: 1.0.6(solid-js@1.9.9) + tinyglobby: 0.2.15 + vinxi: 0.5.8(@types/node@25.0.3)(db0@0.3.2)(ioredis@5.7.0)(jiti@2.6.0)(terser@5.44.1)(yaml@2.8.1) + vite-plugin-solid: 2.11.8(@testing-library/jest-dom@6.8.0)(solid-js@1.9.9)(vite@6.3.6(@types/node@25.0.3)(jiti@2.6.0)(terser@5.44.1)(yaml@2.8.1)) + transitivePeerDependencies: + - '@testing-library/jest-dom' + - solid-js + - supports-color + - vite + '@solidjs/testing-library@0.8.10(@solidjs/router@0.15.3(solid-js@1.9.9))(solid-js@1.9.9)': dependencies: '@testing-library/dom': 10.4.1 @@ -6836,7 +7056,7 @@ snapshots: consola: 3.4.2 defu: 6.1.4 get-port-please: 3.2.0 - h3: 1.15.3 + h3: 1.15.4 http-shutdown: 1.2.2 jiti: 1.21.7 mlly: 1.8.0 @@ -7588,6 +7808,8 @@ snapshots: dependencies: uncrypto: 0.1.3 + csp-header@5.2.1: {} + css.escape@1.5.1: {} cssesc@3.0.0: {} @@ -7667,6 +7889,8 @@ snapshots: whatwg-mimetype: 4.0.0 whatwg-url: 14.2.0 + date-fns@3.6.0: {} + dax-sh@0.43.2: dependencies: '@deno/shim-deno': 0.19.2 @@ -8671,6 +8895,8 @@ snapshots: dependencies: semver: 7.7.2 + marked@12.0.2: {} + math-intrinsics@1.1.0: {} mdast-util-to-hast@13.2.0: @@ -9508,6 +9734,12 @@ snapshots: shebang-regex@3.0.0: {} + shieldwall@0.4.0(@solidjs/start@packages+start): + dependencies: + '@solidjs/start': link:packages/start + csp-header: 5.2.1 + h3: 1.15.4 + shiki@1.29.2: dependencies: '@shikijs/core': 1.29.2 @@ -10051,6 +10283,21 @@ snapshots: picomatch: 4.0.3 webpack-virtual-modules: 0.6.2 + unstorage@1.10.2(ioredis@5.7.0): + dependencies: + anymatch: 3.1.3 + chokidar: 3.6.0 + destr: 2.0.5 + h3: 1.15.4 + listhen: 1.9.0 + lru-cache: 10.4.3 + mri: 1.2.0 + node-fetch-native: 1.6.7 + ofetch: 1.4.1 + ufo: 1.6.1 + optionalDependencies: + ioredis: 5.7.0 + unstorage@1.17.1(db0@0.3.2)(ioredis@5.7.0): dependencies: anymatch: 3.1.3 diff --git a/pnpm-workspace.yaml b/pnpm-workspace.yaml index a52b018da..0202392bf 100644 --- a/pnpm-workspace.yaml +++ b/pnpm-workspace.yaml @@ -1,11 +1,12 @@ packages: - packages/* - apps/* - - '!**/.tmp/**' + - apps/fixtures/* + - "!**/.tmp/**" ignoredBuiltDependencies: - - '@prisma/client' - - '@prisma/engines' + - "@prisma/client" + - "@prisma/engines" - better-sqlite3 - msw - prisma