@jami-studio/core 0.92.29 → 0.92.31
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/corpus/README.md +1 -1
- package/corpus/core/CHANGELOG.md +30 -0
- package/corpus/core/package.json +1 -1
- package/corpus/core/src/agent/production-agent.ts +7360 -7302
- package/corpus/core/src/client/route-warmup.tsx +565 -543
- package/corpus/core/src/collab/param.ts +24 -0
- package/corpus/core/src/collab/routes.ts +187 -191
- package/corpus/core/src/collab/struct-routes.ts +134 -133
- package/dist/agent/production-agent.d.ts +15 -1
- package/dist/agent/production-agent.d.ts.map +1 -1
- package/dist/agent/production-agent.js +61 -15
- package/dist/agent/production-agent.js.map +1 -1
- package/dist/client/route-warmup.d.ts +10 -0
- package/dist/client/route-warmup.d.ts.map +1 -1
- package/dist/client/route-warmup.js +18 -1
- package/dist/client/route-warmup.js.map +1 -1
- package/dist/collab/param.d.ts +3 -0
- package/dist/collab/param.d.ts.map +1 -0
- package/dist/collab/param.js +26 -0
- package/dist/collab/param.js.map +1 -0
- package/dist/collab/routes.d.ts.map +1 -1
- package/dist/collab/routes.js +6 -5
- package/dist/collab/routes.js.map +1 -1
- package/dist/collab/struct-routes.d.ts +1 -1
- package/dist/collab/struct-routes.d.ts.map +1 -1
- package/dist/collab/struct-routes.js +5 -4
- package/dist/collab/struct-routes.js.map +1 -1
- package/package.json +1 -1
|
@@ -1,543 +1,565 @@
|
|
|
1
|
-
import { useEffect } from "react";
|
|
2
|
-
import { matchRoutes, type RouteObject } from "react-router";
|
|
3
|
-
|
|
4
|
-
import {
|
|
5
|
-
mergeAgentNativeRouteWarmupConfig,
|
|
6
|
-
type AgentNativeRouteWarmupConfigInput,
|
|
7
|
-
type AgentNativeRouteWarmupResolvedConfig,
|
|
8
|
-
type AgentNativeRouteWarmupStrategy,
|
|
9
|
-
} from "../shared/route-warmup-config.js";
|
|
10
|
-
|
|
11
|
-
declare const __AGENT_NATIVE_ROUTE_WARMUP_CONFIG__:
|
|
12
|
-
| AgentNativeRouteWarmupConfigInput
|
|
13
|
-
| string
|
|
14
|
-
| undefined;
|
|
15
|
-
|
|
16
|
-
type ReactRouterManifestRoute = {
|
|
17
|
-
id: string;
|
|
18
|
-
parentId?: string;
|
|
19
|
-
path?: string;
|
|
20
|
-
index?: boolean;
|
|
21
|
-
module?: string;
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
const
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
let
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
|
|
65
|
-
if (
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
}
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
if (
|
|
108
|
-
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
}
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
appPath === "/
|
|
119
|
-
appPath.startsWith("/
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
return
|
|
128
|
-
}
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
if (
|
|
135
|
-
if (
|
|
136
|
-
return
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
.
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
const
|
|
221
|
-
if (
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
])
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
function
|
|
252
|
-
const
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
272
|
-
|
|
273
|
-
|
|
274
|
-
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
}
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
}
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
|
|
352
|
-
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
}
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
371
|
-
|
|
372
|
-
|
|
373
|
-
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
|
|
378
|
-
|
|
379
|
-
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
|
|
387
|
-
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
401
|
-
|
|
402
|
-
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
409
|
-
|
|
410
|
-
|
|
411
|
-
|
|
412
|
-
|
|
413
|
-
|
|
414
|
-
|
|
415
|
-
|
|
416
|
-
|
|
417
|
-
|
|
418
|
-
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
|
|
428
|
-
const
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
|
|
432
|
-
|
|
433
|
-
|
|
434
|
-
if (
|
|
435
|
-
|
|
436
|
-
|
|
437
|
-
|
|
438
|
-
|
|
439
|
-
|
|
440
|
-
|
|
441
|
-
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
if (
|
|
454
|
-
|
|
455
|
-
|
|
456
|
-
|
|
457
|
-
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
|
|
477
|
-
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
493
|
-
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
500
|
-
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
|
|
504
|
-
|
|
505
|
-
|
|
506
|
-
|
|
507
|
-
|
|
508
|
-
|
|
509
|
-
|
|
510
|
-
|
|
511
|
-
|
|
512
|
-
|
|
513
|
-
|
|
514
|
-
|
|
515
|
-
|
|
516
|
-
|
|
517
|
-
|
|
518
|
-
|
|
519
|
-
|
|
520
|
-
|
|
521
|
-
|
|
522
|
-
|
|
523
|
-
|
|
524
|
-
|
|
525
|
-
|
|
526
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
530
|
-
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
1
|
+
import { useEffect } from "react";
|
|
2
|
+
import { matchRoutes, type RouteObject } from "react-router";
|
|
3
|
+
|
|
4
|
+
import {
|
|
5
|
+
mergeAgentNativeRouteWarmupConfig,
|
|
6
|
+
type AgentNativeRouteWarmupConfigInput,
|
|
7
|
+
type AgentNativeRouteWarmupResolvedConfig,
|
|
8
|
+
type AgentNativeRouteWarmupStrategy,
|
|
9
|
+
} from "../shared/route-warmup-config.js";
|
|
10
|
+
|
|
11
|
+
declare const __AGENT_NATIVE_ROUTE_WARMUP_CONFIG__:
|
|
12
|
+
| AgentNativeRouteWarmupConfigInput
|
|
13
|
+
| string
|
|
14
|
+
| undefined;
|
|
15
|
+
|
|
16
|
+
type ReactRouterManifestRoute = {
|
|
17
|
+
id: string;
|
|
18
|
+
parentId?: string;
|
|
19
|
+
path?: string;
|
|
20
|
+
index?: boolean;
|
|
21
|
+
module?: string;
|
|
22
|
+
hasLoader?: boolean;
|
|
23
|
+
hasAction?: boolean;
|
|
24
|
+
clientActionModule?: string;
|
|
25
|
+
clientLoaderModule?: string;
|
|
26
|
+
hydrateFallbackModule?: string;
|
|
27
|
+
imports?: string[];
|
|
28
|
+
};
|
|
29
|
+
|
|
30
|
+
type ReactRouterManifest = {
|
|
31
|
+
routes?: Record<string, ReactRouterManifestRoute>;
|
|
32
|
+
};
|
|
33
|
+
|
|
34
|
+
type WarmupRouteObject = {
|
|
35
|
+
id: string;
|
|
36
|
+
path?: string;
|
|
37
|
+
index?: boolean;
|
|
38
|
+
children?: WarmupRouteObject[];
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
type LinkWarmupMode = Exclude<AgentNativeRouteWarmupStrategy, "off" | "marked">;
|
|
42
|
+
|
|
43
|
+
declare global {
|
|
44
|
+
interface Window {
|
|
45
|
+
__reactRouterContext?: { basename?: string };
|
|
46
|
+
__reactRouterManifest?: ReactRouterManifest;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
const PREFETCH_ATTR = "data-an-prefetch";
|
|
51
|
+
const warmedDataRoutes = new Set<string>();
|
|
52
|
+
const warmedRouteAssets = new Set<string>();
|
|
53
|
+
|
|
54
|
+
let cachedManifest: ReactRouterManifest | undefined;
|
|
55
|
+
let cachedManifestRoutesSignature = "";
|
|
56
|
+
let cachedManifestRouteTree: WarmupRouteObject[] = [];
|
|
57
|
+
|
|
58
|
+
export interface AgentNativeRouteWarmupProps {
|
|
59
|
+
config?: AgentNativeRouteWarmupConfigInput;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function parseBuildTimeRouteWarmupConfig(
|
|
63
|
+
raw: AgentNativeRouteWarmupConfigInput | string | undefined,
|
|
64
|
+
): AgentNativeRouteWarmupConfigInput | undefined {
|
|
65
|
+
if (typeof raw !== "string") return raw;
|
|
66
|
+
const trimmed = raw.trim();
|
|
67
|
+
if (!trimmed) return undefined;
|
|
68
|
+
try {
|
|
69
|
+
return JSON.parse(trimmed) as AgentNativeRouteWarmupConfigInput;
|
|
70
|
+
} catch {
|
|
71
|
+
// Some test/build paths may inject a bare strategy string instead of JSON.
|
|
72
|
+
return raw as AgentNativeRouteWarmupConfigInput;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
function getBuildTimeRouteWarmupConfig():
|
|
77
|
+
| AgentNativeRouteWarmupConfigInput
|
|
78
|
+
| undefined {
|
|
79
|
+
try {
|
|
80
|
+
if (typeof __AGENT_NATIVE_ROUTE_WARMUP_CONFIG__ !== "undefined") {
|
|
81
|
+
return parseBuildTimeRouteWarmupConfig(
|
|
82
|
+
__AGENT_NATIVE_ROUTE_WARMUP_CONFIG__,
|
|
83
|
+
);
|
|
84
|
+
}
|
|
85
|
+
} catch {
|
|
86
|
+
// Some non-Vite test/runtime paths do not define the global.
|
|
87
|
+
}
|
|
88
|
+
return undefined;
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
function getRouteWarmupConfig(
|
|
92
|
+
config: AgentNativeRouteWarmupConfigInput | undefined,
|
|
93
|
+
): AgentNativeRouteWarmupResolvedConfig {
|
|
94
|
+
return mergeAgentNativeRouteWarmupConfig(
|
|
95
|
+
getBuildTimeRouteWarmupConfig(),
|
|
96
|
+
config,
|
|
97
|
+
);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
function normalizeBasename(basename: string | undefined): string {
|
|
101
|
+
if (!basename || basename === "/") return "/";
|
|
102
|
+
return basename.startsWith("/") ? basename.replace(/\/+$/, "") : "/";
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
function stripBasename(pathname: string): string {
|
|
106
|
+
const basename = normalizeBasename(window.__reactRouterContext?.basename);
|
|
107
|
+
if (basename === "/") return pathname;
|
|
108
|
+
if (pathname === basename) return "/";
|
|
109
|
+
if (pathname.startsWith(`${basename}/`)) {
|
|
110
|
+
return pathname.slice(basename.length) || "/";
|
|
111
|
+
}
|
|
112
|
+
return pathname;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function isFrameworkOrApiPath(pathname: string): boolean {
|
|
116
|
+
const appPath = stripBasename(pathname);
|
|
117
|
+
return (
|
|
118
|
+
appPath === "/_agent-native" ||
|
|
119
|
+
appPath.startsWith("/_agent-native/") ||
|
|
120
|
+
appPath === "/api" ||
|
|
121
|
+
appPath.startsWith("/api/")
|
|
122
|
+
);
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function hrefUrl(href: string): URL | null {
|
|
126
|
+
try {
|
|
127
|
+
return new URL(href, window.location.href);
|
|
128
|
+
} catch {
|
|
129
|
+
return null;
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
function isWarmableRouteUrl(url: URL): boolean {
|
|
134
|
+
if (url.origin !== window.location.origin) return false;
|
|
135
|
+
if (url.pathname === window.location.pathname && url.hash) return false;
|
|
136
|
+
if (isFrameworkOrApiPath(url.pathname)) return false;
|
|
137
|
+
if (/\.\w+$/.test(url.pathname)) return false;
|
|
138
|
+
return true;
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
function isWarmableAnchor(link: HTMLAnchorElement): boolean {
|
|
142
|
+
if (link.hasAttribute("download")) return false;
|
|
143
|
+
if (link.target && link.target !== "_self") return false;
|
|
144
|
+
const url = hrefUrl(link.href);
|
|
145
|
+
return url ? isWarmableRouteUrl(url) : false;
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function dataRouteUrlForHref(href: string): string | null {
|
|
149
|
+
const url = hrefUrl(href);
|
|
150
|
+
if (!url || !isWarmableRouteUrl(url)) return null;
|
|
151
|
+
|
|
152
|
+
const pathname = url.pathname.replace(/\/+$/, "") || "/";
|
|
153
|
+
if (pathname === "/") return null;
|
|
154
|
+
url.pathname = `${pathname}.data`;
|
|
155
|
+
url.hash = "";
|
|
156
|
+
return url.href;
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
function hasReactRouterManifestRoutes(): boolean {
|
|
160
|
+
const routes = window.__reactRouterManifest?.routes;
|
|
161
|
+
return Boolean(routes && Object.keys(routes).length > 0);
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* Whether ANY route in the client manifest advertises a server loader or
|
|
166
|
+
* action. Static-shell deployments (Cloudflare Pages worker without a React
|
|
167
|
+
* Router request handler) strip these flags at build time — `.data` requests
|
|
168
|
+
* can never be served there, so warming them only produces 404 noise.
|
|
169
|
+
*/
|
|
170
|
+
function manifestAdvertisesServerData(): boolean {
|
|
171
|
+
for (const route of Object.values(
|
|
172
|
+
window.__reactRouterManifest?.routes ?? {},
|
|
173
|
+
)) {
|
|
174
|
+
if (route.hasLoader || route.hasAction) return true;
|
|
175
|
+
}
|
|
176
|
+
return false;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
function manifestRoutesSignature(
|
|
180
|
+
routes: Record<string, ReactRouterManifestRoute> | undefined,
|
|
181
|
+
): string {
|
|
182
|
+
return Object.values(routes ?? {})
|
|
183
|
+
.map((route) =>
|
|
184
|
+
[
|
|
185
|
+
route.id,
|
|
186
|
+
route.parentId ?? "",
|
|
187
|
+
route.path ?? "",
|
|
188
|
+
route.index ? "1" : "0",
|
|
189
|
+
].join("\0"),
|
|
190
|
+
)
|
|
191
|
+
.sort()
|
|
192
|
+
.join("\n");
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
function getManifestRouteTree(
|
|
196
|
+
manifest: ReactRouterManifest,
|
|
197
|
+
): WarmupRouteObject[] {
|
|
198
|
+
const routesSignature = manifestRoutesSignature(manifest.routes);
|
|
199
|
+
if (
|
|
200
|
+
manifest === cachedManifest &&
|
|
201
|
+
routesSignature === cachedManifestRoutesSignature
|
|
202
|
+
) {
|
|
203
|
+
return cachedManifestRouteTree;
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const manifestRoutes = Object.values(manifest.routes ?? {});
|
|
207
|
+
const nodes = new Map<string, WarmupRouteObject>();
|
|
208
|
+
for (const route of manifestRoutes) {
|
|
209
|
+
nodes.set(route.id, {
|
|
210
|
+
id: route.id,
|
|
211
|
+
path: route.path,
|
|
212
|
+
index: route.index || undefined,
|
|
213
|
+
});
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
const tree: WarmupRouteObject[] = [];
|
|
217
|
+
for (const route of manifestRoutes) {
|
|
218
|
+
const node = nodes.get(route.id);
|
|
219
|
+
if (!node) continue;
|
|
220
|
+
const parent = route.parentId ? nodes.get(route.parentId) : null;
|
|
221
|
+
if (parent) {
|
|
222
|
+
parent.children ??= [];
|
|
223
|
+
parent.children.push(node);
|
|
224
|
+
} else {
|
|
225
|
+
tree.push(node);
|
|
226
|
+
}
|
|
227
|
+
}
|
|
228
|
+
|
|
229
|
+
cachedManifest = manifest;
|
|
230
|
+
cachedManifestRoutesSignature = routesSignature;
|
|
231
|
+
cachedManifestRouteTree = tree;
|
|
232
|
+
return tree;
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
function assetUrlForManifestPath(assetPath: string): string | null {
|
|
236
|
+
try {
|
|
237
|
+
const url = new URL(assetPath, window.location.origin);
|
|
238
|
+
if (url.origin !== window.location.origin) return null;
|
|
239
|
+
// The framework package is often consumed from prebuilt core dist, where
|
|
240
|
+
// Vite does not replace `import.meta.env` in this module. Use the React
|
|
241
|
+
// Router manifest itself to distinguish production client assets from dev
|
|
242
|
+
// source module ids. Production manifests point at immutable Vite chunks;
|
|
243
|
+
// dev manifests point at raw TS/TSX modules that should not be warmed.
|
|
244
|
+
if (!/\/assets\/[^/?#]+\.m?js$/.test(url.pathname)) return null;
|
|
245
|
+
return url.href;
|
|
246
|
+
} catch {
|
|
247
|
+
return null;
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
function hasWarmableRouteAssets(): boolean {
|
|
252
|
+
for (const route of Object.values(
|
|
253
|
+
window.__reactRouterManifest?.routes ?? {},
|
|
254
|
+
)) {
|
|
255
|
+
for (const assetPath of [
|
|
256
|
+
route.module,
|
|
257
|
+
route.clientActionModule,
|
|
258
|
+
route.clientLoaderModule,
|
|
259
|
+
route.hydrateFallbackModule,
|
|
260
|
+
...(route.imports ?? []),
|
|
261
|
+
]) {
|
|
262
|
+
if (assetPath && assetUrlForManifestPath(assetPath)) return true;
|
|
263
|
+
}
|
|
264
|
+
}
|
|
265
|
+
return false;
|
|
266
|
+
}
|
|
267
|
+
|
|
268
|
+
function routeAssetUrlsForHref(href: string): string[] {
|
|
269
|
+
const manifest = window.__reactRouterManifest;
|
|
270
|
+
if (!manifest?.routes) return [];
|
|
271
|
+
|
|
272
|
+
const url = hrefUrl(href);
|
|
273
|
+
if (!url || !isWarmableRouteUrl(url)) return [];
|
|
274
|
+
|
|
275
|
+
const basename = normalizeBasename(window.__reactRouterContext?.basename);
|
|
276
|
+
const matches =
|
|
277
|
+
matchRoutes(
|
|
278
|
+
getManifestRouteTree(manifest) as unknown as RouteObject[],
|
|
279
|
+
url.pathname,
|
|
280
|
+
basename,
|
|
281
|
+
) ?? [];
|
|
282
|
+
const assetUrls: string[] = [];
|
|
283
|
+
|
|
284
|
+
for (const match of matches) {
|
|
285
|
+
const routeId = match.route.id;
|
|
286
|
+
if (!routeId) continue;
|
|
287
|
+
const route = manifest.routes[routeId];
|
|
288
|
+
if (!route) continue;
|
|
289
|
+
for (const assetPath of [
|
|
290
|
+
route.module,
|
|
291
|
+
route.clientActionModule,
|
|
292
|
+
route.clientLoaderModule,
|
|
293
|
+
route.hydrateFallbackModule,
|
|
294
|
+
...(route.imports ?? []),
|
|
295
|
+
]) {
|
|
296
|
+
if (!assetPath) continue;
|
|
297
|
+
const assetUrl = assetUrlForManifestPath(assetPath);
|
|
298
|
+
if (assetUrl) assetUrls.push(assetUrl);
|
|
299
|
+
}
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
return assetUrls;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
function seedExistingModulepreloads() {
|
|
306
|
+
for (const link of document.querySelectorAll<HTMLLinkElement>(
|
|
307
|
+
'link[rel="modulepreload"][href]',
|
|
308
|
+
)) {
|
|
309
|
+
warmedRouteAssets.add(link.href);
|
|
310
|
+
}
|
|
311
|
+
}
|
|
312
|
+
|
|
313
|
+
function warmRouteAssetsForHref(href: string) {
|
|
314
|
+
for (const assetUrl of routeAssetUrlsForHref(href)) {
|
|
315
|
+
if (warmedRouteAssets.has(assetUrl)) continue;
|
|
316
|
+
warmedRouteAssets.add(assetUrl);
|
|
317
|
+
|
|
318
|
+
const link = document.createElement("link");
|
|
319
|
+
link.rel = "modulepreload";
|
|
320
|
+
link.href = assetUrl;
|
|
321
|
+
document.head.appendChild(link);
|
|
322
|
+
}
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
function linkWarmupMode(
|
|
326
|
+
link: HTMLAnchorElement,
|
|
327
|
+
strategy: AgentNativeRouteWarmupStrategy,
|
|
328
|
+
): LinkWarmupMode | "none" {
|
|
329
|
+
const explicit = link.getAttribute(PREFETCH_ATTR)?.trim().toLowerCase();
|
|
330
|
+
if (explicit === "none" || explicit === "off" || explicit === "false") {
|
|
331
|
+
return "none";
|
|
332
|
+
}
|
|
333
|
+
if (
|
|
334
|
+
explicit === "render" ||
|
|
335
|
+
explicit === "intent" ||
|
|
336
|
+
explicit === "viewport"
|
|
337
|
+
) {
|
|
338
|
+
return explicit;
|
|
339
|
+
}
|
|
340
|
+
if (strategy === "off" || strategy === "marked") return "none";
|
|
341
|
+
return strategy;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
function selectorWarmupMode(link: HTMLAnchorElement): "render" | "none" {
|
|
345
|
+
const explicit = link.getAttribute(PREFETCH_ATTR)?.trim().toLowerCase();
|
|
346
|
+
return explicit === "none" || explicit === "off" || explicit === "false"
|
|
347
|
+
? "none"
|
|
348
|
+
: "render";
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
function renderWarmupLinksForSelector(selector: string): HTMLAnchorElement[] {
|
|
352
|
+
let elements: Element[];
|
|
353
|
+
try {
|
|
354
|
+
elements = Array.from(document.querySelectorAll(selector));
|
|
355
|
+
} catch {
|
|
356
|
+
return [];
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
const links: HTMLAnchorElement[] = [];
|
|
360
|
+
const seen = new Set<HTMLAnchorElement>();
|
|
361
|
+
for (const element of elements) {
|
|
362
|
+
const link =
|
|
363
|
+
element instanceof HTMLAnchorElement
|
|
364
|
+
? element
|
|
365
|
+
: (element.querySelector<HTMLAnchorElement>("a[href]") ??
|
|
366
|
+
element.closest<HTMLAnchorElement>("a[href]"));
|
|
367
|
+
if (!link || seen.has(link)) continue;
|
|
368
|
+
seen.add(link);
|
|
369
|
+
links.push(link);
|
|
370
|
+
}
|
|
371
|
+
return links;
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function resetRouteWarmupCachesForTests() {
|
|
375
|
+
cachedManifest = undefined;
|
|
376
|
+
cachedManifestRoutesSignature = "";
|
|
377
|
+
cachedManifestRouteTree = [];
|
|
378
|
+
warmedDataRoutes.clear();
|
|
379
|
+
warmedRouteAssets.clear();
|
|
380
|
+
}
|
|
381
|
+
|
|
382
|
+
/**
|
|
383
|
+
* Warms React Router route data and matched route JS without using native link
|
|
384
|
+
* prefetch. React Router's built-in `<Link prefetch>` does both pieces, but
|
|
385
|
+
* its data side emits `<link rel="prefetch" as="fetch">`; Chrome sends
|
|
386
|
+
* `Sec-Purpose: prefetch` for that request and Cloudflare Speed Brain can 503
|
|
387
|
+
* dynamic `.data` routes before the CDN/origin can serve the cacheable result.
|
|
388
|
+
*
|
|
389
|
+
* Keep `.data` warmup as ordinary `fetch()` and JS warmup as `modulepreload`
|
|
390
|
+
* unless production providers stop rejecting `Sec-Purpose: prefetch`.
|
|
391
|
+
*/
|
|
392
|
+
export function AgentNativeRouteWarmup({
|
|
393
|
+
config,
|
|
394
|
+
}: AgentNativeRouteWarmupProps) {
|
|
395
|
+
useEffect(() => {
|
|
396
|
+
const resolved = getRouteWarmupConfig(config);
|
|
397
|
+
if (resolved.strategy === "off") {
|
|
398
|
+
return;
|
|
399
|
+
}
|
|
400
|
+
// Legacy SPA builds still mount the AgentPanel but do not expose React
|
|
401
|
+
// Router framework `.data` endpoints or a route asset manifest. Only warm
|
|
402
|
+
// route data/modules when that manifest is present; otherwise this would
|
|
403
|
+
// generate noisy `/<path>.data` 404s for apps that cannot serve them.
|
|
404
|
+
const hasManifestRoutes = hasReactRouterManifestRoutes();
|
|
405
|
+
// Vite dev manifests contain raw source module ids. Warming those with
|
|
406
|
+
// modulepreload can route through React Router's dev SSR loader and make
|
|
407
|
+
// local servers log false-positive internal errors. Keep route warmup to
|
|
408
|
+
// manifests that point at built JS assets, where SSR `.data` requests have
|
|
409
|
+
// the CDN cache headers this feature relies on.
|
|
410
|
+
const hasRouteAssets = hasManifestRoutes && hasWarmableRouteAssets();
|
|
411
|
+
// `.data` warmup only makes sense when the deployment can actually serve
|
|
412
|
+
// React Router single-fetch requests — static-shell workers strip the
|
|
413
|
+
// hasLoader/hasAction flags, and warming there guarantees a 404 per link.
|
|
414
|
+
const warmData =
|
|
415
|
+
resolved.data && hasRouteAssets && manifestAdvertisesServerData();
|
|
416
|
+
const warmModules = resolved.modules && hasRouteAssets;
|
|
417
|
+
if (!warmData && !warmModules) return;
|
|
418
|
+
|
|
419
|
+
const connection = (
|
|
420
|
+
navigator as Navigator & { connection?: { saveData?: boolean } }
|
|
421
|
+
).connection;
|
|
422
|
+
if (connection?.saveData) return;
|
|
423
|
+
|
|
424
|
+
if (warmModules) seedExistingModulepreloads();
|
|
425
|
+
|
|
426
|
+
const queue: string[] = [];
|
|
427
|
+
const queuedDataRoutes = new Set<string>();
|
|
428
|
+
const observedLinks = new WeakSet<HTMLAnchorElement>();
|
|
429
|
+
let active = 0;
|
|
430
|
+
let stopped = false;
|
|
431
|
+
let scheduleTimer: number | undefined;
|
|
432
|
+
|
|
433
|
+
const pump = () => {
|
|
434
|
+
if (stopped || !warmData) return;
|
|
435
|
+
while (active < resolved.maxConcurrent && queue.length > 0) {
|
|
436
|
+
const href = queue.shift();
|
|
437
|
+
if (!href) continue;
|
|
438
|
+
active += 1;
|
|
439
|
+
window
|
|
440
|
+
.fetch(href, { credentials: "same-origin", cache: "force-cache" })
|
|
441
|
+
.catch(() => {})
|
|
442
|
+
.finally(() => {
|
|
443
|
+
active -= 1;
|
|
444
|
+
window.setTimeout(pump, 50);
|
|
445
|
+
});
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
const warmHref = (href: string) => {
|
|
450
|
+
if (warmModules) warmRouteAssetsForHref(href);
|
|
451
|
+
if (!warmData) return;
|
|
452
|
+
const dataUrl = dataRouteUrlForHref(href);
|
|
453
|
+
if (!dataUrl || warmedDataRoutes.has(dataUrl)) return;
|
|
454
|
+
warmedDataRoutes.add(dataUrl);
|
|
455
|
+
if (queuedDataRoutes.has(dataUrl)) return;
|
|
456
|
+
queuedDataRoutes.add(dataUrl);
|
|
457
|
+
queue.push(dataUrl);
|
|
458
|
+
pump();
|
|
459
|
+
};
|
|
460
|
+
|
|
461
|
+
const viewportObserver =
|
|
462
|
+
typeof IntersectionObserver === "undefined"
|
|
463
|
+
? null
|
|
464
|
+
: new IntersectionObserver((entries) => {
|
|
465
|
+
for (const entry of entries) {
|
|
466
|
+
if (!entry.isIntersecting) continue;
|
|
467
|
+
const link = entry.target as HTMLAnchorElement;
|
|
468
|
+
viewportObserver?.unobserve(link);
|
|
469
|
+
warmHref(link.href);
|
|
470
|
+
}
|
|
471
|
+
});
|
|
472
|
+
|
|
473
|
+
const scan = () => {
|
|
474
|
+
if (warmModules) seedExistingModulepreloads();
|
|
475
|
+
|
|
476
|
+
for (const link of renderWarmupLinksForSelector(resolved.selector)) {
|
|
477
|
+
if (!isWarmableAnchor(link)) continue;
|
|
478
|
+
const mode = selectorWarmupMode(link);
|
|
479
|
+
if (mode === "none") continue;
|
|
480
|
+
warmHref(link.href);
|
|
481
|
+
}
|
|
482
|
+
|
|
483
|
+
for (const link of document.querySelectorAll<HTMLAnchorElement>(
|
|
484
|
+
"a[href]",
|
|
485
|
+
)) {
|
|
486
|
+
if (!isWarmableAnchor(link)) continue;
|
|
487
|
+
const mode = linkWarmupMode(link, resolved.strategy);
|
|
488
|
+
if (mode === "none") continue;
|
|
489
|
+
if (mode === "render") {
|
|
490
|
+
warmHref(link.href);
|
|
491
|
+
continue;
|
|
492
|
+
}
|
|
493
|
+
if (mode === "viewport") {
|
|
494
|
+
if (!viewportObserver) {
|
|
495
|
+
warmHref(link.href);
|
|
496
|
+
} else if (!observedLinks.has(link)) {
|
|
497
|
+
observedLinks.add(link);
|
|
498
|
+
viewportObserver.observe(link);
|
|
499
|
+
}
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
};
|
|
503
|
+
|
|
504
|
+
const schedule = () => {
|
|
505
|
+
if (scheduleTimer !== undefined) window.clearTimeout(scheduleTimer);
|
|
506
|
+
scheduleTimer = window.setTimeout(() => {
|
|
507
|
+
scheduleTimer = undefined;
|
|
508
|
+
scan();
|
|
509
|
+
}, 0);
|
|
510
|
+
};
|
|
511
|
+
|
|
512
|
+
const warmFromIntent = (event: Event) => {
|
|
513
|
+
const target = event.target;
|
|
514
|
+
if (!(target instanceof Element)) return;
|
|
515
|
+
const link = target.closest<HTMLAnchorElement>("a[href]");
|
|
516
|
+
if (!link || !isWarmableAnchor(link)) return;
|
|
517
|
+
const mode = linkWarmupMode(link, resolved.strategy);
|
|
518
|
+
if (mode === "none") return;
|
|
519
|
+
warmHref(link.href);
|
|
520
|
+
};
|
|
521
|
+
|
|
522
|
+
schedule();
|
|
523
|
+
const observer = new MutationObserver(schedule);
|
|
524
|
+
// The render-warmup selector is configurable, so it may depend on class or
|
|
525
|
+
// custom data attributes. Watch all attribute changes and debounce rescans.
|
|
526
|
+
observer.observe(document.documentElement, {
|
|
527
|
+
subtree: true,
|
|
528
|
+
childList: true,
|
|
529
|
+
attributes: true,
|
|
530
|
+
});
|
|
531
|
+
|
|
532
|
+
document.addEventListener("pointerover", warmFromIntent, {
|
|
533
|
+
capture: true,
|
|
534
|
+
passive: true,
|
|
535
|
+
});
|
|
536
|
+
document.addEventListener("touchstart", warmFromIntent, {
|
|
537
|
+
capture: true,
|
|
538
|
+
passive: true,
|
|
539
|
+
});
|
|
540
|
+
document.addEventListener("focusin", warmFromIntent, true);
|
|
541
|
+
|
|
542
|
+
return () => {
|
|
543
|
+
stopped = true;
|
|
544
|
+
if (scheduleTimer !== undefined) window.clearTimeout(scheduleTimer);
|
|
545
|
+
observer.disconnect();
|
|
546
|
+
viewportObserver?.disconnect();
|
|
547
|
+
document.removeEventListener("pointerover", warmFromIntent, true);
|
|
548
|
+
document.removeEventListener("touchstart", warmFromIntent, true);
|
|
549
|
+
document.removeEventListener("focusin", warmFromIntent, true);
|
|
550
|
+
};
|
|
551
|
+
}, [config]);
|
|
552
|
+
|
|
553
|
+
return null;
|
|
554
|
+
}
|
|
555
|
+
|
|
556
|
+
export const __routeWarmupInternalsForTests = {
|
|
557
|
+
getManifestRouteTree,
|
|
558
|
+
hasReactRouterManifestRoutes,
|
|
559
|
+
hasWarmableRouteAssets,
|
|
560
|
+
manifestAdvertisesServerData,
|
|
561
|
+
parseBuildTimeRouteWarmupConfig,
|
|
562
|
+
renderWarmupLinksForSelector,
|
|
563
|
+
routeAssetUrlsForHref,
|
|
564
|
+
resetRouteWarmupCachesForTests,
|
|
565
|
+
};
|