@tanstack/start-server-core 1.132.0-alpha.3 → 1.132.0-alpha.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/esm/createServerRpc.d.ts +2 -0
- package/dist/esm/createServerRpc.js +3 -1
- package/dist/esm/createServerRpc.js.map +1 -1
- package/dist/esm/serializer/ServerFunctionSerializationAdapter.js +2 -1
- package/dist/esm/serializer/ServerFunctionSerializationAdapter.js.map +1 -1
- package/dist/esm/server-functions-handler.js +73 -29
- package/dist/esm/server-functions-handler.js.map +1 -1
- package/package.json +4 -4
- package/src/createServerRpc.ts +2 -0
- package/src/serializer/ServerFunctionSerializationAdapter.ts +2 -2
- package/src/server-functions-handler.ts +86 -29
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { TSS_SERVER_FUNCTION } from '@tanstack/start-client-core';
|
|
1
2
|
export declare const createServerRpc: (functionId: string, splitImportFn: (...args: any) => any) => ((...args: any) => any) & {
|
|
2
3
|
url: string;
|
|
3
4
|
functionId: string;
|
|
5
|
+
[TSS_SERVER_FUNCTION]: boolean;
|
|
4
6
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TSS_SERVER_FUNCTION } from "@tanstack/start-client-core";
|
|
1
2
|
import invariant from "tiny-invariant";
|
|
2
3
|
let baseUrl;
|
|
3
4
|
function sanitizeBase(base) {
|
|
@@ -16,7 +17,8 @@ const createServerRpc = (functionId, splitImportFn) => {
|
|
|
16
17
|
const url = baseUrl + functionId;
|
|
17
18
|
return Object.assign(splitImportFn, {
|
|
18
19
|
url,
|
|
19
|
-
functionId
|
|
20
|
+
functionId,
|
|
21
|
+
[TSS_SERVER_FUNCTION]: true
|
|
20
22
|
});
|
|
21
23
|
};
|
|
22
24
|
export {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"createServerRpc.js","sources":["../../src/createServerRpc.ts"],"sourcesContent":["import invariant from 'tiny-invariant'\n\nlet baseUrl: string\nfunction sanitizeBase(base: string) {\n return base.replace(/^\\/|\\/$/g, '')\n}\n\nexport const createServerRpc = (\n functionId: string,\n splitImportFn: (...args: any) => any,\n) => {\n if (!baseUrl) {\n const sanitizedAppBase = sanitizeBase(process.env.TSS_APP_BASE || '/')\n const sanitizedServerBase = sanitizeBase(process.env.TSS_SERVER_FN_BASE!)\n baseUrl = `${sanitizedAppBase ? `/${sanitizedAppBase}` : ''}/${sanitizedServerBase}/`\n }\n invariant(\n splitImportFn,\n '🚨splitImportFn required for the server functions server runtime, but was not provided.',\n )\n\n const url = baseUrl + functionId\n\n return Object.assign(splitImportFn, {\n url,\n functionId,\n })\n}\n"],"names":[],"mappings":"
|
|
1
|
+
{"version":3,"file":"createServerRpc.js","sources":["../../src/createServerRpc.ts"],"sourcesContent":["import { TSS_SERVER_FUNCTION } from '@tanstack/start-client-core'\nimport invariant from 'tiny-invariant'\n\nlet baseUrl: string\nfunction sanitizeBase(base: string) {\n return base.replace(/^\\/|\\/$/g, '')\n}\n\nexport const createServerRpc = (\n functionId: string,\n splitImportFn: (...args: any) => any,\n) => {\n if (!baseUrl) {\n const sanitizedAppBase = sanitizeBase(process.env.TSS_APP_BASE || '/')\n const sanitizedServerBase = sanitizeBase(process.env.TSS_SERVER_FN_BASE!)\n baseUrl = `${sanitizedAppBase ? `/${sanitizedAppBase}` : ''}/${sanitizedServerBase}/`\n }\n invariant(\n splitImportFn,\n '🚨splitImportFn required for the server functions server runtime, but was not provided.',\n )\n\n const url = baseUrl + functionId\n\n return Object.assign(splitImportFn, {\n url,\n functionId,\n [TSS_SERVER_FUNCTION]: true,\n })\n}\n"],"names":[],"mappings":";;AAGA,IAAI;AACJ,SAAS,aAAa,MAAc;AAClC,SAAO,KAAK,QAAQ,YAAY,EAAE;AACpC;AAEO,MAAM,kBAAkB,CAC7B,YACA,kBACG;AACH,MAAI,CAAC,SAAS;AACZ,UAAM,mBAAmB,aAAa,QAAQ,IAAI,gBAAgB,GAAG;AACrE,UAAM,sBAAsB,aAAa,QAAQ,IAAI,kBAAmB;AACxE,cAAU,GAAG,mBAAmB,IAAI,gBAAgB,KAAK,EAAE,IAAI,mBAAmB;AAAA,EACpF;AACA;AAAA,IACE;AAAA,IACA;AAAA,EAAA;AAGF,QAAM,MAAM,UAAU;AAEtB,SAAO,OAAO,OAAO,eAAe;AAAA,IAClC;AAAA,IACA;AAAA,IACA,CAAC,mBAAmB,GAAG;AAAA,EAAA,CACxB;AACH;"}
|
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { createSerializationAdapter } from "@tanstack/router-core";
|
|
2
|
+
import { TSS_SERVER_FUNCTION } from "@tanstack/start-client-core";
|
|
2
3
|
import { createServerRpc } from "../createServerRpc.js";
|
|
3
4
|
import { getServerFnById } from "../getServerFnById.js";
|
|
4
5
|
const ServerFunctionSerializationAdapter = createSerializationAdapter({
|
|
5
6
|
key: "$TSS/serverfn",
|
|
6
|
-
test: (v) =>
|
|
7
|
+
test: (v) => v[TSS_SERVER_FUNCTION],
|
|
7
8
|
toSerializable: ({ functionId }) => ({ functionId }),
|
|
8
9
|
fromSerializable: ({ functionId }) => {
|
|
9
10
|
const fn = async (opts, signal) => {
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ServerFunctionSerializationAdapter.js","sources":["../../../src/serializer/ServerFunctionSerializationAdapter.ts"],"sourcesContent":["import { createSerializationAdapter } from '@tanstack/router-core'\nimport { createServerRpc } from '../createServerRpc'\nimport { getServerFnById } from '../getServerFnById'\n\nexport const ServerFunctionSerializationAdapter = createSerializationAdapter({\n key: '$TSS/serverfn',\n test: (v): v is { functionId: string }
|
|
1
|
+
{"version":3,"file":"ServerFunctionSerializationAdapter.js","sources":["../../../src/serializer/ServerFunctionSerializationAdapter.ts"],"sourcesContent":["import { createSerializationAdapter } from '@tanstack/router-core'\nimport { TSS_SERVER_FUNCTION } from '@tanstack/start-client-core'\nimport { createServerRpc } from '../createServerRpc'\nimport { getServerFnById } from '../getServerFnById'\n\nexport const ServerFunctionSerializationAdapter = createSerializationAdapter({\n key: '$TSS/serverfn',\n test: (v): v is { functionId: string } => v[TSS_SERVER_FUNCTION],\n toSerializable: ({ functionId }) => ({ functionId }),\n fromSerializable: ({ functionId }) => {\n const fn = async (opts: any, signal: any): Promise<any> => {\n const serverFn = await getServerFnById(functionId)\n const result = await serverFn(opts ?? {}, signal)\n return result.result\n }\n return createServerRpc(functionId, fn)\n },\n})\n"],"names":[],"mappings":";;;;AAKO,MAAM,qCAAqC,2BAA2B;AAAA,EAC3E,KAAK;AAAA,EACL,MAAM,CAAC,MAAmC,EAAE,mBAAmB;AAAA,EAC/D,gBAAgB,CAAC,EAAE,kBAAkB,EAAE,WAAA;AAAA,EACvC,kBAAkB,CAAC,EAAE,iBAAiB;AACpC,UAAM,KAAK,OAAO,MAAW,WAA8B;AACzD,YAAM,WAAW,MAAM,gBAAgB,UAAU;AACjD,YAAM,SAAS,MAAM,SAAS,QAAQ,CAAA,GAAI,MAAM;AAChD,aAAO,OAAO;AAAA,IAChB;AACA,WAAO,gBAAgB,YAAY,EAAE;AAAA,EACvC;AACF,CAAC;"}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { isNotFound } from "@tanstack/router-core";
|
|
2
2
|
import invariant from "tiny-invariant";
|
|
3
|
-
import {
|
|
4
|
-
import { toCrossJSONStream,
|
|
3
|
+
import { TSS_FORMDATA_CONTEXT, X_TSS_SERIALIZED } from "@tanstack/start-client-core";
|
|
4
|
+
import { toCrossJSONStream, toCrossJSONAsync, fromJSON } from "seroval";
|
|
5
5
|
import { getResponse } from "./request-response.js";
|
|
6
6
|
import { getServerFnById } from "./getServerFnById.js";
|
|
7
7
|
import { getSerovalPlugins } from "./serializer/getSerovalPlugins.js";
|
|
@@ -53,8 +53,8 @@ const handleServerAction = async ({ request }) => {
|
|
|
53
53
|
"GET requests with FormData payloads are not supported"
|
|
54
54
|
);
|
|
55
55
|
const formData = await request.formData();
|
|
56
|
-
const serializedContext = formData.get(
|
|
57
|
-
formData.delete(
|
|
56
|
+
const serializedContext = formData.get(TSS_FORMDATA_CONTEXT);
|
|
57
|
+
formData.delete(TSS_FORMDATA_CONTEXT);
|
|
58
58
|
const params = {
|
|
59
59
|
context: {},
|
|
60
60
|
data: formData
|
|
@@ -102,37 +102,74 @@ const handleServerAction = async ({ request }) => {
|
|
|
102
102
|
return isNotFoundResponse(result);
|
|
103
103
|
}
|
|
104
104
|
const response2 = getResponse();
|
|
105
|
-
let
|
|
105
|
+
let nonStreamingBody = void 0;
|
|
106
106
|
if (result !== void 0) {
|
|
107
|
-
|
|
107
|
+
let done = false;
|
|
108
|
+
const callbacks = {
|
|
109
|
+
onParse: (value) => {
|
|
110
|
+
nonStreamingBody = value;
|
|
111
|
+
},
|
|
112
|
+
onDone: () => {
|
|
113
|
+
done = true;
|
|
114
|
+
},
|
|
115
|
+
onError: (error) => {
|
|
116
|
+
throw error;
|
|
117
|
+
}
|
|
118
|
+
};
|
|
119
|
+
toCrossJSONStream(result, {
|
|
120
|
+
refs: /* @__PURE__ */ new Map(),
|
|
121
|
+
plugins: serovalPlugins,
|
|
122
|
+
onParse(value) {
|
|
123
|
+
callbacks.onParse(value);
|
|
124
|
+
},
|
|
125
|
+
onDone() {
|
|
126
|
+
callbacks.onDone();
|
|
127
|
+
},
|
|
128
|
+
onError: (error) => {
|
|
129
|
+
callbacks.onError(error);
|
|
130
|
+
}
|
|
131
|
+
});
|
|
132
|
+
if (done) {
|
|
133
|
+
return new Response(
|
|
134
|
+
nonStreamingBody ? JSON.stringify(nonStreamingBody) : void 0,
|
|
135
|
+
{
|
|
136
|
+
status: response2?.status,
|
|
137
|
+
statusText: response2?.statusText,
|
|
138
|
+
headers: {
|
|
139
|
+
"Content-Type": "application/json",
|
|
140
|
+
[X_TSS_SERIALIZED]: "true"
|
|
141
|
+
}
|
|
142
|
+
}
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
const stream = new ReadableStream({
|
|
108
146
|
start(controller2) {
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
},
|
|
115
|
-
onDone: () => {
|
|
116
|
-
try {
|
|
117
|
-
controller2.close();
|
|
118
|
-
} catch (error) {
|
|
119
|
-
controller2.error(error);
|
|
120
|
-
}
|
|
121
|
-
},
|
|
122
|
-
onError: (error) => {
|
|
147
|
+
callbacks.onParse = (value) => controller2.enqueue(JSON.stringify(value) + "\n");
|
|
148
|
+
callbacks.onDone = () => {
|
|
149
|
+
try {
|
|
150
|
+
controller2.close();
|
|
151
|
+
} catch (error) {
|
|
123
152
|
controller2.error(error);
|
|
124
153
|
}
|
|
125
|
-
}
|
|
154
|
+
};
|
|
155
|
+
callbacks.onError = (error) => controller2.error(error);
|
|
156
|
+
if (nonStreamingBody !== void 0) {
|
|
157
|
+
callbacks.onParse(nonStreamingBody);
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
});
|
|
161
|
+
return new Response(stream, {
|
|
162
|
+
status: response2?.status,
|
|
163
|
+
statusText: response2?.statusText,
|
|
164
|
+
headers: {
|
|
165
|
+
"Content-Type": "application/x-ndjson",
|
|
166
|
+
[X_TSS_SERIALIZED]: "true"
|
|
126
167
|
}
|
|
127
168
|
});
|
|
128
169
|
}
|
|
129
|
-
return new Response(
|
|
170
|
+
return new Response(void 0, {
|
|
130
171
|
status: response2?.status,
|
|
131
|
-
statusText: response2?.statusText
|
|
132
|
-
headers: {
|
|
133
|
-
"Content-Type": "application/x-ndjson; charset=utf-8",
|
|
134
|
-
"x-tss-serialized": "true"
|
|
135
|
-
}
|
|
172
|
+
statusText: response2?.statusText
|
|
136
173
|
});
|
|
137
174
|
} catch (error) {
|
|
138
175
|
if (error instanceof Response) {
|
|
@@ -147,10 +184,17 @@ const handleServerAction = async ({ request }) => {
|
|
|
147
184
|
console.error(error);
|
|
148
185
|
console.info();
|
|
149
186
|
const serializedError = JSON.stringify(
|
|
150
|
-
await Promise.resolve(
|
|
187
|
+
await Promise.resolve(
|
|
188
|
+
toCrossJSONAsync(error, {
|
|
189
|
+
refs: /* @__PURE__ */ new Map(),
|
|
190
|
+
plugins: serovalPlugins
|
|
191
|
+
})
|
|
192
|
+
)
|
|
151
193
|
);
|
|
194
|
+
const response2 = getResponse();
|
|
152
195
|
return new Response(serializedError, {
|
|
153
|
-
status: 500,
|
|
196
|
+
status: response2?.status ?? 500,
|
|
197
|
+
statusText: response2?.statusText,
|
|
154
198
|
headers: {
|
|
155
199
|
"Content-Type": "application/json",
|
|
156
200
|
"x-tss-serialized": "true"
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"server-functions-handler.js","sources":["../../src/server-functions-handler.ts"],"sourcesContent":["import { isNotFound } from '@tanstack/router-core'\nimport invariant from 'tiny-invariant'\nimport { TSR_FORMDATA_CONTEXT } from '@tanstack/start-client-core'\nimport { fromJSON, toCrossJSONStream, toJSONAsync } from 'seroval'\nimport { getResponse } from './request-response'\nimport { getServerFnById } from './getServerFnById'\nimport { getSerovalPlugins } from './serializer/getSerovalPlugins'\n\nfunction sanitizeBase(base: string | undefined) {\n if (!base) {\n throw new Error(\n '🚨 process.env.TSS_SERVER_FN_BASE is required in start/server-handler/index',\n )\n }\n\n return base.replace(/^\\/|\\/$/g, '')\n}\n\nexport const handleServerAction = async ({ request }: { request: Request }) => {\n const controller = new AbortController()\n const signal = controller.signal\n const abort = () => controller.abort()\n request.signal.addEventListener('abort', abort)\n\n const method = request.method\n const url = new URL(request.url, 'http://localhost:3000')\n // extract the serverFnId from the url as host/_serverFn/:serverFnId\n // Define a regex to match the path and extract the :thing part\n const regex = new RegExp(\n `${sanitizeBase(process.env.TSS_SERVER_FN_BASE)}/([^/?#]+)`,\n )\n\n // Execute the regex\n const match = url.pathname.match(regex)\n const serverFnId = match ? match[1] : null\n const search = Object.fromEntries(url.searchParams.entries()) as {\n payload?: any\n createServerFn?: boolean\n }\n\n const isCreateServerFn = 'createServerFn' in search\n const isRaw = 'raw' in search\n\n if (typeof serverFnId !== 'string') {\n throw new Error('Invalid server action param for serverFnId: ' + serverFnId)\n }\n\n const action = await getServerFnById(serverFnId)\n\n // Known FormData 'Content-Type' header values\n const formDataContentTypes = [\n 'multipart/form-data',\n 'application/x-www-form-urlencoded',\n ]\n\n const contentType = request.headers.get('Content-Type')\n const serovalPlugins = getSerovalPlugins()\n\n function parsePayload(payload: any) {\n const parsedPayload = fromJSON(payload, { plugins: serovalPlugins })\n return parsedPayload\n }\n\n const response = await (async () => {\n try {\n let result = await (async () => {\n // FormData\n if (\n formDataContentTypes.some(\n (type) => contentType && contentType.includes(type),\n )\n ) {\n // We don't support GET requests with FormData payloads... that seems impossible\n invariant(\n method.toLowerCase() !== 'get',\n 'GET requests with FormData payloads are not supported',\n )\n const formData = await request.formData()\n const serializedContext = formData.get(TSR_FORMDATA_CONTEXT)\n formData.delete(TSR_FORMDATA_CONTEXT)\n\n const params = {\n context: {} as any,\n data: formData,\n }\n if (typeof serializedContext === 'string') {\n try {\n params.context = parsePayload(JSON.parse(serializedContext))\n } catch {}\n }\n\n return await action(params, signal)\n }\n\n // Get requests use the query string\n if (method.toLowerCase() === 'get') {\n invariant(\n isCreateServerFn,\n 'expected GET request to originate from createServerFn',\n )\n // By default the payload is the search params\n let payload: any = search.payload\n // If there's a payload, we should try to parse it\n payload = payload ? await parsePayload(JSON.parse(payload)) : payload\n\n // Send it through!\n return await action(payload, signal)\n }\n\n if (method.toLowerCase() !== 'post') {\n throw new Error('expected POST method')\n }\n\n if (!contentType || !contentType.includes('application/json')) {\n throw new Error('expected application/json content type')\n }\n\n const jsonPayload = await request.json()\n\n // If this POST request was created by createServerFn,\n // its payload will be the only argument\n if (isCreateServerFn) {\n const payload = await parsePayload(jsonPayload)\n return await action(payload, signal)\n }\n\n // Otherwise, we'll spread the payload. Need to\n // support `use server` functions that take multiple\n // arguments.\n return await action(...jsonPayload)\n })()\n\n // Any time we get a Response back, we should just\n // return it immediately.\n if (result.result instanceof Response) {\n return result.result\n }\n\n // If this is a non createServerFn request, we need to\n // pull out the result from the result object\n if (!isCreateServerFn) {\n result = result.result\n\n // The result might again be a response,\n // and if it is, return it.\n if (result instanceof Response) {\n return result\n }\n }\n\n // TODO: RSCs Where are we getting this package?\n // if (isValidElement(result)) {\n // const { renderToPipeableStream } = await import(\n // // @ts-expect-error\n // 'react-server-dom/server'\n // )\n\n // const pipeableStream = renderToPipeableStream(result)\n\n // setHeaders(event, {\n // 'Content-Type': 'text/x-component',\n // } as any)\n\n // sendStream(event, response)\n // event._handled = true\n\n // return new Response(null, { status: 200 })\n // }\n\n if (isNotFound(result)) {\n return isNotFoundResponse(result)\n }\n\n const response = getResponse()\n\n let body = undefined\n if (result !== undefined) {\n body = new ReadableStream({\n start(controller) {\n toCrossJSONStream(result, {\n refs: new Map(),\n plugins: serovalPlugins,\n onParse: (value) => {\n controller.enqueue(JSON.stringify(value) + '\\n')\n },\n onDone: () => {\n try {\n controller.close()\n } catch (error) {\n controller.error(error)\n }\n },\n onError: (error) => {\n controller.error(error)\n },\n })\n },\n })\n }\n return new Response(body, {\n status: response?.status,\n statusText: response?.statusText,\n headers: {\n 'Content-Type': 'application/x-ndjson; charset=utf-8',\n 'x-tss-serialized': 'true',\n },\n })\n } catch (error: any) {\n if (error instanceof Response) {\n return error\n }\n // else if (\n // isPlainObject(error) &&\n // 'result' in error &&\n // error.result instanceof Response\n // ) {\n // return error.result\n // }\n\n // Currently this server-side context has no idea how to\n // build final URLs, so we need to defer that to the client.\n // The client will check for __redirect and __notFound keys,\n // and if they exist, it will handle them appropriately.\n\n if (isNotFound(error)) {\n return isNotFoundResponse(error)\n }\n\n console.info()\n console.info('Server Fn Error!')\n console.info()\n console.error(error)\n console.info()\n\n const serializedError = JSON.stringify(\n await Promise.resolve(toJSONAsync(error)),\n )\n return new Response(serializedError, {\n status: 500,\n headers: {\n 'Content-Type': 'application/json',\n 'x-tss-serialized': 'true',\n },\n })\n }\n })()\n\n request.signal.removeEventListener('abort', abort)\n\n if (isRaw) {\n return response\n }\n\n return response\n}\n\nfunction isNotFoundResponse(error: any) {\n const { headers, ...rest } = error\n\n return new Response(JSON.stringify(rest), {\n status: 404,\n headers: {\n 'Content-Type': 'application/json',\n ...(headers || {}),\n },\n })\n}\n"],"names":["response","controller"],"mappings":"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"}
|
|
1
|
+
{"version":3,"file":"server-functions-handler.js","sources":["../../src/server-functions-handler.ts"],"sourcesContent":["import { isNotFound } from '@tanstack/router-core'\nimport invariant from 'tiny-invariant'\nimport {\n TSS_FORMDATA_CONTEXT,\n X_TSS_SERIALIZED,\n} from '@tanstack/start-client-core'\nimport { fromJSON, toCrossJSONAsync, toCrossJSONStream } from 'seroval'\nimport { getResponse } from './request-response'\nimport { getServerFnById } from './getServerFnById'\nimport { getSerovalPlugins } from './serializer/getSerovalPlugins'\n\nfunction sanitizeBase(base: string | undefined) {\n if (!base) {\n throw new Error(\n '🚨 process.env.TSS_SERVER_FN_BASE is required in start/server-handler/index',\n )\n }\n\n return base.replace(/^\\/|\\/$/g, '')\n}\n\nexport const handleServerAction = async ({ request }: { request: Request }) => {\n const controller = new AbortController()\n const signal = controller.signal\n const abort = () => controller.abort()\n request.signal.addEventListener('abort', abort)\n\n const method = request.method\n const url = new URL(request.url, 'http://localhost:3000')\n // extract the serverFnId from the url as host/_serverFn/:serverFnId\n // Define a regex to match the path and extract the :thing part\n const regex = new RegExp(\n `${sanitizeBase(process.env.TSS_SERVER_FN_BASE)}/([^/?#]+)`,\n )\n\n // Execute the regex\n const match = url.pathname.match(regex)\n const serverFnId = match ? match[1] : null\n const search = Object.fromEntries(url.searchParams.entries()) as {\n payload?: any\n createServerFn?: boolean\n }\n\n const isCreateServerFn = 'createServerFn' in search\n const isRaw = 'raw' in search\n\n if (typeof serverFnId !== 'string') {\n throw new Error('Invalid server action param for serverFnId: ' + serverFnId)\n }\n\n const action = await getServerFnById(serverFnId)\n\n // Known FormData 'Content-Type' header values\n const formDataContentTypes = [\n 'multipart/form-data',\n 'application/x-www-form-urlencoded',\n ]\n\n const contentType = request.headers.get('Content-Type')\n const serovalPlugins = getSerovalPlugins()\n\n function parsePayload(payload: any) {\n const parsedPayload = fromJSON(payload, { plugins: serovalPlugins })\n return parsedPayload\n }\n\n const response = await (async () => {\n try {\n let result = await (async () => {\n // FormData\n if (\n formDataContentTypes.some(\n (type) => contentType && contentType.includes(type),\n )\n ) {\n // We don't support GET requests with FormData payloads... that seems impossible\n invariant(\n method.toLowerCase() !== 'get',\n 'GET requests with FormData payloads are not supported',\n )\n const formData = await request.formData()\n const serializedContext = formData.get(TSS_FORMDATA_CONTEXT)\n formData.delete(TSS_FORMDATA_CONTEXT)\n\n const params = {\n context: {} as any,\n data: formData,\n }\n if (typeof serializedContext === 'string') {\n try {\n params.context = parsePayload(JSON.parse(serializedContext))\n } catch {}\n }\n\n return await action(params, signal)\n }\n\n // Get requests use the query string\n if (method.toLowerCase() === 'get') {\n invariant(\n isCreateServerFn,\n 'expected GET request to originate from createServerFn',\n )\n // By default the payload is the search params\n let payload: any = search.payload\n // If there's a payload, we should try to parse it\n payload = payload ? await parsePayload(JSON.parse(payload)) : payload\n\n // Send it through!\n return await action(payload, signal)\n }\n\n if (method.toLowerCase() !== 'post') {\n throw new Error('expected POST method')\n }\n\n if (!contentType || !contentType.includes('application/json')) {\n throw new Error('expected application/json content type')\n }\n\n const jsonPayload = await request.json()\n\n // If this POST request was created by createServerFn,\n // its payload will be the only argument\n if (isCreateServerFn) {\n const payload = await parsePayload(jsonPayload)\n return await action(payload, signal)\n }\n\n // Otherwise, we'll spread the payload. Need to\n // support `use server` functions that take multiple\n // arguments.\n return await action(...jsonPayload)\n })()\n\n // Any time we get a Response back, we should just\n // return it immediately.\n if (result.result instanceof Response) {\n return result.result\n }\n\n // If this is a non createServerFn request, we need to\n // pull out the result from the result object\n if (!isCreateServerFn) {\n result = result.result\n\n // The result might again be a response,\n // and if it is, return it.\n if (result instanceof Response) {\n return result\n }\n }\n\n // TODO: RSCs Where are we getting this package?\n // if (isValidElement(result)) {\n // const { renderToPipeableStream } = await import(\n // // @ts-expect-error\n // 'react-server-dom/server'\n // )\n\n // const pipeableStream = renderToPipeableStream(result)\n\n // setHeaders(event, {\n // 'Content-Type': 'text/x-component',\n // } as any)\n\n // sendStream(event, response)\n // event._handled = true\n\n // return new Response(null, { status: 200 })\n // }\n\n if (isNotFound(result)) {\n return isNotFoundResponse(result)\n }\n\n const response = getResponse()\n let nonStreamingBody: any = undefined\n\n if (result !== undefined) {\n // first run without the stream in case `result` does not need streaming\n let done = false as boolean\n const callbacks: {\n onParse: (value: any) => void\n onDone: () => void\n onError: (error: any) => void\n } = {\n onParse: (value) => {\n nonStreamingBody = value\n },\n onDone: () => {\n done = true\n },\n onError: (error) => {\n throw error\n },\n }\n toCrossJSONStream(result, {\n refs: new Map(),\n plugins: serovalPlugins,\n onParse(value) {\n callbacks.onParse(value)\n },\n onDone() {\n callbacks.onDone()\n },\n onError: (error) => {\n callbacks.onError(error)\n },\n })\n if (done) {\n return new Response(\n nonStreamingBody ? JSON.stringify(nonStreamingBody) : undefined,\n {\n status: response?.status,\n statusText: response?.statusText,\n headers: {\n 'Content-Type': 'application/json',\n [X_TSS_SERIALIZED]: 'true',\n },\n },\n )\n }\n\n // not done yet, we need to stream\n const stream = new ReadableStream({\n start(controller) {\n callbacks.onParse = (value) =>\n controller.enqueue(JSON.stringify(value) + '\\n')\n callbacks.onDone = () => {\n try {\n controller.close()\n } catch (error) {\n controller.error(error)\n }\n }\n callbacks.onError = (error) => controller.error(error)\n // stream the initial body\n if (nonStreamingBody !== undefined) {\n callbacks.onParse(nonStreamingBody)\n }\n },\n })\n return new Response(stream, {\n status: response?.status,\n statusText: response?.statusText,\n headers: {\n 'Content-Type': 'application/x-ndjson',\n [X_TSS_SERIALIZED]: 'true',\n },\n })\n }\n\n return new Response(undefined, {\n status: response?.status,\n statusText: response?.statusText,\n })\n } catch (error: any) {\n if (error instanceof Response) {\n return error\n }\n // else if (\n // isPlainObject(error) &&\n // 'result' in error &&\n // error.result instanceof Response\n // ) {\n // return error.result\n // }\n\n // Currently this server-side context has no idea how to\n // build final URLs, so we need to defer that to the client.\n // The client will check for __redirect and __notFound keys,\n // and if they exist, it will handle them appropriately.\n\n if (isNotFound(error)) {\n return isNotFoundResponse(error)\n }\n\n console.info()\n console.info('Server Fn Error!')\n console.info()\n console.error(error)\n console.info()\n\n const serializedError = JSON.stringify(\n await Promise.resolve(\n toCrossJSONAsync(error, {\n refs: new Map(),\n plugins: serovalPlugins,\n }),\n ),\n )\n const response = getResponse()\n return new Response(serializedError, {\n status: response?.status ?? 500,\n statusText: response?.statusText,\n headers: {\n 'Content-Type': 'application/json',\n 'x-tss-serialized': 'true',\n },\n })\n }\n })()\n\n request.signal.removeEventListener('abort', abort)\n\n if (isRaw) {\n return response\n }\n\n return response\n}\n\nfunction isNotFoundResponse(error: any) {\n const { headers, ...rest } = error\n\n return new Response(JSON.stringify(rest), {\n status: 404,\n headers: {\n 'Content-Type': 'application/json',\n ...(headers || {}),\n },\n })\n}\n"],"names":["response","controller"],"mappings":"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"}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@tanstack/start-server-core",
|
|
3
|
-
"version": "1.132.0-alpha.
|
|
3
|
+
"version": "1.132.0-alpha.4",
|
|
4
4
|
"description": "Modern and scalable routing for React applications",
|
|
5
5
|
"author": "Tanner Linsley",
|
|
6
6
|
"license": "MIT",
|
|
@@ -50,9 +50,9 @@
|
|
|
50
50
|
"seroval": "^1.3.2",
|
|
51
51
|
"tiny-invariant": "^1.3.3",
|
|
52
52
|
"@tanstack/history": "1.132.0-alpha.1",
|
|
53
|
-
"@tanstack/start-client-core": "1.132.0-alpha.
|
|
54
|
-
"@tanstack/
|
|
55
|
-
"@tanstack/
|
|
53
|
+
"@tanstack/start-client-core": "1.132.0-alpha.4",
|
|
54
|
+
"@tanstack/start-storage-context": "1.132.0-alpha.4",
|
|
55
|
+
"@tanstack/router-core": "1.132.0-alpha.4"
|
|
56
56
|
},
|
|
57
57
|
"scripts": {}
|
|
58
58
|
}
|
package/src/createServerRpc.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { TSS_SERVER_FUNCTION } from '@tanstack/start-client-core'
|
|
1
2
|
import invariant from 'tiny-invariant'
|
|
2
3
|
|
|
3
4
|
let baseUrl: string
|
|
@@ -24,5 +25,6 @@ export const createServerRpc = (
|
|
|
24
25
|
return Object.assign(splitImportFn, {
|
|
25
26
|
url,
|
|
26
27
|
functionId,
|
|
28
|
+
[TSS_SERVER_FUNCTION]: true,
|
|
27
29
|
})
|
|
28
30
|
}
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { createSerializationAdapter } from '@tanstack/router-core'
|
|
2
|
+
import { TSS_SERVER_FUNCTION } from '@tanstack/start-client-core'
|
|
2
3
|
import { createServerRpc } from '../createServerRpc'
|
|
3
4
|
import { getServerFnById } from '../getServerFnById'
|
|
4
5
|
|
|
5
6
|
export const ServerFunctionSerializationAdapter = createSerializationAdapter({
|
|
6
7
|
key: '$TSS/serverfn',
|
|
7
|
-
test: (v): v is { functionId: string } =>
|
|
8
|
-
typeof v == 'function' && 'functionId' in v,
|
|
8
|
+
test: (v): v is { functionId: string } => v[TSS_SERVER_FUNCTION],
|
|
9
9
|
toSerializable: ({ functionId }) => ({ functionId }),
|
|
10
10
|
fromSerializable: ({ functionId }) => {
|
|
11
11
|
const fn = async (opts: any, signal: any): Promise<any> => {
|
|
@@ -1,7 +1,10 @@
|
|
|
1
1
|
import { isNotFound } from '@tanstack/router-core'
|
|
2
2
|
import invariant from 'tiny-invariant'
|
|
3
|
-
import {
|
|
4
|
-
|
|
3
|
+
import {
|
|
4
|
+
TSS_FORMDATA_CONTEXT,
|
|
5
|
+
X_TSS_SERIALIZED,
|
|
6
|
+
} from '@tanstack/start-client-core'
|
|
7
|
+
import { fromJSON, toCrossJSONAsync, toCrossJSONStream } from 'seroval'
|
|
5
8
|
import { getResponse } from './request-response'
|
|
6
9
|
import { getServerFnById } from './getServerFnById'
|
|
7
10
|
import { getSerovalPlugins } from './serializer/getSerovalPlugins'
|
|
@@ -76,8 +79,8 @@ export const handleServerAction = async ({ request }: { request: Request }) => {
|
|
|
76
79
|
'GET requests with FormData payloads are not supported',
|
|
77
80
|
)
|
|
78
81
|
const formData = await request.formData()
|
|
79
|
-
const serializedContext = formData.get(
|
|
80
|
-
formData.delete(
|
|
82
|
+
const serializedContext = formData.get(TSS_FORMDATA_CONTEXT)
|
|
83
|
+
formData.delete(TSS_FORMDATA_CONTEXT)
|
|
81
84
|
|
|
82
85
|
const params = {
|
|
83
86
|
context: {} as any,
|
|
@@ -172,38 +175,85 @@ export const handleServerAction = async ({ request }: { request: Request }) => {
|
|
|
172
175
|
}
|
|
173
176
|
|
|
174
177
|
const response = getResponse()
|
|
178
|
+
let nonStreamingBody: any = undefined
|
|
175
179
|
|
|
176
|
-
let body = undefined
|
|
177
180
|
if (result !== undefined) {
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
181
|
+
// first run without the stream in case `result` does not need streaming
|
|
182
|
+
let done = false as boolean
|
|
183
|
+
const callbacks: {
|
|
184
|
+
onParse: (value: any) => void
|
|
185
|
+
onDone: () => void
|
|
186
|
+
onError: (error: any) => void
|
|
187
|
+
} = {
|
|
188
|
+
onParse: (value) => {
|
|
189
|
+
nonStreamingBody = value
|
|
190
|
+
},
|
|
191
|
+
onDone: () => {
|
|
192
|
+
done = true
|
|
193
|
+
},
|
|
194
|
+
onError: (error) => {
|
|
195
|
+
throw error
|
|
196
|
+
},
|
|
197
|
+
}
|
|
198
|
+
toCrossJSONStream(result, {
|
|
199
|
+
refs: new Map(),
|
|
200
|
+
plugins: serovalPlugins,
|
|
201
|
+
onParse(value) {
|
|
202
|
+
callbacks.onParse(value)
|
|
203
|
+
},
|
|
204
|
+
onDone() {
|
|
205
|
+
callbacks.onDone()
|
|
206
|
+
},
|
|
207
|
+
onError: (error) => {
|
|
208
|
+
callbacks.onError(error)
|
|
209
|
+
},
|
|
210
|
+
})
|
|
211
|
+
if (done) {
|
|
212
|
+
return new Response(
|
|
213
|
+
nonStreamingBody ? JSON.stringify(nonStreamingBody) : undefined,
|
|
214
|
+
{
|
|
215
|
+
status: response?.status,
|
|
216
|
+
statusText: response?.statusText,
|
|
217
|
+
headers: {
|
|
218
|
+
'Content-Type': 'application/json',
|
|
219
|
+
[X_TSS_SERIALIZED]: 'true',
|
|
192
220
|
},
|
|
193
|
-
|
|
221
|
+
},
|
|
222
|
+
)
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
// not done yet, we need to stream
|
|
226
|
+
const stream = new ReadableStream({
|
|
227
|
+
start(controller) {
|
|
228
|
+
callbacks.onParse = (value) =>
|
|
229
|
+
controller.enqueue(JSON.stringify(value) + '\n')
|
|
230
|
+
callbacks.onDone = () => {
|
|
231
|
+
try {
|
|
232
|
+
controller.close()
|
|
233
|
+
} catch (error) {
|
|
194
234
|
controller.error(error)
|
|
195
|
-
}
|
|
196
|
-
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
callbacks.onError = (error) => controller.error(error)
|
|
238
|
+
// stream the initial body
|
|
239
|
+
if (nonStreamingBody !== undefined) {
|
|
240
|
+
callbacks.onParse(nonStreamingBody)
|
|
241
|
+
}
|
|
242
|
+
},
|
|
243
|
+
})
|
|
244
|
+
return new Response(stream, {
|
|
245
|
+
status: response?.status,
|
|
246
|
+
statusText: response?.statusText,
|
|
247
|
+
headers: {
|
|
248
|
+
'Content-Type': 'application/x-ndjson',
|
|
249
|
+
[X_TSS_SERIALIZED]: 'true',
|
|
197
250
|
},
|
|
198
251
|
})
|
|
199
252
|
}
|
|
200
|
-
|
|
253
|
+
|
|
254
|
+
return new Response(undefined, {
|
|
201
255
|
status: response?.status,
|
|
202
256
|
statusText: response?.statusText,
|
|
203
|
-
headers: {
|
|
204
|
-
'Content-Type': 'application/x-ndjson; charset=utf-8',
|
|
205
|
-
'x-tss-serialized': 'true',
|
|
206
|
-
},
|
|
207
257
|
})
|
|
208
258
|
} catch (error: any) {
|
|
209
259
|
if (error instanceof Response) {
|
|
@@ -233,10 +283,17 @@ export const handleServerAction = async ({ request }: { request: Request }) => {
|
|
|
233
283
|
console.info()
|
|
234
284
|
|
|
235
285
|
const serializedError = JSON.stringify(
|
|
236
|
-
await Promise.resolve(
|
|
286
|
+
await Promise.resolve(
|
|
287
|
+
toCrossJSONAsync(error, {
|
|
288
|
+
refs: new Map(),
|
|
289
|
+
plugins: serovalPlugins,
|
|
290
|
+
}),
|
|
291
|
+
),
|
|
237
292
|
)
|
|
293
|
+
const response = getResponse()
|
|
238
294
|
return new Response(serializedError, {
|
|
239
|
-
status: 500,
|
|
295
|
+
status: response?.status ?? 500,
|
|
296
|
+
statusText: response?.statusText,
|
|
240
297
|
headers: {
|
|
241
298
|
'Content-Type': 'application/json',
|
|
242
299
|
'x-tss-serialized': 'true',
|