@tanstack/start-server-core 1.132.0-alpha.2 → 1.132.0-alpha.21
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 +6 -0
- package/dist/esm/createServerRpc.js +27 -0
- package/dist/esm/createServerRpc.js.map +1 -0
- package/dist/esm/createStartHandler.js +30 -15
- package/dist/esm/createStartHandler.js.map +1 -1
- package/dist/esm/getServerFnById.d.ts +1 -0
- package/dist/esm/getServerFnById.js +30 -0
- package/dist/esm/getServerFnById.js.map +1 -0
- package/dist/esm/index.d.ts +1 -0
- package/dist/esm/index.js +2 -0
- package/dist/esm/index.js.map +1 -1
- package/dist/esm/serializer/ServerFunctionSerializationAdapter.d.ts +5 -0
- package/dist/esm/serializer/ServerFunctionSerializationAdapter.js +25 -0
- package/dist/esm/serializer/ServerFunctionSerializationAdapter.js.map +1 -0
- package/dist/esm/serializer/getSerovalPlugins.d.ts +3 -0
- package/dist/esm/serializer/getSerovalPlugins.js +13 -0
- package/dist/esm/serializer/getSerovalPlugins.js.map +1 -0
- package/dist/esm/server-functions-handler.js +121 -81
- package/dist/esm/server-functions-handler.js.map +1 -1
- package/package.json +7 -6
- package/src/createServerRpc.ts +30 -0
- package/src/createStartHandler.ts +76 -61
- package/src/getServerFnById.ts +33 -0
- package/src/global.d.ts +1 -1
- package/src/index.tsx +2 -0
- package/src/serializer/ServerFunctionSerializationAdapter.ts +24 -0
- package/src/serializer/getSerovalPlugins.ts +10 -0
- package/src/server-functions-handler.ts +137 -114
|
@@ -1,9 +1,10 @@
|
|
|
1
1
|
import { isNotFound } from "@tanstack/router-core";
|
|
2
2
|
import invariant from "tiny-invariant";
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
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 (
|
|
95
|
-
(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
|
-
|
|
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
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
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
|
|
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(...
|
|
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
|
-
|
|
132
|
-
|
|
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/
|
|
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
|
-
|
|
154
|
-
|
|
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":";;;;;;;AAWA,SAAS,aAAa,MAA0B;AAC9C,MAAI,CAAC,MAAM;AACT,UAAM,IAAI;AAAA,MACR;AAAA,IAAA;AAAA,EAEJ;AAEA,SAAO,KAAK,QAAQ,YAAY,EAAE;AACpC;AAEO,MAAM,qBAAqB,OAAO,EAAE,cAAoC;AAC7E,QAAM,aAAa,IAAI,gBAAA;AACvB,QAAM,SAAS,WAAW;AAC1B,QAAM,QAAQ,MAAM,WAAW,MAAA;AAC/B,UAAQ,OAAO,iBAAiB,SAAS,KAAK;AAE9C,QAAM,SAAS,QAAQ;AACvB,QAAM,MAAM,IAAI,IAAI,QAAQ,KAAK,uBAAuB;AAGxD,QAAM,QAAQ,IAAI;AAAA,IAChB,GAAG,aAAa,QAAQ,IAAI,kBAAkB,CAAC;AAAA,EAAA;AAIjD,QAAM,QAAQ,IAAI,SAAS,MAAM,KAAK;AACtC,QAAM,aAAa,QAAQ,MAAM,CAAC,IAAI;AACtC,QAAM,SAAS,OAAO,YAAY,IAAI,aAAa,SAAS;AAK5D,QAAM,mBAAmB,oBAAoB;AAE7C,MAAI,OAAO,eAAe,UAAU;AAClC,UAAM,IAAI,MAAM,iDAAiD,UAAU;AAAA,EAC7E;AAEA,QAAM,SAAS,MAAM,gBAAgB,UAAU;AAG/C,QAAM,uBAAuB;AAAA,IAC3B;AAAA,IACA;AAAA,EAAA;AAGF,QAAM,cAAc,QAAQ,QAAQ,IAAI,cAAc;AACtD,QAAM,iBAAiB,kBAAA;AAEvB,WAAS,aAAa,SAAc;AAClC,UAAM,gBAAgB,SAAS,SAAS,EAAE,SAAS,gBAAgB;AACnE,WAAO;AAAA,EACT;AAEA,QAAM,WAAW,OAAO,YAAY;AAClC,QAAI;AACF,UAAI,SAAS,OAAO,YAAY;AAE9B,YACE,qBAAqB;AAAA,UACnB,CAAC,SAAS,eAAe,YAAY,SAAS,IAAI;AAAA,QAAA,GAEpD;AAEA;AAAA,YACE,OAAO,kBAAkB;AAAA,YACzB;AAAA,UAAA;AAEF,gBAAM,WAAW,MAAM,QAAQ,SAAA;AAC/B,gBAAM,oBAAoB,SAAS,IAAI,oBAAoB;AAC3D,mBAAS,OAAO,oBAAoB;AAEpC,gBAAM,SAAS;AAAA,YACb,SAAS,CAAA;AAAA,YACT,MAAM;AAAA,UAAA;AAER,cAAI,OAAO,sBAAsB,UAAU;AACzC,gBAAI;AACF,qBAAO,UAAU,aAAa,KAAK,MAAM,iBAAiB,CAAC;AAAA,YAC7D,QAAQ;AAAA,YAAC;AAAA,UACX;AAEA,iBAAO,MAAM,OAAO,QAAQ,MAAM;AAAA,QACpC;AAGA,YAAI,OAAO,YAAA,MAAkB,OAAO;AAClC;AAAA,YACE;AAAA,YACA;AAAA,UAAA;AAGF,cAAI,UAAe,OAAO;AAE1B,oBAAU,UAAU,MAAM,aAAa,KAAK,MAAM,OAAO,CAAC,IAAI;AAG9D,iBAAO,MAAM,OAAO,SAAS,MAAM;AAAA,QACrC;AAEA,YAAI,OAAO,YAAA,MAAkB,QAAQ;AACnC,gBAAM,IAAI,MAAM,sBAAsB;AAAA,QACxC;AAEA,YAAI,CAAC,eAAe,CAAC,YAAY,SAAS,kBAAkB,GAAG;AAC7D,gBAAM,IAAI,MAAM,wCAAwC;AAAA,QAC1D;AAEA,cAAM,cAAc,MAAM,QAAQ,KAAA;AAIlC,YAAI,kBAAkB;AACpB,gBAAM,UAAU,MAAM,aAAa,WAAW;AAC9C,iBAAO,MAAM,OAAO,SAAS,MAAM;AAAA,QACrC;AAKA,eAAO,MAAM,OAAO,GAAG,WAAW;AAAA,MACpC,GAAA;AAIA,UAAI,OAAO,kBAAkB,UAAU;AACrC,eAAO,OAAO;AAAA,MAChB;AAIA,UAAI,CAAC,kBAAkB;AACrB,iBAAS,OAAO;AAIhB,YAAI,kBAAkB,UAAU;AAC9B,iBAAO;AAAA,QACT;AAAA,MACF;AAqBA,UAAI,WAAW,MAAM,GAAG;AACtB,eAAO,mBAAmB,MAAM;AAAA,MAClC;AAEA,YAAMA,YAAW,YAAA;AACjB,UAAI,mBAAwB;AAE5B,UAAI,WAAW,QAAW;AAExB,YAAI,OAAO;AACX,cAAM,YAIF;AAAA,UACF,SAAS,CAAC,UAAU;AAClB,+BAAmB;AAAA,UACrB;AAAA,UACA,QAAQ,MAAM;AACZ,mBAAO;AAAA,UACT;AAAA,UACA,SAAS,CAAC,UAAU;AAClB,kBAAM;AAAA,UACR;AAAA,QAAA;AAEF,0BAAkB,QAAQ;AAAA,UACxB,0BAAU,IAAA;AAAA,UACV,SAAS;AAAA,UACT,QAAQ,OAAO;AACb,sBAAU,QAAQ,KAAK;AAAA,UACzB;AAAA,UACA,SAAS;AACP,sBAAU,OAAA;AAAA,UACZ;AAAA,UACA,SAAS,CAAC,UAAU;AAClB,sBAAU,QAAQ,KAAK;AAAA,UACzB;AAAA,QAAA,CACD;AACD,YAAI,MAAM;AACR,iBAAO,IAAI;AAAA,YACT,mBAAmB,KAAK,UAAU,gBAAgB,IAAI;AAAA,YACtD;AAAA,cACE,QAAQA,WAAU;AAAA,cAClB,YAAYA,WAAU;AAAA,cACtB,SAAS;AAAA,gBACP,gBAAgB;AAAA,gBAChB,CAAC,gBAAgB,GAAG;AAAA,cAAA;AAAA,YACtB;AAAA,UACF;AAAA,QAEJ;AAGA,cAAM,SAAS,IAAI,eAAe;AAAA,UAChC,MAAMC,aAAY;AAChB,sBAAU,UAAU,CAAC,UACnBA,YAAW,QAAQ,KAAK,UAAU,KAAK,IAAI,IAAI;AACjD,sBAAU,SAAS,MAAM;AACvB,kBAAI;AACFA,4BAAW,MAAA;AAAA,cACb,SAAS,OAAO;AACdA,4BAAW,MAAM,KAAK;AAAA,cACxB;AAAA,YACF;AACA,sBAAU,UAAU,CAAC,UAAUA,YAAW,MAAM,KAAK;AAErD,gBAAI,qBAAqB,QAAW;AAClC,wBAAU,QAAQ,gBAAgB;AAAA,YACpC;AAAA,UACF;AAAA,QAAA,CACD;AACD,eAAO,IAAI,SAAS,QAAQ;AAAA,UAC1B,QAAQD,WAAU;AAAA,UAClB,YAAYA,WAAU;AAAA,UACtB,SAAS;AAAA,YACP,gBAAgB;AAAA,YAChB,CAAC,gBAAgB,GAAG;AAAA,UAAA;AAAA,QACtB,CACD;AAAA,MACH;AAEA,aAAO,IAAI,SAAS,QAAW;AAAA,QAC7B,QAAQA,WAAU;AAAA,QAClB,YAAYA,WAAU;AAAA,MAAA,CACvB;AAAA,IACH,SAAS,OAAY;AACnB,UAAI,iBAAiB,UAAU;AAC7B,eAAO;AAAA,MACT;AAcA,UAAI,WAAW,KAAK,GAAG;AACrB,eAAO,mBAAmB,KAAK;AAAA,MACjC;AAEA,cAAQ,KAAA;AACR,cAAQ,KAAK,kBAAkB;AAC/B,cAAQ,KAAA;AACR,cAAQ,MAAM,KAAK;AACnB,cAAQ,KAAA;AAER,YAAM,kBAAkB,KAAK;AAAA,QAC3B,MAAM,QAAQ;AAAA,UACZ,iBAAiB,OAAO;AAAA,YACtB,0BAAU,IAAA;AAAA,YACV,SAAS;AAAA,UAAA,CACV;AAAA,QAAA;AAAA,MACH;AAEF,YAAMA,YAAW,YAAA;AACjB,aAAO,IAAI,SAAS,iBAAiB;AAAA,QACnC,QAAQA,WAAU,UAAU;AAAA,QAC5B,YAAYA,WAAU;AAAA,QACtB,SAAS;AAAA,UACP,gBAAgB;AAAA,UAChB,CAAC,gBAAgB,GAAG;AAAA,QAAA;AAAA,MACtB,CACD;AAAA,IACH;AAAA,EACF,GAAA;AAEA,UAAQ,OAAO,oBAAoB,SAAS,KAAK;AAEjD,SAAO;AACT;AAEA,SAAS,mBAAmB,OAAY;AACtC,QAAM,EAAE,SAAS,GAAG,KAAA,IAAS;AAE7B,SAAO,IAAI,SAAS,KAAK,UAAU,IAAI,GAAG;AAAA,IACxC,QAAQ;AAAA,IACR,SAAS;AAAA,MACP,gBAAgB;AAAA,MAChB,GAAI,WAAW,CAAA;AAAA,IAAC;AAAA,EAClB,CACD;AACH;"}
|
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.21",
|
|
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.
|
|
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.
|
|
53
|
-
"@tanstack/start-
|
|
54
|
-
"@tanstack/start-
|
|
53
|
+
"@tanstack/router-core": "1.132.0-alpha.21",
|
|
54
|
+
"@tanstack/start-storage-context": "1.132.0-alpha.21",
|
|
55
|
+
"@tanstack/start-client-core": "1.132.0-alpha.21"
|
|
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 =
|
|
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
|
-
|
|
184
|
-
|
|
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
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
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
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
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
|
-
|
|
214
|
-
|
|
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
|
-
|
|
226
|
+
// Attach the server-side SSR utils to the client-side router
|
|
227
|
+
attachRouterServerSsrUtils(router, startRoutesManifest)
|
|
217
228
|
|
|
218
|
-
|
|
219
|
-
if (router.state.redirect) {
|
|
220
|
-
return router.state.redirect
|
|
221
|
-
}
|
|
229
|
+
await router.load()
|
|
222
230
|
|
|
223
|
-
|
|
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
|
-
|
|
226
|
-
const response = await cb({
|
|
227
|
-
request,
|
|
228
|
-
router,
|
|
229
|
-
responseHeaders,
|
|
230
|
-
})
|
|
236
|
+
await router.serverSsr!.dehydrate()
|
|
231
237
|
|
|
232
|
-
|
|
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
|
-
|
|
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