Skip to content

Commit 7d9595d

Browse files
committed
instrument lazy - functioni version w/type errors still
1 parent abcb86d commit 7d9595d

File tree

3 files changed

+188
-15
lines changed

3 files changed

+188
-15
lines changed

packages/react-router/__tests__/router/instrumentation-test.ts

Lines changed: 142 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,52 @@
1+
import { LoaderFunction } from "../../lib/router/utils";
12
import { cleanup, setup } from "./utils/data-router-setup";
2-
import { createFormData } from "./utils/utils";
3+
import { createDeferred, createFormData, tick } from "./utils/utils";
34

45
// Detect any failures inside the router navigate code
56
afterEach(() => {
67
cleanup();
78
});
89

910
describe("instrumentation", () => {
11+
it("allows instrumentation of lazy", async () => {
12+
let spy = jest.fn();
13+
let lazyDfd = createDeferred<{ loader: LoaderFunction }>();
14+
let t = setup({
15+
routes: [
16+
{
17+
index: true,
18+
},
19+
{
20+
id: "page",
21+
path: "/page",
22+
lazy: () => lazyDfd.promise,
23+
},
24+
],
25+
unstable_instrumentRoute: (route) => {
26+
route.instrument({
27+
async lazy(lazy) {
28+
spy("start");
29+
await lazy();
30+
spy("end");
31+
},
32+
});
33+
},
34+
});
35+
36+
await t.navigate("/page");
37+
expect(spy.mock.calls).toEqual([["start"]]);
38+
39+
await lazyDfd.resolve({ loader: () => "PAGE" });
40+
expect(spy.mock.calls).toEqual([["start"], ["end"]]);
41+
await tick();
42+
expect(spy.mock.calls).toEqual([["start"], ["end"]]);
43+
expect(t.router.state).toMatchObject({
44+
navigation: { state: "idle" },
45+
location: { pathname: "/page" },
46+
loaderData: { page: "PAGE" },
47+
});
48+
});
49+
1050
it("allows instrumentation of loaders", async () => {
1151
let spy = jest.fn();
1252
let t = setup({
@@ -80,6 +120,107 @@ describe("instrumentation", () => {
80120
});
81121
});
82122

123+
it("allows instrumentation of loaders when lazy is used", async () => {
124+
let spy = jest.fn();
125+
let lazyDfd = createDeferred<{ loader: LoaderFunction }>();
126+
let loaderDfd = createDeferred();
127+
let t = setup({
128+
routes: [
129+
{
130+
index: true,
131+
},
132+
{
133+
id: "page",
134+
path: "/page",
135+
lazy: () => lazyDfd.promise,
136+
},
137+
],
138+
unstable_instrumentRoute: (route) => {
139+
route.instrument({
140+
async loader(loader) {
141+
spy("start");
142+
await loader();
143+
spy("end");
144+
},
145+
});
146+
},
147+
});
148+
149+
await t.navigate("/page");
150+
expect(spy).not.toHaveBeenCalled();
151+
152+
await lazyDfd.resolve({ loader: () => loaderDfd.promise });
153+
expect(spy.mock.calls).toEqual([["start"]]);
154+
155+
await loaderDfd.resolve("PAGE");
156+
expect(spy.mock.calls).toEqual([["start"], ["end"]]);
157+
158+
await tick();
159+
expect(t.router.state).toMatchObject({
160+
navigation: { state: "idle" },
161+
location: { pathname: "/page" },
162+
loaderData: { page: "PAGE" },
163+
});
164+
});
165+
166+
it("does not double-instrument when a static `loader` is used alongside `lazy`", async () => {
167+
let spy = jest.fn();
168+
let lazyDfd = createDeferred<{ loader: LoaderFunction }>();
169+
let warnSpy = jest.spyOn(console, "warn").mockImplementation(() => {});
170+
let t = setup({
171+
routes: [
172+
{
173+
index: true,
174+
},
175+
{
176+
id: "page",
177+
path: "/page",
178+
loader: true,
179+
lazy: () => lazyDfd.promise,
180+
},
181+
],
182+
unstable_instrumentRoute: (route) => {
183+
route.instrument({
184+
async loader(loader) {
185+
spy("start");
186+
await loader();
187+
spy("end");
188+
},
189+
});
190+
},
191+
});
192+
193+
let A = await t.navigate("/page");
194+
expect(spy.mock.calls).toEqual([["start"]]);
195+
await lazyDfd.resolve({ action: () => "ACTION", loader: () => "WRONG" });
196+
await A.loaders.page.resolve("PAGE");
197+
await tick();
198+
expect(spy.mock.calls).toEqual([["start"], ["end"]]);
199+
expect(t.router.state).toMatchObject({
200+
navigation: { state: "idle" },
201+
location: { pathname: "/page" },
202+
loaderData: { page: "PAGE" },
203+
});
204+
spy.mockClear();
205+
206+
let B = await t.navigate("/page", {
207+
formMethod: "POST",
208+
formData: createFormData({}),
209+
});
210+
expect(spy).not.toHaveBeenCalled();
211+
await B.loaders.page.resolve("PAGE");
212+
await tick();
213+
expect(spy.mock.calls).toEqual([["start"], ["end"]]);
214+
expect(t.router.state).toMatchObject({
215+
navigation: { state: "idle" },
216+
location: { pathname: "/page" },
217+
loaderData: { page: "PAGE" },
218+
});
219+
spy.mockClear();
220+
221+
warnSpy.mockRestore();
222+
});
223+
83224
it("provides read-only information to instrumentation wrappers", async () => {
84225
let spy = jest.fn();
85226
let t = setup({

packages/react-router/lib/router/instrumentation.ts

Lines changed: 44 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,10 @@
11
import type {
22
ActionFunction,
33
AgnosticDataRouteObject,
4+
LazyRouteFunction,
45
LoaderFunction,
56
LoaderFunctionArgs,
7+
MaybePromise,
68
RouterContextProvider,
79
} from "./utils";
810

@@ -23,7 +25,13 @@ type InstrumentHandlerFunction = (
2325
info: InstrumentationInfo,
2426
) => MaybePromise<void>;
2527

28+
type InstrumentLazyFunction = (
29+
handler: () => undefined,
30+
info: InstrumentationInfo,
31+
) => MaybePromise<void>;
32+
2633
type Instrumentations = {
34+
lazy?: InstrumentLazyFunction;
2735
loader?: InstrumentHandlerFunction;
2836
action?: InstrumentHandlerFunction;
2937
};
@@ -35,14 +43,18 @@ type InstrumentableRoute = {
3543
instrument(instrumentations: Instrumentations): void;
3644
};
3745

46+
const UninstrumentedSymbol = Symbol("Uninstrumented");
47+
3848
export type unstable_InstrumentRouteFunction = (
3949
route: InstrumentableRoute,
4050
) => void;
4151

42-
function getInstrumentedHandler<H extends LoaderFunction | ActionFunction>(
43-
impls: InstrumentHandlerFunction[],
44-
handler: H,
45-
) {
52+
function getInstrumentedHandler<
53+
H extends
54+
| LazyRouteFunction<AgnosticDataRouteObject>
55+
| LoaderFunction
56+
| ActionFunction,
57+
>(impls: InstrumentHandlerFunction[], handler: H): H | null {
4658
if (impls.length === 0) {
4759
return null;
4860
}
@@ -54,7 +66,8 @@ function getInstrumentedHandler<H extends LoaderFunction | ActionFunction>(
5466
value = await handler(...args);
5567
},
5668
) as unknown as (info: InstrumentationInfo) => Promise<void>;
57-
await composed(getInstrumentationInfo(args[0]));
69+
let composedArgs = args[0] ? [getInstrumentationInfo(args[0])] : [];
70+
await composed(...composedArgs);
5871
return value;
5972
};
6073
}
@@ -84,10 +97,6 @@ export function getInstrumentationUpdates(
8497
unstable_instrumentRoute: unstable_InstrumentRouteFunction,
8598
route: AgnosticDataRouteObject,
8699
) {
87-
let updates: {
88-
loader?: LoaderFunction;
89-
action?: ActionFunction;
90-
} = {};
91100
let instrumentations: Instrumentations[] = [];
92101
unstable_instrumentRoute({
93102
id: route.id,
@@ -97,18 +106,43 @@ export function getInstrumentationUpdates(
97106
instrumentations.push(i);
98107
},
99108
});
109+
110+
let updates: {
111+
loader?: AgnosticDataRouteObject["loader"];
112+
action?: AgnosticDataRouteObject["action"];
113+
lazy?: AgnosticDataRouteObject["lazy"];
114+
} = {};
115+
100116
if (instrumentations.length > 0) {
117+
if (typeof route.lazy === "function") {
118+
let instrumented = getInstrumentedHandler(
119+
instrumentations
120+
.map((i) => i.lazy)
121+
.filter(Boolean) as InstrumentHandlerFunction[],
122+
route.lazy,
123+
);
124+
if (instrumented) {
125+
updates.lazy = instrumented;
126+
}
127+
}
128+
101129
if (typeof route.loader === "function") {
130+
// @ts-expect-error
131+
let original = route.loader[UninstrumentedSymbol] ?? route.loader;
102132
let instrumented = getInstrumentedHandler(
103133
instrumentations
104134
.map((i) => i.loader)
105135
.filter(Boolean) as InstrumentHandlerFunction[],
106-
route.loader,
136+
original,
107137
);
108138
if (instrumented) {
139+
// @ts-expect-error Avoid double-instrumentation on lazy calls to
140+
// `mapRouteProperties`
141+
instrumented[UninstrumentedSymbol] = original;
109142
updates.loader = instrumented;
110143
}
111144
}
145+
112146
if (typeof route.action === "function") {
113147
let instrumented = getInstrumentedHandler(
114148
instrumentations

packages/react-router/lib/router/router.ts

Lines changed: 2 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -9,10 +9,8 @@ import {
99
parsePath,
1010
warning,
1111
} from "./history";
12-
import {
13-
getInstrumentationUpdates,
14-
unstable_InstrumentRouteFunction,
15-
} from "./instrumentation";
12+
import type { unstable_InstrumentRouteFunction } from "./instrumentation";
13+
import { getInstrumentationUpdates } from "./instrumentation";
1614
import type {
1715
AgnosticDataRouteMatch,
1816
AgnosticDataRouteObject,

0 commit comments

Comments
 (0)