@reckona/mreact-router 0.0.175 → 0.0.177

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.
Files changed (64) hide show
  1. package/dist/adapters/cloudflare.d.ts.map +1 -1
  2. package/dist/adapters/cloudflare.js +5 -49
  3. package/dist/adapters/cloudflare.js.map +1 -1
  4. package/dist/build.d.ts +1 -0
  5. package/dist/build.d.ts.map +1 -1
  6. package/dist/build.js +90 -0
  7. package/dist/build.js.map +1 -1
  8. package/dist/built-assets.d.ts +11 -0
  9. package/dist/built-assets.d.ts.map +1 -0
  10. package/dist/built-assets.js +130 -0
  11. package/dist/built-assets.js.map +1 -0
  12. package/dist/built-runtime.d.ts +53 -0
  13. package/dist/built-runtime.d.ts.map +1 -0
  14. package/dist/built-runtime.js +178 -0
  15. package/dist/built-runtime.js.map +1 -0
  16. package/dist/built-server-module-artifacts.d.ts +19 -0
  17. package/dist/built-server-module-artifacts.d.ts.map +1 -0
  18. package/dist/built-server-module-artifacts.js +217 -0
  19. package/dist/built-server-module-artifacts.js.map +1 -0
  20. package/dist/client-manifest-assets.d.ts +16 -0
  21. package/dist/client-manifest-assets.d.ts.map +1 -0
  22. package/dist/client-manifest-assets.js +60 -0
  23. package/dist/client-manifest-assets.js.map +1 -0
  24. package/dist/client.d.ts.map +1 -1
  25. package/dist/client.js +79 -13
  26. package/dist/client.js.map +1 -1
  27. package/dist/node-server.d.ts +29 -0
  28. package/dist/node-server.d.ts.map +1 -0
  29. package/dist/node-server.js +81 -0
  30. package/dist/node-server.js.map +1 -0
  31. package/dist/render.d.ts +9 -0
  32. package/dist/render.d.ts.map +1 -1
  33. package/dist/render.js +20 -41
  34. package/dist/render.js.map +1 -1
  35. package/dist/route-dispatch.d.ts +2 -0
  36. package/dist/route-dispatch.d.ts.map +1 -0
  37. package/dist/route-dispatch.js +16 -0
  38. package/dist/route-dispatch.js.map +1 -0
  39. package/dist/route-loader-runtime.d.ts +22 -0
  40. package/dist/route-loader-runtime.d.ts.map +1 -0
  41. package/dist/route-loader-runtime.js +30 -0
  42. package/dist/route-loader-runtime.js.map +1 -0
  43. package/dist/routes.d.ts +2 -0
  44. package/dist/routes.d.ts.map +1 -1
  45. package/dist/routes.js +48 -1
  46. package/dist/routes.js.map +1 -1
  47. package/dist/serve.d.ts +4 -3
  48. package/dist/serve.d.ts.map +1 -1
  49. package/dist/serve.js +84 -582
  50. package/dist/serve.js.map +1 -1
  51. package/package.json +11 -11
  52. package/src/adapters/cloudflare.ts +5 -60
  53. package/src/build.ts +143 -0
  54. package/src/built-assets.ts +164 -0
  55. package/src/built-runtime.ts +312 -0
  56. package/src/built-server-module-artifacts.ts +340 -0
  57. package/src/client-manifest-assets.ts +92 -0
  58. package/src/client.ts +79 -13
  59. package/src/node-server.ts +129 -0
  60. package/src/render.ts +38 -60
  61. package/src/route-dispatch.ts +17 -0
  62. package/src/route-loader-runtime.ts +54 -0
  63. package/src/routes.ts +70 -1
  64. package/src/serve.ts +105 -863
