@lazarv/react-server 0.0.0-experimental-43e79e6-20230928
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/LICENSE +21 -0
- package/README.md +5 -0
- package/bin/cli.mjs +93 -0
- package/bin/commands/build.mjs +19 -0
- package/bin/commands/dev.mjs +23 -0
- package/bin/commands/start.mjs +16 -0
- package/bin/loader.mjs +38 -0
- package/client/ActionState.mjs +16 -0
- package/client/ClientOnly.jsx +14 -0
- package/client/ClientProvider.jsx +243 -0
- package/client/ErrorBoundary.jsx +45 -0
- package/client/FlightContext.mjs +3 -0
- package/client/Link.jsx +59 -0
- package/client/Params.mjs +15 -0
- package/client/ReactServerComponent.jsx +77 -0
- package/client/Refresh.jsx +52 -0
- package/client/components.mjs +28 -0
- package/client/context.mjs +6 -0
- package/client/entry.client.jsx +146 -0
- package/client/index.jsx +6 -0
- package/client/navigation.jsx +4 -0
- package/config/context.mjs +37 -0
- package/config/index.mjs +114 -0
- package/lib/build/action.mjs +57 -0
- package/lib/build/banner.mjs +13 -0
- package/lib/build/chunks.mjs +26 -0
- package/lib/build/client.mjs +114 -0
- package/lib/build/custom-logger.mjs +13 -0
- package/lib/build/dependencies.mjs +54 -0
- package/lib/build/resolve.mjs +101 -0
- package/lib/build/server.mjs +142 -0
- package/lib/build/static.mjs +89 -0
- package/lib/dev/action.mjs +63 -0
- package/lib/dev/create-logger.mjs +52 -0
- package/lib/dev/create-server.mjs +208 -0
- package/lib/dev/modules.mjs +20 -0
- package/lib/dev/ssr-handler.mjs +135 -0
- package/lib/handlers/error.mjs +153 -0
- package/lib/handlers/not-found.mjs +5 -0
- package/lib/handlers/redirect.mjs +1 -0
- package/lib/handlers/rewrite.mjs +1 -0
- package/lib/handlers/static.mjs +120 -0
- package/lib/handlers/trailing-slash.mjs +12 -0
- package/lib/plugins/react-server.mjs +73 -0
- package/lib/plugins/use-client.mjs +135 -0
- package/lib/plugins/use-server.mjs +175 -0
- package/lib/start/action.mjs +110 -0
- package/lib/start/create-server.mjs +111 -0
- package/lib/start/manifest.mjs +104 -0
- package/lib/start/ssr-handler.mjs +134 -0
- package/lib/sys.mjs +49 -0
- package/lib/utils/merge.mjs +31 -0
- package/lib/utils/server-address.mjs +14 -0
- package/memory-cache/index.mjs +125 -0
- package/package.json +81 -0
- package/react-server.d.ts +209 -0
- package/server/ErrorBoundary.jsx +14 -0
- package/server/RemoteComponent.jsx +210 -0
- package/server/Route.jsx +108 -0
- package/server/actions.mjs +72 -0
- package/server/cache.mjs +19 -0
- package/server/client-component.mjs +62 -0
- package/server/context.mjs +32 -0
- package/server/cookies.mjs +14 -0
- package/server/entry.server.jsx +972 -0
- package/server/error-boundary.jsx +2 -0
- package/server/http-headers.mjs +8 -0
- package/server/http-status.mjs +6 -0
- package/server/index.mjs +14 -0
- package/server/logger.mjs +15 -0
- package/server/module-loader.mjs +20 -0
- package/server/redirects.mjs +45 -0
- package/server/remote-component.jsx +2 -0
- package/server/request.mjs +37 -0
- package/server/revalidate.mjs +22 -0
- package/server/rewrites.mjs +0 -0
- package/server/router.jsx +6 -0
- package/server/runtime.mjs +32 -0
- package/server/symbols.mjs +24 -0
|
@@ -0,0 +1,972 @@
|
|
|
1
|
+
import { createHash } from "node:crypto";
|
|
2
|
+
import { ReadableStream } from "node:stream/web";
|
|
3
|
+
|
|
4
|
+
import { ActionStateContext } from "@lazarv/react-server/client/ActionState.mjs";
|
|
5
|
+
import {
|
|
6
|
+
concat,
|
|
7
|
+
copyBytesFrom,
|
|
8
|
+
immediate,
|
|
9
|
+
} from "@lazarv/react-server/lib/sys.mjs";
|
|
10
|
+
import {
|
|
11
|
+
asyncLocalStorage,
|
|
12
|
+
callServerReference,
|
|
13
|
+
serverReferenceMap,
|
|
14
|
+
} from "@lazarv/react-server/server/actions.mjs";
|
|
15
|
+
import { clientReferenceMap } from "@lazarv/react-server/server/client-component.mjs";
|
|
16
|
+
import { context$, getContext } from "@lazarv/react-server/server/context.mjs";
|
|
17
|
+
import { status } from "@lazarv/react-server/server/http-status.mjs";
|
|
18
|
+
import { init$ as revalidate$ } from "@lazarv/react-server/server/revalidate.mjs";
|
|
19
|
+
import {
|
|
20
|
+
CACHE_CONTEXT,
|
|
21
|
+
CLIENT_COMPONENTS,
|
|
22
|
+
ERROR_CONTEXT,
|
|
23
|
+
FLIGHT_CACHE,
|
|
24
|
+
FORM_DATA_PARSER,
|
|
25
|
+
HTML_CACHE,
|
|
26
|
+
HTTP_CONTEXT,
|
|
27
|
+
HTTP_HEADERS,
|
|
28
|
+
HTTP_STATUS,
|
|
29
|
+
LOGGER_CONTEXT,
|
|
30
|
+
MAIN_MODULE,
|
|
31
|
+
OUTLET_CACHE,
|
|
32
|
+
REDIRECT_CONTEXT,
|
|
33
|
+
SERVER_CONTEXT,
|
|
34
|
+
SSR_CONTROLLER,
|
|
35
|
+
STYLES_CONTEXT,
|
|
36
|
+
} from "@lazarv/react-server/server/symbols.mjs";
|
|
37
|
+
import dom from "react-dom/server.edge";
|
|
38
|
+
import edge from "react-server-dom-webpack/client.edge";
|
|
39
|
+
import server from "react-server-dom-webpack/server.edge";
|
|
40
|
+
|
|
41
|
+
globalThis.__webpack_chunk_load__ = async (id) => {
|
|
42
|
+
return Promise.resolve(serverReferenceMap.get(id));
|
|
43
|
+
};
|
|
44
|
+
|
|
45
|
+
const decoder = new TextDecoder();
|
|
46
|
+
const encoder = new TextEncoder();
|
|
47
|
+
|
|
48
|
+
const outletRegExp =
|
|
49
|
+
/__react_server_remote_component_outlet_([a-zA-Z0-9_]+)__/g;
|
|
50
|
+
|
|
51
|
+
function getOutletOffset(outlet) {
|
|
52
|
+
return createHash("shake256", { outputLength: 2 })
|
|
53
|
+
.update(outlet)
|
|
54
|
+
.digest("hex");
|
|
55
|
+
}
|
|
56
|
+
function applyOutletOffset(rsc, offset) {
|
|
57
|
+
return rsc
|
|
58
|
+
.replaceAll(/^([0-9a-f]+):/gm, (match, lineId) => `${offset + lineId}:`)
|
|
59
|
+
.replaceAll(/"\$([0-9a-f]+)"/gm, (match, ref) => `"$${offset + ref}"`)
|
|
60
|
+
.replaceAll(/"\$@([0-9a-f]+)"/gm, (match, ref) => `"$${offset + ref}"`)
|
|
61
|
+
.replaceAll(/"\$L([0-9a-f]+)"/gm, (match, ref) => `"$L${offset + ref}"`)
|
|
62
|
+
.replaceAll(/"\$F([0-9a-f]+)"/gm, (match, ref) => `"$F${offset + ref}"`);
|
|
63
|
+
}
|
|
64
|
+
function applyOutletOffsetToHTML(html, offset) {
|
|
65
|
+
return html
|
|
66
|
+
.replaceAll(
|
|
67
|
+
/<(div hidden|template) id="([SPB]):([0-9a-f]+)">/g,
|
|
68
|
+
(match, element, type, id) => `<${element} id="${type}:${offset + id}">`
|
|
69
|
+
)
|
|
70
|
+
.replaceAll(
|
|
71
|
+
/\$R([A-Z]+)\("([SPB]):([0-9a-f]+)","([SPB]):([0-9a-f]+)"\)/g,
|
|
72
|
+
(match, command, targetType, targetId, sourceType, sourceId) =>
|
|
73
|
+
`$R${command}("${targetType}:${offset + targetId}", "${sourceType}:${
|
|
74
|
+
offset + sourceId
|
|
75
|
+
}")`
|
|
76
|
+
);
|
|
77
|
+
}
|
|
78
|
+
async function processRemote(value, queue) {
|
|
79
|
+
try {
|
|
80
|
+
let chunk = decoder.decode(value);
|
|
81
|
+
|
|
82
|
+
const remotes = chunk.match(outletRegExp);
|
|
83
|
+
if (remotes) {
|
|
84
|
+
for (const remote of remotes) {
|
|
85
|
+
const [key, outlet] = outletRegExp.exec(remote);
|
|
86
|
+
const offset = getOutletOffset(outlet);
|
|
87
|
+
const remotePromise = getContext(key);
|
|
88
|
+
if (remotePromise) {
|
|
89
|
+
const Component = await remotePromise;
|
|
90
|
+
if (queue && Component.stream) {
|
|
91
|
+
queue.push(Component.stream.getReader());
|
|
92
|
+
}
|
|
93
|
+
chunk = chunk.replace(key, `$L${offset}0`);
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return encoder.encode(chunk);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
return value;
|
|
101
|
+
} catch (e) {
|
|
102
|
+
console.error(e);
|
|
103
|
+
return value;
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
export async function render(Component) {
|
|
108
|
+
const logger = getContext(LOGGER_CONTEXT);
|
|
109
|
+
try {
|
|
110
|
+
// eslint-disable-next-line no-async-promise-executor
|
|
111
|
+
const streaming = new Promise(async (resolve, reject) => {
|
|
112
|
+
const context = getContext(HTTP_CONTEXT);
|
|
113
|
+
try {
|
|
114
|
+
revalidate$();
|
|
115
|
+
|
|
116
|
+
const accept = context.request.headers.get("accept");
|
|
117
|
+
const standalone = accept.includes(";standalone");
|
|
118
|
+
const isRemote = accept.includes(";remote");
|
|
119
|
+
const outlet = (
|
|
120
|
+
context.request.headers.get("react-server-outlet") ?? "PAGE_ROOT"
|
|
121
|
+
).replace(/[^a-zA-Z0-9_]/g, "_");
|
|
122
|
+
|
|
123
|
+
const Styles = async () => {
|
|
124
|
+
const styles = getContext(STYLES_CONTEXT);
|
|
125
|
+
return (
|
|
126
|
+
<>
|
|
127
|
+
{styles.map((link) => {
|
|
128
|
+
const href = link?.id || link;
|
|
129
|
+
return (
|
|
130
|
+
<link
|
|
131
|
+
key={href}
|
|
132
|
+
rel="stylesheet"
|
|
133
|
+
href={href}
|
|
134
|
+
// eslint-disable-next-line react/no-unknown-property
|
|
135
|
+
precedence="default"
|
|
136
|
+
/>
|
|
137
|
+
);
|
|
138
|
+
})}
|
|
139
|
+
</>
|
|
140
|
+
);
|
|
141
|
+
};
|
|
142
|
+
const ComponentWithStyles = (
|
|
143
|
+
<>
|
|
144
|
+
<Styles />
|
|
145
|
+
<Component />
|
|
146
|
+
</>
|
|
147
|
+
);
|
|
148
|
+
let app = (
|
|
149
|
+
<ActionStateContext.Provider
|
|
150
|
+
value={{
|
|
151
|
+
input: [],
|
|
152
|
+
formData: null,
|
|
153
|
+
data: null,
|
|
154
|
+
error: null,
|
|
155
|
+
actionId: null,
|
|
156
|
+
}}
|
|
157
|
+
>
|
|
158
|
+
{ComponentWithStyles}
|
|
159
|
+
</ActionStateContext.Provider>
|
|
160
|
+
);
|
|
161
|
+
|
|
162
|
+
const isFormData = context.request.headers
|
|
163
|
+
.get("content-type")
|
|
164
|
+
?.includes("multipart/form-data");
|
|
165
|
+
let actionId =
|
|
166
|
+
context.request.headers.get("react-server-action") ?? null;
|
|
167
|
+
if (
|
|
168
|
+
"POST,PUT,PATCH,DELETE".includes(context.request.method) &&
|
|
169
|
+
(actionId || isFormData)
|
|
170
|
+
) {
|
|
171
|
+
let input = [];
|
|
172
|
+
let formData = null;
|
|
173
|
+
try {
|
|
174
|
+
if (isFormData) {
|
|
175
|
+
let files = [];
|
|
176
|
+
formData = await getContext(FORM_DATA_PARSER)(context.request, {
|
|
177
|
+
handleFile: async ({ body, ...info }) => {
|
|
178
|
+
const [file, formFile] = body.tee();
|
|
179
|
+
files.push({ info, file });
|
|
180
|
+
|
|
181
|
+
const reader = formFile.getReader();
|
|
182
|
+
for (;;) {
|
|
183
|
+
const { done } = await reader.read();
|
|
184
|
+
if (done) break;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
return `__react_server_file_index__${files.length - 1}__`;
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
|
|
191
|
+
if (actionId) {
|
|
192
|
+
[formData] = await server.decodeReply(formData);
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const data = {};
|
|
196
|
+
for (const [key, value] of formData.entries()) {
|
|
197
|
+
if (key.startsWith("$ACTION_ID_")) {
|
|
198
|
+
actionId = key.slice(11);
|
|
199
|
+
} else {
|
|
200
|
+
if (value.startsWith("__react_server_file_index__")) {
|
|
201
|
+
const { info, file } =
|
|
202
|
+
files[parseInt(value.replace(/\D/g, ""), 10)];
|
|
203
|
+
|
|
204
|
+
data[key] = new Response(file, {
|
|
205
|
+
headers: {
|
|
206
|
+
"content-type": info.contentType,
|
|
207
|
+
},
|
|
208
|
+
});
|
|
209
|
+
} else {
|
|
210
|
+
data[key] = value;
|
|
211
|
+
}
|
|
212
|
+
}
|
|
213
|
+
}
|
|
214
|
+
input = [data];
|
|
215
|
+
} else {
|
|
216
|
+
input = await server.decodeReply(await context.request.text());
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
if (!serverReferenceMap.has(actionId)) {
|
|
220
|
+
await new Promise((actionResolve) => {
|
|
221
|
+
asyncLocalStorage.run({ actionId }, async () => {
|
|
222
|
+
try {
|
|
223
|
+
const actionFlight = server.renderToReadableStream(
|
|
224
|
+
app,
|
|
225
|
+
clientReferenceMap
|
|
226
|
+
);
|
|
227
|
+
const actionFlightReader = actionFlight.getReader();
|
|
228
|
+
await actionFlightReader.read();
|
|
229
|
+
} catch (e) {
|
|
230
|
+
// expected
|
|
231
|
+
}
|
|
232
|
+
asyncLocalStorage.exit(() => {
|
|
233
|
+
actionResolve();
|
|
234
|
+
});
|
|
235
|
+
});
|
|
236
|
+
});
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
if (actionId) {
|
|
240
|
+
try {
|
|
241
|
+
const data = await callServerReference(
|
|
242
|
+
actionId,
|
|
243
|
+
...(input ?? [])
|
|
244
|
+
);
|
|
245
|
+
|
|
246
|
+
const redirect = getContext(REDIRECT_CONTEXT);
|
|
247
|
+
if (redirect?.response) {
|
|
248
|
+
return resolve(redirect.response);
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
app = (
|
|
252
|
+
<ActionStateContext.Provider
|
|
253
|
+
value={{ formData, data, error: null, actionId }}
|
|
254
|
+
>
|
|
255
|
+
{ComponentWithStyles}
|
|
256
|
+
</ActionStateContext.Provider>
|
|
257
|
+
);
|
|
258
|
+
} catch (error) {
|
|
259
|
+
const redirect = getContext(REDIRECT_CONTEXT);
|
|
260
|
+
if (redirect?.response) {
|
|
261
|
+
return resolve(redirect.response);
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
logger.error(error);
|
|
265
|
+
|
|
266
|
+
app = (
|
|
267
|
+
<ActionStateContext.Provider
|
|
268
|
+
value={{
|
|
269
|
+
formData,
|
|
270
|
+
data: null,
|
|
271
|
+
error: error instanceof Error ? error.message : error,
|
|
272
|
+
actionId,
|
|
273
|
+
}}
|
|
274
|
+
>
|
|
275
|
+
{ComponentWithStyles}
|
|
276
|
+
</ActionStateContext.Provider>
|
|
277
|
+
);
|
|
278
|
+
}
|
|
279
|
+
}
|
|
280
|
+
} catch (error) {
|
|
281
|
+
const redirect = getContext(REDIRECT_CONTEXT);
|
|
282
|
+
if (redirect?.response) {
|
|
283
|
+
return resolve(redirect.response);
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
logger.error(error);
|
|
287
|
+
|
|
288
|
+
app = (
|
|
289
|
+
<ActionStateContext.Provider
|
|
290
|
+
value={{
|
|
291
|
+
formData,
|
|
292
|
+
data: null,
|
|
293
|
+
error: error instanceof Error ? error.message : error,
|
|
294
|
+
actionId,
|
|
295
|
+
}}
|
|
296
|
+
>
|
|
297
|
+
{ComponentWithStyles}
|
|
298
|
+
</ActionStateContext.Provider>
|
|
299
|
+
);
|
|
300
|
+
}
|
|
301
|
+
}
|
|
302
|
+
|
|
303
|
+
const ifModifiedSince =
|
|
304
|
+
context.request.headers.get("if-modified-since");
|
|
305
|
+
const noCache =
|
|
306
|
+
context.request.headers.get("cache-control") === "no-cache";
|
|
307
|
+
const cacheType = isRemote
|
|
308
|
+
? OUTLET_CACHE
|
|
309
|
+
: accept.includes("text/x-component")
|
|
310
|
+
? FLIGHT_CACHE
|
|
311
|
+
: HTML_CACHE;
|
|
312
|
+
|
|
313
|
+
if (ifModifiedSince && !noCache) {
|
|
314
|
+
const hasCache = await getContext(CACHE_CONTEXT)?.get([
|
|
315
|
+
context.url,
|
|
316
|
+
accept,
|
|
317
|
+
cacheType,
|
|
318
|
+
ifModifiedSince,
|
|
319
|
+
]);
|
|
320
|
+
|
|
321
|
+
if (hasCache) {
|
|
322
|
+
return resolve(
|
|
323
|
+
new Response(null, {
|
|
324
|
+
status: 304,
|
|
325
|
+
statusText: "Not Modified",
|
|
326
|
+
})
|
|
327
|
+
);
|
|
328
|
+
}
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
const lastModified = new Date().toUTCString();
|
|
332
|
+
if (accept.includes("text/x-component")) {
|
|
333
|
+
if (!noCache) {
|
|
334
|
+
const responseFromCache = await getContext(CACHE_CONTEXT)?.get([
|
|
335
|
+
context.url,
|
|
336
|
+
accept,
|
|
337
|
+
FLIGHT_CACHE,
|
|
338
|
+
]);
|
|
339
|
+
if (responseFromCache) {
|
|
340
|
+
const stream = new ReadableStream({
|
|
341
|
+
type: "bytes",
|
|
342
|
+
async start(controller) {
|
|
343
|
+
controller.enqueue(new Uint8Array(responseFromCache.buffer));
|
|
344
|
+
controller.close();
|
|
345
|
+
},
|
|
346
|
+
});
|
|
347
|
+
return resolve(
|
|
348
|
+
new Response(stream, {
|
|
349
|
+
status: responseFromCache.status,
|
|
350
|
+
statusText: responseFromCache.statusText,
|
|
351
|
+
headers: {
|
|
352
|
+
"content-type": "text/x-component",
|
|
353
|
+
"cache-control":
|
|
354
|
+
context.request.headers.get("cache-control") ===
|
|
355
|
+
"no-cache"
|
|
356
|
+
? "no-cache"
|
|
357
|
+
: "must-revalidate",
|
|
358
|
+
"last-modified": lastModified,
|
|
359
|
+
...(getContext(HTTP_HEADERS) ?? {}),
|
|
360
|
+
},
|
|
361
|
+
})
|
|
362
|
+
);
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
|
|
366
|
+
const stream = new ReadableStream({
|
|
367
|
+
type: "bytes",
|
|
368
|
+
async start(controller) {
|
|
369
|
+
const outletQueue = [];
|
|
370
|
+
const flight = server.renderToReadableStream(
|
|
371
|
+
app,
|
|
372
|
+
clientReferenceMap,
|
|
373
|
+
{
|
|
374
|
+
onError(err) {
|
|
375
|
+
const viteDevServer = getContext(SERVER_CONTEXT);
|
|
376
|
+
viteDevServer?.ssrFixStacktrace?.(err);
|
|
377
|
+
},
|
|
378
|
+
}
|
|
379
|
+
);
|
|
380
|
+
|
|
381
|
+
const reader = flight.getReader();
|
|
382
|
+
let done = false;
|
|
383
|
+
const payload = [];
|
|
384
|
+
let breakOnNewLine = false;
|
|
385
|
+
while (!done) {
|
|
386
|
+
const { value: _value, done: _done } = await reader.read();
|
|
387
|
+
done = _done;
|
|
388
|
+
if (_value) {
|
|
389
|
+
const redirect = getContext(REDIRECT_CONTEXT);
|
|
390
|
+
if (redirect?.response) {
|
|
391
|
+
controller.close();
|
|
392
|
+
return resolve(redirect.response);
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const value = await processRemote(_value, outletQueue);
|
|
396
|
+
payload.push(copyBytesFrom(value));
|
|
397
|
+
|
|
398
|
+
const endsWithNewLine = value[value.length - 1] === 0x0a;
|
|
399
|
+
if (breakOnNewLine && endsWithNewLine) {
|
|
400
|
+
break;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
const lastNewLine = value.lastIndexOf(0x0a);
|
|
404
|
+
if (
|
|
405
|
+
(value[0] === 0x30 && value[1] === 0x3a) ||
|
|
406
|
+
(lastNewLine > 0 &&
|
|
407
|
+
lastNewLine < value.length - 2 &&
|
|
408
|
+
value[lastNewLine + 1] === 0x30 &&
|
|
409
|
+
value[lastNewLine + 2] === 0x3a)
|
|
410
|
+
) {
|
|
411
|
+
if (endsWithNewLine) {
|
|
412
|
+
break;
|
|
413
|
+
} else {
|
|
414
|
+
breakOnNewLine = true;
|
|
415
|
+
}
|
|
416
|
+
}
|
|
417
|
+
}
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
controller.enqueue(new Uint8Array(concat(payload)));
|
|
421
|
+
|
|
422
|
+
const httpStatus = getContext(HTTP_STATUS) ?? {
|
|
423
|
+
status: 200,
|
|
424
|
+
statusText: "OK",
|
|
425
|
+
};
|
|
426
|
+
resolve(
|
|
427
|
+
new Response(stream, {
|
|
428
|
+
...httpStatus,
|
|
429
|
+
headers: {
|
|
430
|
+
"content-type": "text/x-component",
|
|
431
|
+
"cache-control":
|
|
432
|
+
context.request.headers.get("cache-control") ===
|
|
433
|
+
"no-cache"
|
|
434
|
+
? "no-cache"
|
|
435
|
+
: "must-revalidate",
|
|
436
|
+
"last-modified": lastModified,
|
|
437
|
+
...(getContext(HTTP_HEADERS) ?? {}),
|
|
438
|
+
},
|
|
439
|
+
})
|
|
440
|
+
);
|
|
441
|
+
|
|
442
|
+
while (!done) {
|
|
443
|
+
const { value, done: _done } = await reader.read();
|
|
444
|
+
done = _done;
|
|
445
|
+
if (value) {
|
|
446
|
+
const bytesValue = await processRemote(value, outletQueue);
|
|
447
|
+
payload.push(copyBytesFrom(bytesValue));
|
|
448
|
+
controller.enqueue(bytesValue);
|
|
449
|
+
}
|
|
450
|
+
}
|
|
451
|
+
|
|
452
|
+
await Promise.all(
|
|
453
|
+
outletQueue.map(async (outletReader) => {
|
|
454
|
+
let done = false;
|
|
455
|
+
while (!done) {
|
|
456
|
+
const { value, done: _done } = await outletReader.read();
|
|
457
|
+
done = _done;
|
|
458
|
+
|
|
459
|
+
if (value?.rsc) {
|
|
460
|
+
const bytesValue = encoder.encode(
|
|
461
|
+
applyOutletOffset(value.rsc, getOutletOffset(outlet))
|
|
462
|
+
);
|
|
463
|
+
payload.push(copyBytesFrom(bytesValue));
|
|
464
|
+
controller.enqueue(bytesValue);
|
|
465
|
+
}
|
|
466
|
+
}
|
|
467
|
+
})
|
|
468
|
+
);
|
|
469
|
+
|
|
470
|
+
controller.close();
|
|
471
|
+
|
|
472
|
+
getContext(CACHE_CONTEXT)?.set(
|
|
473
|
+
[context.url, accept, FLIGHT_CACHE, lastModified],
|
|
474
|
+
{
|
|
475
|
+
...httpStatus,
|
|
476
|
+
buffer: concat(payload),
|
|
477
|
+
}
|
|
478
|
+
);
|
|
479
|
+
},
|
|
480
|
+
});
|
|
481
|
+
} else if (accept.includes("text/html")) {
|
|
482
|
+
if (!noCache) {
|
|
483
|
+
const responseFromCache = await getContext(CACHE_CONTEXT)?.get([
|
|
484
|
+
context.url,
|
|
485
|
+
accept,
|
|
486
|
+
cacheType,
|
|
487
|
+
]);
|
|
488
|
+
if (responseFromCache) {
|
|
489
|
+
const stream = new ReadableStream({
|
|
490
|
+
type: "bytes",
|
|
491
|
+
async start(controller) {
|
|
492
|
+
controller.enqueue(new Uint8Array(responseFromCache.buffer));
|
|
493
|
+
controller.close();
|
|
494
|
+
},
|
|
495
|
+
});
|
|
496
|
+
return resolve(
|
|
497
|
+
new Response(stream, {
|
|
498
|
+
status: responseFromCache.status,
|
|
499
|
+
statusText: responseFromCache.statusText,
|
|
500
|
+
headers: {
|
|
501
|
+
"content-type": "text/html",
|
|
502
|
+
"cache-control":
|
|
503
|
+
context.request.headers.get("cache-control") ===
|
|
504
|
+
"no-cache"
|
|
505
|
+
? "no-cache"
|
|
506
|
+
: "must-revalidate",
|
|
507
|
+
"last-modified": lastModified,
|
|
508
|
+
...(getContext(HTTP_HEADERS) ?? {}),
|
|
509
|
+
},
|
|
510
|
+
})
|
|
511
|
+
);
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
|
|
515
|
+
let flightWriter = false;
|
|
516
|
+
const flight = server.renderToReadableStream(
|
|
517
|
+
app,
|
|
518
|
+
clientReferenceMap,
|
|
519
|
+
{
|
|
520
|
+
onError(e) {
|
|
521
|
+
if (!flightWriter) {
|
|
522
|
+
const redirect = getContext(REDIRECT_CONTEXT);
|
|
523
|
+
if (redirect?.response) {
|
|
524
|
+
return resolve(redirect.response);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
status(
|
|
528
|
+
e.status || 500,
|
|
529
|
+
e.statusText || "Internal Server Error"
|
|
530
|
+
);
|
|
531
|
+
|
|
532
|
+
const viteDevServer = getContext(SERVER_CONTEXT);
|
|
533
|
+
viteDevServer?.ssrFixStacktrace?.(e);
|
|
534
|
+
logger.error(e);
|
|
535
|
+
}
|
|
536
|
+
},
|
|
537
|
+
}
|
|
538
|
+
);
|
|
539
|
+
const stream = new ReadableStream({
|
|
540
|
+
type: "bytes",
|
|
541
|
+
async start(controller) {
|
|
542
|
+
try {
|
|
543
|
+
context$(SSR_CONTROLLER, controller);
|
|
544
|
+
|
|
545
|
+
const payload = [];
|
|
546
|
+
let outletQueue = [];
|
|
547
|
+
const [renderStream, forwardStream] = flight.tee();
|
|
548
|
+
|
|
549
|
+
const decoder = new TextDecoder();
|
|
550
|
+
const encoder = new TextEncoder();
|
|
551
|
+
|
|
552
|
+
const tree = edge.createFromReadableStream(renderStream);
|
|
553
|
+
|
|
554
|
+
const forwardReader = forwardStream.getReader();
|
|
555
|
+
|
|
556
|
+
let hydrated = false;
|
|
557
|
+
let hasClientComponent = false;
|
|
558
|
+
let bootstrapped = false;
|
|
559
|
+
const bootstrapScripts = standalone
|
|
560
|
+
? []
|
|
561
|
+
: [
|
|
562
|
+
`const moduleCache = new Map();
|
|
563
|
+
self.__webpack_require__ = function (id) {
|
|
564
|
+
id = id.startsWith("http") ? id : "/${
|
|
565
|
+
import.meta.env.DEV ? "@fs" : ""
|
|
566
|
+
}" + id;
|
|
567
|
+
if (!moduleCache.has(id)) {
|
|
568
|
+
const modulePromise = import(id);
|
|
569
|
+
modulePromise.then(
|
|
570
|
+
(module) => {
|
|
571
|
+
modulePromise.value = module;
|
|
572
|
+
modulePromise.status = "fulfilled";
|
|
573
|
+
},
|
|
574
|
+
(reason) => {
|
|
575
|
+
modulePromise.reason = reason;
|
|
576
|
+
modulePromise.status = "rejected";
|
|
577
|
+
}
|
|
578
|
+
);
|
|
579
|
+
moduleCache.set(id, modulePromise);
|
|
580
|
+
}
|
|
581
|
+
return moduleCache.get(id);
|
|
582
|
+
};`.replace(/\n/g, ""),
|
|
583
|
+
];
|
|
584
|
+
const bootstrapModules = standalone
|
|
585
|
+
? []
|
|
586
|
+
: getContext(MAIN_MODULE);
|
|
587
|
+
|
|
588
|
+
const html = await dom.renderToReadableStream(tree);
|
|
589
|
+
const htmlReader = html.getReader();
|
|
590
|
+
|
|
591
|
+
const start = () => {
|
|
592
|
+
const httpStatus = getContext(HTTP_STATUS) ?? {
|
|
593
|
+
status: 200,
|
|
594
|
+
statusText: "OK",
|
|
595
|
+
};
|
|
596
|
+
resolve(
|
|
597
|
+
new Response(stream, {
|
|
598
|
+
...httpStatus,
|
|
599
|
+
headers: {
|
|
600
|
+
"content-type": "text/html",
|
|
601
|
+
"cache-control":
|
|
602
|
+
context.request.headers.get("cache-control") ===
|
|
603
|
+
"no-cache"
|
|
604
|
+
? "no-cache"
|
|
605
|
+
: "must-revalidate",
|
|
606
|
+
"last-modified": lastModified,
|
|
607
|
+
...(getContext(HTTP_HEADERS) ?? {}),
|
|
608
|
+
},
|
|
609
|
+
})
|
|
610
|
+
);
|
|
611
|
+
};
|
|
612
|
+
|
|
613
|
+
const redirect = () => {
|
|
614
|
+
const redirect = getContext(REDIRECT_CONTEXT);
|
|
615
|
+
if (redirect?.response) {
|
|
616
|
+
controller.close();
|
|
617
|
+
resolve(redirect.response);
|
|
618
|
+
}
|
|
619
|
+
};
|
|
620
|
+
|
|
621
|
+
let forwardReady = null;
|
|
622
|
+
let htmlReady = null;
|
|
623
|
+
let outletReady = null;
|
|
624
|
+
|
|
625
|
+
let forwardDone = false;
|
|
626
|
+
let forwardNext = null;
|
|
627
|
+
const forwardWorker = async function* () {
|
|
628
|
+
await (outletReady || htmlReady);
|
|
629
|
+
|
|
630
|
+
let done = false;
|
|
631
|
+
|
|
632
|
+
const interrupt = new Promise((resolve) =>
|
|
633
|
+
immediate(() => resolve("interrupt"))
|
|
634
|
+
);
|
|
635
|
+
|
|
636
|
+
let _resolve;
|
|
637
|
+
forwardReady = new Promise((resolve) => {
|
|
638
|
+
_resolve = resolve;
|
|
639
|
+
});
|
|
640
|
+
|
|
641
|
+
let force = false;
|
|
642
|
+
while (!done || force) {
|
|
643
|
+
const read = forwardNext
|
|
644
|
+
? forwardNext
|
|
645
|
+
: forwardReader.read();
|
|
646
|
+
const res = await Promise.race([read, interrupt]);
|
|
647
|
+
|
|
648
|
+
if (res === "interrupt") {
|
|
649
|
+
forwardNext = read;
|
|
650
|
+
done = true;
|
|
651
|
+
break;
|
|
652
|
+
}
|
|
653
|
+
|
|
654
|
+
forwardNext = null;
|
|
655
|
+
|
|
656
|
+
const { value, done: _done } = res;
|
|
657
|
+
forwardDone = _done;
|
|
658
|
+
|
|
659
|
+
hasClientComponent =
|
|
660
|
+
getContext(CLIENT_COMPONENTS)?.size > 0;
|
|
661
|
+
|
|
662
|
+
if (_done) break;
|
|
663
|
+
|
|
664
|
+
if (value) {
|
|
665
|
+
const lines = decoder.decode(value).split("\n");
|
|
666
|
+
force = value[value.length - 1] !== 0x0a;
|
|
667
|
+
|
|
668
|
+
if (lines.some((l) => l.startsWith("0:"))) {
|
|
669
|
+
if (!bootstrapped) {
|
|
670
|
+
if (!isRemote) {
|
|
671
|
+
bootstrapScripts.unshift(
|
|
672
|
+
`self.__flightStream__${outlet}__=new TransformStream();self.__flightWriter__${outlet}__=self.__flightStream__${outlet}__.writable.getWriter();self.__flightEncoder__${outlet}__=new TextEncoder();`
|
|
673
|
+
);
|
|
674
|
+
}
|
|
675
|
+
bootstrapped = true;
|
|
676
|
+
}
|
|
677
|
+
}
|
|
678
|
+
|
|
679
|
+
const bytesValue = await processRemote(value);
|
|
680
|
+
if (isRemote) {
|
|
681
|
+
if (hydrated) {
|
|
682
|
+
yield bytesValue;
|
|
683
|
+
} else {
|
|
684
|
+
bootstrapScripts.push(bytesValue);
|
|
685
|
+
}
|
|
686
|
+
} else {
|
|
687
|
+
const chunk = `self.__flightWriter__${outlet}__.write(self.__flightEncoder__${outlet}__.encode(${JSON.stringify(
|
|
688
|
+
decoder.decode(bytesValue)
|
|
689
|
+
)}));`;
|
|
690
|
+
if (hydrated) {
|
|
691
|
+
const script = encoder.encode(
|
|
692
|
+
`<script>document.currentScript.parentNode.removeChild(document.currentScript);${chunk}</script>`
|
|
693
|
+
);
|
|
694
|
+
yield script;
|
|
695
|
+
} else {
|
|
696
|
+
bootstrapScripts.push(chunk);
|
|
697
|
+
}
|
|
698
|
+
}
|
|
699
|
+
|
|
700
|
+
if (bootstrapped && !force) {
|
|
701
|
+
break;
|
|
702
|
+
}
|
|
703
|
+
}
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
_resolve();
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
let htmlDone = false;
|
|
710
|
+
let htmlNext = null;
|
|
711
|
+
const htmlWorker = async function* () {
|
|
712
|
+
await forwardReady;
|
|
713
|
+
|
|
714
|
+
let done = false;
|
|
715
|
+
|
|
716
|
+
const interrupt = new Promise((resolve) =>
|
|
717
|
+
immediate(() => resolve("interrupt"))
|
|
718
|
+
);
|
|
719
|
+
|
|
720
|
+
let _resolve;
|
|
721
|
+
htmlReady = new Promise((resolve) => {
|
|
722
|
+
_resolve = resolve;
|
|
723
|
+
});
|
|
724
|
+
|
|
725
|
+
let buffer = "";
|
|
726
|
+
let remoteFlightResponse = "";
|
|
727
|
+
|
|
728
|
+
let force = false;
|
|
729
|
+
let hasNewLine = true;
|
|
730
|
+
while (!done || force) {
|
|
731
|
+
const read = htmlNext ? htmlNext : htmlReader.read();
|
|
732
|
+
const res = await Promise.race([read, interrupt]);
|
|
733
|
+
|
|
734
|
+
if (res === "interrupt") {
|
|
735
|
+
htmlNext = read;
|
|
736
|
+
done = true;
|
|
737
|
+
break;
|
|
738
|
+
}
|
|
739
|
+
|
|
740
|
+
htmlNext = null;
|
|
741
|
+
|
|
742
|
+
const { value, done: _done } = res;
|
|
743
|
+
htmlDone = _done;
|
|
744
|
+
|
|
745
|
+
if (_done) break;
|
|
746
|
+
|
|
747
|
+
if (value) {
|
|
748
|
+
hasNewLine = value[value.length - 1] === 0x0a;
|
|
749
|
+
force = value[value.length - 1] !== 0x3e;
|
|
750
|
+
|
|
751
|
+
let chunk = decoder.decode(value);
|
|
752
|
+
buffer += chunk;
|
|
753
|
+
|
|
754
|
+
if (chunk.endsWith("<!--/$-->")) {
|
|
755
|
+
done = true;
|
|
756
|
+
}
|
|
757
|
+
}
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
if (isRemote && !hasNewLine) {
|
|
761
|
+
buffer += "\n";
|
|
762
|
+
}
|
|
763
|
+
|
|
764
|
+
const remotes = buffer.match(outletRegExp);
|
|
765
|
+
if (remotes) {
|
|
766
|
+
for await (const match of remotes) {
|
|
767
|
+
const [key, remoteOutlet] = outletRegExp.exec(match);
|
|
768
|
+
const outletComponentPromise = getContext(key);
|
|
769
|
+
if (outletComponentPromise) {
|
|
770
|
+
const outletComponent = await outletComponentPromise;
|
|
771
|
+
if (outletComponent.stream) {
|
|
772
|
+
outletQueue.push(outletComponent.stream.getReader());
|
|
773
|
+
}
|
|
774
|
+
const outletOffset = getOutletOffset(remoteOutlet);
|
|
775
|
+
const { html, rsc } = outletComponent;
|
|
776
|
+
const remoteChunk = applyOutletOffset(
|
|
777
|
+
rsc,
|
|
778
|
+
outletOffset
|
|
779
|
+
);
|
|
780
|
+
remoteFlightResponse = `<script>self.__flightWriter__${outlet}__.write(self.__flightEncoder__${outlet}__.encode(${JSON.stringify(
|
|
781
|
+
remoteChunk
|
|
782
|
+
)}));</script>`;
|
|
783
|
+
buffer = buffer.replace(
|
|
784
|
+
match,
|
|
785
|
+
applyOutletOffsetToHTML(html, outletOffset)
|
|
786
|
+
);
|
|
787
|
+
}
|
|
788
|
+
}
|
|
789
|
+
}
|
|
790
|
+
|
|
791
|
+
if (buffer.length > 0) {
|
|
792
|
+
yield encoder.encode(remoteFlightResponse + buffer);
|
|
793
|
+
}
|
|
794
|
+
|
|
795
|
+
if (!hydrated && bootstrapped && hasClientComponent) {
|
|
796
|
+
if (isRemote) {
|
|
797
|
+
yield new Uint8Array(concat(bootstrapScripts));
|
|
798
|
+
} else {
|
|
799
|
+
// TODO: bootstrapScripts should be buffers instead of strings, fix script parts should be pre-encoded buffers then yield copy of those buffers
|
|
800
|
+
const script = encoder.encode(
|
|
801
|
+
`<script>document.currentScript.parentNode.removeChild(document.currentScript);${bootstrapScripts.join(
|
|
802
|
+
""
|
|
803
|
+
)}</script>${bootstrapModules
|
|
804
|
+
.map(
|
|
805
|
+
(mod) =>
|
|
806
|
+
`<script type="module" src="${mod}" async></script>`
|
|
807
|
+
)
|
|
808
|
+
.join("")}`
|
|
809
|
+
);
|
|
810
|
+
yield script;
|
|
811
|
+
}
|
|
812
|
+
|
|
813
|
+
hydrated = true;
|
|
814
|
+
}
|
|
815
|
+
|
|
816
|
+
_resolve();
|
|
817
|
+
};
|
|
818
|
+
|
|
819
|
+
let outletDone = false;
|
|
820
|
+
let outletNext = null;
|
|
821
|
+
const outletWorker = async function* () {
|
|
822
|
+
await htmlReady;
|
|
823
|
+
|
|
824
|
+
let interrupted = false;
|
|
825
|
+
|
|
826
|
+
const interrupt = new Promise((resolve) =>
|
|
827
|
+
immediate(() => resolve("interrupt"))
|
|
828
|
+
);
|
|
829
|
+
|
|
830
|
+
let _resolve;
|
|
831
|
+
outletReady = new Promise((resolve) => {
|
|
832
|
+
_resolve = resolve;
|
|
833
|
+
});
|
|
834
|
+
|
|
835
|
+
while (outletQueue.length > 0) {
|
|
836
|
+
if (interrupted) break;
|
|
837
|
+
|
|
838
|
+
const outletReader = outletQueue.shift();
|
|
839
|
+
let done = outletReader.done || false;
|
|
840
|
+
|
|
841
|
+
while (!done) {
|
|
842
|
+
const read = outletNext
|
|
843
|
+
? outletNext
|
|
844
|
+
: outletReader.read();
|
|
845
|
+
const res = await Promise.race([read, interrupt]);
|
|
846
|
+
|
|
847
|
+
if (res === "interrupt") {
|
|
848
|
+
outletNext = read;
|
|
849
|
+
done = true;
|
|
850
|
+
interrupted = true;
|
|
851
|
+
outletQueue.push(outletReader);
|
|
852
|
+
break;
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
outletNext = null;
|
|
856
|
+
|
|
857
|
+
const { value, done: _done } = res;
|
|
858
|
+
outletReader.done = _done;
|
|
859
|
+
|
|
860
|
+
if (_done) break;
|
|
861
|
+
|
|
862
|
+
if (value) {
|
|
863
|
+
const { outlet: remoteOutlet, rsc, html } = value;
|
|
864
|
+
|
|
865
|
+
if (html) {
|
|
866
|
+
const remoteHtml = applyOutletOffsetToHTML(
|
|
867
|
+
html,
|
|
868
|
+
getOutletOffset(remoteOutlet)
|
|
869
|
+
);
|
|
870
|
+
yield encoder.encode(remoteHtml);
|
|
871
|
+
}
|
|
872
|
+
|
|
873
|
+
if (rsc) {
|
|
874
|
+
let remoteChunk = applyOutletOffset(
|
|
875
|
+
rsc,
|
|
876
|
+
getOutletOffset(remoteOutlet)
|
|
877
|
+
);
|
|
878
|
+
if (isRemote) {
|
|
879
|
+
if (hasClientComponent) {
|
|
880
|
+
yield encoder.encode(remoteChunk);
|
|
881
|
+
} else {
|
|
882
|
+
bootstrapScripts.push(remoteChunk);
|
|
883
|
+
}
|
|
884
|
+
} else {
|
|
885
|
+
const chunk = `self.__flightWriter__${outlet}__.write(self.__flightEncoder__${outlet}__.encode(${JSON.stringify(
|
|
886
|
+
remoteChunk
|
|
887
|
+
)}));`;
|
|
888
|
+
const script = encoder.encode(
|
|
889
|
+
`<script>${chunk}</script>`
|
|
890
|
+
);
|
|
891
|
+
yield script;
|
|
892
|
+
}
|
|
893
|
+
}
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
}
|
|
897
|
+
|
|
898
|
+
outletDone = outletQueue.length === 0 && !outletNext;
|
|
899
|
+
|
|
900
|
+
_resolve();
|
|
901
|
+
};
|
|
902
|
+
|
|
903
|
+
const worker = async function* () {
|
|
904
|
+
while (!(forwardDone && htmlDone && outletDone)) {
|
|
905
|
+
for await (const value of forwardWorker()) {
|
|
906
|
+
yield value;
|
|
907
|
+
}
|
|
908
|
+
|
|
909
|
+
for await (const value of htmlWorker()) {
|
|
910
|
+
yield value;
|
|
911
|
+
}
|
|
912
|
+
|
|
913
|
+
if (!(await Promise.race([streaming, false]))) {
|
|
914
|
+
if (!redirect()) start();
|
|
915
|
+
}
|
|
916
|
+
|
|
917
|
+
for await (const value of outletWorker()) {
|
|
918
|
+
yield value;
|
|
919
|
+
}
|
|
920
|
+
}
|
|
921
|
+
};
|
|
922
|
+
|
|
923
|
+
const render = async () => {
|
|
924
|
+
for await (const value of worker()) {
|
|
925
|
+
payload.push(copyBytesFrom(value));
|
|
926
|
+
controller.enqueue(value);
|
|
927
|
+
}
|
|
928
|
+
|
|
929
|
+
const httpStatus = getContext(HTTP_STATUS) ?? {
|
|
930
|
+
status: 200,
|
|
931
|
+
statusText: "OK",
|
|
932
|
+
};
|
|
933
|
+
getContext(CACHE_CONTEXT)?.set(
|
|
934
|
+
[context.url, accept, cacheType, lastModified],
|
|
935
|
+
{
|
|
936
|
+
...httpStatus,
|
|
937
|
+
buffer: concat(payload),
|
|
938
|
+
}
|
|
939
|
+
);
|
|
940
|
+
|
|
941
|
+
controller.close();
|
|
942
|
+
};
|
|
943
|
+
|
|
944
|
+
render();
|
|
945
|
+
} catch (e) {
|
|
946
|
+
logger.error(e);
|
|
947
|
+
return resolve(await getContext(ERROR_CONTEXT)?.(e));
|
|
948
|
+
}
|
|
949
|
+
},
|
|
950
|
+
});
|
|
951
|
+
} else {
|
|
952
|
+
return resolve(
|
|
953
|
+
new Response(null, {
|
|
954
|
+
status: 404,
|
|
955
|
+
statusText: "Not Found",
|
|
956
|
+
})
|
|
957
|
+
);
|
|
958
|
+
}
|
|
959
|
+
} catch (e) {
|
|
960
|
+
logger.error(e);
|
|
961
|
+
getContext(ERROR_CONTEXT)?.(e)?.then(resolve, reject);
|
|
962
|
+
}
|
|
963
|
+
});
|
|
964
|
+
return streaming;
|
|
965
|
+
} catch (e) {
|
|
966
|
+
logger.error(e);
|
|
967
|
+
return new Response(null, {
|
|
968
|
+
status: 500,
|
|
969
|
+
statusText: "Internal Server Error",
|
|
970
|
+
});
|
|
971
|
+
}
|
|
972
|
+
}
|