@uniflowed/router 0.0.0-alpha.18 → 0.0.0-alpha.21
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/client.js +187 -5
- package/handler.js +15 -6
- package/index.js +29 -6
- package/internal/action-wire.js +6 -3
- package/internal/boundaries.js +557 -0
- package/internal/devtools.js +2 -2
- package/internal/diagnostics.js +1 -1
- package/internal/hydration.js +232 -39
- package/internal/inspector.js +615 -0
- package/internal/payload-rows.js +258 -0
- package/internal/payload.js +669 -0
- package/internal/runtime.js +754 -50
- package/internal/stream.js +87 -17
- package/middleware.js +3 -3
- package/package.json +5 -4
- package/server.js +72 -17
package/internal/hydration.js
CHANGED
|
@@ -176,6 +176,78 @@ const TEXT_NODE = 3;
|
|
|
176
176
|
const ELEMENT_NODE = 1;
|
|
177
177
|
const COMMENT_NODE = 8;
|
|
178
178
|
|
|
179
|
+
type DetachedHeadStyle = {|
|
|
180
|
+
readonly anchor: Comment,
|
|
181
|
+
readonly style: HTMLStyleElement,
|
|
182
|
+
|};
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Hide development-only head artifacts before React compares the document.
|
|
186
|
+
*
|
|
187
|
+
* `uf dev` injects the React DevTools hook, the Fast Refresh preamble and
|
|
188
|
+
* Vite's dev styles into the head before the client entry. By the time
|
|
189
|
+
* `hydrate` runs the scripts have done their job; leaving them in a
|
|
190
|
+
* document-root app makes React compare bytes the application never rendered
|
|
191
|
+
* and report a hydration mismatch for uf's own instrumentation.
|
|
192
|
+
*
|
|
193
|
+
* The styles are different: the page still needs them. They are detached only
|
|
194
|
+
* for the instant in which React claims the document, and the returned function
|
|
195
|
+
* puts them back in the same order.
|
|
196
|
+
*/
|
|
197
|
+
export function prepareDevHeadForHydration(document: Document): () => void {
|
|
198
|
+
for (const script of document.head.querySelectorAll("script")) {
|
|
199
|
+
if (isDevHeadScript(script)) {
|
|
200
|
+
script.remove();
|
|
201
|
+
}
|
|
202
|
+
}
|
|
203
|
+
for (const child of Array.from(document.head.childNodes)) {
|
|
204
|
+
if (isIgnorableHeadWhitespace(child)) {
|
|
205
|
+
child.remove();
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
const detached = [];
|
|
209
|
+
for (const style of document.head.querySelectorAll("style")) {
|
|
210
|
+
if (isViteDevStyle(style)) {
|
|
211
|
+
const anchor = document.createComment("uf dev style");
|
|
212
|
+
document.head.insertBefore(anchor, style);
|
|
213
|
+
style.remove();
|
|
214
|
+
detached.push({ anchor, style });
|
|
215
|
+
}
|
|
216
|
+
}
|
|
217
|
+
return () => restoreDevHeadStyles(document, detached);
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
function isDevHeadScript(script: HTMLScriptElement): boolean {
|
|
221
|
+
if (script.getAttribute("data-uf-dev-head-preamble") != null) {
|
|
222
|
+
return true;
|
|
223
|
+
}
|
|
224
|
+
const src = script.getAttribute("src");
|
|
225
|
+
if (src == null) {
|
|
226
|
+
return false;
|
|
227
|
+
}
|
|
228
|
+
const path = src.startsWith("http") ? new URL(src).pathname : src;
|
|
229
|
+
return path === "/@vite/client" || path === "/@id/__x00__virtual:uf/client";
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
function isViteDevStyle(style: HTMLStyleElement): boolean {
|
|
233
|
+
return style.getAttribute("data-vite-dev-id") != null;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
function restoreDevHeadStyles(
|
|
237
|
+
document: Document,
|
|
238
|
+
detached: $ReadOnlyArray<DetachedHeadStyle>,
|
|
239
|
+
): void {
|
|
240
|
+
for (const { anchor, style } of detached) {
|
|
241
|
+
const parent = anchor.parentNode;
|
|
242
|
+
if (parent != null) {
|
|
243
|
+
parent.insertBefore(style, anchor);
|
|
244
|
+
parent.removeChild(anchor);
|
|
245
|
+
} else {
|
|
246
|
+
document.head.appendChild(style);
|
|
247
|
+
}
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
179
251
|
/**
|
|
180
252
|
* What the server sent, taken out of the document before React touches it.
|
|
181
253
|
*
|
|
@@ -233,11 +305,43 @@ function parseServerMarkup(
|
|
|
233
305
|
function children(node: Node): Array<Node> {
|
|
234
306
|
const out = [];
|
|
235
307
|
for (const child of node.childNodes) {
|
|
308
|
+
if (isDevHeadArtifact(child)) {
|
|
309
|
+
continue;
|
|
310
|
+
}
|
|
236
311
|
out.push(child);
|
|
237
312
|
}
|
|
238
313
|
return out;
|
|
239
314
|
}
|
|
240
315
|
|
|
316
|
+
function isDevHeadArtifact(node: Node): boolean {
|
|
317
|
+
if (isIgnorableHeadWhitespace(node)) {
|
|
318
|
+
return true;
|
|
319
|
+
}
|
|
320
|
+
if (node.nodeType === COMMENT_NODE && node.textContent === "uf dev style") {
|
|
321
|
+
return true;
|
|
322
|
+
}
|
|
323
|
+
if (node.nodeType !== ELEMENT_NODE) {
|
|
324
|
+
return false;
|
|
325
|
+
}
|
|
326
|
+
const element = node as $FlowFixMe as Element;
|
|
327
|
+
if (element.tagName === "STYLE" && element.getAttribute("data-vite-dev-id") != null) {
|
|
328
|
+
return true;
|
|
329
|
+
}
|
|
330
|
+
if (element.tagName !== "SCRIPT") {
|
|
331
|
+
return false;
|
|
332
|
+
}
|
|
333
|
+
return isDevHeadScript(element as $FlowFixMe as HTMLScriptElement);
|
|
334
|
+
}
|
|
335
|
+
|
|
336
|
+
function isIgnorableHeadWhitespace(node: Node): boolean {
|
|
337
|
+
return (
|
|
338
|
+
node.nodeType === TEXT_NODE &&
|
|
339
|
+
node.parentNode != null &&
|
|
340
|
+
(node.parentNode as $FlowFixMe as Element).tagName === "HEAD" &&
|
|
341
|
+
(node.textContent ?? "").trim() === ""
|
|
342
|
+
);
|
|
343
|
+
}
|
|
344
|
+
|
|
241
345
|
/**
|
|
242
346
|
* Whether a message React handed to `onRecoverableError` is about hydration.
|
|
243
347
|
*
|
|
@@ -656,34 +760,48 @@ function isHostElement(name: string): boolean {
|
|
|
656
760
|
*/
|
|
657
761
|
export function formatHydrationReport(report: HydrationReport): string {
|
|
658
762
|
const lines = [];
|
|
659
|
-
|
|
660
|
-
lines.push(
|
|
763
|
+
lines.push(reportTitle(report));
|
|
764
|
+
lines.push("");
|
|
765
|
+
lines.push(
|
|
766
|
+
"The server HTML and the browser's first render disagreed. React repaired the page, but the first paint may not be the UI you meant to ship.",
|
|
767
|
+
);
|
|
661
768
|
lines.push("");
|
|
662
769
|
const difference = report.difference;
|
|
663
770
|
if (difference == null) {
|
|
771
|
+
lines.push("What changed");
|
|
664
772
|
lines.push(report.note ?? "There is nothing to compare.");
|
|
665
773
|
} else {
|
|
666
|
-
lines.push(
|
|
774
|
+
lines.push("What changed");
|
|
775
|
+
lines.push(`path ${difference.path}`);
|
|
667
776
|
if (difference.attribute != null) {
|
|
668
|
-
lines.push(`
|
|
777
|
+
lines.push(`attribute ${difference.attribute}`);
|
|
669
778
|
}
|
|
670
|
-
lines.push(`server
|
|
671
|
-
lines.push(`
|
|
779
|
+
lines.push(`server rendered ${difference.server ?? "(nothing)"}`);
|
|
780
|
+
lines.push(`browser rendered ${difference.client ?? "(nothing)"}`);
|
|
672
781
|
}
|
|
673
782
|
if (report.explanation !== "") {
|
|
674
783
|
lines.push("");
|
|
784
|
+
lines.push("Likely cause");
|
|
675
785
|
lines.push(report.explanation);
|
|
786
|
+
lines.push("");
|
|
787
|
+
lines.push("Try this next");
|
|
676
788
|
lines.push(report.remedy);
|
|
677
789
|
}
|
|
678
790
|
if (report.components.length > 1) {
|
|
679
791
|
lines.push("");
|
|
680
|
-
lines.push(`
|
|
792
|
+
lines.push(`Component trail: ${report.components.join(" < ")}`);
|
|
681
793
|
}
|
|
682
794
|
lines.push("");
|
|
683
|
-
lines.push(`React
|
|
795
|
+
lines.push(`React's original message: ${headline(report.message)}`);
|
|
684
796
|
return lines.join("\n");
|
|
685
797
|
}
|
|
686
798
|
|
|
799
|
+
function reportTitle(report: HydrationReport): string {
|
|
800
|
+
return report.components.length > 0
|
|
801
|
+
? `Hydration mismatch in <${report.components[0]}>`
|
|
802
|
+
: "Hydration mismatch";
|
|
803
|
+
}
|
|
804
|
+
|
|
687
805
|
/**
|
|
688
806
|
* React's sentence, without the list of causes underneath it.
|
|
689
807
|
*
|
|
@@ -707,26 +825,61 @@ const OVERLAY_STYLE = `
|
|
|
707
825
|
position: fixed;
|
|
708
826
|
inset: auto 1rem 1rem 1rem;
|
|
709
827
|
z-index: 2147483647;
|
|
710
|
-
max-height:
|
|
828
|
+
max-height: min(78vh, 720px);
|
|
711
829
|
overflow: auto;
|
|
712
|
-
padding: 1rem
|
|
713
|
-
border: 1px solid #
|
|
714
|
-
border-radius:
|
|
715
|
-
background: #
|
|
716
|
-
color: #
|
|
717
|
-
font:
|
|
718
|
-
box-shadow: 0
|
|
719
|
-
}
|
|
720
|
-
|
|
721
|
-
|
|
722
|
-
|
|
723
|
-
|
|
724
|
-
|
|
725
|
-
|
|
830
|
+
padding: 1rem;
|
|
831
|
+
border: 1px solid #fecaca;
|
|
832
|
+
border-radius: 8px;
|
|
833
|
+
background: #fffafa;
|
|
834
|
+
color: #1f2937;
|
|
835
|
+
font: 14px/1.55 ui-sans-serif, system-ui, -apple-system, BlinkMacSystemFont, "Segoe UI", sans-serif;
|
|
836
|
+
box-shadow: 0 18px 60px rgba(15, 23, 42, 0.24);
|
|
837
|
+
}
|
|
838
|
+
.top { display: grid; gap: 0.35rem; padding-right: 2rem; }
|
|
839
|
+
.badge {
|
|
840
|
+
color: #be123c;
|
|
841
|
+
font-size: 0.75rem;
|
|
842
|
+
font-weight: 800;
|
|
843
|
+
letter-spacing: 0;
|
|
844
|
+
text-transform: uppercase;
|
|
845
|
+
}
|
|
846
|
+
h2 { margin: 0; color: #111827; font-size: 1.05rem; line-height: 1.25; }
|
|
847
|
+
h3 { margin: 0 0 0.45rem; color: #9f1239; font-size: 0.82rem; line-height: 1.25; }
|
|
848
|
+
.lead { margin: 0; color: #4b5563; }
|
|
849
|
+
section {
|
|
850
|
+
margin-top: 0.85rem;
|
|
851
|
+
padding-top: 0.85rem;
|
|
852
|
+
border-top: 1px solid #fee2e2;
|
|
853
|
+
}
|
|
854
|
+
dl {
|
|
855
|
+
display: grid;
|
|
856
|
+
grid-template-columns: max-content minmax(0, 1fr);
|
|
857
|
+
gap: 0.35rem 0.85rem;
|
|
858
|
+
margin: 0;
|
|
859
|
+
}
|
|
860
|
+
dt { color: #9f1239; font-weight: 800; }
|
|
861
|
+
dd {
|
|
862
|
+
margin: 0;
|
|
863
|
+
color: #111827;
|
|
864
|
+
overflow-wrap: anywhere;
|
|
865
|
+
white-space: pre-wrap;
|
|
866
|
+
font-family: ui-monospace, SFMono-Regular, Menlo, monospace;
|
|
867
|
+
font-size: 0.82rem;
|
|
868
|
+
}
|
|
869
|
+
p { margin: 0; color: #374151; }
|
|
870
|
+
.react { color: #6b7280; font-size: 0.82rem; }
|
|
726
871
|
button {
|
|
727
|
-
position: absolute; top: 0.
|
|
728
|
-
border:
|
|
729
|
-
|
|
872
|
+
position: absolute; top: 0.65rem; right: 0.65rem;
|
|
873
|
+
border: 1px solid #fecaca;
|
|
874
|
+
border-radius: 8px;
|
|
875
|
+
background: #ffffff;
|
|
876
|
+
cursor: pointer;
|
|
877
|
+
font: inherit;
|
|
878
|
+
color: #9f1239;
|
|
879
|
+
padding: 0.3rem 0.55rem;
|
|
880
|
+
}
|
|
881
|
+
@media (min-width: 760px) {
|
|
882
|
+
.panel { left: auto; width: min(680px, calc(100vw - 2rem)); }
|
|
730
883
|
}
|
|
731
884
|
`;
|
|
732
885
|
|
|
@@ -758,17 +911,34 @@ export function showHydrationReport(report: HydrationReport, document: Document)
|
|
|
758
911
|
|
|
759
912
|
const panel = document.createElement("div");
|
|
760
913
|
panel.className = "panel";
|
|
914
|
+
panel.setAttribute("role", "dialog");
|
|
915
|
+
panel.setAttribute("aria-modal", "false");
|
|
916
|
+
panel.setAttribute("aria-labelledby", "uf-hydration-title");
|
|
917
|
+
panel.setAttribute("aria-describedby", "uf-hydration-lead");
|
|
761
918
|
|
|
919
|
+
const top = document.createElement("div");
|
|
920
|
+
top.className = "top";
|
|
921
|
+
const badge = document.createElement("div");
|
|
922
|
+
badge.className = "badge";
|
|
923
|
+
badge.textContent = "uf dev hydration report";
|
|
924
|
+
top.appendChild(badge);
|
|
762
925
|
const heading = document.createElement("h2");
|
|
763
|
-
heading.
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
926
|
+
heading.id = "uf-hydration-title";
|
|
927
|
+
heading.textContent = reportTitle(report);
|
|
928
|
+
top.appendChild(heading);
|
|
929
|
+
const lead = paragraph(
|
|
930
|
+
document,
|
|
931
|
+
"The server HTML and the browser's first render did not match. React repaired it, but this is the first place to look.",
|
|
932
|
+
"lead",
|
|
933
|
+
);
|
|
934
|
+
lead.id = "uf-hydration-lead";
|
|
935
|
+
top.appendChild(lead);
|
|
936
|
+
panel.appendChild(top);
|
|
768
937
|
|
|
769
938
|
const dismiss = document.createElement("button");
|
|
770
939
|
dismiss.type = "button";
|
|
771
|
-
dismiss.textContent = "
|
|
940
|
+
dismiss.textContent = "Close";
|
|
941
|
+
dismiss.setAttribute("aria-label", "Close hydration report");
|
|
772
942
|
dismiss.addEventListener("click", () => host.remove());
|
|
773
943
|
panel.appendChild(dismiss);
|
|
774
944
|
|
|
@@ -784,21 +954,44 @@ export function showHydrationReport(report: HydrationReport, document: Document)
|
|
|
784
954
|
addRow(document, rows, "server", difference.server ?? "(nothing)");
|
|
785
955
|
addRow(document, rows, "client", difference.client ?? "(nothing)");
|
|
786
956
|
}
|
|
787
|
-
|
|
788
|
-
addRow(document, rows, "rendered by", report.components.join(" < "));
|
|
789
|
-
}
|
|
790
|
-
panel.appendChild(rows);
|
|
957
|
+
panel.appendChild(section(document, "What changed", rows));
|
|
791
958
|
|
|
792
959
|
if (report.explanation !== "") {
|
|
793
|
-
panel.appendChild(
|
|
794
|
-
|
|
960
|
+
panel.appendChild(
|
|
961
|
+
section(document, "Likely cause", paragraph(document, report.explanation, null)),
|
|
962
|
+
);
|
|
963
|
+
panel.appendChild(section(document, "Try this next", paragraph(document, report.remedy, null)));
|
|
795
964
|
}
|
|
796
|
-
|
|
965
|
+
if (report.components.length > 1) {
|
|
966
|
+
panel.appendChild(
|
|
967
|
+
section(
|
|
968
|
+
document,
|
|
969
|
+
"Component trail",
|
|
970
|
+
paragraph(document, report.components.join(" < "), null),
|
|
971
|
+
),
|
|
972
|
+
);
|
|
973
|
+
}
|
|
974
|
+
panel.appendChild(
|
|
975
|
+
section(
|
|
976
|
+
document,
|
|
977
|
+
"React's original message",
|
|
978
|
+
paragraph(document, headline(report.message), "react"),
|
|
979
|
+
),
|
|
980
|
+
);
|
|
797
981
|
|
|
798
982
|
root.appendChild(panel);
|
|
799
983
|
body.appendChild(host);
|
|
800
984
|
}
|
|
801
985
|
|
|
986
|
+
function section(document: Document, title: string, child: Element): Element {
|
|
987
|
+
const element = document.createElement("section");
|
|
988
|
+
const heading = document.createElement("h3");
|
|
989
|
+
heading.textContent = title;
|
|
990
|
+
element.appendChild(heading);
|
|
991
|
+
element.appendChild(child);
|
|
992
|
+
return element;
|
|
993
|
+
}
|
|
994
|
+
|
|
802
995
|
function addRow(document: Document, rows: Element, label: string, value: string): void {
|
|
803
996
|
const term = document.createElement("dt");
|
|
804
997
|
term.textContent = label;
|