@uniflowed/router 0.0.0-alpha.4 → 0.0.0-alpha.41

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.
Files changed (46) hide show
  1. package/action.js +301 -0
  2. package/client.js +295 -8
  3. package/handler.js +127 -106
  4. package/http-client.js +104 -0
  5. package/index.js +72 -4
  6. package/internal/action-endpoint.js +435 -0
  7. package/internal/action-wire.js +608 -0
  8. package/internal/base-path.js +175 -0
  9. package/internal/boundaries.js +481 -0
  10. package/internal/boundary-data.js +88 -0
  11. package/internal/compose.js +490 -0
  12. package/internal/devtools.js +131 -0
  13. package/internal/diagnostics.js +169 -0
  14. package/internal/error-view.js +193 -0
  15. package/internal/flight-browser.js +238 -0
  16. package/internal/flight-chunks.js +205 -0
  17. package/internal/flight-ssr.js +78 -0
  18. package/internal/flight.js +183 -0
  19. package/internal/head.js +219 -0
  20. package/internal/hydration.js +1085 -0
  21. package/internal/inspector.js +626 -0
  22. package/internal/native-links.js +67 -0
  23. package/internal/native-tree.js +89 -0
  24. package/internal/navigation-cache.js +181 -0
  25. package/internal/payload-rows.js +270 -0
  26. package/internal/payload.js +685 -0
  27. package/internal/prepare-document.js +49 -0
  28. package/internal/react-version.js +77 -0
  29. package/internal/request.js +43 -0
  30. package/internal/resolve.js +1613 -0
  31. package/internal/resolved-summary.js +199 -0
  32. package/internal/routing.js +478 -0
  33. package/internal/runtime.js +1613 -548
  34. package/internal/server-route.js +58 -0
  35. package/internal/shell.js +115 -0
  36. package/internal/stream.js +1099 -0
  37. package/middleware.js +350 -0
  38. package/native-navigation.js +217 -0
  39. package/native.js +416 -0
  40. package/package.json +48 -7
  41. package/routing.js +51 -0
  42. package/rsc-client.js +120 -0
  43. package/rsc-ssr.js +446 -0
  44. package/rsc.js +381 -0
  45. package/server-components.js +159 -0
  46. package/server.js +450 -75
