@tanstack/start-server-core 1.132.0-alpha.2 → 1.132.0-alpha.20

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,9 +1,10 @@
1
1
  import { isNotFound } from "@tanstack/router-core";
2
2
  import invariant from "tiny-invariant";
3
- import { startSerializer } from "@tanstack/start-client-core";
4
- import { VIRTUAL_MODULES } from "./virtual-modules.js";
5
- import { loadVirtualModule } from "./loadVirtualModule.js";
3
+ import { TSS_FORMDATA_CONTEXT, X_TSS_SERIALIZED } from "@tanstack/start-client-core";
4
+ import { toCrossJSONStream, toCrossJSONAsync, fromJSON } from "seroval";
6
5
  import { getResponse } from "./request-response.js";
6
+ import { getServerFnById } from "./getServerFnById.js";
7
+ import { getSerovalPlugins } from "./serializer/getSerovalPlugins.js";
7
8
  function sanitizeBase(base) {
8
9
  if (!base) {
9
10
  throw new Error(
@@ -12,59 +13,6 @@ function sanitizeBase(base) {
12
13
  }
13
14
  return base.replace(/^\/|\/$/g, "");
14
15
  }
15
- async function revive(root, reviver) {
16
- async function reviveNode(holder2, key) {
17
- const value = holder2[key];
18
- if (value && typeof value === "object") {
19
- await Promise.all(Object.keys(value).map((k) => reviveNode(value, k)));
20
- }
21
- if (reviver) {
22
- holder2[key] = await reviver(key, holder2[key]);
23
- }
24
- }
25
- const holder = { "": root };
26
- await reviveNode(holder, "");
27
- return holder[""];
28
- }
29
- async function reviveServerFns(key, value) {
30
- if (value && value.__serverFn === true && value.functionId) {
31
- const serverFn = await getServerFnById(value.functionId);
32
- return async (opts, signal) => {
33
- const result = await serverFn(opts ?? {}, signal);
34
- return result.result;
35
- };
36
- }
37
- return value;
38
- }
39
- async function getServerFnById(serverFnId) {
40
- const { default: serverFnManifest } = await loadVirtualModule(
41
- VIRTUAL_MODULES.serverFnManifest
42
- );
43
- const serverFnInfo = serverFnManifest[serverFnId];
44
- if (!serverFnInfo) {
45
- console.info("serverFnManifest", serverFnManifest);
46
- throw new Error("Server function info not found for " + serverFnId);
47
- }
48
- const fnModule = await serverFnInfo.importer();
49
- if (!fnModule) {
50
- console.info("serverFnInfo", serverFnInfo);
51
- throw new Error("Server function module not resolved for " + serverFnId);
52
- }
53
- const action = fnModule[serverFnInfo.functionName];
54
- if (!action) {
55
- console.info("serverFnInfo", serverFnInfo);
56
- console.info("fnModule", fnModule);
57
- throw new Error(
58
- `Server function module export not resolved for serverFn ID: ${serverFnId}`
59
- );
60
- }
61
- return action;
62
- }
63
- async function parsePayload(payload) {
64
- const parsedPayload = startSerializer.parse(payload);
65
- await revive(parsedPayload, reviveServerFns);
66
- return parsedPayload;
67
- }
68
16
  const handleServerAction = async ({ request }) => {
69
17
  const controller = new AbortController();
70
18
  const signal = controller.signal;
@@ -79,7 +27,6 @@ const handleServerAction = async ({ request }) => {
79
27
  const serverFnId = match ? match[1] : null;
80
28
  const search = Object.fromEntries(url.searchParams.entries());
81
29
  const isCreateServerFn = "createServerFn" in search;
82
- const isRaw = "raw" in search;
83
30
  if (typeof serverFnId !== "string") {
84
31
  throw new Error("Invalid server action param for serverFnId: " + serverFnId);
85
32
  }
@@ -88,32 +35,58 @@ const handleServerAction = async ({ request }) => {
88
35
  "multipart/form-data",
89
36
  "application/x-www-form-urlencoded"
90
37
  ];
38
+ const contentType = request.headers.get("Content-Type");
39
+ const serovalPlugins = getSerovalPlugins();
40
+ function parsePayload(payload) {
41
+ const parsedPayload = fromJSON(payload, { plugins: serovalPlugins });
42
+ return parsedPayload;
43
+ }
91
44
  const response = await (async () => {
92
45
  try {
93
46
  let result = await (async () => {
94
- if (request.headers.get("Content-Type") && formDataContentTypes.some(
95
- (type) => request.headers.get("Content-Type")?.includes(type)
47
+ if (formDataContentTypes.some(
48
+ (type) => contentType && contentType.includes(type)
96
49
  )) {
97
50
  invariant(
98
51
  method.toLowerCase() !== "get",
99
52
  "GET requests with FormData payloads are not supported"
100
53
  );
101
- return await action(await request.formData(), signal);
54
+ const formData = await request.formData();
55
+ const serializedContext = formData.get(TSS_FORMDATA_CONTEXT);
56
+ formData.delete(TSS_FORMDATA_CONTEXT);
57
+ const params = {
58
+ context: {},
59
+ data: formData
60
+ };
61
+ if (typeof serializedContext === "string") {
62
+ try {
63
+ params.context = parsePayload(JSON.parse(serializedContext));
64
+ } catch {
65
+ }
66
+ }
67
+ return await action(params, signal);
102
68
  }
103
69
  if (method.toLowerCase() === "get") {
104
- let payload2 = search;
105
- if (isCreateServerFn) {
106
- payload2 = search.payload;
107
- }
108
- payload2 = payload2 ? await parsePayload(payload2) : payload2;
109
- return await action(payload2, signal);
70
+ invariant(
71
+ isCreateServerFn,
72
+ "expected GET request to originate from createServerFn"
73
+ );
74
+ let payload = search.payload;
75
+ payload = payload ? await parsePayload(JSON.parse(payload)) : payload;
76
+ return await action(payload, signal);
77
+ }
78
+ if (method.toLowerCase() !== "post") {
79
+ throw new Error("expected POST method");
80
+ }
81
+ if (!contentType || !contentType.includes("application/json")) {
82
+ throw new Error("expected application/json content type");
110
83
  }
111
- const jsonPayloadAsString = await request.text();
112
- const payload = await parsePayload(jsonPayloadAsString);
84
+ const jsonPayload = await request.json();
113
85
  if (isCreateServerFn) {
86
+ const payload = await parsePayload(jsonPayload);
114
87
  return await action(payload, signal);
115
88
  }
116
- return await action(...payload, signal);
89
+ return await action(...jsonPayload);
117
90
  })();
118
91
  if (result.result instanceof Response) {
119
92
  return result.result;
@@ -128,16 +101,75 @@ const handleServerAction = async ({ request }) => {
128
101
  return isNotFoundResponse(result);
129
102
  }
130
103
  const response2 = getResponse();
131
- return new Response(
132
- result !== void 0 ? startSerializer.stringify(result) : void 0,
133
- {
104
+ let nonStreamingBody = void 0;
105
+ if (result !== void 0) {
106
+ let done = false;
107
+ const callbacks = {
108
+ onParse: (value) => {
109
+ nonStreamingBody = value;
110
+ },
111
+ onDone: () => {
112
+ done = true;
113
+ },
114
+ onError: (error) => {
115
+ throw error;
116
+ }
117
+ };
118
+ toCrossJSONStream(result, {
119
+ refs: /* @__PURE__ */ new Map(),
120
+ plugins: serovalPlugins,
121
+ onParse(value) {
122
+ callbacks.onParse(value);
123
+ },
124
+ onDone() {
125
+ callbacks.onDone();
126
+ },
127
+ onError: (error) => {
128
+ callbacks.onError(error);
129
+ }
130
+ });
131
+ if (done) {
132
+ return new Response(
133
+ nonStreamingBody ? JSON.stringify(nonStreamingBody) : void 0,
134
+ {
135
+ status: response2?.status,
136
+ statusText: response2?.statusText,
137
+ headers: {
138
+ "Content-Type": "application/json",
139
+ [X_TSS_SERIALIZED]: "true"
140
+ }
141
+ }
142
+ );
143
+ }
144
+ const stream = new ReadableStream({
145
+ start(controller2) {
146
+ callbacks.onParse = (value) => controller2.enqueue(JSON.stringify(value) + "\n");
147
+ callbacks.onDone = () => {
148
+ try {
149
+ controller2.close();
150
+ } catch (error) {
151
+ controller2.error(error);
152
+ }
153
+ };
154
+ callbacks.onError = (error) => controller2.error(error);
155
+ if (nonStreamingBody !== void 0) {
156
+ callbacks.onParse(nonStreamingBody);
157
+ }
158
+ }
159
+ });
160
+ return new Response(stream, {
134
161
  status: response2?.status,
135
162
  statusText: response2?.statusText,
136
163
  headers: {
137
- "Content-Type": "application/json"
164
+ "Content-Type": "application/x-ndjson",
165
+ [X_TSS_SERIALIZED]: "true"
138
166
  }
139
- }
140
- );
167
+ });
168
+ }
169
+ return new Response(void 0, {
170
+ status: response2?.status,
171
+ statusText: response2?.statusText
172
+ });
141
173
  } catch (error) {
142
174
  if (error instanceof Response) {
143
175
  return error;
@@ -150,18 +182,26 @@ const handleServerAction = async ({ request }) => {
150
182
  console.info();
151
183
  console.error(error);
152
184
  console.info();
153
- return new Response(startSerializer.stringify(error), {
154
- status: 500,
185
+ const serializedError = JSON.stringify(
186
+ await Promise.resolve(
187
+ toCrossJSONAsync(error, {
188
+ refs: /* @__PURE__ */ new Map(),
189
+ plugins: serovalPlugins
190
+ })
191
+ )
192
+ );
193
+ const response2 = getResponse();
194
+ return new Response(serializedError, {
195
+ status: response2?.status ?? 500,
196
+ statusText: response2?.statusText,
155
197
  headers: {
156
- "Content-Type": "application/json"
198
+ "Content-Type": "application/json",
199
+ [X_TSS_SERIALIZED]: "true"
157
200
  }
158
201
  });
159
202
  }
160
203
  })();
161
204
  request.signal.removeEventListener("abort", abort);
162
- if (isRaw) {
163
- return response;
164
- }
165
205
  return response;
166
206
  };
167
207
  function isNotFoundResponse(error) {
@@ -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 { startSerializer } from '@tanstack/start-client-core'\nimport { VIRTUAL_MODULES } from './virtual-modules'\nimport { loadVirtualModule } from './loadVirtualModule'\nimport { getResponse } from './request-response'\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\nasync function revive(root: any, reviver?: (key: string, value: any) => any) {\n async function reviveNode(holder: any, key: string) {\n const value = holder[key]\n\n if (value && typeof value === 'object') {\n await Promise.all(Object.keys(value).map((k) => reviveNode(value, k)))\n }\n\n if (reviver) {\n holder[key] = await reviver(key, holder[key])\n }\n }\n\n const holder = { '': root }\n await reviveNode(holder, '')\n return holder['']\n}\n\nasync function reviveServerFns(key: string, value: any) {\n if (value && value.__serverFn === true && value.functionId) {\n const serverFn = await getServerFnById(value.functionId)\n return async (opts: any, signal: any): Promise<any> => {\n const result = await serverFn(opts ?? {}, signal)\n return result.result\n }\n }\n return value\n}\n\nasync function getServerFnById(serverFnId: string) {\n const { default: serverFnManifest } = await loadVirtualModule(\n VIRTUAL_MODULES.serverFnManifest,\n )\n\n const serverFnInfo = serverFnManifest[serverFnId]\n\n if (!serverFnInfo) {\n console.info('serverFnManifest', serverFnManifest)\n throw new Error('Server function info not found for ' + serverFnId)\n }\n\n const fnModule = await serverFnInfo.importer()\n\n if (!fnModule) {\n console.info('serverFnInfo', serverFnInfo)\n throw new Error('Server function module not resolved for ' + serverFnId)\n }\n\n const action = fnModule[serverFnInfo.functionName]\n\n if (!action) {\n console.info('serverFnInfo', serverFnInfo)\n console.info('fnModule', fnModule)\n throw new Error(\n `Server function module export not resolved for serverFn ID: ${serverFnId}`,\n )\n }\n return action\n}\n\nasync function parsePayload(payload: any) {\n const parsedPayload = startSerializer.parse(payload)\n await revive(parsedPayload, reviveServerFns)\n return parsedPayload\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 response = await (async () => {\n try {\n let result = await (async () => {\n // FormData\n if (\n request.headers.get('Content-Type') &&\n formDataContentTypes.some((type) =>\n request.headers.get('Content-Type')?.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\n return await action(await request.formData(), signal)\n }\n\n // Get requests use the query string\n if (method.toLowerCase() === 'get') {\n // By default the payload is the search params\n let payload: any = search\n\n // If this GET request was created by createServerFn,\n // then the payload will be on the payload param\n if (isCreateServerFn) {\n payload = search.payload\n }\n\n // If there's a payload, we should try to parse it\n payload = payload ? await parsePayload(payload) : payload\n\n // Send it through!\n return await action(payload, signal)\n }\n\n // This must be a POST request, likely JSON???\n const jsonPayloadAsString = await request.text()\n\n // We should probably try to deserialize the payload\n // as JSON, but we'll just pass it through for now.\n const payload = await parsePayload(jsonPayloadAsString)\n\n // If this POST request was created by createServerFn,\n // it's payload will be the only argument\n if (isCreateServerFn) {\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(...(payload as any), signal)\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 // if (!search.createServerFn) {\n // result = result.result\n // }\n\n // else if (\n // isPlainObject(result) &&\n // 'result' in result &&\n // result.result instanceof Response\n // ) {\n // return result.result\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 return new Response(\n result !== undefined ? startSerializer.stringify(result) : undefined,\n {\n status: response?.status,\n statusText: response?.statusText,\n headers: {\n 'Content-Type': 'application/json',\n },\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 return new Response(startSerializer.stringify(error), {\n status: 500,\n headers: {\n 'Content-Type': 'application/json',\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":["holder","payload","response"],"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\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 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.2",
3
+ "version": "1.132.0-alpha.20",
4
4
  "description": "Modern and scalable routing for React applications",
5
5
  "author": "Tanner Linsley",
6
6
  "license": "MIT",
@@ -40,18 +40,19 @@
40
40
  "src"
41
41
  ],
42
42
  "engines": {
43
- "node": ">=12"
43
+ "node": ">=22.12.0"
44
44
  },
45
45
  "dependencies": {
46
46
  "@standard-schema/spec": "^1.0.0",
47
47
  "cookie-es": "^2.0.0",
48
48
  "fetchdts": "^0.1.6",
49
- "h3": "2.0.0-beta.3",
49
+ "h3": "2.0.0-beta.4",
50
+ "seroval": "^1.3.2",
50
51
  "tiny-invariant": "^1.3.3",
51
52
  "@tanstack/history": "1.132.0-alpha.1",
52
- "@tanstack/router-core": "1.132.0-alpha.2",
53
- "@tanstack/start-client-core": "1.132.0-alpha.2",
54
- "@tanstack/start-storage-context": "1.132.0-alpha.2"
53
+ "@tanstack/start-client-core": "1.132.0-alpha.20",
54
+ "@tanstack/router-core": "1.132.0-alpha.20",
55
+ "@tanstack/start-storage-context": "1.132.0-alpha.20"
55
56
  },
56
57
  "scripts": {}
57
58
  }
@@ -0,0 +1,30 @@
1
+ import { TSS_SERVER_FUNCTION } from '@tanstack/start-client-core'
2
+ import invariant from 'tiny-invariant'
3
+
4
+ let baseUrl: string
5
+ function sanitizeBase(base: string) {
6
+ return base.replace(/^\/|\/$/g, '')
7
+ }
8
+
9
+ export const createServerRpc = (
10
+ functionId: string,
11
+ splitImportFn: (...args: any) => any,
12
+ ) => {
13
+ if (!baseUrl) {
14
+ const sanitizedAppBase = sanitizeBase(process.env.TSS_APP_BASE || '/')
15
+ const sanitizedServerBase = sanitizeBase(process.env.TSS_SERVER_FN_BASE!)
16
+ baseUrl = `${sanitizedAppBase ? `/${sanitizedAppBase}` : ''}/${sanitizedServerBase}/`
17
+ }
18
+ invariant(
19
+ splitImportFn,
20
+ '🚨splitImportFn required for the server functions server runtime, but was not provided.',
21
+ )
22
+
23
+ const url = baseUrl + functionId
24
+
25
+ return Object.assign(splitImportFn, {
26
+ url,
27
+ functionId,
28
+ [TSS_SERVER_FUNCTION]: true,
29
+ })
30
+ }
@@ -5,11 +5,13 @@ import {
5
5
  mergeHeaders,
6
6
  } from '@tanstack/start-client-core'
7
7
  import {
8
+ executeRewriteInput,
8
9
  getMatchedRoutes,
9
10
  isRedirect,
10
11
  isResolvedRedirect,
11
12
  joinPaths,
12
13
  processRouteTree,
14
+ rewriteBasepath,
13
15
  trimPath,
14
16
  } from '@tanstack/router-core'
15
17
  import { attachRouterServerSsrUtils } from '@tanstack/router-core/ssr/server'
@@ -21,6 +23,7 @@ import { VIRTUAL_MODULES } from './virtual-modules'
21
23
  import { loadVirtualModule } from './loadVirtualModule'
22
24
 
23
25
  import { HEADERS } from './constants'
26
+ import { ServerFunctionSerializationAdapter } from './serializer/ServerFunctionSerializationAdapter'
24
27
  import type {
25
28
  AnyServerRouteWithTypes,
26
29
  ServerRouteMethodHandlerFn,
@@ -72,6 +75,17 @@ export function createStartHandler<TRouter extends AnyRouter>({
72
75
  const originalFetch = globalThis.fetch
73
76
 
74
77
  const startRequestResolver: RequestHandler = async (request) => {
78
+ function getOrigin() {
79
+ const originHeader = request.headers.get('Origin')
80
+ if (originHeader) {
81
+ return originHeader
82
+ }
83
+ try {
84
+ return new URL(request.url).origin
85
+ } catch (_) {}
86
+ return 'http://localhost'
87
+ }
88
+
75
89
  // Patching fetch function to use our request resolver
76
90
  // if the input starts with `/` which is a common pattern for
77
91
  // client-side routing.
@@ -83,14 +97,6 @@ export function createStartHandler<TRouter extends AnyRouter>({
83
97
  return startRequestResolver(fetchRequest)
84
98
  }
85
99
 
86
- function getOrigin() {
87
- return (
88
- request.headers.get('Origin') ||
89
- request.headers.get('Referer') ||
90
- 'http://localhost'
91
- )
92
- }
93
-
94
100
  if (typeof input === 'string' && input.startsWith('/')) {
95
101
  // e.g: fetch('/api/data')
96
102
  const url = new URL(input, getOrigin())
@@ -111,18 +117,13 @@ export function createStartHandler<TRouter extends AnyRouter>({
111
117
  }
112
118
 
113
119
  const url = new URL(request.url)
114
- const href = decodeURIComponent(url.href.replace(url.origin, ''))
120
+ const href = url.href.replace(url.origin, '')
115
121
 
116
122
  const APP_BASE = process.env.TSS_APP_BASE || '/'
117
123
 
118
124
  // TODO how does this work with base path? does the router need to be configured the same as APP_BASE?
119
125
  const router = await createRouter()
120
126
 
121
- // Create a history for the client-side router
122
- const history = createMemoryHistory({
123
- initialEntries: [href],
124
- })
125
-
126
127
  // Update the client-side router with the history
127
128
  const isPrerendering = process.env.TSS_PRERENDERING === 'true'
128
129
  // env var is set during dev is SPA mode is enabled
@@ -133,13 +134,26 @@ export function createStartHandler<TRouter extends AnyRouter>({
133
134
  // the header is set by the prerender plugin
134
135
  isShell = request.headers.get(HEADERS.TSS_SHELL) === 'true'
135
136
  }
137
+ // insert start specific default serialization adapters
138
+ const serializationAdapters = (
139
+ router.options.serializationAdapters ?? []
140
+ ).concat(ServerFunctionSerializationAdapter)
141
+
142
+ // Create a history for the client-side router
143
+ const history = createMemoryHistory({
144
+ initialEntries: [href],
145
+ })
146
+
147
+ const origin = router.options.origin ?? getOrigin()
136
148
  router.update({
137
149
  history,
138
150
  isShell,
139
151
  isPrerendering,
152
+ serializationAdapters,
153
+ origin,
140
154
  })
141
155
 
142
- const response = await (async () => {
156
+ const response = await runWithStartContext({ router }, async () => {
143
157
  try {
144
158
  if (!process.env.TSS_SERVER_FN_BASE) {
145
159
  throw new Error(
@@ -179,59 +193,58 @@ export function createStartHandler<TRouter extends AnyRouter>({
179
193
  }
180
194
  }
181
195
 
182
- const executeRouter = () =>
183
- runWithStartContext({ router }, async () => {
184
- const requestAcceptHeader = request.headers.get('Accept') || '*/*'
185
- const splitRequestAcceptHeader = requestAcceptHeader.split(',')
186
-
187
- const supportedMimeTypes = ['*/*', 'text/html']
188
- const isRouterAcceptSupported = supportedMimeTypes.some(
189
- (mimeType) =>
190
- splitRequestAcceptHeader.some((acceptedMimeType) =>
191
- acceptedMimeType.trim().startsWith(mimeType),
192
- ),
193
- )
196
+ const executeRouter = async () => {
197
+ const requestAcceptHeader = request.headers.get('Accept') || '*/*'
198
+ const splitRequestAcceptHeader = requestAcceptHeader.split(',')
194
199
 
195
- if (!isRouterAcceptSupported) {
196
- return json(
197
- {
198
- error: 'Only HTML requests are supported here',
199
- },
200
- {
201
- status: 500,
202
- },
203
- )
204
- }
200
+ const supportedMimeTypes = ['*/*', 'text/html']
201
+ const isRouterAcceptSupported = supportedMimeTypes.some(
202
+ (mimeType) =>
203
+ splitRequestAcceptHeader.some((acceptedMimeType) =>
204
+ acceptedMimeType.trim().startsWith(mimeType),
205
+ ),
206
+ )
205
207
 
206
- // if the startRoutesManifest is not loaded yet, load it once
207
- if (startRoutesManifest === null) {
208
- startRoutesManifest = await getStartManifest({
209
- basePath: APP_BASE,
210
- })
211
- }
208
+ if (!isRouterAcceptSupported) {
209
+ return json(
210
+ {
211
+ error: 'Only HTML requests are supported here',
212
+ },
213
+ {
214
+ status: 500,
215
+ },
216
+ )
217
+ }
212
218
 
213
- // Attach the server-side SSR utils to the client-side router
214
- attachRouterServerSsrUtils(router, startRoutesManifest)
219
+ // if the startRoutesManifest is not loaded yet, load it once
220
+ if (startRoutesManifest === null) {
221
+ startRoutesManifest = await getStartManifest({
222
+ basePath: APP_BASE,
223
+ })
224
+ }
215
225
 
216
- await router.load()
226
+ // Attach the server-side SSR utils to the client-side router
227
+ attachRouterServerSsrUtils(router, startRoutesManifest)
217
228
 
218
- // If there was a redirect, skip rendering the page at all
219
- if (router.state.redirect) {
220
- return router.state.redirect
221
- }
229
+ await router.load()
222
230
 
223
- await router.serverSsr!.dehydrate()
231
+ // If there was a redirect, skip rendering the page at all
232
+ if (router.state.redirect) {
233
+ return router.state.redirect
234
+ }
224
235
 
225
- const responseHeaders = getStartResponseHeaders({ router })
226
- const response = await cb({
227
- request,
228
- router,
229
- responseHeaders,
230
- })
236
+ await router.serverSsr!.dehydrate()
231
237
 
232
- return response
238
+ const responseHeaders = getStartResponseHeaders({ router })
239
+ const response = await cb({
240
+ request,
241
+ router,
242
+ responseHeaders,
233
243
  })
234
244
 
245
+ return response
246
+ }
247
+
235
248
  // If we have a server route tree, then we try matching to see if we have a
236
249
  // server route that matches the request.
237
250
  if (processedServerRouteTree) {
@@ -256,7 +269,7 @@ export function createStartHandler<TRouter extends AnyRouter>({
256
269
 
257
270
  throw err
258
271
  }
259
- })()
272
+ })
260
273
 
261
274
  if (isRedirect(response)) {
262
275
  if (isResolvedRedirect(response)) {
@@ -329,12 +342,14 @@ async function handleServerRoutes(opts: {
329
342
  basePath: string
330
343
  executeRouter: () => Promise<Response>
331
344
  }) {
332
- const url = new URL(opts.request.url)
345
+ let url = new URL(opts.request.url)
346
+ if (opts.basePath) {
347
+ url = executeRewriteInput(rewriteBasepath({ basepath: opts.basePath }), url)
348
+ }
333
349
  const pathname = url.pathname
334
350
 
335
351
  const serverTreeResult = getMatchedRoutes<AnyServerRouteWithTypes>({
336
352
  pathname,
337
- basepath: opts.basePath,
338
353
  caseSensitive: true,
339
354
  routesByPath: opts.processedServerRouteTree.routesByPath,
340
355
  routesById: opts.processedServerRouteTree.routesById,
@@ -0,0 +1,33 @@
1
+ import { loadVirtualModule } from './loadVirtualModule'
2
+ import { VIRTUAL_MODULES } from './virtual-modules'
3
+
4
+ export async function getServerFnById(serverFnId: string) {
5
+ const { default: serverFnManifest } = await loadVirtualModule(
6
+ VIRTUAL_MODULES.serverFnManifest,
7
+ )
8
+
9
+ const serverFnInfo = serverFnManifest[serverFnId]
10
+
11
+ if (!serverFnInfo) {
12
+ console.info('serverFnManifest', serverFnManifest)
13
+ throw new Error('Server function info not found for ' + serverFnId)
14
+ }
15
+
16
+ const fnModule = await serverFnInfo.importer()
17
+
18
+ if (!fnModule) {
19
+ console.info('serverFnInfo', serverFnInfo)
20
+ throw new Error('Server function module not resolved for ' + serverFnId)
21
+ }
22
+
23
+ const action = fnModule[serverFnInfo.functionName]
24
+
25
+ if (!action) {
26
+ console.info('serverFnInfo', serverFnInfo)
27
+ console.info('fnModule', fnModule)
28
+ throw new Error(
29
+ `Server function module export not resolved for serverFn ID: ${serverFnId}`,
30
+ )
31
+ }
32
+ return action
33
+ }
package/src/global.d.ts CHANGED
@@ -3,7 +3,7 @@ declare global {
3
3
  interface ProcessEnv {
4
4
  TSS_APP_BASE?: string
5
5
  TSS_SERVER_FN_BASE?: string
6
- TSS_OUTPUT_PUBLIC_DIR?: string
6
+ TSS_CLIENT_OUTPUT_DIR?: string
7
7
  TSS_SHELL?: 'true' | 'false'
8
8
  TSS_PRERENDERING?: 'true' | 'false'
9
9
  TSS_DEV_SERVER?: 'true' | 'false'