@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.
@@ -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":";AAEA,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,EAAA,CACD;AACH;"}
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) => typeof v == "function" && "functionId" in 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 } =>\n typeof v == 'function' && 'functionId' in v,\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":";;;AAIO,MAAM,qCAAqC,2BAA2B;AAAA,EAC3E,KAAK;AAAA,EACL,MAAM,CAAC,MACL,OAAO,KAAK,cAAc,gBAAgB;AAAA,EAC5C,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
+ {"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 { TSR_FORMDATA_CONTEXT } from "@tanstack/start-client-core";
4
- import { toCrossJSONStream, toJSONAsync, fromJSON } from "seroval";
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(TSR_FORMDATA_CONTEXT);
57
- formData.delete(TSR_FORMDATA_CONTEXT);
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 body = void 0;
105
+ let nonStreamingBody = void 0;
106
106
  if (result !== void 0) {
107
- body = new ReadableStream({
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
- toCrossJSONStream(result, {
110
- refs: /* @__PURE__ */ new Map(),
111
- plugins: serovalPlugins,
112
- onParse: (value) => {
113
- controller2.enqueue(JSON.stringify(value) + "\n");
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(body, {
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(toJSONAsync(error))
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",
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.3",
54
- "@tanstack/router-core": "1.132.0-alpha.3",
55
- "@tanstack/start-storage-context": "1.132.0-alpha.3"
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
  }
@@ -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 { TSR_FORMDATA_CONTEXT } from '@tanstack/start-client-core'
4
- import { fromJSON, toCrossJSONStream, toJSONAsync } from 'seroval'
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(TSR_FORMDATA_CONTEXT)
80
- formData.delete(TSR_FORMDATA_CONTEXT)
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
- body = new ReadableStream({
179
- start(controller) {
180
- toCrossJSONStream(result, {
181
- refs: new Map(),
182
- plugins: serovalPlugins,
183
- onParse: (value) => {
184
- controller.enqueue(JSON.stringify(value) + '\n')
185
- },
186
- onDone: () => {
187
- try {
188
- controller.close()
189
- } catch (error) {
190
- controller.error(error)
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
- onError: (error) => {
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
- return new Response(body, {
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(toJSONAsync(error)),
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',