@@ -0,0 +1,340 @@
1
+ import { readFile } from "node:fs/promises";
2
+ import { dirname, isAbsolute, join, normalize, sep } from "node:path";
3
+ import type { BuiltServerModuleArtifact } from "./build.js";
4
+ import { routeShellCandidates } from "./route-shells.js";
5
+
6
+ export interface BuiltServerModuleArtifactRuntime {
7
+ appDir: string;
8
+ serverModuleArtifactLoads: Map<string, Promise<void>>;
9
+ serverModuleClosureFiles: Map<string, readonly string[]>;
10
+ serverModuleFiles: ReadonlyMap<string, string>;
11
+ serverModuleRenderFiles: ReadonlyMap<string, string>;
12
+ serverModuleRequestFiles: ReadonlyMap<string, string>;
13
+ serverModules: Map<string, BuiltServerModuleArtifact>;
14
+ serverSourceFiles: ReadonlyMap<string, string>;
15
+ }
16
+
17
+ export type BuiltServerModuleArtifactKind = "all" | "render" | "request";
18
+
19
+ export async function loadBuiltServerModuleArtifacts(
20
+ runtime: BuiltServerModuleArtifactRuntime,
21
+ files: Iterable<string>,
22
+ kind: BuiltServerModuleArtifactKind = "all",
23
+ ): Promise<void> {
24
+ for (const file of files) {
25
+ await loadBuiltServerModuleArtifact(runtime, file, kind);
26
+ }
27
+ }
28
+
29
+ async function loadBuiltServerModuleArtifact(
30
+ runtime: BuiltServerModuleArtifactRuntime,
31
+ file: string,
32
+ kind: BuiltServerModuleArtifactKind = "all",
33
+ ): Promise<void> {
34
+ if (kind === "all") {
35
+ await loadBuiltServerModuleArtifact(runtime, file, "request");
36
+ await loadBuiltServerModuleArtifact(runtime, file, "render");
37
+ return;
38
+ }
39
+
40
+ const artifactPath =
41
+ kind === "request"
42
+ ? runtime.serverModuleRequestFiles.get(file) ?? runtime.serverModuleFiles.get(file)
43
+ : runtime.serverModuleRenderFiles.get(file) ?? runtime.serverModuleFiles.get(file);
44
+
45
+ if (artifactPath === undefined) {
46
+ return;
47
+ }
48
+
49
+ const cached = runtime.serverModuleArtifactLoads.get(`${kind}\0${file}`);
50
+
51
+ if (cached !== undefined) {
52
+ await cached;
53
+ return;
54
+ }
55
+
56
+ const loaded = readFile(artifactPath, "utf8")
57
+ .then((text) => {
58
+ const existing = runtime.serverModules.get(file) ?? {};
59
+ runtime.serverModules.set(
60
+ file,
61
+ mergeBuiltServerModuleArtifacts(
62
+ existing,
63
+ hydrateBuiltServerModuleArtifact(
64
+ parseBuiltJsonArtifact<BuiltServerModuleArtifact>(
65
+ text,
66
+ artifactPath,
67
+ `built server module artifact for ${file}`,
68
+ ),
69
+ builtServerDirForArtifactPath(artifactPath),
70
+ ),
71
+ ),
72
+ );
73
+ })
74
+ .catch((error) => {
75
+ runtime.serverModuleArtifactLoads.delete(`${kind}\0${file}`);
76
+ if (isMissingFileError(error)) {
77
+ throw builtArtifactReadError(`built server module artifact for ${file}`, artifactPath, error);
78
+ }
79
+ throw error;
80
+ });
81
+ runtime.serverModuleArtifactLoads.set(`${kind}\0${file}`, loaded);
82
+
83
+ await loaded;
84
+ }
85
+
86
+ export function allBuiltServerModuleFiles(
87
+ runtime: BuiltServerModuleArtifactRuntime,
88
+ ): Iterable<string> {
89
+ return new Set([
90
+ ...runtime.serverModuleFiles.keys(),
91
+ ...runtime.serverModuleRequestFiles.keys(),
92
+ ...runtime.serverModuleRenderFiles.keys(),
93
+ ]);
94
+ }
95
+
96
+ function builtServerDirForArtifactPath(artifactPath: string): string {
97
+ const marker = `${sep}server-modules${sep}`;
98
+ const index = artifactPath.lastIndexOf(marker);
99
+
100
+ return index === -1 ? dirname(dirname(artifactPath)) : artifactPath.slice(0, index);
101
+ }
102
+
103
+ function hydrateBuiltServerModuleArtifact(
104
+ artifact: BuiltServerModuleArtifact,
105
+ serverDir: string,
106
+ ): BuiltServerModuleArtifact {
107
+ return {
108
+ ...(artifact.analysis === undefined ? {} : { analysis: artifact.analysis }),
109
+ ...(artifact.loader === undefined
110
+ ? {}
111
+ : { loader: hydrateBuiltServerModuleOutput(artifact.loader, serverDir) }),
112
+ ...(artifact.routeMetadata === undefined
113
+ ? {}
114
+ : { routeMetadata: hydrateBuiltServerModuleOutput(artifact.routeMetadata, serverDir) }),
115
+ ...(artifact.request === undefined
116
+ ? {}
117
+ : { request: hydrateBuiltServerModuleOutput(artifact.request, serverDir) }),
118
+ ...(artifact.stream === undefined
119
+ ? {}
120
+ : { stream: hydrateBuiltServerModuleOutput(artifact.stream, serverDir) }),
121
+ ...(artifact.string === undefined
122
+ ? {}
123
+ : { string: hydrateBuiltServerModuleOutput(artifact.string, serverDir) }),
124
+ };
125
+ }
126
+
127
+ function hydrateBuiltServerModuleOutput<T extends { moduleFile?: string | undefined }>(
128
+ output: T,
129
+ serverDir: string,
130
+ ): T {
131
+ if (output.moduleFile === undefined || isAbsolute(output.moduleFile)) {
132
+ return output;
133
+ }
134
+
135
+ return {
136
+ ...output,
137
+ moduleFile: join(serverDir, safeBuiltServerManifestFilePath(output.moduleFile)),
138
+ };
139
+ }
140
+
141
+ function mergeBuiltServerModuleArtifacts(
142
+ existing: BuiltServerModuleArtifact,
143
+ loaded: BuiltServerModuleArtifact,
144
+ ): BuiltServerModuleArtifact {
145
+ return {
146
+ ...existing,
147
+ ...loaded,
148
+ };
149
+ }
150
+
151
+ export async function loadBuiltServerModuleArtifactsForRequest(
152
+ runtime: BuiltServerModuleArtifactRuntime,
153
+ routeFile: string | undefined,
154
+ options: {
155
+ includeRender?: boolean | undefined;
156
+ includeShells?: boolean | undefined;
157
+ } = {},
158
+ ): Promise<void> {
159
+ const roots = [
160
+ join(runtime.appDir, "middleware.ts"),
161
+ join(runtime.appDir, "middleware.mreact.ts"),
162
+ ...(routeFile === undefined
163
+ ? []
164
+ : [
165
+ routeFile,
166
+ ...(options.includeShells === false ? [] : shellFilesForRoute(runtime, routeFile)),
167
+ ]),
168
+ ];
169
+ const seen = new Set<string>();
170
+
171
+ for (const file of roots) {
172
+ await loadBuiltServerModuleArtifactClosure(runtime, file, seen, "request");
173
+ }
174
+
175
+ if (options.includeRender === true) {
176
+ seen.clear();
177
+ for (const file of roots) {
178
+ await loadBuiltServerModuleArtifactClosure(runtime, file, seen, "render");
179
+ }
180
+ }
181
+ }
182
+
183
+ async function loadBuiltServerModuleArtifactClosure(
184
+ runtime: BuiltServerModuleArtifactRuntime,
185
+ file: string,
186
+ seen: Set<string>,
187
+ kind: BuiltServerModuleArtifactKind,
188
+ ): Promise<void> {
189
+ for (const closureFile of builtServerModuleClosureFiles(runtime, file)) {
190
+ if (seen.has(closureFile)) {
191
+ continue;
192
+ }
193
+ seen.add(closureFile);
194
+ await loadBuiltServerModuleArtifact(runtime, closureFile, kind);
195
+ }
196
+ }
197
+
198
+ function builtServerModuleClosureFiles(
199
+ runtime: BuiltServerModuleArtifactRuntime,
200
+ file: string,
201
+ ): readonly string[] {
202
+ const cached = runtime.serverModuleClosureFiles.get(file);
203
+ if (cached !== undefined) {
204
+ return cached;
205
+ }
206
+
207
+ const closure: string[] = [];
208
+ collectBuiltServerModuleClosureFiles(runtime, file, new Set(), closure);
209
+ runtime.serverModuleClosureFiles.set(file, closure);
210
+ return closure;
211
+ }
212
+
213
+ function collectBuiltServerModuleClosureFiles(
214
+ runtime: BuiltServerModuleArtifactRuntime,
215
+ file: string,
216
+ seen: Set<string>,
217
+ closure: string[],
218
+ ): void {
219
+ if (seen.has(file)) {
220
+ return;
221
+ }
222
+ seen.add(file);
223
+ closure.push(file);
224
+
225
+ const source = runtime.serverSourceFiles.get(file);
226
+ if (source === undefined) {
227
+ return;
228
+ }
229
+
230
+ for (const specifier of localServerModuleSpecifiers(source)) {
231
+ const resolved = resolveBuiltLocalServerSourceImport(runtime, file, specifier);
232
+ if (resolved !== undefined) {
233
+ collectBuiltServerModuleClosureFiles(runtime, resolved, seen, closure);
234
+ }
235
+ }
236
+ }
237
+
238
+ const localServerModuleImportPattern =
239
+ /\b(?:import|export)\s+(?:type\s+)?(?:[^"']*?\s+from\s*)?["'](?<source>\.{1,2}\/[^"']+)["']/g;
240
+
241
+ function localServerModuleSpecifiers(code: string): string[] {
242
+ const specifiers = new Set<string>();
243
+ localServerModuleImportPattern.lastIndex = 0;
244
+
245
+ for (const match of code.matchAll(localServerModuleImportPattern)) {
246
+ const source = match.groups?.source;
247
+
248
+ if (source !== undefined) {
249
+ specifiers.add(source);
250
+ }
251
+ }
252
+
253
+ return Array.from(specifiers);
254
+ }
255
+
256
+ function resolveBuiltLocalServerSourceImport(
257
+ runtime: BuiltServerModuleArtifactRuntime,
258
+ fromFile: string,
259
+ specifier: string,
260
+ ): string | undefined {
261
+ const base = join(dirname(fromFile), specifier);
262
+
263
+ for (const candidate of localServerSourceImportCandidates(base)) {
264
+ if (runtime.serverSourceFiles.has(candidate)) {
265
+ return candidate;
266
+ }
267
+ }
268
+
269
+ return undefined;
270
+ }
271
+
272
+ function localServerSourceImportCandidates(base: string): string[] {
273
+ const candidates = [base];
274
+
275
+ if (base.endsWith(".js")) {
276
+ const withoutJs = base.slice(0, -".js".length);
277
+ candidates.push(`${withoutJs}.ts`, `${withoutJs}.tsx`, `${withoutJs}.mreact.tsx`);
278
+ } else if (base.endsWith(".jsx")) {
279
+ const withoutJsx = base.slice(0, -".jsx".length);
280
+ candidates.push(`${withoutJsx}.tsx`, `${withoutJsx}.mreact.tsx`);
281
+ } else if (base.endsWith(".mreact")) {
282
+ candidates.push(`${base}.tsx`);
283
+ } else {
284
+ candidates.push(
285
+ `${base}.ts`,
286
+ `${base}.tsx`,
287
+ `${base}.mreact.tsx`,
288
+ join(base, "index.ts"),
289
+ join(base, "index.tsx"),
290
+ join(base, "index.mreact.tsx"),
291
+ );
292
+ }
293
+
294
+ return candidates;
295
+ }
296
+
297
+ function shellFilesForRoute(
298
+ runtime: BuiltServerModuleArtifactRuntime,
299
+ routeFile: string,
300
+ ): string[] {
301
+ return routeShellCandidates(runtime.appDir, routeFile)
302
+ .map((candidate) => candidate.file)
303
+ .filter((file) => runtime.serverSourceFiles.has(file));
304
+ }
305
+
306
+ function isMissingFileError(error: unknown): boolean {
307
+ return (
308
+ typeof error === "object" &&
309
+ error !== null &&
310
+ "code" in error &&
311
+ (error as { code?: unknown }).code === "ENOENT"
312
+ );
313
+ }
314
+
315
+ function builtArtifactReadError(label: string, artifactPath: string, error: unknown): Error {
316
+ const prefix = isMissingFileError(error) ? "Missing" : "Unable to read";
317
+ const detail = error instanceof Error && error.message !== "" ? `: ${error.message}` : "";
318
+
319
+ return new Error(`${prefix} ${label}: ${artifactPath}${detail}`, { cause: error });
320
+ }
321
+
322
+ function parseBuiltJsonArtifact<T>(text: string, artifactPath: string, label: string): T {
323
+ try {
324
+ return JSON.parse(text) as T;
325
+ } catch (error) {
326
+ const detail = error instanceof Error && error.message !== "" ? `: ${error.message}` : "";
327
+
328
+ throw new Error(`Invalid ${label}: ${artifactPath}${detail}`, { cause: error });
329
+ }
330
+ }
331
+
332
+ function safeBuiltServerManifestFilePath(pathname: string): string {
333
+ const normalized = normalize(pathname);
334
+
335
+ if (isAbsolute(normalized) || normalized === ".." || normalized.startsWith("../")) {
336
+ throw new Error(`Invalid built app manifest file path: ${pathname}`);
337
+ }
338
+
339
+ return normalized;
340
+ }
@@ -0,0 +1,92 @@
1
+ export interface ClientManifestAssetRoute {
2
+ css?: readonly string[] | undefined;
3
+ imports?: readonly string[] | undefined;
4
+ navigationScript?: string | undefined;
5
+ script?: string | undefined;
6
+ sourceMap?: string | undefined;
7
+ }
8
+
9
+ export interface ClientManifestAssetSource {
10
+ assets?: readonly string[] | undefined;
11
+ routes: readonly ClientManifestAssetRoute[];
12
+ }
13
+
14
+ export function clientManifestAssetPaths(
15
+ manifest: ClientManifestAssetSource,
16
+ options: {
17
+ extraPaths?: readonly string[] | undefined;
18
+ prefix?: string | undefined;
19
+ } = {},
20
+ ): Set<string> {
21
+ const prefix = normalizeClientManifestAssetPrefix(options.prefix ?? "");
22
+ const paths = new Set<string>([`${prefix}manifest.json`]);
23
+
24
+ for (const route of manifest.routes) {
25
+ for (const asset of [
26
+ route.script,
27
+ route.sourceMap,
28
+ route.navigationScript,
29
+ ...(route.css ?? []),
30
+ ...(route.imports ?? []),
31
+ ]) {
32
+ const path = safeClientManifestAssetPath(asset);
33
+
34
+ if (path !== undefined) {
35
+ paths.add(`${prefix}${path}`);
36
+ }
37
+ }
38
+ }
39
+
40
+ for (const asset of manifest.assets ?? []) {
41
+ const path = safeClientManifestAssetPath(asset);
42
+
43
+ if (path !== undefined) {
44
+ paths.add(`${prefix}${path}`);
45
+ }
46
+ }
47
+
48
+ for (const asset of options.extraPaths ?? []) {
49
+ const path = safeClientManifestAssetPath(asset);
50
+
51
+ if (path !== undefined) {
52
+ paths.add(`${prefix}${path}`);
53
+ }
54
+ }
55
+
56
+ return paths;
57
+ }
58
+
59
+ function normalizeClientManifestAssetPrefix(prefix: string): string {
60
+ if (prefix === "") {
61
+ return "";
62
+ }
63
+
64
+ const withLeadingSlash = prefix.startsWith("/") ? prefix : `/${prefix}`;
65
+
66
+ return withLeadingSlash.endsWith("/") ? withLeadingSlash : `${withLeadingSlash}/`;
67
+ }
68
+
69
+ function safeClientManifestAssetPath(asset: string | undefined): string | undefined {
70
+ if (asset === undefined || asset === "" || asset.startsWith("/") || asset.includes("\\")) {
71
+ return undefined;
72
+ }
73
+
74
+ const segments = asset.split("/");
75
+
76
+ return segments.some((segment) => unsafeClientManifestAssetSegment(segment))
77
+ ? undefined
78
+ : segments.join("/");
79
+ }
80
+
81
+ function unsafeClientManifestAssetSegment(segment: string): boolean {
82
+ if (segment === "" || segment === "." || segment === "..") {
83
+ return true;
84
+ }
85
+
86
+ try {
87
+ const decoded = decodeURIComponent(segment);
88
+ return decoded === "." || decoded === ".." || decoded.includes("/") || decoded.includes("\\");
89
+ } catch {
90
+ return true;
91
+ }
92
+ }
package/src/client.ts CHANGED
@@ -2270,6 +2270,7 @@ export async function buildClientRouteEntrySource(
2270
2270
  },
2271
2271
  fetchRevalidationInstalled: false,
2272
2272
  installed: false,
2273
+ pendingHtmlFetches: new Map(),
2273
2274
  prefetchedUrls: new Set(),
2274
2275
  prefetchedScripts: new Set(),
2275
2276
  reloadNextNavigationFetch: false,
@@ -2657,7 +2658,7 @@ export async function __mreactPrefetch(url) {
2657
2658
  return true;
2658
2659
  }
2659
2660
 
2660
- __mreactNavigationState.prefetchedUrls.add(href);
2661
+ __mreactRememberPrefetchedUrl(href);
2661
2662
 
2662
2663
  const script = __mreactRouteScriptForNavigationUrl(href);
2663
2664
 
@@ -2673,17 +2674,39 @@ function __mreactPrefetchNavigationHtml(href) {
2673
2674
  return true;
2674
2675
  }
2675
2676
 
2676
- return __mreactFetchNavigationHtml(href).then((html) => {
2677
- if (typeof html !== "string") {
2678
- return false;
2679
- }
2680
-
2681
- __mreactRememberNavigationHtml(href, html);
2682
- return true;
2683
- }).catch(() => false);
2677
+ return __mreactFetchNavigationHtmlOnce(href)
2678
+ .then((html) => typeof html === "string")
2679
+ .catch(() => false);
2684
2680
  }
