@uniflowed/router 0.0.0-alpha.16 → 0.0.0-alpha.18
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/internal/stream.js +42 -8
- package/package.json +3 -3
- package/server.js +43 -0
package/internal/stream.js
CHANGED
|
@@ -307,9 +307,15 @@ function queueDestination(queue: ChunkQueue): NodeDestination {
|
|
|
307
307
|
async function* assembled(
|
|
308
308
|
chunks: AsyncGenerator<string, void, void>,
|
|
309
309
|
shell: DocumentShell,
|
|
310
|
+
transformHead?: (html: string) => Promise<string>,
|
|
310
311
|
): AsyncGenerator<string, void, void> {
|
|
311
312
|
let held = "";
|
|
312
313
|
let shape = "unknown";
|
|
314
|
+
// The opening chunk is the only one the hook sees, and every path below
|
|
315
|
+
// reaches exactly one of them. Awaiting here rather than at each `yield`
|
|
316
|
+
// keeps the four of them from drifting apart.
|
|
317
|
+
const opening = async (html: string): Promise<string> =>
|
|
318
|
+
transformHead == null ? html : await transformHead(html);
|
|
313
319
|
|
|
314
320
|
for await (const chunk of chunks) {
|
|
315
321
|
if (shape === "document-open" || shape === "shell-open") {
|
|
@@ -329,7 +335,7 @@ async function* assembled(
|
|
|
329
335
|
continue;
|
|
330
336
|
}
|
|
331
337
|
shape = "shell-open";
|
|
332
|
-
yield shell.open + split.head + shell.body + split.rest;
|
|
338
|
+
yield await opening(shell.open + split.head + shell.body + split.rest);
|
|
333
339
|
held = "";
|
|
334
340
|
continue;
|
|
335
341
|
}
|
|
@@ -337,7 +343,7 @@ async function* assembled(
|
|
|
337
343
|
const close = held.indexOf("</head>");
|
|
338
344
|
if (close !== -1) {
|
|
339
345
|
shape = "document-open";
|
|
340
|
-
yield ufDoctype(held.slice(0, close) + shell.head + held.slice(close));
|
|
346
|
+
yield await opening(ufDoctype(held.slice(0, close) + shell.head + held.slice(close)));
|
|
341
347
|
held = "";
|
|
342
348
|
continue;
|
|
343
349
|
}
|
|
@@ -345,7 +351,9 @@ async function* assembled(
|
|
|
345
351
|
const body = held.search(/<body[\s>]/i);
|
|
346
352
|
if (body !== -1) {
|
|
347
353
|
shape = "document-open";
|
|
348
|
-
yield
|
|
354
|
+
yield await opening(
|
|
355
|
+
ufDoctype(`${held.slice(0, body)}<head>${shell.head}</head>${held.slice(body)}`),
|
|
356
|
+
);
|
|
349
357
|
held = "";
|
|
350
358
|
}
|
|
351
359
|
}
|
|
@@ -355,14 +363,14 @@ async function* assembled(
|
|
|
355
363
|
// `<body>` in it, or a shell that is hoistable elements all the way down.
|
|
356
364
|
// There is nothing left to wait for in any of them.
|
|
357
365
|
if (shape === "document") {
|
|
358
|
-
yield ufDoctype(held + shell.head);
|
|
366
|
+
yield await opening(ufDoctype(held + shell.head));
|
|
359
367
|
shape = "document-open";
|
|
360
368
|
} else if (shape === "shell" || shape === "unknown") {
|
|
361
369
|
// `complete` is not consulted: nothing more is coming, so a run that was
|
|
362
370
|
// still open is over and whatever was left of it is markup like any other.
|
|
363
371
|
const split = hoisted(held);
|
|
364
372
|
shape = "shell-open";
|
|
365
|
-
yield shell.open + split.head + shell.body + split.rest;
|
|
373
|
+
yield await opening(shell.open + split.head + shell.body + split.rest);
|
|
366
374
|
}
|
|
367
375
|
if (shape === "shell-open") {
|
|
368
376
|
yield shell.close;
|
|
@@ -582,6 +590,28 @@ export type RenderOptions = {|
|
|
|
582
590
|
* shell's own failure is not reported here — it rejects instead.
|
|
583
591
|
*/
|
|
584
592
|
readonly onError: (error: mixed) => void,
|
|
593
|
+
/**
|
|
594
|
+
* Rewrite the opening chunk — everything up to and including the head —
|
|
595
|
+
* before it goes out.
|
|
596
|
+
*
|
|
597
|
+
* For `uf dev`, and only for it. Vite's `transformIndexHtml` rewrites asset
|
|
598
|
+
* URLs and injects `/@vite/client` and the refresh preamble, and it is a
|
|
599
|
+
* *whole document* hook, so the development server used to collect the page
|
|
600
|
+
* and transform it at the end. That made the one place a developer would
|
|
601
|
+
* notice streaming the one place it did not happen: a slow page showed
|
|
602
|
+
* nothing until it was finished, and `_uf.loading.js` looked broken.
|
|
603
|
+
* See ubugeeei-prod/uf#374.
|
|
604
|
+
*
|
|
605
|
+
* The hook only ever sees the head, which is what makes this safe. Vite's
|
|
606
|
+
* injections are string-based against `<head>`, and its dev hook handles a
|
|
607
|
+
* document that ends mid-`<body>` without complaint — checked against Vite
|
|
608
|
+
* 8.2.2 before this existed, because "the parse step is the risk" was the
|
|
609
|
+
* open question on that issue.
|
|
610
|
+
*
|
|
611
|
+
* Absent everywhere else. `uf start`, `uf preview` and every deploy adapter
|
|
612
|
+
* have no such hook and stream already.
|
|
613
|
+
*/
|
|
614
|
+
readonly transformHead?: (html: string) => Promise<string>,
|
|
585
615
|
|};
|
|
586
616
|
|
|
587
617
|
/**
|
|
@@ -601,7 +631,9 @@ export function renderDocument(node: React.Node, options: RenderOptions): Promis
|
|
|
601
631
|
const { pipe, abort } = ReactDOMServer.renderToPipeableStream(node, {
|
|
602
632
|
onShellReady() {
|
|
603
633
|
pipe(queueDestination(queue));
|
|
604
|
-
resolve(
|
|
634
|
+
resolve(
|
|
635
|
+
bodyOf(assembled(queue.chunks(), options.shell, options.transformHead), () => abort()),
|
|
636
|
+
);
|
|
605
637
|
},
|
|
606
638
|
onShellError(error: mixed) {
|
|
607
639
|
reject(error);
|
|
@@ -661,7 +693,7 @@ export function renderWithReadableStream(
|
|
|
661
693
|
const controller = new AbortController();
|
|
662
694
|
return render(node, { onError: options.onError, signal: controller.signal }).then(
|
|
663
695
|
(stream: ByteSource) =>
|
|
664
|
-
bodyOf(assembled(decoded(stream), options.shell), () => {
|
|
696
|
+
bodyOf(assembled(decoded(stream), options.shell, options.transformHead), () => {
|
|
665
697
|
controller.abort();
|
|
666
698
|
}),
|
|
667
699
|
);
|
|
@@ -712,7 +744,9 @@ export async function prerenderDocument(node: React.Node, options: RenderOptions
|
|
|
712
744
|
typeof ReactDOMStatic.prerenderToNodeStream === "function"
|
|
713
745
|
? await ReactDOMStatic.prerenderToNodeStream(node, settings)
|
|
714
746
|
: await ReactDOMStatic.prerender(node, settings);
|
|
715
|
-
return bodyOf(
|
|
747
|
+
return bodyOf(
|
|
748
|
+
assembled(preludeChunks(result.prelude), options.shell, options.transformHead),
|
|
749
|
+
).text();
|
|
716
750
|
}
|
|
717
751
|
|
|
718
752
|
/**
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@uniflowed/router",
|
|
3
|
-
"version": "0.0.0-alpha.
|
|
3
|
+
"version": "0.0.0-alpha.18",
|
|
4
4
|
"description": "The file-system router for Flow React applications: matching, layouts, loaders, navigation, server rendering and hydration.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
@@ -33,7 +33,7 @@
|
|
|
33
33
|
"react-dom": ">=19"
|
|
34
34
|
},
|
|
35
35
|
"dependencies": {
|
|
36
|
-
"@uniflowed/hooks": "0.0.0-alpha.
|
|
37
|
-
"@uniflowed/server": "0.0.0-alpha.
|
|
36
|
+
"@uniflowed/hooks": "0.0.0-alpha.18",
|
|
37
|
+
"@uniflowed/server": "0.0.0-alpha.18"
|
|
38
38
|
}
|
|
39
39
|
}
|
package/server.js
CHANGED
|
@@ -112,6 +112,47 @@ export type RenderOptions = {|
|
|
|
112
112
|
* bytes. `uf dev` reports them in the terminal; a production host logs them.
|
|
113
113
|
*/
|
|
114
114
|
readonly onError?: (error: mixed) => void,
|
|
115
|
+
/**
|
|
116
|
+
* Rewrite the opening chunk — everything up to and including the head —
|
|
117
|
+
* before it goes out.
|
|
118
|
+
*
|
|
119
|
+
* For `uf dev` and nothing else. Vite's `transformIndexHtml` injects
|
|
120
|
+
* `/@vite/client` and the refresh preamble and rewrites asset URLs, and it
|
|
121
|
+
* is a *whole document* hook, so the development server used to collect the
|
|
122
|
+
* page and transform it at the end. That made the one place a developer
|
|
123
|
+
* would notice streaming the one place it did not happen: a slow page showed
|
|
124
|
+
* nothing until it was finished, and `_uf.loading.js` looked broken.
|
|
125
|
+
* See ubugeeei-prod/uf#374.
|
|
126
|
+
*
|
|
127
|
+
* A production host passes nothing here and streams as it always did.
|
|
128
|
+
*
|
|
129
|
+
* # What a plugin that injects into the body gets
|
|
130
|
+
*
|
|
131
|
+
* `transformIndexHtml` is a whole-document hook and this hands it one chunk,
|
|
132
|
+
* so `injectTo` is answered against a document that stops inside `<body>`.
|
|
133
|
+
* Measured against Vite 8.2.2, injecting all four positions into a whole
|
|
134
|
+
* document and into a head-only one:
|
|
135
|
+
*
|
|
136
|
+
* | `injectTo` | whole document | streamed |
|
|
137
|
+
* | -------------- | ------------------- | -------- |
|
|
138
|
+
* | `head-prepend` | after `<head>` | same |
|
|
139
|
+
* | `head` | before `</head>` | same |
|
|
140
|
+
* | `body-prepend` | after `<body>` | same |
|
|
141
|
+
* | `body` | before `</body>` | **after `<body>`** |
|
|
142
|
+
*
|
|
143
|
+
* Nothing is dropped — every tag still reaches the document — but a `body`
|
|
144
|
+
* tag lands at the *top* of the body rather than after the content, because
|
|
145
|
+
* the content has not been rendered yet when the hook runs. For a `<script>`
|
|
146
|
+
* that expects a complete DOM that is a real difference, and it is the price
|
|
147
|
+
* of streaming: a hook that wants the whole document and a server that sends
|
|
148
|
+
* the head first cannot both be satisfied.
|
|
149
|
+
*
|
|
150
|
+
* uf's own injections are `head` and `head-prepend`, and Vite's client is
|
|
151
|
+
* head-injected, so this is about a third-party plugin. `dev-head-transform`
|
|
152
|
+
* in `tests/library` pins the table above, so the day it changes is a failing
|
|
153
|
+
* test rather than a surprise.
|
|
154
|
+
*/
|
|
155
|
+
readonly transformHead?: (html: string) => Promise<string>,
|
|
115
156
|
|};
|
|
116
157
|
|
|
117
158
|
/** The two ids the server writes and the client reads. */
|
|
@@ -285,6 +326,7 @@ export function createRenderer(options: {|
|
|
|
285
326
|
body = await renderDocument(<App url={url} initial={resolved} />, {
|
|
286
327
|
shell: shellFor(assets),
|
|
287
328
|
onError,
|
|
329
|
+
transformHead: settings?.transformHead,
|
|
288
330
|
});
|
|
289
331
|
streaming = true;
|
|
290
332
|
// Recovered before the shell was ready: a `<Suspense>` boundary whose
|
|
@@ -315,6 +357,7 @@ export function createRenderer(options: {|
|
|
|
315
357
|
body = await renderDocument(<App url={url} initial={resolved} />, {
|
|
316
358
|
shell: shellFor(assets),
|
|
317
359
|
onError,
|
|
360
|
+
transformHead: settings?.transformHead,
|
|
318
361
|
});
|
|
319
362
|
}
|
|
320
363
|
|