@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.
@@ -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
- const where = report.components.length > 0 ? ` in <${report.components[0]}>` : "";
660
- lines.push(`Hydration mismatch${where}`);
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(`at ${difference.path}`);
774
+ lines.push("What changed");
775
+ lines.push(`path ${difference.path}`);
667
776
  if (difference.attribute != null) {
668
- lines.push(`in the ${difference.attribute} attribute`);
777
+ lines.push(`attribute ${difference.attribute}`);
669
778
  }
670
- lines.push(`server ${difference.server ?? "(nothing)"}`);
671
- lines.push(`client ${difference.client ?? "(nothing)"}`);
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(`Rendered by: ${report.components.join(" < ")}`);
792
+ lines.push(`Component trail: ${report.components.join(" < ")}`);
681
793
  }
682
794
  lines.push("");
683
- lines.push(`React said: ${headline(report.message)}`);
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: 60vh;
828
+ max-height: min(78vh, 720px);
711
829
  overflow: auto;
712
- padding: 1rem 1.25rem;
713
- border: 1px solid #f0b9b9;
714
- border-radius: 6px;
715
- background: #fff5f5;
716
- color: #2b1b1b;
717
- font: 13px/1.55 ui-monospace, SFMono-Regular, Menlo, monospace;
718
- box-shadow: 0 8px 30px rgba(0, 0, 0, 0.18);
719
- }
720
- h2 { margin: 0 0 0.5rem; font-size: 13px; font-weight: 700; }
721
- dl { display: grid; grid-template-columns: max-content 1fr; gap: 0.15rem 1rem; margin: 0 0 0.75rem; }
722
- dt { color: #8a4b4b; }
723
- dd { margin: 0; overflow-wrap: anywhere; white-space: pre-wrap; }
724
- p { margin: 0 0 0.5rem; font-family: ui-sans-serif, system-ui, sans-serif; }
725
- .react { color: #6b5555; }
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.5rem; right: 0.5rem;
728
- border: 0; background: transparent; cursor: pointer;
729
- font: inherit; color: #8a4b4b;
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.textContent =
764
- report.components.length > 0
765
- ? `Hydration mismatch in <${report.components[0]}>`
766
- : "Hydration mismatch";
767
- panel.appendChild(heading);
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 = "close";
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
- if (report.components.length > 1) {
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(paragraph(document, report.explanation, null));
794
- panel.appendChild(paragraph(document, report.remedy, null));
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
- panel.appendChild(paragraph(document, `React said: ${headline(report.message)}`, "react"));
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;