2685
2681
 
2686
2682
  const __mreactNavigationHtmlCacheMaxEntries = 64;
2683
+ const __mreactNavigationPrefetchHistoryMaxEntries = 64;
2684
+
2685
+ function __mreactRememberPrefetchedUrl(href) {
2686
+ __mreactRememberPrefetchHistory(__mreactNavigationState.prefetchedUrls, href);
2687
+ }
2688
+
2689
+ function __mreactRememberPrefetchedScript(href) {
2690
+ __mreactRememberPrefetchHistory(__mreactNavigationState.prefetchedScripts, href);
2691
+ }
2692
+
2693
+ function __mreactRememberPrefetchHistory(history, href) {
2694
+ if (history.has(href)) {
2695
+ history.delete(href);
2696
+ }
2697
+
2698
+ history.add(href);
2699
+
2700
+ while (history.size > __mreactNavigationPrefetchHistoryMaxEntries) {
2701
+ const oldestHref = history.keys().next().value;
2702
+
2703
+ if (oldestHref === undefined) {
2704
+ return;
2705
+ }
2706
+
2707
+ history.delete(oldestHref);
2708
+ }
2709
+ }
2687
2710
 
2688
2711
  function __mreactCachedNavigationHtml(href) {
2689
2712
  const html = __mreactNavigationState.cache.get(href);
@@ -2712,6 +2735,7 @@ function __mreactRememberNavigationHtml(href, html) {
2712
2735
  }
2713
2736
 
2714
2737
  __mreactNavigationState.cache.delete(oldestHref);
2738
+ __mreactNavigationState.prefetchedUrls.delete(oldestHref);
2715
2739
  }