@@ -0,0 +1,1099 @@
1
+ // @flow
2
+ //
3
+ // Internal to `@uniflowed/router`: a document as bytes rather than a string.
4
+ //
5
+ // `server.js` decides *what* the document says; this decides how it leaves.
6
+ // The split is here because the two answers are unrelated — a redirect, an
7
+ // error page and a page that suspends for half a second all produce the same
8
+ // three shapes for a host to choose from — and because everything below is
9
+ // about React's two server renderers and the difference between them, which is
10
+ // not something the renderer above should have to spell out twice.
11
+ //
12
+ // # The head is written before the body
13
+ //
14
+ // `packages/web/head.js` says this from the other side, as the reason `useHead`
15
+ // does nothing on a server: once the body is streaming, the head has gone and
16
+ // no component can still change it. This module is what makes that true rather
17
+ // than merely claimed.
18
+ //
19
+ // It is also the one thing that stops a document from being a straight
20
+ // pass-through of React's chunks. uf has head content React does not know
21
+ // about — the built asset URLs and the loader data the client hydrates from —
22
+ // and both have to be in the head. So the first chunks are held until the head
23
+ // is complete, the tags go in, and everything after that is forwarded
24
+ // untouched. What is held is bounded by the head: React writes `<head>` before
25
+ // any body content, so waiting for `</head>` never waits for a page.
26
+ //
27
+ // That is the whole of the buffering, and it is worth being precise about what
28
+ // it does *not* delay. A `<Suspense>` fallback is part of the shell, so it goes
29
+ // out with the head; the content that replaces it arrives in later chunks that
30
+ // pass straight through. The fallback is not what is being waited on — it is
31
+ // what is being sent.
32
+ //
33
+ // # Two renderers, and how the host picks
34
+ //
35
+ // `renderToPipeableStream` exists in React's Node build and not in its
36
+ // Web-standard ones; `renderToReadableStream` is in both. A namespace import is
37
+ // what makes that a runtime question rather than an import-time crash: a named
38
+ // import of `renderToPipeableStream` from `react-dom/server` in a worker is a
39
+ // module that will not link, and the failure is a blank deploy rather than a
40
+ // message. So the check is `typeof …` on the namespace, once, below.
41
+
42
+ import * as React from "react";
43
+ import * as ReactDOMServer from "react-dom/server";
44
+ import * as ReactDOMStatic from "react-dom/static";
45
+
46
+ import { createChunkEncoder } from "./flight-chunks.js";
47
+ import { type StreamRecord, inspected } from "./inspector.js";
48
+
49
+ /**
50
+ * Where a document is written, when the host has a Node stream.
51
+ *
52
+ * The three methods React's own `pipe` uses, and nothing else. Typed here
53
+ * rather than imported so this module holds no Node types: a worker bundles it
54
+ * too, and `stream$Writable` in the signature would be a Node type in a file
55
+ * that must not need one.
56
+ */
57
+ export type WritableLike = {
58
+ readonly write: (chunk: string) => mixed,
59
+ readonly end: () => mixed,
60
+ ...
61
+ };
62
+
63
+ /**
64
+ * A rendered document, in whichever shape the host can take.
65
+ *
66
+ * Three methods rather than one, because the three hosts uf actually has want
67
+ * three different things and converting between them costs a copy of the
68
+ * document: `uf dev` has a `ServerResponse`, `uf start` builds a `Response`,
69
+ * and `uf build` and the tests want the text. Each is a single pass over the
70
+ * same chunks, so exactly one of them may be called.
71
+ */
72
+ export type DocumentBody = {|
73
+ /** Write the document into a Node response. Resolves when the last byte is in. */
74
+ readonly pipe: (destination: WritableLike) => Promise<void>,
75
+ /** The document as a web stream, for `new Response(…)`. */
76
+ readonly stream: () => ReadableStream,
77
+ /** The whole document, once it is finished. */
78
+ readonly text: () => Promise<string>,
79
+ |};
80
+
81
+ /**
82
+ * The parts of React's Node destination that Fizz actually uses.
83
+ *
84
+ * Written out rather than `any`, and rather than `stream$Writable`: this module
85
+ * is bundled for workers too, so a Node type in a signature here would be a Node
86
+ * type in a file that must not need one. What is below is the whole contract —
87
+ * if React starts calling something else, this stops compiling, which is the
88
+ * point of writing it down.
89
+ */
90
+ type NodeDestination = {
91
+ readonly write: (chunk: string | Uint8Array) => boolean,
92
+ readonly end: () => mixed,
93
+ readonly destroy: (error?: mixed) => mixed,
94
+ readonly on: (event: string, listener: (...args: Array<mixed>) => mixed) => mixed,
95
+ readonly once: (event: string, listener: (...args: Array<mixed>) => mixed) => mixed,
96
+ readonly off: () => mixed,
97
+ readonly removeListener: () => mixed,
98
+ readonly emit: () => boolean,
99
+ };
100
+
101
+ /** The controller a `ReadableStream` source is handed. */
102
+ type StreamController = {
103
+ readonly enqueue: (chunk: Uint8Array) => mixed,
104
+ readonly close: () => mixed,
105
+ ...
106
+ };
107
+
108
+ /**
109
+ * A stream of bytes, as much of one as this module reads.
110
+ *
111
+ * Both of React's web-shaped outputs are one — `renderToReadableStream`'s
112
+ * result and `react-dom/static`'s `prelude` — and neither is typed by anything
113
+ * uf can import, so the shape it is used through is stated here.
114
+ */
115
+ type ByteSource = {
116
+ readonly getReader: () => {
117
+ readonly read: () => Promise<{ readonly done?: boolean, readonly value?: Uint8Array, ... }>,
118
+ readonly releaseLock: () => mixed,
119
+ ...
120
+ },
121
+ ...
122
+ };
123
+
124
+ /** How the document is assembled around the app's markup. */
125
+ export type DocumentShell = {|
126
+ /**
127
+ * Head tags for an app that renders its own `<html>`, inserted before the
128
+ * `</head>` React writes.
129
+ */
130
+ readonly head: string,
131
+ /**
132
+ * For an app that renders no document: everything up to the point uf's own
133
+ * head can still take tags — so it ends *inside* an open `<head>`.
134
+ */
135
+ readonly open: string,
136
+ /** The rest of that head, and everything up to the app's markup. */
137
+ readonly body: string,
138
+ /** Everything after it. */
139
+ readonly close: string,
140
+ |};
141
+
142
+ /** How much unread output the producer is allowed to run ahead by. */
143
+ const HIGH_WATER_MARK = 16;
144
+
145
+ /**
146
+ * The chunks of one render, as an async iterable, with backpressure in both
147
+ * directions.
148
+ *
149
+ * React's Node renderer pushes and a `ReadableStream` pulls, and a host may be
150
+ * slower than either. One queue in the middle answers all three: the producer
151
+ * is told to stop once `HIGH_WATER_MARK` chunks are waiting — which is the
152
+ * `false` from `write` that React's `pipe` honours — and is let go again when
153
+ * the consumer has caught up.
154
+ *
155
+ * Without it a slow client would be answered by a server holding an entire
156
+ * rendered document per request in memory, which is the failure mode streaming
157
+ * exists to avoid; a queue that only ever grows would have been streaming in
158
+ * shape and buffering in fact.
159
+ */
160
+ class ChunkQueue {
161
+ #chunks: Array<string> = [];
162
+ #ended: boolean = false;
163
+ #failure: mixed = null;
164
+ #failed: boolean = false;
165
+ #wake: ?() => void = null;
166
+ #drain: Array<() => void> = [];
167
+
168
+ /** Add a chunk. Returns whether the producer may keep going. */
169
+ push(chunk: string): boolean {
170
+ this.#chunks.push(chunk);
171
+ this.#ring();
172
+ return this.#chunks.length < HIGH_WATER_MARK;
173
+ }
174
+
175
+ /** No more chunks are coming. */
176
+ end(): void {
177
+ this.#ended = true;
178
+ this.#ring();
179
+ }
180
+
181
+ /** The render failed after the shell went out; the document is truncated. */
182
+ fail(error: mixed): void {
183
+ this.#failed = true;
184
+ this.#failure = error;
185
+ this.#ended = true;
186
+ this.#ring();
187
+ }
188
+
189
+ /** Run `resume` when there is room again. */
190
+ onDrain(resume: () => void): void {
191
+ this.#drain.push(resume);
192
+ }
193
+
194
+ #ring(): void {
195
+ const wake = this.#wake;
196
+ this.#wake = null;
197
+ if (wake != null) {
198
+ wake();
199
+ }
200
+ }
201
+
202
+ #room(): void {
203
+ if (this.#chunks.length >= HIGH_WATER_MARK || this.#drain.length === 0) {
204
+ return;
205
+ }
206
+ const waiting = this.#drain;
207
+ this.#drain = [];
208
+ for (const resume of waiting) {
209
+ resume();
210
+ }
211
+ }
212
+
213
+ async *chunks(): AsyncGenerator<string, void, void> {
214
+ while (true) {
215
+ while (this.#chunks.length > 0) {
216
+ const chunk = this.#chunks.shift();
217
+ this.#room();
218
+ if (chunk != null) {
219
+ yield chunk;
220
+ }
221
+ }
222
+ if (this.#failed) {
223
+ throw this.#failure;
224
+ }
225
+ if (this.#ended) {
226
+ return;
227
+ }
228
+ await new Promise<void>((resolve) => {
229
+ this.#wake = resolve;
230
+ });
231
+ }
232
+ }
233
+ }
234
+
235
+ /**
236
+ * A destination React's `pipe` will write into, backed by a queue.
237
+ *
238
+ * React's Node renderer wants a `Writable`, and the only parts of one it uses
239
+ * are `write`, `end`, `destroy` and the `drain` and `error` events. Handing it
240
+ * the real thing would mean importing `node:stream` into a module a worker also
241
+ * loads, for four methods.
242
+ */
243
+ function queueDestination(queue: ChunkQueue): NodeDestination {
244
+ const decoder = new TextDecoder();
245
+ const listeners: Map<string, Array<(...args: Array<mixed>) => mixed>> = new Map();
246
+ const destination = {
247
+ write(chunk: string | Uint8Array): boolean {
248
+ const text = typeof chunk === "string" ? chunk : decoder.decode(chunk, { stream: true });
249
+ const room = queue.push(text);
250
+ if (!room) {
251
+ queue.onDrain(() => {
252
+ for (const listener of listeners.get("drain") ?? []) {
253
+ listener();
254
+ }
255
+ });
256
+ }
257
+ return room;
258
+ },
259
+ end(): mixed {
260
+ queue.end();
261
+ return destination;
262
+ },
263
+ destroy(error?: mixed): mixed {
264
+ if (error != null) {
265
+ queue.fail(error);
266
+ } else {
267
+ queue.end();
268
+ }
269
+ return destination;
270
+ },
271
+ on(event: string, listener: (...args: Array<mixed>) => mixed): mixed {
272
+ listeners.set(event, [...(listeners.get(event) ?? []), listener]);
273
+ return destination;
274
+ },
275
+ once(event: string, listener: (...args: Array<mixed>) => mixed): mixed {
276
+ return destination.on(event, listener);
277
+ },
278
+ off(): mixed {
279
+ return destination;
280
+ },
281
+ removeListener(): mixed {
282
+ return destination;
283
+ },
284
+ emit(): boolean {
285
+ return false;
286
+ },
287
+ };
288
+ return destination;
289
+ }
290
+
291
+ /**
292
+ * Insert uf's head tags, or wrap markup that is not a document.
293
+ *
294
+ * The two shapes `assemble` used to decide between, decided the same way and at
295
+ * the same moment — on the opening bytes rather than on a finished string. An
296
+ * app whose root layout renders `<html>` owns the document and React writes its
297
+ * head; an app that renders only content gets the minimal shell around
298
+ * `<div id="uf-root">` that the client hydrates instead.
299
+ *
300
+ * The document case waits for `</head>`, because that is where the tags go. If
301
+ * React writes a document with no head at all — an app whose root layout is
302
+ * `<html><body>` — the tags go in a head of uf's own, inserted after the
303
+ * opening tag, which is what the browser would have synthesized anyway.
304
+ *
305
+ * The shell case waits for the end of the run of hoistable elements React
306
+ * opened with, because that is where *its* tags go — see [`hoisted`]. Both
307
+ * waits are bounded by the head, and both are the same idea: the head goes out
308
+ * once, and everything that belongs in it has to be in hand by then.
309
+ */
310
+ async function* assembled(
311
+ chunks: AsyncGenerator<string, void, void>,
312
+ shell: DocumentShell,
313
+ transformHead?: (html: string) => Promise<string>,
314
+ ): AsyncGenerator<string, void, void> {
315
+ let held = "";
316
+ let shape = "unknown";
317
+ // The opening chunk is the only one the hook sees, and every path below
318
+ // reaches exactly one of them. Awaiting here rather than at each `yield`
319
+ // keeps them from drifting apart. The hook sees only the document opening:
320
+ // Vite's HTML parser is happy with an open body, but not with a React chunk
321
+ // that happens to end inside an attribute.
322
+ const opening = async (html: string, rest: string = ""): Promise<string> => {
323
+ const split = transformableOpening(html);
324
+ const transformed = transformHead == null ? split.opening : await transformHead(split.opening);
325
+ return transformed + split.rest + rest;
326
+ };
327
+
328
+ for await (const chunk of chunks) {
329
+ if (shape === "document-open" || shape === "shell-open") {
330
+ yield chunk;
331
+ continue;
332
+ }
333
+ held += chunk;
334
+ if (shape === "unknown") {
335
+ shape = documentShape(held);
336
+ if (shape === "unknown") {
337
+ continue;
338
+ }
339
+ }
340
+ if (shape === "shell") {
341
+ const split = hoisted(held);
342
+ if (!split.complete) {
343
+ continue;
344
+ }
345
+ shape = "shell-open";
346
+ yield await opening(shell.open + split.head + shell.body, split.rest);
347
+ held = "";
348
+ continue;
349
+ }
350
+ // A document, and the tags go where its head closes.
351
+ const close = held.indexOf("</head>");
352
+ if (close !== -1) {
353
+ shape = "document-open";
354
+ yield await opening(ufDoctype(held.slice(0, close) + shell.head + held.slice(close)));
355
+ held = "";
356
+ continue;
357
+ }
358
+ // `<body` before `</head>` means React wrote no head; give the tags one.
359
+ const body = held.search(/<body[\s>]/i);
360
+ if (body !== -1) {
361
+ const bodyEnd = held.indexOf(">", body);
362
+ if (bodyEnd === -1) {
363
+ continue;
364
+ }
365
+ shape = "document-open";
366
+ yield await opening(
367
+ ufDoctype(`${held.slice(0, body)}<head>${shell.head}</head>${held.slice(body)}`),
368
+ );
369
+ held = "";
370
+ }
371
+ }
372
+
373
+ // The render ended before the decision could be made, or before what was
374
+ // being waited for arrived: an empty document, one with neither `</head>` nor
375
+ // `<body>` in it, or a shell that is hoistable elements all the way down.
376
+ // There is nothing left to wait for in any of them.
377
+ if (shape === "document") {
378
+ yield await opening(ufDoctype(held + shell.head));
379
+ shape = "document-open";
380
+ } else if (shape === "shell" || shape === "unknown") {
381
+ // `complete` is not consulted: nothing more is coming, so a run that was
382
+ // still open is over and whatever was left of it is markup like any other.
383
+ const split = hoisted(held);
384
+ shape = "shell-open";
385
+ yield await opening(shell.open + split.head + shell.body, split.rest);
386
+ }
387
+ if (shape === "shell-open") {
388
+ yield shell.close;
389
+ } else {
390
+ // The newline `assemble` ended a document with, kept: `uf build` writes
391
+ // these to files, and a file without a trailing newline is a diff with a
392
+ // "" in it forever.
393
+ yield "\n";
394
+ }
395
+ }
396
+
397
+ function transformableOpening(html: string): {| readonly opening: string, readonly rest: string |} {
398
+ const lower = html.toLowerCase();
399
+ const headEnd = lower.indexOf("</head>");
400
+ if (headEnd === -1) {
401
+ return { opening: html, rest: "" };
402
+ }
403
+ const afterHead = headEnd + "</head>".length;
404
+ const body = lower.indexOf("<body", afterHead);
405
+ if (body === -1) {
406
+ return { opening: html.slice(0, afterHead), rest: html.slice(afterHead) };
407
+ }
408
+ const bodyEnd = html.indexOf(">", body);
409
+ if (bodyEnd === -1) {
410
+ return { opening: html.slice(0, afterHead), rest: html.slice(afterHead) };
411
+ }
412
+ const afterBody = bodyEnd + 1;
413
+ return { opening: html.slice(0, afterBody), rest: html.slice(afterBody) };
414
+ }
415
+
416
+ /**
417
+ * The head elements React opened the app's markup with, split from the rest.
418
+ *
419
+ * `complete` is false while the buffer might still be in the middle of one — a
420
+ * chunk that ends inside `<meta cont`, or after a `<link>` and before whatever
421
+ * follows it. Deciding on an incomplete buffer would be a classification that
422
+ * depends on where React split its output, which is the bug `documentShape`
423
+ * above is written the way it is to avoid. The split is filled in either way,
424
+ * because the caller that has run out of chunks has nothing left to wait for
425
+ * and wants it.
426
+ *
427
+ * # Why a leading run rather than the whole document
428
+ *
429
+ * React hoists a `<title>`, a `<meta>` and a `<link>` into the `<head>` of a
430
+ * document *it* rendered. uf's shell is not one — React is handed the app, not
431
+ * the document — so with the shell every one of those landed in the body, and
432
+ * `<link rel="canonical">` in a body is a canonical link Google does not read.
433
+ * The fix is for uf to do the hoisting into the head it wrote itself.
434
+ *
435
+ * A leading run is what can be hoisted without holding the document. `RouteView`
436
+ * renders the route's metadata first, before the layouts and the page, so the
437
+ * run is exactly that metadata and the wait ends at the first byte of the
438
+ * application's own markup. Scanning further would mean buffering an arbitrary
439
+ * amount of a document to find a `<meta>` that might be at the end of it, which
440
+ * is streaming in shape and buffering in fact — the same trade this module
441
+ * refuses in `ChunkQueue`. So a tag a component renders further in stays where
442
+ * it is, and on a client React will hoist it into `document.head` itself.
443
+ *
444
+ * # Reading React's markup with a regular expression
445
+ *
446
+ * Which is only safe because it is React's. React escapes `>` in an attribute
447
+ * value and `<` in text, so the first `>` after an opening tag ends it and the
448
+ * first `</title>` ends a title — neither can appear inside one. This function
449
+ * is not an HTML parser and must never be handed markup from anywhere else.
450
+ */
451
+ function hoisted(held: string): HoistedHead {
452
+ let index = 0;
453
+ while (index < held.length) {
454
+ const rest = held.slice(index);
455
+ const split = { head: held.slice(0, index), rest, complete: false };
456
+ const open = rest.match(/^<(title|meta|link)(?=[\s/>])/i);
457
+ if (open == null) {
458
+ // Not a hoistable element, or not yet enough bytes to say it is not one.
459
+ return { ...split, complete: !couldOpenHoistable(rest) };
460
+ }
461
+ const close = held.indexOf(">", index);
462
+ if (close === -1) {
463
+ return split;
464
+ }
465
+ if (open[1].toLowerCase() !== "title") {
466
+ index = close + 1;
467
+ continue;
468
+ }
469
+ const end = held.indexOf("</title>", close);
470
+ if (end === -1) {
471
+ return split;
472
+ }
473
+ index = end + "</title>".length;
474
+ }
475
+ // Every byte so far is a complete hoistable element, and the next one may
476
+ // still be on its way — the case a document that is metadata and nothing else
477
+ // ends in, and the reason the loop is bounded by the buffer rather than by
478
+ // `true`: a `while (true)` here is a function the checker reads as returning
479
+ // `void` on a path it cannot see is unreachable.
480
+ return { head: held, rest: "", complete: false };
481
+ }
482
+
483
+ /** What [`hoisted`] found, and whether more bytes could still change it. */
484
+ type HoistedHead = {|
485
+ /** The hoistable elements the markup opened with. */
486
+ readonly head: string,
487
+ /** Everything after them. */
488
+ readonly rest: string,
489
+ /** Whether the run is known to have ended. */
490
+ readonly complete: boolean,
491
+ |};
492
+
493
+ /**
494
+ * Whether `rest` could still turn into a hoistable element once more bytes
495
+ * arrive.
496
+ *
497
+ * True for `"<"` and for every proper prefix of `<title`, `<meta` and `<link` —
498
+ * the states a chunk boundary can leave the buffer in. False for `<main`, and
499
+ * false for `<titlebar>`, which is somebody's component and not a title however
500
+ * much of it has arrived.
501
+ */
502
+ function couldOpenHoistable(rest: string): boolean {
503
+ const text = rest.toLowerCase();
504
+ return ["<title", "<meta", "<link"].some((tag) => tag.startsWith(text));
505
+ }
506
+
507
+ /**
508
+ * uf's spelling of the doctype, in place of whichever one React wrote.
509
+ *
510
+ * A doctype is case-insensitive, so this is a formatting choice and not a
511
+ * correctness one — and uf already made it: `redirectDocument` and the shell
512
+ * around an app that renders no document both write `<!doctype html>`. Leaving
513
+ * React's `<!DOCTYPE html>` here would mean a project's documents were spelled
514
+ * one way or the other depending on whether its root layout renders `<html>`,
515
+ * which is not a distinction anybody asked for.
516
+ */
517
+ function ufDoctype(document: string): string {
518
+ const existing = document.match(/^\s*<!doctype[^>]*>\s*/i);
519
+ const rest = existing == null ? document.replace(/^\s+/, "") : document.slice(existing[0].length);
520
+ return `<!doctype html>\n${rest}`;
521
+ }
522
+
523
+ /**
524
+ * Whether the bytes so far are the start of a document, and `"unknown"` when
525
+ * they are still too few to say.
526
+ *
527
+ * Read off the buffer rather than off the first chunk, because React decides
528
+ * where to split its output and a classification that depended on the split
529
+ * would be a bug that only appeared under load. `<!DOCTYPE html>` alone is the
530
+ * case that makes the third answer necessary: it is a complete token, it is
531
+ * not yet `<html`, and calling it either answer would be wrong.
532
+ */
533
+ function documentShape(held: string): string {
534
+ const text = held.replace(/^\s+/, "").toLowerCase();
535
+ if (text === "") {
536
+ return "unknown";
537
+ }
538
+ let rest = text;
539
+ if (text.startsWith("<!doctype")) {
540
+ const close = text.indexOf(">");
541
+ if (close === -1) {
542
+ return "unknown";
543
+ }
544
+ rest = text.slice(close + 1).replace(/^\s+/, "");
545
+ } else if ("<!doctype".startsWith(text)) {
546
+ return "unknown";
547
+ }
548
+ if (rest === "" || "<html".startsWith(rest)) {
549
+ return "unknown";
550
+ }
551
+ // The delimiter matters: `<htmlish>` is somebody's component, not a document.
552
+ return /^<html[\s>]/.test(rest) ? "document" : "shell";
553
+ }
554
+
555
+ /**
556
+ * The three shapes, over one pass of the chunks.
557
+ *
558
+ * `stop` is what a reader that gives up has to be able to do. A `HEAD` asks for
559
+ * a document and wants none of it, and a browser that navigates away closes the
560
+ * socket — and in both cases React is still rendering into a queue whose
561
+ * consumer is gone. Without this it renders until the queue is full and then
562
+ * waits for a drain that is never coming, which is a request that never ends.
563
+ */
564
+ function bodyOf(chunks: AsyncGenerator<string, void, void>, stop?: () => void): DocumentBody {
565
+ return {
566
+ async pipe(destination: WritableLike): Promise<void> {
567
+ // `finally`, so a render that fails partway still closes the response.
568
+ // The alternative is a client holding an open connection to a document
569
+ // that stopped, waiting for bytes nobody is going to send.
570
+ try {
571
+ for await (const chunk of chunks) {
572
+ destination.write(chunk);
573
+ }
574
+ } finally {
575
+ destination.end();
576
+ }
577
+ },
578
+ stream(): ReadableStream {
579
+ const encoder = new TextEncoder();
580
+ // `pull`, not a loop in `start`: the queue's backpressure only means
581
+ // anything if this end waits to be asked.
582
+ return new ReadableStream({
583
+ async pull(controller: StreamController) {
584
+ const next = await chunks.next();
585
+ if (next.done === true) {
586
+ controller.close();
587
+ return;
588
+ }
589
+ controller.enqueue(encoder.encode(next.value));
590
+ },
591
+ cancel(): void {
592
+ stop?.();
593
+ void chunks.return(undefined);
594
+ },
595
+ });
596
+ },
597
+ async text(): Promise<string> {
598
+ let out = "";
599
+ for await (const chunk of chunks) {
600
+ out += chunk;
601
+ }
602
+ return out;
603
+ },
604
+ };
605
+ }
606
+
607
+ /** A document that is already text — a redirect, or a caller's own markup. */
608
+ export function bodyOfText(html: string): DocumentBody {
609
+ async function* one(): AsyncGenerator<string, void, void> {
610
+ yield html;
611
+ }
612
+ return bodyOf(one());
613
+ }
614
+
615
+ /** What React is told about a render, over both renderers. */
616
+ export type RenderOptions = {|
617
+ readonly shell: DocumentShell,
618
+ /**
619
+ * Every exception React recovers from, including the ones inside a
620
+ * `<Suspense>` that it answered by streaming the boundary's fallback. The
621
+ * shell's own failure is not reported here — it rejects instead.
622
+ */
623
+ readonly onError: (error: mixed) => void,
624
+ /**
625
+ * Rewrite the document opening — the head and, when present, the body start
626
+ * tag — before it goes out.
627
+ *
628
+ * For `uf dev`, and only for it. Vite's `transformIndexHtml` rewrites asset
629
+ * URLs and injects `/@vite/client` and the refresh preamble, and it is a
630
+ * *whole document* hook, so the development server used to collect the page
631
+ * and transform it at the end. That made the one place a developer would
632
+ * notice streaming the one place it did not happen: a slow page showed
633
+ * nothing until it was finished, and `$loading.js` looked broken.
634
+ * See ubugeeei-prod/uf#374.
635
+ *
636
+ * The hook never sees application body markup, which is what makes this
637
+ * safe. Vite's injections are string-based against `<head>` and `<body>`,
638
+ * and its parser accepts a document that stops after the body start tag.
639
+ * It does not accept a React chunk that ends in the middle of an attribute.
640
+ *
641
+ * Absent everywhere else. `uf start`, `uf preview` and every deploy adapter
642
+ * have no such hook and stream already.
643
+ */
644
+ readonly transformHead?: (html: string) => Promise<string>,
645
+ /**
646
+ * Told what left, in what order, and what each chunk built.
647
+ *
648
+ * For `uf dev`, like `transformHead` above, and absent everywhere else —
649
+ * which is what makes it free rather than cheap: with nothing supplied no
650
+ * recorder is constructed, `./inspector.js` is never entered, and the
651
+ * generator a host consumes is the same object it was. See that module for
652
+ * what is done with the record and why it is a development affordance rather
653
+ * than a metric.
654
+ *
655
+ * Called once per document, after the last chunk and also after a render that
656
+ * was abandoned partway. `prerenderDocument` never calls it: a build resolves
657
+ * everything before it writes a byte, so there is no order to report.
658
+ */
659
+ readonly onStream?: (record: StreamRecord) => void,
660
+ /**
661
+ * The Flight payload this document's tree was read from, to write into the
662
+ * document for the browser to hydrate from.
663
+ *
664
+ * Absent for a document rendered from the route's modules, which is what
665
+ * every document was before ubugeeei-prod/uf#519 and what `app.rsc: false`
666
+ * still asks for. Present, it is copied into the document as it arrives, by
667
+ * [`interleaved`], which says where it may and may not go.
668
+ */
669
+ readonly payload?: ReadableStream<Uint8Array>,
670
+ /**
671
+ * This response's Content-Security-Policy nonce, or absent for none.
672
+ *
673
+ * Absent is what every render had before nonces existed and what every
674
+ * render still has in a project that has not asked for one, so a document
675
+ * written without it is byte-for-byte the document uf has always written.
676
+ *
677
+ * Present, it reaches three places, and it has to reach all three or the
678
+ * page is broken rather than merely unprotected: React's own option, which
679
+ * nonces every inline script React emits — the runtime that reveals a
680
+ * `<Suspense>` boundary, and the bootstrap; `shell.open`/`shell.body`, which
681
+ * carry the client entry; and the payload chunk elements, which
682
+ * [`interleaved`] writes as text.
683
+ *
684
+ * `prerenderDocument` is deliberately never given one. See its own paragraph.
685
+ */
686
+ readonly nonce?: string | null,
687
+ |};
688
+
689
+ /**
690
+ * `chunks`, recorded on the way out when `uf dev` asked for it.
691
+ *
692
+ * The wrapping goes here rather than around `assembled` in each of the callers
693
+ * so that what is recorded is unambiguous: these are the bytes the host is
694
+ * handed, head and all, and not React's own output on the way past.
695
+ */
696
+ function outgoing(
697
+ chunks: AsyncGenerator<string, void, void>,
698
+ onStream?: (record: StreamRecord) => void,
699
+ ): AsyncGenerator<string, void, void> {
700
+ return onStream == null ? chunks : inspected(chunks, onStream, () => performance.now());
701
+ }
702
+
703
+ /**
704
+ * Stream `node` as a document, resolving once the shell is ready.
705
+ *
706
+ * Resolving on the shell rather than on the whole document is the entire point:
707
+ * the caller has a status and a body to answer with while the page is still
708
+ * rendering. It rejects when the *shell* throws, and that is a different event
709
+ * from a page throwing — nothing has been written yet, so the caller can still
710
+ * resolve the error route and render it instead, which is what `createRenderer`
711
+ * does and what ubugeeei-prod/uf#257 is about.
712
+ */
713
+ export function renderDocument(node: React.Node, options: RenderOptions): Promise<DocumentBody> {
714
+ const queue = new ChunkQueue();
715
+ return new Promise((resolve, reject) => {
716
+ if (typeof ReactDOMServer.renderToPipeableStream === "function") {
717
+ const { pipe, abort } = ReactDOMServer.renderToPipeableStream(node, {
718
+ onShellReady() {
719
+ pipe(queueDestination(queue));
720
+ resolve(
721
+ bodyOf(
722
+ outgoing(
723
+ withPayload(
724
+ assembled(queue.chunks(), options.shell, options.transformHead),
725
+ options.payload,
726
+ options.nonce,
727
+ ),
728
+ options.onStream,
729
+ ),
730
+ () => abort(),
731
+ ),
732
+ );
733
+ },
734
+ onShellError(error: mixed) {
735
+ reject(error);
736
+ },
737
+ onError: options.onError,
738
+ // Every inline script React writes for this document, from one option:
739
+ // the streaming runtime that reveals a boundary and patches a segment,
740
+ // and the bootstrap. uf nonces the scripts it writes itself; these are
741
+ // React's, and there is no other way to reach them.
742
+ nonce: options.nonce ?? undefined,
743
+ });
744
+ return;
745
+ }
746
+ // A Web-standard host: no `pipe`, and the stream itself is what is
747
+ // awaited. `renderToReadableStream`'s promise settles on the shell, which
748
+ // is the same moment `onShellReady` is.
749
+ renderWithReadableStream(ReactDOMServer.renderToReadableStream, node, options).then(
750
+ resolve,
751
+ reject,
752
+ );
753
+ });
754
+ }
755
+
756
+ /**
757
+ * A `renderToReadableStream`, as this module calls one.
758
+ *
759
+ * Written down so [`renderWithReadableStream`] can be driven with something
760
+ * that is not React's. The branch below only runs on a host that has no
761
+ * `renderToPipeableStream` — a worker, never a test process — and a branch no
762
+ * test can reach is exactly how it came to be the one missing the cancellation
763
+ * its Node twin has had since it was written.
764
+ */
765
+ type ReadableStreamRenderer = (
766
+ node: React.Node,
767
+ settings: {|
768
+ readonly onError: (error: mixed) => void,
769
+ readonly signal: AbortSignal,
770
+ readonly nonce?: string,
771
+ |},
772
+ ) => Promise<ByteSource>;
773
+
774
+ /**
775
+ * The Web-standard half of [`renderDocument`], with a way to stop the render.
776
+ *
777
+ * The Node path holds `renderToPipeableStream`'s own `abort` and calls it when
778
+ * the consumer gives up. This one has no such handle, so it renders under an
779
+ * `AbortSignal` and aborts it in the same place — and without that,
780
+ * `releaseLock` in [`decoded`] merely detaches the reader while React goes on
781
+ * rendering into a stream nobody will ever read again, for however long the
782
+ * page's slowest boundary takes.
783
+ *
784
+ * That is not the exotic case. A `HEAD` cancels, and so does every browser that
785
+ * navigates away mid-document; on a worker each one would leave a render
786
+ * running against whatever CPU budget the host meters. The two paths answer
787
+ * every other question the same way, and this was the last one where they
788
+ * disagreed.
789
+ */
790
+ export function renderWithReadableStream(
791
+ render: ReadableStreamRenderer,
792
+ node: React.Node,
793
+ options: RenderOptions,
794
+ ): Promise<DocumentBody> {
795
+ const controller = new AbortController();
796
+ return render(node, {
797
+ onError: options.onError,
798
+ signal: controller.signal,
799
+ nonce: options.nonce ?? undefined,
800
+ }).then((stream: ByteSource) =>
801
+ bodyOf(
802
+ outgoing(
803
+ withPayload(
804
+ assembled(decoded(stream), options.shell, options.transformHead),
805
+ options.payload,
806
+ options.nonce,
807
+ ),
808
+ options.onStream,
809
+ ),
810
+ () => {
811
+ controller.abort();
812
+ },
813
+ ),
814
+ );
815
+ }
816
+
817
+ /** A web stream of bytes, as the string chunks the rest of this module speaks. */
818
+ async function* decoded(stream: ByteSource): AsyncGenerator<string, void, void> {
819
+ const decoder = new TextDecoder();
820
+ const reader = stream.getReader();
821
+ try {
822
+ while (true) {
823
+ const { done, value } = await reader.read();
824
+ if (done === true) {
825
+ const rest = decoder.decode();
826
+ if (rest !== "") {
827
+ yield rest;
828
+ }
829
+ return;
830
+ }
831
+ yield decoder.decode(value, { stream: true });
832
+ }
833
+ } finally {
834
+ // Reached when a consumer stops early — `return()` on this generator lands
835
+ // here — and the lock has to go back or the render behind it never ends.
836
+ reader.releaseLock();
837
+ }
838
+ }
839
+
840
+ /**
841
+ * Render `node` to a finished document, with everything resolved.
842
+ *
843
+ * `uf build`'s renderer, and deliberately not `renderDocument` with the chunks
844
+ * joined up. React has two server renderers and they answer two different
845
+ * questions: the streaming one sends a fallback and then patches it from a
846
+ * script, because a browser is on the other end and the point is what it can
847
+ * paint first; the static one waits, and writes the resolved content where the
848
+ * fallback would have been. A file in `dist/` has no first paint to optimize
849
+ * and no guarantee that whatever serves it runs scripts at all, so it wants the
850
+ * second — an `index.html` full of `<template>` placeholders waiting for
851
+ * `$RC()` would be a page that is blank to a crawler and to `curl`.
852
+ *
853
+ * That is the static/streaming split, and it is this function versus the one
854
+ * above rather than a flag threaded through one of them.
855
+ */
856
+ export async function prerenderDocument(node: React.Node, options: RenderOptions): Promise<string> {
857
+ const settings = { onError: options.onError };
858
+ const result =
859
+ typeof ReactDOMStatic.prerenderToNodeStream === "function"
860
+ ? await ReactDOMStatic.prerenderToNodeStream(node, settings)
861
+ : await ReactDOMStatic.prerender(node, settings);
862
+ return bodyOf(
863
+ withPayload(
864
+ assembled(preludeChunks(result.prelude), options.shell, options.transformHead),
865
+ options.payload,
866
+ ),
867
+ ).text();
868
+ }
869
+
870
+ /** `chunks` unchanged when there is no payload, and [`interleaved`] with one. */
871
+ function withPayload(
872
+ chunks: AsyncGenerator<string, void, void>,
873
+ payload: ?ReadableStream<Uint8Array>,
874
+ nonce?: string | null,
875
+ ): AsyncGenerator<string, void, void> {
876
+ return payload == null ? chunks : interleaved(chunks, payload, nonce);
877
+ }
878
+
879
+ /**
880
+ * A document's chunks, with the Flight payload it was rendered from written
881
+ * into it as it arrives.
882
+ *
883
+ * Four rules, and each is the answer to a way the obvious version is wrong.
884
+ *
885
+ * **Nothing before the head.** The first chunk this is handed is the whole
886
+ * opening of the document — `assembled` does not let one go until the head is
887
+ * complete — and a payload element written in front of it would sit before
888
+ * `<head>`, where the parser would open a body for it and every tag after it
889
+ * would land in the wrong element.
890
+ *
891
+ * **Written as soon as it exists.** A payload row usually exists before the
892
+ * HTML rendered from it — React's client reads the row, then the boundary
893
+ * renders — so waiting for the next HTML chunk would put the browser's copy
894
+ * behind the markup it hydrates. Each HTML chunk that ends between elements is
895
+ * followed by whatever payload is waiting, and a payload that arrives while the
896
+ * HTML is idle there is written then, without a chunk of HTML to follow.
897
+ *
898
+ * **Only between elements.** React hands its HTML on through a fixed-size
899
+ * buffer, so a chunk can end anywhere: inside a tag, an attribute's value, a
900
+ * comment, a character reference or an inline `<script>`. A payload element
901
+ * written after such a chunk is not an element. It is part of the attribute,
902
+ * the comment or the text it landed in, the browser never reads it, and React's
903
+ * client closes the payload with rows missing, which is the "Connection closed"
904
+ * a page reports instead of hydrating. So a waiting payload goes out only where
905
+ * the HTML written so far ends between elements, which [`advanced`] follows
906
+ * from chunk to chunk, and otherwise waits for the HTML that finishes what is
907
+ * open.
908
+ *
909
+ * **Outside replaceable boundaries.** Even between tags, a payload inside a
910
+ * Suspense fallback disappears when React reveals its content. A browser that
911
+ * loads the Flight reader afterwards then sees an incomplete stream. React's
912
+ * boundary comments keep those ranges closed to injection until their end.
913
+ *
914
+ * **`</body></html>` waits for the end of the payload.** The HTML can finish
915
+ * first — the last boundary's markup is rendered from rows that are already
916
+ * written — and a payload element after `</html>` is one the parser moves
917
+ * rather than one React expects. So the closing tags are held back, the rest of
918
+ * the payload and its end marker are written, and the tags go last. For a
919
+ * document uf wraps, the closing run is `</div></body></html>`, and only the
920
+ * part from `</body>` is held: the root element React hydrates must hold
921
+ * nothing React did not render.
922
+ *
923
+ * A consumer that stops early stops the payload too: the reader is cancelled
924
+ * in the `finally`, which is where `return()` on this generator lands.
925
+ */
926
+ async function* interleaved(
927
+ chunks: AsyncGenerator<string, void, void>,
928
+ payload: ReadableStream<Uint8Array>,
929
+ nonce?: string | null,
930
+ ): AsyncGenerator<string, void, void> {
931
+ const encoder = createChunkEncoder(nonce);
932
+ const reader = payload.getReader();
933
+ let written = "";
934
+ let ended = false;
935
+ let wake: ?() => void = null;
936
+ const ring = () => {
937
+ const resume = wake;
938
+ wake = null;
939
+ if (resume != null) {
940
+ resume();
941
+ }
942
+ };
943
+ const pumping = (async () => {
944
+ try {
945
+ while (true) {
946
+ const step = await reader.read();
947
+ if (step.done === true) {
948
+ break;
949
+ }
950
+ if (step.value != null) {
951
+ written += encoder.encode(step.value);
952
+ ring();
953
+ }
954
+ }
955
+ } catch {
956
+ // A payload that failed partway is still ended, with the end marker, so
957
+ // the browser's reader closes and React reports the rows it never got
958
+ // rather than waiting for them for as long as the page is open.
959
+ } finally {
960
+ written += encoder.end();
961
+ ended = true;
962
+ ring();
963
+ }
964
+ })();
965
+
966
+ // Nothing written yet, and nothing may go before the head.
967
+ let boundary: Boundary = NOTHING_WRITTEN;
968
+ let closing = "";
969
+ try {
970
+ let next = chunks.next();
971
+ while (true) {
972
+ if (boundary.safe && written !== "") {
973
+ const out = written;
974
+ written = "";
975
+ yield out;
976
+ }
977
+ const idle = new Promise<null>((resolve) => {
978
+ wake = () => resolve(null);
979
+ });
980
+ const outcome = await Promise.race([next, idle]);
981
+ if (outcome == null) {
982
+ continue;
983
+ }
984
+ if (outcome.done === true) {
985
+ break;
986
+ }
987
+ let text = outcome.value;
988
+ const close = text.search(/<\/body>\s*<\/html>\s*$/i);
989
+ if (close !== -1) {
990
+ closing = text.slice(close);
991
+ text = text.slice(0, close);
992
+ }
993
+ if (text !== "") {
994
+ yield text;
995
+ boundary = advanced(boundary, text);
996
+ }
997
+ next = chunks.next();
998
+ }
999
+ while (!ended || written !== "") {
1000
+ if (written !== "") {
1001
+ const out = written;
1002
+ written = "";
1003
+ yield out;
1004
+ continue;
1005
+ }
1006
+ await new Promise<void>((resolve) => {
1007
+ wake = resolve;
1008
+ if (ended || written !== "") {
1009
+ ring();
1010
+ }
1011
+ });
1012
+ }
1013
+ await pumping;
1014
+ yield closing;
1015
+ } finally {
1016
+ if (!ended) {
1017
+ void reader.cancel();
1018
+ }
1019
+ }
1020
+ }
1021
+
1022
+ /** Where the HTML written so far leaves the next payload element. */
1023
+ type Boundary = {|
1024
+ /** Whether a payload element may be written now. */
1025
+ readonly safe: boolean,
1026
+ /** The `script` or `style` element the HTML is inside, if it is inside one. */
1027
+ readonly rawText: string | null,
1028
+ /** Suspense/Activity ranges React can replace before hydration starts. */
1029
+ readonly replaceable: number,
1030
+ /** A tag, or a comment, the HTML has started and not yet finished. */
1031
+ readonly open: string,
1032
+ |};
1033
+
1034
+ const NOTHING_WRITTEN: Boundary = { safe: false, rawText: null, replaceable: 0, open: "" };
1035
+
1036
+ /**
1037
+ * `boundary`, once `html` has been written after it.
1038
+ *
1039
+ * A payload element may follow HTML that ends with a `>` outside a `<script>`
1040
+ * and a `<style>`. The test can be that short because React wrote the HTML: it
1041
+ * escapes `<` and `>` in text and in attribute values, so a `>` it wrote closes
1042
+ * a tag or a comment, and HTML that ends any other way ends inside one, or
1043
+ * inside a character reference or a run of text. What React does not escape is
1044
+ * the content of an inline script or stylesheet, where a `>` is code, so an
1045
+ * opening `<script>` or `<style>` is followed to its closing tag. A tag split
1046
+ * across two chunks is carried in `open` and read whole with the next one.
1047
+ */
1048
+ function advanced(boundary: Boundary, html: string): Boundary {
1049
+ const text = boundary.open + html;
1050
+ let rawText = boundary.rawText;
1051
+ let replaceable = boundary.replaceable;
1052
+ const tags = /<!--([\s\S]*?)-->|<(\/?)(script|style)(?=[\s/>])[^>]*>/gi;
1053
+ let tag = tags.exec(text);
1054
+ while (tag != null) {
1055
+ const comment = tag[1];
1056
+ if (comment != null) {
1057
+ if (rawText == null) {
1058
+ if (/^[\$&][?!~]?$/.test(comment)) replaceable += 1;
1059
+ else if (comment === "/$" || comment === "/&") replaceable = Math.max(0, replaceable - 1);
1060
+ }
1061
+ } else {
1062
+ const name = tag[3].toLowerCase();
1063
+ if (tag[2] === "/") {
1064
+ if (rawText === name) rawText = null;
1065
+ } else if (rawText == null) {
1066
+ rawText = name;
1067
+ }
1068
+ }
1069
+ tag = tags.exec(text);
1070
+ }
1071
+ const start = text.lastIndexOf("<");
1072
+ return {
1073
+ safe: rawText == null && replaceable === 0 && text.endsWith(">"),
1074
+ rawText,
1075
+ replaceable,
1076
+ open: start > text.lastIndexOf(">") ? text.slice(start) : "",
1077
+ };
1078
+ }
1079
+
1080
+ /**
1081
+ * The prelude of a static prerender, whichever stream this build produced.
1082
+ *
1083
+ * `prerenderToNodeStream` hands back a Node `Readable` and `prerender` a web
1084
+ * `ReadableStream`, and which one a build has depends on which React entry
1085
+ * point exists — so the union is real rather than defensive, and `getReader`
1086
+ * is what tells them apart.
1087
+ */
1088
+ async function* preludeChunks(
1089
+ prelude: ByteSource | AsyncIterable<string | Uint8Array>,
1090
+ ): AsyncGenerator<string, void, void> {
1091
+ if (typeof prelude.getReader === "function") {
1092
+ yield* decoded(prelude);
1093
+ return;
1094
+ }
1095
+ const decoder = new TextDecoder();
1096
+ for await (const chunk of prelude) {
1097
+ yield typeof chunk === "string" ? chunk : decoder.decode(chunk, { stream: true });
1098
+ }
1099
+ }