@uniflowed/router 0.0.0-alpha.41 → 0.0.0-alpha.43
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/instrumentation.js +92 -0
- package/internal/action-endpoint.js +4 -1
- package/internal/resolve.js +5 -1
- package/internal/runtime.js +8 -2
- package/internal/server-instrumentation.js +12 -0
- package/internal/stream.js +121 -15
- package/package.json +6 -4
- package/rsc.js +18 -4
- package/server.js +13 -1
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
export type NavigationTiming = {|
|
|
4
|
+
readonly kind: "document" | "router",
|
|
5
|
+
readonly pathname: string,
|
|
6
|
+
readonly duration: number,
|
|
7
|
+
readonly status: "complete" | "error",
|
|
8
|
+
|};
|
|
9
|
+
|
|
10
|
+
export type ClientInstrumentation = {|
|
|
11
|
+
readonly register?: () => void | Promise<void>,
|
|
12
|
+
readonly onError?: (
|
|
13
|
+
error: mixed,
|
|
14
|
+
context: {| readonly source: "error" | "unhandledrejection" | "navigation" | "startup" |},
|
|
15
|
+
) => void | Promise<void>,
|
|
16
|
+
readonly onNavigation?: (timing: NavigationTiming) => void | Promise<void>,
|
|
17
|
+
|};
|
|
18
|
+
|
|
19
|
+
const observers: Set<ClientInstrumentation> = new Set();
|
|
20
|
+
|
|
21
|
+
function notify(body: () => mixed): void {
|
|
22
|
+
Promise.resolve()
|
|
23
|
+
.then(body)
|
|
24
|
+
.catch((error) => console.error("uf client instrumentation failed", error));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/** Installed by the client entry before hydration, with disposal during HMR. */
|
|
28
|
+
export async function installClientInstrumentation(
|
|
29
|
+
hooks: ClientInstrumentation,
|
|
30
|
+
): Promise<() => void> {
|
|
31
|
+
const error = (event: ErrorEvent) =>
|
|
32
|
+
notify(() => hooks.onError?.(event.error ?? event.message, { source: "error" }));
|
|
33
|
+
const rejection = (event: PromiseRejectionEvent) =>
|
|
34
|
+
notify(() => hooks.onError?.(event.reason, { source: "unhandledrejection" }));
|
|
35
|
+
window.addEventListener("error", error);
|
|
36
|
+
window.addEventListener("unhandledrejection", rejection);
|
|
37
|
+
observers.add(hooks);
|
|
38
|
+
let performanceObserver = null;
|
|
39
|
+
if (
|
|
40
|
+
typeof PerformanceObserver === "function" &&
|
|
41
|
+
PerformanceObserver.supportedEntryTypes.includes("navigation")
|
|
42
|
+
) {
|
|
43
|
+
performanceObserver = new PerformanceObserver((list) => {
|
|
44
|
+
for (const entry of list.getEntries()) {
|
|
45
|
+
const timing: NavigationTiming = {
|
|
46
|
+
kind: "document",
|
|
47
|
+
pathname: new URL(entry.name).pathname,
|
|
48
|
+
duration: entry.duration,
|
|
49
|
+
status: "complete",
|
|
50
|
+
};
|
|
51
|
+
notify(() => hooks.onNavigation?.(timing));
|
|
52
|
+
}
|
|
53
|
+
});
|
|
54
|
+
performanceObserver.observe({ type: "navigation", buffered: true });
|
|
55
|
+
}
|
|
56
|
+
const dispose = () => {
|
|
57
|
+
window.removeEventListener("error", error);
|
|
58
|
+
window.removeEventListener("unhandledrejection", rejection);
|
|
59
|
+
observers.delete(hooks);
|
|
60
|
+
performanceObserver?.disconnect();
|
|
61
|
+
};
|
|
62
|
+
try {
|
|
63
|
+
await hooks.register?.();
|
|
64
|
+
} catch (failure) {
|
|
65
|
+
notify(() => hooks.onError?.(failure, { source: "startup" }));
|
|
66
|
+
dispose();
|
|
67
|
+
}
|
|
68
|
+
return dispose;
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
/** The router's asynchronous navigation work; a hard navigation has browser timing entries. */
|
|
72
|
+
export async function observeNavigation<T>(to: string, body: () => Promise<T>): Promise<T> {
|
|
73
|
+
if (observers.size === 0) return body();
|
|
74
|
+
const started = performance.now();
|
|
75
|
+
const pathname = new URL(to, window.location.href).pathname;
|
|
76
|
+
let status: NavigationTiming["status"] = "complete";
|
|
77
|
+
try {
|
|
78
|
+
return await body();
|
|
79
|
+
} catch (error) {
|
|
80
|
+
status = "error";
|
|
81
|
+
for (const hooks of observers) notify(() => hooks.onError?.(error, { source: "navigation" }));
|
|
82
|
+
throw error;
|
|
83
|
+
} finally {
|
|
84
|
+
const timing: NavigationTiming = {
|
|
85
|
+
kind: "router",
|
|
86
|
+
pathname,
|
|
87
|
+
duration: performance.now() - started,
|
|
88
|
+
status,
|
|
89
|
+
};
|
|
90
|
+
for (const hooks of observers) notify(() => hooks.onNavigation?.(timing));
|
|
91
|
+
}
|
|
92
|
+
}
|
|
@@ -88,6 +88,8 @@
|
|
|
88
88
|
// it. Written here because this is the file somebody reads before deciding
|
|
89
89
|
// otherwise.
|
|
90
90
|
|
|
91
|
+
import { traceRequestPhase, reportRequestError } from "@uniflowed/server/instrumentation";
|
|
92
|
+
|
|
91
93
|
import { asResponder, nativeActionAllowed } from "@uniflowed/server/host";
|
|
92
94
|
|
|
93
95
|
import {
|
|
@@ -232,7 +234,7 @@ export function createActionDispatcher(options: {|
|
|
|
232
234
|
// `decodeActionArguments` produced, so what is unchecked here is the
|
|
233
235
|
// shape of the function and not the shape of the payload.
|
|
234
236
|
const call = action as $FlowFixMe;
|
|
235
|
-
result = await call(...args);
|
|
237
|
+
result = await traceRequestPhase("action", () => call(...args));
|
|
236
238
|
} catch (error) {
|
|
237
239
|
report(record, error);
|
|
238
240
|
return refusal(500);
|
|
@@ -430,6 +432,7 @@ function refusal(status: number): Response {
|
|
|
430
432
|
* here and nowhere the caller can read.
|
|
431
433
|
*/
|
|
432
434
|
function report(record: ActionRecord, error: mixed): void {
|
|
435
|
+
reportRequestError(error, "action");
|
|
433
436
|
// eslint-disable-next-line no-console
|
|
434
437
|
console.error(`uf: server action \`${record.export}\` in \`${record.module}\` failed`, error);
|
|
435
438
|
}
|
package/internal/resolve.js
CHANGED
|
@@ -722,6 +722,8 @@ export type ResolveOptions = {|
|
|
|
722
722
|
readonly defer?: boolean,
|
|
723
723
|
/** The route pattern, the moment the URL matches one; see [`resolveMatch`]. */
|
|
724
724
|
readonly onMatch?: (pattern: string) => void,
|
|
725
|
+
/** The server supplies tracing without importing server code into the browser. */
|
|
726
|
+
readonly runLoader?: (body: () => mixed | Promise<mixed>) => Promise<mixed>,
|
|
725
727
|
|};
|
|
726
728
|
|
|
727
729
|
async function resolveRoute(
|
|
@@ -799,7 +801,9 @@ async function resolveRoute(
|
|
|
799
801
|
let data: mixed = options?.data;
|
|
800
802
|
let deferred: ?Promise<mixed> = null;
|
|
801
803
|
if (options?.skipLoader !== true && typeof page.loader === "function") {
|
|
802
|
-
const
|
|
804
|
+
const loader = page.loader;
|
|
805
|
+
const loadData = () => loader({ params: matched.params, searchParams, pathname });
|
|
806
|
+
const running = options?.runLoader == null ? loadData() : options.runLoader(loadData);
|
|
803
807
|
const canDefer =
|
|
804
808
|
options?.defer === true &&
|
|
805
809
|
(matched.route.loading ?? []).length > 0 &&
|
package/internal/runtime.js
CHANGED
|
@@ -20,6 +20,8 @@
|
|
|
20
20
|
|
|
21
21
|
"use client";
|
|
22
22
|
|
|
23
|
+
import { observeNavigation } from "../instrumentation.js";
|
|
24
|
+
|
|
23
25
|
import type * as React from "react";
|
|
24
26
|
import {
|
|
25
27
|
Suspense,
|
|
@@ -665,7 +667,9 @@ component ModuleRouter(url: string, initial: ResolvedRoute, children: React.Node
|
|
|
665
667
|
setResolved(next);
|
|
666
668
|
};
|
|
667
669
|
|
|
668
|
-
const navigate =
|
|
670
|
+
const navigate = (to: string, options?: NavigateOptions): Promise<void> =>
|
|
671
|
+
observeNavigation(to, () => navigateTo(to, options));
|
|
672
|
+
const navigateTo = async (to: string, options?: NavigateOptions): Promise<void> => {
|
|
669
673
|
if (!isBrowser()) {
|
|
670
674
|
return;
|
|
671
675
|
}
|
|
@@ -985,7 +989,9 @@ component FlightRouter(flight: Promise<FlightRoot>, children: React.Node) {
|
|
|
985
989
|
// Read once per render, for the reason `ModuleRouter` reads it once.
|
|
986
990
|
const navigation = navigationMode();
|
|
987
991
|
|
|
988
|
-
const navigate =
|
|
992
|
+
const navigate = (to: string, options?: NavigateOptions): Promise<void> =>
|
|
993
|
+
observeNavigation(to, () => navigateTo(to, options));
|
|
994
|
+
const navigateTo = async (to: string, options?: NavigateOptions): Promise<void> => {
|
|
989
995
|
if (!isBrowser()) {
|
|
990
996
|
return;
|
|
991
997
|
}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
// @flow
|
|
2
|
+
|
|
3
|
+
import { traceRequestPhase } from "@uniflowed/server/instrumentation";
|
|
4
|
+
import { NotFoundError, RedirectError } from "./routing.js";
|
|
5
|
+
|
|
6
|
+
export function traceLoader(body: () => mixed | Promise<mixed>): Promise<mixed> {
|
|
7
|
+
return traceRequestPhase(
|
|
8
|
+
"loader",
|
|
9
|
+
async () => body(),
|
|
10
|
+
(error) => error instanceof RedirectError || error instanceof NotFoundError,
|
|
11
|
+
);
|
|
12
|
+
}
|
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.43",
|
|
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",
|
|
@@ -27,7 +27,8 @@
|
|
|
27
27
|
"./handler": "./handler.js",
|
|
28
28
|
"./middleware": "./middleware.js",
|
|
29
29
|
"./native-navigation": "./native-navigation.js",
|
|
30
|
-
"./http-client": "./http-client.js"
|
|
30
|
+
"./http-client": "./http-client.js",
|
|
31
|
+
"./instrumentation": "./instrumentation.js"
|
|
31
32
|
},
|
|
32
33
|
"files": [
|
|
33
34
|
"action.js",
|
|
@@ -45,6 +46,7 @@
|
|
|
45
46
|
"server.js",
|
|
46
47
|
"native-navigation.js",
|
|
47
48
|
"http-client.js",
|
|
49
|
+
"instrumentation.js",
|
|
48
50
|
"!*.test.js"
|
|
49
51
|
],
|
|
50
52
|
"peerDependencies": {
|
|
@@ -69,7 +71,7 @@
|
|
|
69
71
|
}
|
|
70
72
|
},
|
|
71
73
|
"dependencies": {
|
|
72
|
-
"@uniflowed/hooks": "0.0.0-alpha.
|
|
73
|
-
"@uniflowed/server": "0.0.0-alpha.
|
|
74
|
+
"@uniflowed/hooks": "0.0.0-alpha.43",
|
|
75
|
+
"@uniflowed/server": "0.0.0-alpha.43"
|
|
74
76
|
}
|
|
75
77
|
}
|
package/rsc.js
CHANGED
|
@@ -44,6 +44,8 @@
|
|
|
44
44
|
// in this graph, and a boundary module without the directive is refused here,
|
|
45
45
|
// with its file named, rather than by React with a sentence about functions.
|
|
46
46
|
|
|
47
|
+
import { traceLoader } from "./internal/server-instrumentation.js";
|
|
48
|
+
|
|
47
49
|
import * as React from "react";
|
|
48
50
|
import { use } from "react";
|
|
49
51
|
// React's Flight server. Only this graph can load it: it refuses to evaluate
|
|
@@ -51,6 +53,7 @@ import { use } from "react";
|
|
|
51
53
|
import { renderToReadableStream } from "react-server-dom-parcel/server";
|
|
52
54
|
|
|
53
55
|
import { noteRoute } from "@uniflowed/server/host";
|
|
56
|
+
import { reportRequestError } from "@uniflowed/server/instrumentation";
|
|
54
57
|
|
|
55
58
|
import { BoundaryReporter } from "./internal/boundaries.js";
|
|
56
59
|
import { routeBoundaries } from "./internal/boundary-data.js";
|
|
@@ -175,7 +178,7 @@ export function createFlightRenderer(options: {|
|
|
|
175
178
|
intercepted ??
|
|
176
179
|
// `onMatch` records the route pattern on the request before the
|
|
177
180
|
// loader runs, so every line the loader logs names its route.
|
|
178
|
-
(await resolveMatch(table, url, { defer, onMatch: noteRoute }));
|
|
181
|
+
(await resolveMatch(table, url, { defer, onMatch: noteRoute, runLoader: traceLoader }));
|
|
179
182
|
} else {
|
|
180
183
|
resolved = await resolveFailure(table, url, failure.error);
|
|
181
184
|
}
|
|
@@ -199,13 +202,20 @@ export function createFlightRenderer(options: {|
|
|
|
199
202
|
</>
|
|
200
203
|
);
|
|
201
204
|
const root: FlightRoot = { route: state, tree };
|
|
202
|
-
const
|
|
205
|
+
const failure = renderFailure(route);
|
|
206
|
+
if (failure != null) reportRequestError(failure, "render");
|
|
207
|
+
const report = (error) => {
|
|
208
|
+
reportRequestError(error, "render");
|
|
209
|
+
if (settings?.onError != null) return settings.onError(error);
|
|
210
|
+
console.error(error);
|
|
211
|
+
return undefined;
|
|
212
|
+
};
|
|
203
213
|
// Inside the route's store, so a server component's `useRoute()` finds the
|
|
204
214
|
// route however many `await`s into the render it asks.
|
|
205
215
|
const stream = withServerRoute(state, () =>
|
|
206
216
|
renderToReadableStream(root, {
|
|
207
217
|
// Handed over as it is, so that the digest it returns reaches the row.
|
|
208
|
-
//
|
|
218
|
+
// Our callback preserves React's console fallback when none was given.
|
|
209
219
|
onError: report,
|
|
210
220
|
signal: settings?.signal,
|
|
211
221
|
}),
|
|
@@ -225,7 +235,11 @@ async function resolveFlightInterception(
|
|
|
225
235
|
return null;
|
|
226
236
|
}
|
|
227
237
|
try {
|
|
228
|
-
const base = await resolveMatch(table, baseUrl, {
|
|
238
|
+
const base = await resolveMatch(table, baseUrl, {
|
|
239
|
+
defer,
|
|
240
|
+
onMatch: noteRoute,
|
|
241
|
+
runLoader: traceLoader,
|
|
242
|
+
});
|
|
229
243
|
return await resolveInterception(table, base, url);
|
|
230
244
|
} catch (error) {
|
|
231
245
|
if (error instanceof RedirectError) {
|
package/server.js
CHANGED
|
@@ -24,6 +24,8 @@
|
|
|
24
24
|
// which.
|
|
25
25
|
|
|
26
26
|
import { currentNonce, noteRoute } from "@uniflowed/server/host";
|
|
27
|
+
import { traceLoader } from "./internal/server-instrumentation.js";
|
|
28
|
+
|
|
27
29
|
import * as React from "react";
|
|
28
30
|
|
|
29
31
|
import {
|
|
@@ -335,7 +337,11 @@ export function createRenderer(options: {|
|
|
|
335
337
|
try {
|
|
336
338
|
return {
|
|
337
339
|
kind: "route",
|
|
338
|
-
route: await resolveMatch(table, url, {
|
|
340
|
+
route: await resolveMatch(table, url, {
|
|
341
|
+
defer,
|
|
342
|
+
onMatch: noteRoute,
|
|
343
|
+
runLoader: traceLoader,
|
|
344
|
+
}),
|
|
339
345
|
};
|
|
340
346
|
} catch (error) {
|
|
341
347
|
if (error instanceof RedirectError) {
|
|
@@ -512,3 +518,9 @@ export function shellDocument(assets: RenderAssets): string {
|
|
|
512
518
|
const shell = shellFor(assets);
|
|
513
519
|
return `${shell.open}${shell.body}${shell.close}`;
|
|
514
520
|
}
|
|
521
|
+
|
|
522
|
+
export {
|
|
523
|
+
createInstrumentation,
|
|
524
|
+
instrumentRender,
|
|
525
|
+
traceRequestPhase,
|
|
526
|
+
} from "@uniflowed/server/instrumentation";
|