@uniflowed/router 0.0.0-alpha.42 → 0.0.0-alpha.44
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 +121 -15
- package/package.json +3 -3
package/internal/stream.js
CHANGED
|
@@ -311,9 +311,16 @@ async function* assembled(
|
|
|
311
311
|
chunks: AsyncGenerator<string, void, void>,
|
|
312
312
|
shell: DocumentShell,
|
|
313
313
|
transformHead?: (html: string) => Promise<string>,
|
|
314
|
+
layout?: Layout,
|
|
314
315
|
): AsyncGenerator<string, void, void> {
|
|
315
316
|
let held = "";
|
|
316
317
|
let shape = "unknown";
|
|
318
|
+
// A document uf wraps is the only one whose root is not `<body>` itself.
|
|
319
|
+
const wrapped = () => {
|
|
320
|
+
if (layout != null) {
|
|
321
|
+
layout.rootDepth = advanced(NOTHING_WRITTEN, shell.open + shell.body, 0).depth ?? 0;
|
|
322
|
+
}
|
|
323
|
+
};
|
|
317
324
|
// The opening chunk is the only one the hook sees, and every path below
|
|
318
325
|
// reaches exactly one of them. Awaiting here rather than at each `yield`
|
|
319
326
|
// keeps them from drifting apart. The hook sees only the document opening:
|
|
@@ -343,6 +350,7 @@ async function* assembled(
|
|
|
343
350
|
continue;
|
|
344
351
|
}
|
|
345
352
|
shape = "shell-open";
|
|
353
|
+
wrapped();
|
|
346
354
|
yield await opening(shell.open + split.head + shell.body, split.rest);
|
|
347
355
|
held = "";
|
|
348
356
|
continue;
|
|
@@ -382,6 +390,7 @@ async function* assembled(
|
|
|
382
390
|
// still open is over and whatever was left of it is markup like any other.
|
|
383
391
|
const split = hoisted(held);
|
|
384
392
|
shape = "shell-open";
|
|
393
|
+
wrapped();
|
|
385
394
|
yield await opening(shell.open + split.head + shell.body, split.rest);
|
|
386
395
|
}
|
|
387
396
|
if (shape === "shell-open") {
|
|
@@ -721,7 +730,9 @@ export function renderDocument(node: React.Node, options: RenderOptions): Promis
|
|
|
721
730
|
bodyOf(
|
|
722
731
|
outgoing(
|
|
723
732
|
withPayload(
|
|
724
|
-
|
|
733
|
+
queue.chunks(),
|
|
734
|
+
options.shell,
|
|
735
|
+
options.transformHead,
|
|
725
736
|
options.payload,
|
|
726
737
|
options.nonce,
|
|
727
738
|
),
|
|
@@ -801,7 +812,9 @@ export function renderWithReadableStream(
|
|
|
801
812
|
bodyOf(
|
|
802
813
|
outgoing(
|
|
803
814
|
withPayload(
|
|
804
|
-
|
|
815
|
+
decoded(stream),
|
|
816
|
+
options.shell,
|
|
817
|
+
options.transformHead,
|
|
805
818
|
options.payload,
|
|
806
819
|
options.nonce,
|
|
807
820
|
),
|
|
@@ -861,26 +874,54 @@ export async function prerenderDocument(node: React.Node, options: RenderOptions
|
|
|
861
874
|
: await ReactDOMStatic.prerender(node, settings);
|
|
862
875
|
return bodyOf(
|
|
863
876
|
withPayload(
|
|
864
|
-
|
|
877
|
+
preludeChunks(result.prelude),
|
|
878
|
+
options.shell,
|
|
879
|
+
options.transformHead,
|
|
865
880
|
options.payload,
|
|
866
881
|
),
|
|
867
882
|
).text();
|
|
868
883
|
}
|
|
869
884
|
|
|
870
|
-
/**
|
|
885
|
+
/**
|
|
886
|
+
* What [`assembled`] learns about a document that [`interleaved`] needs.
|
|
887
|
+
*
|
|
888
|
+
* Which shape a document has is decided on its opening bytes, inside
|
|
889
|
+
* `assembled`, and `interleaved` reads that document's chunks — so by the time
|
|
890
|
+
* it has HTML to follow, the answer is here.
|
|
891
|
+
*/
|
|
892
|
+
type Layout = {|
|
|
893
|
+
/**
|
|
894
|
+
* How many elements are open inside `<body>` where React's root begins: none
|
|
895
|
+
* for a document React wrote, and uf's own container for one uf wraps.
|
|
896
|
+
*/
|
|
897
|
+
rootDepth: number,
|
|
898
|
+
|};
|
|
899
|
+
|
|
900
|
+
/** [`assembled`] alone when there is no payload, and [`interleaved`] with one. */
|
|
871
901
|
function withPayload(
|
|
872
902
|
chunks: AsyncGenerator<string, void, void>,
|
|
903
|
+
shell: DocumentShell,
|
|
904
|
+
transformHead: ?(html: string) => Promise<string>,
|
|
873
905
|
payload: ?ReadableStream<Uint8Array>,
|
|
874
906
|
nonce?: string | null,
|
|
875
907
|
): AsyncGenerator<string, void, void> {
|
|
876
|
-
|
|
908
|
+
if (payload == null) {
|
|
909
|
+
return assembled(chunks, shell, transformHead ?? undefined);
|
|
910
|
+
}
|
|
911
|
+
const layout: Layout = { rootDepth: 0 };
|
|
912
|
+
return interleaved(
|
|
913
|
+
assembled(chunks, shell, transformHead ?? undefined, layout),
|
|
914
|
+
payload,
|
|
915
|
+
nonce,
|
|
916
|
+
layout,
|
|
917
|
+
);
|
|
877
918
|
}
|
|
878
919
|
|
|
879
920
|
/**
|
|
880
921
|
* A document's chunks, with the Flight payload it was rendered from written
|
|
881
922
|
* into it as it arrives.
|
|
882
923
|
*
|
|
883
|
-
*
|
|
924
|
+
* Six rules, and each is the answer to a way the obvious version is wrong.
|
|
884
925
|
*
|
|
885
926
|
* **Nothing before the head.** The first chunk this is handed is the whole
|
|
886
927
|
* opening of the document — `assembled` does not let one go until the head is
|
|
@@ -906,6 +947,19 @@ function withPayload(
|
|
|
906
947
|
* from chunk to chunk, and otherwise waits for the HTML that finishes what is
|
|
907
948
|
* open.
|
|
908
949
|
*
|
|
950
|
+
* **Only where React steps over it.** Between elements is where the *parser*
|
|
951
|
+
* keeps a payload element. React is stricter: hydrating, it steps over an
|
|
952
|
+
* element it did not render only among the children of its root and of
|
|
953
|
+
* `<html>`, `<head>` and `<body>`. Anywhere deeper the element is a child the
|
|
954
|
+
* tree has no fiber for, the hydration fails there, and React throws the
|
|
955
|
+
* server's markup away and renders the document again. A shell that is one
|
|
956
|
+
* large element — a frame around the page, all of it rendered on the server —
|
|
957
|
+
* is written in many pieces, several of which end between two of its children,
|
|
958
|
+
* and a payload that was already waiting used to go out at the first of them.
|
|
959
|
+
* So [`advanced`] also follows how many elements are open inside `<body>`, and
|
|
960
|
+
* a payload waits for the root's own level: between React's top-level
|
|
961
|
+
* elements, which is where every streamed segment is written anyway.
|
|
962
|
+
*
|
|
909
963
|
* **Outside replaceable boundaries.** Even between tags, a payload inside a
|
|
910
964
|
* Suspense fallback disappears when React reveals its content. A browser that
|
|
911
965
|
* loads the Flight reader afterwards then sees an incomplete stream. React's
|
|
@@ -927,6 +981,7 @@ async function* interleaved(
|
|
|
927
981
|
chunks: AsyncGenerator<string, void, void>,
|
|
928
982
|
payload: ReadableStream<Uint8Array>,
|
|
929
983
|
nonce?: string | null,
|
|
984
|
+
layout?: Layout,
|
|
930
985
|
): AsyncGenerator<string, void, void> {
|
|
931
986
|
const encoder = createChunkEncoder(nonce);
|
|
932
987
|
const reader = payload.getReader();
|
|
@@ -992,7 +1047,7 @@ async function* interleaved(
|
|
|
992
1047
|
}
|
|
993
1048
|
if (text !== "") {
|
|
994
1049
|
yield text;
|
|
995
|
-
boundary = advanced(boundary, text);
|
|
1050
|
+
boundary = advanced(boundary, text, layout?.rootDepth ?? 0);
|
|
996
1051
|
}
|
|
997
1052
|
next = chunks.next();
|
|
998
1053
|
}
|
|
@@ -1029,9 +1084,35 @@ type Boundary = {|
|
|
|
1029
1084
|
readonly replaceable: number,
|
|
1030
1085
|
/** A tag, or a comment, the HTML has started and not yet finished. */
|
|
1031
1086
|
readonly open: string,
|
|
1087
|
+
/** How many elements are open inside `<body>`; `null` until it has opened. */
|
|
1088
|
+
readonly depth: number | null,
|
|
1032
1089
|
|};
|
|
1033
1090
|
|
|
1034
|
-
const NOTHING_WRITTEN: Boundary = {
|
|
1091
|
+
const NOTHING_WRITTEN: Boundary = {
|
|
1092
|
+
safe: false,
|
|
1093
|
+
rawText: null,
|
|
1094
|
+
replaceable: 0,
|
|
1095
|
+
open: "",
|
|
1096
|
+
depth: null,
|
|
1097
|
+
};
|
|
1098
|
+
|
|
1099
|
+
/** Elements with no closing tag, for markup whose `/>` React did not write. */
|
|
1100
|
+
const VOID_ELEMENTS: $ReadOnlySet<string> = new Set([
|
|
1101
|
+
"area",
|
|
1102
|
+
"base",
|
|
1103
|
+
"br",
|
|
1104
|
+
"col",
|
|
1105
|
+
"embed",
|
|
1106
|
+
"hr",
|
|
1107
|
+
"img",
|
|
1108
|
+
"input",
|
|
1109
|
+
"link",
|
|
1110
|
+
"meta",
|
|
1111
|
+
"param",
|
|
1112
|
+
"source",
|
|
1113
|
+
"track",
|
|
1114
|
+
"wbr",
|
|
1115
|
+
]);
|
|
1035
1116
|
|
|
1036
1117
|
/**
|
|
1037
1118
|
* `boundary`, once `html` has been written after it.
|
|
@@ -1044,12 +1125,21 @@ const NOTHING_WRITTEN: Boundary = { safe: false, rawText: null, replaceable: 0,
|
|
|
1044
1125
|
* the content of an inline script or stylesheet, where a `>` is code, so an
|
|
1045
1126
|
* opening `<script>` or `<style>` is followed to its closing tag. A tag split
|
|
1046
1127
|
* across two chunks is carried in `open` and read whole with the next one.
|
|
1128
|
+
*
|
|
1129
|
+
* And it may follow only HTML that leaves `rootDepth` elements open inside
|
|
1130
|
+
* `<body>`, or none: the children of React's root and of `<body>` are the ones
|
|
1131
|
+
* hydration steps over. Every tag from `<body>` on is counted for that, React's
|
|
1132
|
+
* void elements by the `/>` it writes them with. Markup React did not write —
|
|
1133
|
+
* `dangerouslySetInnerHTML` — can leave a tag open that the parser would close,
|
|
1134
|
+
* and the count then stays too deep for the rest of the document. That errs the
|
|
1135
|
+
* safe way: the payload waits, and goes out before `</body>` as it always can.
|
|
1047
1136
|
*/
|
|
1048
|
-
function advanced(boundary: Boundary, html: string): Boundary {
|
|
1137
|
+
function advanced(boundary: Boundary, html: string, rootDepth: number): Boundary {
|
|
1049
1138
|
const text = boundary.open + html;
|
|
1050
1139
|
let rawText = boundary.rawText;
|
|
1051
1140
|
let replaceable = boundary.replaceable;
|
|
1052
|
-
|
|
1141
|
+
let depth = boundary.depth;
|
|
1142
|
+
const tags = /<!--([\s\S]*?)-->|<(\/?)([a-z][^\s/>]*)[^>]*?(\/?)>/gi;
|
|
1053
1143
|
let tag = tags.exec(text);
|
|
1054
1144
|
while (tag != null) {
|
|
1055
1145
|
const comment = tag[1];
|
|
@@ -1060,20 +1150,36 @@ function advanced(boundary: Boundary, html: string): Boundary {
|
|
|
1060
1150
|
}
|
|
1061
1151
|
} else {
|
|
1062
1152
|
const name = tag[3].toLowerCase();
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
rawText
|
|
1153
|
+
const closes = tag[2] === "/";
|
|
1154
|
+
if (rawText != null) {
|
|
1155
|
+
// Inside a script or a stylesheet a `<` is code, up to its own end tag.
|
|
1156
|
+
if (closes && rawText === name) {
|
|
1157
|
+
rawText = null;
|
|
1158
|
+
if (depth != null) depth -= 1;
|
|
1159
|
+
}
|
|
1160
|
+
} else if (closes) {
|
|
1161
|
+
if (depth != null) depth -= 1;
|
|
1162
|
+
} else if (depth == null) {
|
|
1163
|
+
if (name === "body") depth = 0;
|
|
1164
|
+
else if (name === "script" || name === "style") rawText = name;
|
|
1165
|
+
} else if (tag[4] !== "/" && !VOID_ELEMENTS.has(name)) {
|
|
1166
|
+
depth += 1;
|
|
1167
|
+
if (name === "script" || name === "style") rawText = name;
|
|
1067
1168
|
}
|
|
1068
1169
|
}
|
|
1069
1170
|
tag = tags.exec(text);
|
|
1070
1171
|
}
|
|
1071
1172
|
const start = text.lastIndexOf("<");
|
|
1072
1173
|
return {
|
|
1073
|
-
safe:
|
|
1174
|
+
safe:
|
|
1175
|
+
rawText == null &&
|
|
1176
|
+
replaceable === 0 &&
|
|
1177
|
+
text.endsWith(">") &&
|
|
1178
|
+
(depth === 0 || (depth != null && depth === rootDepth)),
|
|
1074
1179
|
rawText,
|
|
1075
1180
|
replaceable,
|
|
1076
1181
|
open: start > text.lastIndexOf(">") ? text.slice(start) : "",
|
|
1182
|
+
depth,
|
|
1077
1183
|
};
|
|
1078
1184
|
}
|
|
1079
1185
|
|
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.44",
|
|
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",
|
|
@@ -71,7 +71,7 @@
|
|
|
71
71
|
}
|
|
72
72
|
},
|
|
73
73
|
"dependencies": {
|
|
74
|
-
"@uniflowed/hooks": "0.0.0-alpha.
|
|
75
|
-
"@uniflowed/server": "0.0.0-alpha.
|
|
74
|
+
"@uniflowed/hooks": "0.0.0-alpha.44",
|
|
75
|
+
"@uniflowed/server": "0.0.0-alpha.44"
|
|
76
76
|
}
|
|
77
77
|
}
|