2716
2740
  }
2717
2741
 
@@ -2732,7 +2756,7 @@ function __mreactPrefetchRouteScript(script) {
2732
2756
 
2733
2757
  for (const link of Array.from(document.querySelectorAll('link[rel="modulepreload"][href]'))) {
2734
2758
  if (link.href === href) {
2735
- __mreactNavigationState.prefetchedScripts.add(href);
2759
+ __mreactRememberPrefetchedScript(href);
2736
2760
  return true;
2737
2761
  }
2738
2762
  }
@@ -2741,7 +2765,7 @@ function __mreactPrefetchRouteScript(script) {
2741
2765
  link.rel = "modulepreload";
2742
2766
  link.href = href;
2743
2767
  document.head.appendChild(link);
2744
- __mreactNavigationState.prefetchedScripts.add(href);
2768
+ __mreactRememberPrefetchedScript(href);
2745
2769
  return true;
2746
2770
  }
2747
2771
 
@@ -2760,7 +2784,7 @@ export async function __mreactNavigate(url, options = {}) {
2760
2784
  if (script !== undefined) {
2761
2785
  void __mreactPrefetchRouteScript(script);
2762
2786
  }
2763
- const html = cachedHtml ?? await __mreactFetchNavigationHtml(href);
2787
+ const html = cachedHtml ?? await __mreactFetchNavigationHtmlOnce(href);
2764
2788
 
2765
2789
  if (typeof html !== "string") {
2766
2790
  return false;
@@ -2909,10 +2933,16 @@ export function __mreactInvalidateNavigationCache(path) {
2909
2933
  return;
2910
2934
  }
2911
2935
 
2912
- for (const href of Array.from(__mreactNavigationState.cache.keys())) {
2936
+ const hrefs = new Set([
2937
+ ...Array.from(__mreactNavigationState.cache.keys()),
2938
+ ...Array.from(__mreactNavigationState.pendingHtmlFetches.keys()),
2939
+ ]);
2940
+
2941
+ for (const href of hrefs) {
2913
2942
  if (__mreactNormalizeNavigationPath(href) === normalizedPath) {
2914
2943
  __mreactNavigationState.cache.delete(href);
2915
2944
  __mreactNavigationState.prefetchedUrls.delete(href);
2945
+ __mreactNavigationState.pendingHtmlFetches.delete(href);
2916
2946
  }
2917
2947
  }
2918
2948
  }
@@ -2920,6 +2950,35 @@ export function __mreactInvalidateNavigationCache(path) {
2920
2950
  function __mreactInvalidateAllNavigationCache() {
2921
2951
  __mreactNavigationState.cache.clear();
2922
2952
  __mreactNavigationState.prefetchedUrls.clear();
2953
+ __mreactNavigationState.pendingHtmlFetches.clear();
2954
+ }
2955
+
2956
+ function __mreactFetchNavigationHtmlOnce(href) {
2957
+ const pending = __mreactNavigationState.pendingHtmlFetches.get(href);
2958
+
2959
+ if (pending !== undefined) {
2960
+ return pending;
2961
+ }
2962
+
2963
+ const request = __mreactFetchNavigationHtml(href)
2964
+ .then((html) => {
2965
+ if (
2966
+ typeof html === "string" &&
2967
+ __mreactNavigationState.pendingHtmlFetches.get(href) === request
2968
+ ) {
2969
+ __mreactRememberNavigationHtml(href, html);
2970
+ }
2971
+
2972
+ return html;
2973
+ })
2974
+ .finally(() => {
2975
+ if (__mreactNavigationState.pendingHtmlFetches.get(href) === request) {
2976
+ __mreactNavigationState.pendingHtmlFetches.delete(href);
2977
+ }
2978
+ });
2979
+
2980
+ __mreactNavigationState.pendingHtmlFetches.set(href, request);
2981
+ return request;
2923
2982
  }
2924
2983
 
2925
2984
  function __mreactFetchNavigationHtml(href) {
@@ -3495,6 +3554,13 @@ function __mreactInstallNavigation() {
3495
3554
  location.reload();
3496
3555
  }
3497
3556
  });
3557
+ document.addEventListener("pointerover", (event) => {
3558
+ const anchor = __mreactAnchorFromEvent(event);
3559
+
3560
+ if (anchor !== null && __mreactAnchorPrefetchMode(anchor) === "intent") {
3561
+ void __mreactPrefetch(anchor.href);
3562
+ }
3563
+ }, true);
3498
3564
  document.addEventListener("pointerenter", (event) => {
3499
3565
  const anchor = __mreactAnchorFromEvent(event);
3500
3566