@reckona/mreact-router 0.0.202 → 0.0.204
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/README.md +58 -11
- package/dist/actions.d.ts +8 -3
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +125 -45
- package/dist/actions.js.map +1 -1
- package/dist/adapters/aws-lambda.d.ts +15 -4
- package/dist/adapters/aws-lambda.d.ts.map +1 -1
- package/dist/adapters/aws-lambda.js +301 -63
- package/dist/adapters/aws-lambda.js.map +1 -1
- package/dist/adapters/cloudflare.d.ts +5 -2
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +35 -11
- package/dist/adapters/cloudflare.js.map +1 -1
- package/dist/adapters/edge.d.ts +1 -1
- package/dist/adapters/edge.d.ts.map +1 -1
- package/dist/adapters/edge.js.map +1 -1
- package/dist/adapters/node.d.ts +5 -3
- package/dist/adapters/node.d.ts.map +1 -1
- package/dist/adapters/node.js +2 -1
- package/dist/adapters/node.js.map +1 -1
- package/dist/adapters/static.d.ts.map +1 -1
- package/dist/adapters/static.js +24 -2
- package/dist/adapters/static.js.map +1 -1
- package/dist/build.d.ts +4 -1
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +185 -13
- package/dist/build.js.map +1 -1
- package/dist/built-runtime.d.ts +2 -0
- package/dist/built-runtime.d.ts.map +1 -1
- package/dist/built-runtime.js +16 -0
- package/dist/built-runtime.js.map +1 -1
- package/dist/cache.d.ts +4 -2
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +216 -17
- package/dist/cache.js.map +1 -1
- package/dist/cli-options.d.ts +3 -0
- package/dist/cli-options.d.ts.map +1 -1
- package/dist/cli-options.js +10 -2
- package/dist/cli-options.js.map +1 -1
- package/dist/cli.js +14 -3
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts +5 -0
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +43 -3
- package/dist/client.js.map +1 -1
- package/dist/config.d.ts +3 -0
- package/dist/config.d.ts.map +1 -1
- package/dist/config.js +21 -2
- package/dist/config.js.map +1 -1
- package/dist/csp.d.ts +1 -0
- package/dist/csp.d.ts.map +1 -1
- package/dist/csp.js +9 -0
- package/dist/csp.js.map +1 -1
- package/dist/dehydrate-policy.d.ts +3 -0
- package/dist/dehydrate-policy.d.ts.map +1 -0
- package/dist/dehydrate-policy.js +16 -0
- package/dist/dehydrate-policy.js.map +1 -0
- package/dist/dev-server.d.ts +7 -2
- package/dist/dev-server.d.ts.map +1 -1
- package/dist/dev-server.js +120 -11
- package/dist/dev-server.js.map +1 -1
- package/dist/http.d.ts +1 -1
- package/dist/http.d.ts.map +1 -1
- package/dist/http.js +147 -7
- package/dist/http.js.map +1 -1
- package/dist/index.d.ts +4 -3
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -0
- package/dist/index.js.map +1 -1
- package/dist/logger.d.ts +22 -1
- package/dist/logger.d.ts.map +1 -1
- package/dist/logger.js +22 -8
- package/dist/logger.js.map +1 -1
- package/dist/metadata.d.ts +8 -0
- package/dist/metadata.d.ts.map +1 -1
- package/dist/metadata.js +60 -22
- package/dist/metadata.js.map +1 -1
- package/dist/navigation-marker.d.ts +2 -0
- package/dist/navigation-marker.d.ts.map +1 -0
- package/dist/navigation-marker.js +357 -0
- package/dist/navigation-marker.js.map +1 -0
- package/dist/navigation-runtime.d.ts +1 -1
- package/dist/navigation-runtime.d.ts.map +1 -1
- package/dist/navigation-runtime.js +1 -1
- package/dist/navigation-runtime.js.map +1 -1
- package/dist/navigation.d.ts +1 -0
- package/dist/navigation.d.ts.map +1 -1
- package/dist/navigation.js +1 -1
- package/dist/navigation.js.map +1 -1
- package/dist/node-server.d.ts +5 -2
- package/dist/node-server.d.ts.map +1 -1
- package/dist/node-server.js +75 -6
- package/dist/node-server.js.map +1 -1
- package/dist/prerender-entry.d.ts +17 -1
- package/dist/prerender-entry.d.ts.map +1 -1
- package/dist/prerender-entry.js +98 -3
- package/dist/prerender-entry.js.map +1 -1
- package/dist/render.d.ts +16 -2
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +142 -135
- package/dist/render.js.map +1 -1
- package/dist/serve.d.ts +11 -2
- package/dist/serve.d.ts.map +1 -1
- package/dist/serve.js +67 -41
- package/dist/serve.js.map +1 -1
- package/dist/session.d.ts.map +1 -1
- package/dist/session.js +52 -19
- package/dist/session.js.map +1 -1
- package/dist/upgrade.d.ts +59 -2
- package/dist/upgrade.d.ts.map +1 -1
- package/dist/upgrade.js +282 -1
- package/dist/upgrade.js.map +1 -1
- package/dist/vite-config.d.ts +2 -0
- package/dist/vite-config.d.ts.map +1 -1
- package/dist/vite-config.js +2 -0
- package/dist/vite-config.js.map +1 -1
- package/dist/vite.d.ts +4 -1
- package/dist/vite.d.ts.map +1 -1
- package/dist/vite.js +117 -6
- package/dist/vite.js.map +1 -1
- package/package.json +11 -11
- package/src/actions.ts +146 -60
- package/src/adapters/aws-lambda.ts +426 -115
- package/src/adapters/cloudflare.ts +58 -12
- package/src/adapters/edge.ts +2 -0
- package/src/adapters/node.ts +8 -10
- package/src/adapters/static.ts +31 -2
- package/src/build.ts +217 -13
- package/src/built-runtime.ts +28 -0
- package/src/cache.ts +272 -19
- package/src/cli-options.ts +15 -2
- package/src/cli.ts +16 -2
- package/src/client.ts +47 -3
- package/src/config.ts +43 -6
- package/src/csp.ts +11 -0
- package/src/dehydrate-policy.ts +23 -0
- package/src/dev-server.ts +188 -15
- package/src/http.ts +172 -6
- package/src/index.ts +14 -1
- package/src/logger.ts +47 -10
- package/src/metadata.ts +125 -60
- package/src/navigation-marker.ts +423 -0
- package/src/navigation-runtime.ts +1 -0
- package/src/navigation.ts +1 -1
- package/src/node-server.ts +128 -22
- package/src/prerender-entry.ts +127 -5
- package/src/render.ts +859 -749
- package/src/serve.ts +112 -82
- package/src/session.ts +56 -25
- package/src/upgrade.ts +402 -3
- package/src/vite-config.ts +6 -0
- package/src/vite.ts +157 -5
package/src/cache.ts
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { AsyncLocalStorage } from "node:async_hooks";
|
|
2
|
+
import { responseHeadersContainCspNonce } from "./csp.js";
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
5
|
* Describes the cache lifetime assigned to a rendered route response.
|
|
@@ -73,7 +74,6 @@ export interface MemoryRouteCacheOptions {
|
|
|
73
74
|
|
|
74
75
|
interface AppRouterCacheState {
|
|
75
76
|
activeContexts: RouteCacheContext[];
|
|
76
|
-
invalidatedPaths: Set<string>;
|
|
77
77
|
memoryCache: AppRouterCache;
|
|
78
78
|
storage: AsyncLocalStorage<RouteCacheContext>;
|
|
79
79
|
}
|
|
@@ -82,18 +82,42 @@ interface RouteCacheContext {
|
|
|
82
82
|
cache: AppRouterCache;
|
|
83
83
|
cachePolicy?: RouteCachePolicy | undefined;
|
|
84
84
|
closed?: boolean | undefined;
|
|
85
|
+
externalInvalidationOverflowed: boolean;
|
|
86
|
+
externallyInvalidatedPaths: Set<string>;
|
|
85
87
|
revalidatedPaths: Set<string>;
|
|
86
88
|
}
|
|
87
89
|
|
|
90
|
+
interface AppRouterCacheInvalidationState {
|
|
91
|
+
activeContexts: WeakMap<AppRouterCache, Set<RouteCacheContext>>;
|
|
92
|
+
disabledCaches: WeakSet<AppRouterCache>;
|
|
93
|
+
finalizer: FinalizationRegistry<WeakRef<AppRouterCache>>;
|
|
94
|
+
liveReferences: Set<WeakRef<AppRouterCache>>;
|
|
95
|
+
pendingPaths: WeakMap<AppRouterCache, Set<string>>;
|
|
96
|
+
registeredCaches: WeakSet<AppRouterCache>;
|
|
97
|
+
running: WeakMap<AppRouterCache, Promise<void>>;
|
|
98
|
+
storesBeingInvalidated: WeakMap<AppRouterCache, number>;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
const MAX_PENDING_ROUTE_INVALIDATIONS = 1024;
|
|
102
|
+
|
|
103
|
+
const cacheInvalidationState = ((
|
|
104
|
+
globalThis as {
|
|
105
|
+
__mreactAppRouterCacheInvalidation?: AppRouterCacheInvalidationState;
|
|
106
|
+
}
|
|
107
|
+
).__mreactAppRouterCacheInvalidation ??= createCacheInvalidationState());
|
|
108
|
+
cacheInvalidationState.activeContexts ??= new WeakMap();
|
|
109
|
+
cacheInvalidationState.disabledCaches ??= new WeakSet();
|
|
110
|
+
cacheInvalidationState.storesBeingInvalidated ??= new WeakMap();
|
|
111
|
+
|
|
88
112
|
const cacheState = ((
|
|
89
113
|
globalThis as { __mreactAppRouterCache?: AppRouterCacheState }
|
|
90
114
|
).__mreactAppRouterCache ??= {
|
|
91
115
|
activeContexts: [],
|
|
92
|
-
invalidatedPaths: new Set(),
|
|
93
116
|
memoryCache: createMemoryRouteCache(),
|
|
94
117
|
storage: new AsyncLocalStorage<RouteCacheContext>(),
|
|
95
118
|
});
|
|
96
119
|
cacheState.storage ??= new AsyncLocalStorage<RouteCacheContext>();
|
|
120
|
+
registerRouteCache(cacheState.memoryCache);
|
|
97
121
|
|
|
98
122
|
/**
|
|
99
123
|
* Creates an in-memory route response cache for app-router rendering.
|
|
@@ -164,7 +188,7 @@ export function createMemoryRouteCache(options: MemoryRouteCacheOptions = {}): A
|
|
|
164
188
|
}
|
|
165
189
|
}
|
|
166
190
|
|
|
167
|
-
|
|
191
|
+
const cache: AppRouterCache = {
|
|
168
192
|
deleteByPath(path) {
|
|
169
193
|
const normalizedPath = normalizeRevalidationPath(path);
|
|
170
194
|
const keys = keysByPath.get(normalizedPath);
|
|
@@ -212,6 +236,8 @@ export function createMemoryRouteCache(options: MemoryRouteCacheOptions = {}): A
|
|
|
212
236
|
}
|
|
213
237
|
},
|
|
214
238
|
};
|
|
239
|
+
registerRouteCache(cache);
|
|
240
|
+
return cache;
|
|
215
241
|
}
|
|
216
242
|
|
|
217
243
|
function positiveIntegerOrDefault(value: number | undefined, fallback: number): number {
|
|
@@ -296,6 +322,10 @@ export function cachedRouteResponse(options: {
|
|
|
296
322
|
|
|
297
323
|
const cache = options.cache ?? cacheState.memoryCache;
|
|
298
324
|
|
|
325
|
+
registerRouteCache(cache);
|
|
326
|
+
if (routeCacheIsUnavailable(cache)) {
|
|
327
|
+
return undefined;
|
|
328
|
+
}
|
|
299
329
|
await consumeInvalidations(cache);
|
|
300
330
|
const now = options.now ?? Date.now();
|
|
301
331
|
const cached = await cache.get(options.key, now);
|
|
@@ -310,6 +340,9 @@ export function cachedRouteResponse(options: {
|
|
|
310
340
|
}
|
|
311
341
|
|
|
312
342
|
const headers = new Headers(cached.headers ?? {});
|
|
343
|
+
if (responseHeadersContainCspNonce(headers)) {
|
|
344
|
+
return undefined;
|
|
345
|
+
}
|
|
313
346
|
headers.set("cache-control", cached.cacheControl);
|
|
314
347
|
if (!headers.has("content-type")) {
|
|
315
348
|
headers.set("content-type", "text/html; charset=utf-8");
|
|
@@ -367,6 +400,10 @@ export async function cacheRouteResponse(options: {
|
|
|
367
400
|
const cacheControl = options.policy.cacheControl;
|
|
368
401
|
const status = options.response.status;
|
|
369
402
|
const contentType = options.response.headers.get("content-type") ?? "text/html; charset=utf-8";
|
|
403
|
+
const cache = options.cache ?? cacheState.memoryCache;
|
|
404
|
+
const normalizedPath = normalizeRevalidationPath(options.path);
|
|
405
|
+
registerRouteCache(cache);
|
|
406
|
+
const invalidatedBeforeStore = routeCachePathWasInvalidatedDuringActiveContext(normalizedPath);
|
|
370
407
|
|
|
371
408
|
// A render that read a request header only produced HTML that is correct for
|
|
372
409
|
// that header value. The cache key covers the path and query alone, so the
|
|
@@ -374,15 +411,23 @@ export async function cacheRouteResponse(options: {
|
|
|
374
411
|
// shared caches too, which are keyed the same way.
|
|
375
412
|
if (
|
|
376
413
|
options.headerDependent === true ||
|
|
414
|
+
invalidatedBeforeStore ||
|
|
415
|
+
routeCacheIsUnavailable(cache) ||
|
|
377
416
|
requestCarriesCredentials(options.request) ||
|
|
378
|
-
options.response.headers.has("set-cookie")
|
|
417
|
+
options.response.headers.has("set-cookie") ||
|
|
418
|
+
responseHeadersContainCspNonce(options.response.headers)
|
|
379
419
|
) {
|
|
380
420
|
const headers = new Headers(options.response.headers);
|
|
381
421
|
headers.set("cache-control", "private, no-store");
|
|
382
422
|
if (!headers.has("content-type")) {
|
|
383
423
|
headers.set("content-type", contentType);
|
|
384
424
|
}
|
|
385
|
-
if (
|
|
425
|
+
if (
|
|
426
|
+
options.headerDependent === true ||
|
|
427
|
+
invalidatedBeforeStore ||
|
|
428
|
+
routeCacheIsUnavailable(cache) ||
|
|
429
|
+
responseHeadersContainCspNonce(options.response.headers)
|
|
430
|
+
) {
|
|
386
431
|
headers.set("x-mreact-cache", "DYNAMIC");
|
|
387
432
|
}
|
|
388
433
|
|
|
@@ -410,17 +455,57 @@ export async function cacheRouteResponse(options: {
|
|
|
410
455
|
// from every later secure visitor.
|
|
411
456
|
const hsts = options.strictTransportSecurity;
|
|
412
457
|
|
|
413
|
-
await
|
|
458
|
+
await cache.set(options.key, {
|
|
414
459
|
body,
|
|
415
460
|
cacheControl,
|
|
416
461
|
expiresAt: (options.now ?? Date.now()) + options.policy.revalidateSeconds * 1000,
|
|
417
462
|
headers: storedHeaders,
|
|
418
463
|
...(hsts === undefined ? {} : { strictTransportSecurity: hsts }),
|
|
419
|
-
path:
|
|
464
|
+
path: normalizedPath,
|
|
420
465
|
schemaVersion: ROUTE_CACHE_ENTRY_SCHEMA_VERSION,
|
|
421
466
|
status,
|
|
422
467
|
});
|
|
423
468
|
|
|
469
|
+
if (
|
|
470
|
+
routeCachePathWasInvalidatedDuringActiveContext(normalizedPath) ||
|
|
471
|
+
routeCacheIsUnavailable(cache)
|
|
472
|
+
) {
|
|
473
|
+
beginRouteCacheStoreInvalidation(cache);
|
|
474
|
+
try {
|
|
475
|
+
await cache.deleteByPath(normalizedPath);
|
|
476
|
+
endRouteCacheStoreInvalidation(cache);
|
|
477
|
+
} catch (error) {
|
|
478
|
+
// The invalidation may have completed before this asynchronous set did.
|
|
479
|
+
// If deleting the late write now fails, replace its exact key with an
|
|
480
|
+
// expired entry so another adapter object backed by the same persistent
|
|
481
|
+
// store cannot replay the stale current-schema value.
|
|
482
|
+
try {
|
|
483
|
+
await cache.set(options.key, {
|
|
484
|
+
body: "",
|
|
485
|
+
cacheControl: "no-store",
|
|
486
|
+
expiresAt: 0,
|
|
487
|
+
headers: {},
|
|
488
|
+
path: normalizedPath,
|
|
489
|
+
schemaVersion: ROUTE_CACHE_ENTRY_SCHEMA_VERSION,
|
|
490
|
+
status: 500,
|
|
491
|
+
});
|
|
492
|
+
} catch {
|
|
493
|
+
// The originating cache object still fails closed below even when the
|
|
494
|
+
// persistent backend rejects both deletion and the tombstone write.
|
|
495
|
+
}
|
|
496
|
+
cacheInvalidationState.disabledCaches.add(cache);
|
|
497
|
+
endRouteCacheStoreInvalidation(cache);
|
|
498
|
+
throw error;
|
|
499
|
+
}
|
|
500
|
+
const headers = new Headers(storedHeaders);
|
|
501
|
+
headers.set("cache-control", "private, no-store");
|
|
502
|
+
headers.set("x-mreact-cache", "DYNAMIC");
|
|
503
|
+
if (!headers.has("content-type")) {
|
|
504
|
+
headers.set("content-type", contentType);
|
|
505
|
+
}
|
|
506
|
+
return new Response(body, { headers, status });
|
|
507
|
+
}
|
|
508
|
+
|
|
424
509
|
const headers = new Headers(storedHeaders);
|
|
425
510
|
headers.set("cache-control", cacheControl);
|
|
426
511
|
if (!headers.has("content-type")) {
|
|
@@ -541,7 +626,7 @@ const PUBLIC_ROUTE_CACHE_REQUEST_HEADERS = new Set([
|
|
|
541
626
|
/**
|
|
542
627
|
* Invalidates cached route responses for a normalized app-router path.
|
|
543
628
|
*
|
|
544
|
-
* During a request the invalidation is scoped to that route cache context
|
|
629
|
+
* During a request the invalidation is scoped to that route cache context. Outside a request it is broadcast to every live route cache registered in this process, and later reads wait for their cache's invalidation to finish.
|
|
545
630
|
*/
|
|
546
631
|
export function revalidatePath(path: string): void {
|
|
547
632
|
const normalizedPath = normalizeRevalidationPath(path);
|
|
@@ -552,31 +637,186 @@ export function revalidatePath(path: string): void {
|
|
|
552
637
|
return;
|
|
553
638
|
}
|
|
554
639
|
|
|
555
|
-
|
|
640
|
+
for (const reference of cacheInvalidationState.liveReferences) {
|
|
641
|
+
const cache = reference.deref();
|
|
642
|
+
if (cache === undefined) {
|
|
643
|
+
cacheInvalidationState.liveReferences.delete(reference);
|
|
644
|
+
continue;
|
|
645
|
+
}
|
|
646
|
+
|
|
647
|
+
const pendingPaths = cacheInvalidationState.pendingPaths.get(cache);
|
|
648
|
+
if (
|
|
649
|
+
pendingPaths !== undefined &&
|
|
650
|
+
!cacheInvalidationState.disabledCaches.has(cache) &&
|
|
651
|
+
!pendingPaths.has(normalizedPath)
|
|
652
|
+
) {
|
|
653
|
+
if (pendingPaths.size >= MAX_PENDING_ROUTE_INVALIDATIONS) {
|
|
654
|
+
pendingPaths.clear();
|
|
655
|
+
cacheInvalidationState.disabledCaches.add(cache);
|
|
656
|
+
} else {
|
|
657
|
+
pendingPaths.add(normalizedPath);
|
|
658
|
+
}
|
|
659
|
+
}
|
|
660
|
+
markActiveRouteCacheContextsInvalidated(cache, normalizedPath);
|
|
661
|
+
void startPendingInvalidations(cache).catch(() => {});
|
|
662
|
+
}
|
|
556
663
|
}
|
|
557
664
|
|
|
558
665
|
export async function consumeInvalidations(
|
|
559
666
|
cache: AppRouterCache = cacheState.memoryCache,
|
|
560
667
|
): Promise<void> {
|
|
561
|
-
|
|
668
|
+
registerRouteCache(cache);
|
|
669
|
+
await startPendingInvalidations(cache);
|
|
670
|
+
}
|
|
671
|
+
|
|
672
|
+
function registerRouteCache(cache: AppRouterCache): void {
|
|
673
|
+
if (cacheInvalidationState.registeredCaches.has(cache)) {
|
|
562
674
|
return;
|
|
563
675
|
}
|
|
564
676
|
|
|
565
|
-
|
|
566
|
-
|
|
677
|
+
cacheInvalidationState.registeredCaches.add(cache);
|
|
678
|
+
cacheInvalidationState.pendingPaths.set(cache, new Set());
|
|
679
|
+
const reference = new WeakRef(cache);
|
|
680
|
+
cacheInvalidationState.liveReferences.add(reference);
|
|
681
|
+
cacheInvalidationState.finalizer.register(cache, reference, reference);
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
function startPendingInvalidations(cache: AppRouterCache): Promise<void> {
|
|
685
|
+
const running = cacheInvalidationState.running.get(cache);
|
|
686
|
+
if (running !== undefined) {
|
|
687
|
+
return running;
|
|
567
688
|
}
|
|
568
689
|
|
|
569
|
-
|
|
690
|
+
const paths = cacheInvalidationState.pendingPaths.get(cache);
|
|
691
|
+
if (paths === undefined || paths.size === 0) {
|
|
692
|
+
return Promise.resolve();
|
|
693
|
+
}
|
|
694
|
+
|
|
695
|
+
const task = drainPendingInvalidations(cache, paths);
|
|
696
|
+
cacheInvalidationState.running.set(cache, task);
|
|
697
|
+
void task
|
|
698
|
+
.finally(() => {
|
|
699
|
+
if (cacheInvalidationState.running.get(cache) === task) {
|
|
700
|
+
cacheInvalidationState.running.delete(cache);
|
|
701
|
+
}
|
|
702
|
+
})
|
|
703
|
+
.catch(() => {});
|
|
704
|
+
return task;
|
|
705
|
+
}
|
|
706
|
+
|
|
707
|
+
function createCacheInvalidationState(): AppRouterCacheInvalidationState {
|
|
708
|
+
const liveReferences = new Set<WeakRef<AppRouterCache>>();
|
|
709
|
+
return {
|
|
710
|
+
activeContexts: new WeakMap(),
|
|
711
|
+
disabledCaches: new WeakSet(),
|
|
712
|
+
finalizer: new FinalizationRegistry((reference) => {
|
|
713
|
+
liveReferences.delete(reference);
|
|
714
|
+
}),
|
|
715
|
+
liveReferences,
|
|
716
|
+
pendingPaths: new WeakMap(),
|
|
717
|
+
registeredCaches: new WeakSet(),
|
|
718
|
+
running: new WeakMap(),
|
|
719
|
+
storesBeingInvalidated: new WeakMap(),
|
|
720
|
+
};
|
|
721
|
+
}
|
|
722
|
+
|
|
723
|
+
function routeCacheIsUnavailable(cache: AppRouterCache): boolean {
|
|
724
|
+
return (
|
|
725
|
+
cacheInvalidationState.disabledCaches.has(cache) ||
|
|
726
|
+
(cacheInvalidationState.storesBeingInvalidated.get(cache) ?? 0) > 0
|
|
727
|
+
);
|
|
728
|
+
}
|
|
729
|
+
|
|
730
|
+
function beginRouteCacheStoreInvalidation(cache: AppRouterCache): void {
|
|
731
|
+
cacheInvalidationState.storesBeingInvalidated.set(
|
|
732
|
+
cache,
|
|
733
|
+
(cacheInvalidationState.storesBeingInvalidated.get(cache) ?? 0) + 1,
|
|
734
|
+
);
|
|
735
|
+
}
|
|
736
|
+
|
|
737
|
+
function endRouteCacheStoreInvalidation(cache: AppRouterCache): void {
|
|
738
|
+
const remaining = (cacheInvalidationState.storesBeingInvalidated.get(cache) ?? 1) - 1;
|
|
739
|
+
if (remaining <= 0) {
|
|
740
|
+
cacheInvalidationState.storesBeingInvalidated.delete(cache);
|
|
741
|
+
} else {
|
|
742
|
+
cacheInvalidationState.storesBeingInvalidated.set(cache, remaining);
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
746
|
+
function routeCachePathWasInvalidatedDuringActiveContext(path: string): boolean {
|
|
747
|
+
const context = activeRouteCacheContext();
|
|
748
|
+
return (
|
|
749
|
+
context !== undefined &&
|
|
750
|
+
(context.externalInvalidationOverflowed || context.externallyInvalidatedPaths.has(path))
|
|
751
|
+
);
|
|
752
|
+
}
|
|
753
|
+
|
|
754
|
+
function markActiveRouteCacheContextsInvalidated(
|
|
755
|
+
cache: AppRouterCache,
|
|
756
|
+
normalizedPath: string,
|
|
757
|
+
): void {
|
|
758
|
+
for (const context of cacheInvalidationState.activeContexts.get(cache) ?? []) {
|
|
759
|
+
if (cacheInvalidationState.disabledCaches.has(cache)) {
|
|
760
|
+
context.externalInvalidationOverflowed = true;
|
|
761
|
+
context.externallyInvalidatedPaths.clear();
|
|
762
|
+
} else if (
|
|
763
|
+
!context.externallyInvalidatedPaths.has(normalizedPath) &&
|
|
764
|
+
context.externallyInvalidatedPaths.size >= MAX_PENDING_ROUTE_INVALIDATIONS
|
|
765
|
+
) {
|
|
766
|
+
context.externalInvalidationOverflowed = true;
|
|
767
|
+
context.externallyInvalidatedPaths.clear();
|
|
768
|
+
} else {
|
|
769
|
+
context.externallyInvalidatedPaths.add(normalizedPath);
|
|
770
|
+
}
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
|
|
774
|
+
function registerActiveRouteCacheContext(context: RouteCacheContext): void {
|
|
775
|
+
const contexts = cacheInvalidationState.activeContexts.get(context.cache);
|
|
776
|
+
if (contexts === undefined) {
|
|
777
|
+
cacheInvalidationState.activeContexts.set(context.cache, new Set([context]));
|
|
778
|
+
return;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
contexts.add(context);
|
|
782
|
+
}
|
|
783
|
+
|
|
784
|
+
function unregisterActiveRouteCacheContext(context: RouteCacheContext): void {
|
|
785
|
+
const contexts = cacheInvalidationState.activeContexts.get(context.cache);
|
|
786
|
+
contexts?.delete(context);
|
|
787
|
+
if (contexts?.size === 0) {
|
|
788
|
+
cacheInvalidationState.activeContexts.delete(context.cache);
|
|
789
|
+
}
|
|
790
|
+
}
|
|
791
|
+
|
|
792
|
+
async function drainPendingInvalidations(
|
|
793
|
+
cache: AppRouterCache,
|
|
794
|
+
paths: Set<string>,
|
|
795
|
+
): Promise<void> {
|
|
796
|
+
while (paths.size > 0) {
|
|
797
|
+
const path = paths.values().next().value;
|
|
798
|
+
if (path === undefined) {
|
|
799
|
+
return;
|
|
800
|
+
}
|
|
801
|
+
|
|
802
|
+
await cache.deleteByPath(path);
|
|
803
|
+
paths.delete(path);
|
|
804
|
+
}
|
|
570
805
|
}
|
|
571
806
|
|
|
572
807
|
export async function withRouteCacheContext<T>(
|
|
573
808
|
cache: AppRouterCache | undefined,
|
|
574
809
|
fn: () => T | Promise<T>,
|
|
575
810
|
): Promise<{ cachePolicy: RouteCachePolicy | undefined; revalidatedPaths: string[]; value: T }> {
|
|
811
|
+
const routeCache = cache ?? cacheState.memoryCache;
|
|
812
|
+
registerRouteCache(routeCache);
|
|
576
813
|
const context: RouteCacheContext = {
|
|
577
|
-
cache:
|
|
814
|
+
cache: routeCache,
|
|
815
|
+
externalInvalidationOverflowed: false,
|
|
816
|
+
externallyInvalidatedPaths: new Set(),
|
|
578
817
|
revalidatedPaths: new Set(),
|
|
579
818
|
};
|
|
819
|
+
registerActiveRouteCacheContext(context);
|
|
580
820
|
|
|
581
821
|
return cacheState.storage.run(context, async () => {
|
|
582
822
|
try {
|
|
@@ -592,6 +832,7 @@ export async function withRouteCacheContext<T>(
|
|
|
592
832
|
return { cachePolicy, revalidatedPaths, value };
|
|
593
833
|
} finally {
|
|
594
834
|
context.closed = true;
|
|
835
|
+
unregisterActiveRouteCacheContext(context);
|
|
595
836
|
}
|
|
596
837
|
});
|
|
597
838
|
}
|
|
@@ -600,12 +841,17 @@ export function beginRouteCacheContext(cache: AppRouterCache | undefined): {
|
|
|
600
841
|
readonly cachePolicy: RouteCachePolicy | undefined;
|
|
601
842
|
dispose(): Promise<{ revalidatedPaths: string[] }>;
|
|
602
843
|
} {
|
|
844
|
+
const routeCache = cache ?? cacheState.memoryCache;
|
|
845
|
+
registerRouteCache(routeCache);
|
|
603
846
|
const context: RouteCacheContext = {
|
|
604
|
-
cache:
|
|
847
|
+
cache: routeCache,
|
|
848
|
+
externalInvalidationOverflowed: false,
|
|
849
|
+
externallyInvalidatedPaths: new Set(),
|
|
605
850
|
revalidatedPaths: new Set(),
|
|
606
851
|
};
|
|
607
852
|
|
|
608
853
|
cacheState.activeContexts.push(context);
|
|
854
|
+
registerActiveRouteCacheContext(context);
|
|
609
855
|
|
|
610
856
|
return {
|
|
611
857
|
get cachePolicy() {
|
|
@@ -623,6 +869,7 @@ export function beginRouteCacheContext(cache: AppRouterCache | undefined): {
|
|
|
623
869
|
if (index !== -1) {
|
|
624
870
|
cacheState.activeContexts.splice(index, 1);
|
|
625
871
|
}
|
|
872
|
+
unregisterActiveRouteCacheContext(context);
|
|
626
873
|
}
|
|
627
874
|
|
|
628
875
|
return { revalidatedPaths };
|
|
@@ -650,10 +897,16 @@ export function activeRouteCacheContext(): RouteCacheContext | undefined {
|
|
|
650
897
|
|
|
651
898
|
// Host is excluded from the cache key to prevent attacker-supplied Host
|
|
652
899
|
// headers from fragmenting / poisoning the cache (Issue 068). The Vary
|
|
653
|
-
//
|
|
654
|
-
// expected to handle vhost separation
|
|
655
|
-
|
|
656
|
-
|
|
900
|
+
// dimensions are the request path + query and the document/navigation HTML
|
|
901
|
+
// shape; same-origin reverse proxies are expected to handle vhost separation
|
|
902
|
+
// at their layer.
|
|
903
|
+
export function routeCacheKey(
|
|
904
|
+
appDir: string,
|
|
905
|
+
routePath: string,
|
|
906
|
+
url: URL,
|
|
907
|
+
responseVariant: "document" | "navigation" = "document",
|
|
908
|
+
): string {
|
|
909
|
+
return `${appDir}\0${normalizeRevalidationPath(routePath)}\0${url.pathname}${url.search}\0${responseVariant}`;
|
|
657
910
|
}
|
|
658
911
|
|
|
659
912
|
export function stripRevalidateExport(code: string): string {
|
package/src/cli-options.ts
CHANGED
|
@@ -335,6 +335,7 @@ export function formatCliHelp(command?: string | undefined): string {
|
|
|
335
335
|
"",
|
|
336
336
|
"Options:",
|
|
337
337
|
" --host <host> Bind address. Default: 127.0.0.1. Use 0.0.0.0 inside containers behind explicit port publishing or a reverse proxy.",
|
|
338
|
+
" --port <port> TCP port. Overrides PORT. Default: 3001.",
|
|
338
339
|
" --host-policy=strict|trusted-proxy",
|
|
339
340
|
" Control Host header trust for request origin reconstruction.",
|
|
340
341
|
" --allowed-hosts <host[,host...]>",
|
|
@@ -469,6 +470,18 @@ export function resolveCliDevPort(
|
|
|
469
470
|
return envValue === undefined || envValue === "" ? viteConfigPort : parseCliPort(envValue);
|
|
470
471
|
}
|
|
471
472
|
|
|
473
|
+
export function resolveCliStartPort(
|
|
474
|
+
flagValue: number | undefined,
|
|
475
|
+
env: { PORT?: string | undefined },
|
|
476
|
+
): number {
|
|
477
|
+
if (flagValue !== undefined) {
|
|
478
|
+
return flagValue;
|
|
479
|
+
}
|
|
480
|
+
|
|
481
|
+
const envValue = env.PORT;
|
|
482
|
+
return envValue === undefined || envValue === "" ? 3001 : parseCliPort(envValue, "PORT");
|
|
483
|
+
}
|
|
484
|
+
|
|
472
485
|
export function resolveCliHostPolicy(
|
|
473
486
|
flagValue: RequestHostPolicy | undefined,
|
|
474
487
|
env: { MREACT_ROUTER_HOST_POLICY?: string | undefined },
|
|
@@ -527,7 +540,7 @@ function parseCliRequestLogMode(value: string): CliRequestLogMode {
|
|
|
527
540
|
throw new Error(`Unsupported log mode ${JSON.stringify(value)}. Expected "requests".`);
|
|
528
541
|
}
|
|
529
542
|
|
|
530
|
-
function parseCliPort(value: string): number {
|
|
543
|
+
function parseCliPort(value: string, source = "port"): number {
|
|
531
544
|
const port = Number(value);
|
|
532
545
|
|
|
533
546
|
if (Number.isInteger(port) && port >= 0 && port <= 65535) {
|
|
@@ -535,7 +548,7 @@ function parseCliPort(value: string): number {
|
|
|
535
548
|
}
|
|
536
549
|
|
|
537
550
|
throw new Error(
|
|
538
|
-
`Unsupported
|
|
551
|
+
`Unsupported ${source} ${JSON.stringify(value)}. Expected an integer from 0 to 65535.`,
|
|
539
552
|
);
|
|
540
553
|
}
|
|
541
554
|
|
package/src/cli.ts
CHANGED
|
@@ -25,6 +25,7 @@ import {
|
|
|
25
25
|
resolveCliHost,
|
|
26
26
|
resolveCliHostPolicy,
|
|
27
27
|
resolveCliRequestLogMode,
|
|
28
|
+
resolveCliStartPort,
|
|
28
29
|
resolveCliTrustForwardedProto,
|
|
29
30
|
} from "./cli-options.js";
|
|
30
31
|
import { startDevServer } from "./dev-server.js";
|
|
@@ -63,7 +64,16 @@ if (parsed !== undefined) {
|
|
|
63
64
|
const loaded =
|
|
64
65
|
routeArg === undefined
|
|
65
66
|
? await loadMreactRouterViteConfigDetails({ command: "build", cwd: process.cwd() })
|
|
66
|
-
: {
|
|
67
|
+
: {
|
|
68
|
+
dehydrateOptions: undefined,
|
|
69
|
+
project: { appDir: resolve(routeArg) },
|
|
70
|
+
viteConfig: undefined,
|
|
71
|
+
};
|
|
72
|
+
if (loaded.dehydrateOptions !== undefined) {
|
|
73
|
+
throw new Error(
|
|
74
|
+
"Production builds must configure dehydratePolicyModule instead of an inline dehydrateOptions callback so generated runtimes use the same policy as prerendering.",
|
|
75
|
+
);
|
|
76
|
+
}
|
|
67
77
|
try {
|
|
68
78
|
const result = await buildApp({
|
|
69
79
|
...loaded.project,
|
|
@@ -150,12 +160,16 @@ if (parsed !== undefined) {
|
|
|
150
160
|
routeArg === undefined
|
|
151
161
|
? await loadMreactRouterViteConfigDetails({ command: "serve", cwd: process.cwd() })
|
|
152
162
|
: {
|
|
163
|
+
dehydrateOptions: undefined,
|
|
153
164
|
project: { appDir: resolve(routeArg) },
|
|
154
165
|
serverPort: undefined,
|
|
155
166
|
viteConfig: undefined,
|
|
156
167
|
};
|
|
157
168
|
const server = await startDevServer({
|
|
158
169
|
...loaded.project,
|
|
170
|
+
...(loaded.dehydrateOptions === undefined
|
|
171
|
+
? {}
|
|
172
|
+
: { dehydrateOptions: loaded.dehydrateOptions }),
|
|
159
173
|
hostname: resolveCliHost(parsed.host, process.env),
|
|
160
174
|
logger,
|
|
161
175
|
port: resolveCliDevPort(parsed.port, process.env, loaded.serverPort),
|
|
@@ -169,7 +183,7 @@ if (parsed !== undefined) {
|
|
|
169
183
|
hostname: resolveCliHost(parsed.host, process.env),
|
|
170
184
|
logger,
|
|
171
185
|
outDir: resolve(routeArg ?? ".mreact"),
|
|
172
|
-
port:
|
|
186
|
+
port: resolveCliStartPort(parsed.port, process.env),
|
|
173
187
|
trustForwardedProto: resolveCliTrustForwardedProto(
|
|
174
188
|
parsed.trustForwardedProto,
|
|
175
189
|
process.env,
|
package/src/client.ts
CHANGED
|
@@ -78,6 +78,7 @@ export interface BuildClientRouteOutputOptions {
|
|
|
78
78
|
filename: string;
|
|
79
79
|
minify?: boolean | undefined;
|
|
80
80
|
routePath: string;
|
|
81
|
+
restoreRequestUrl?: boolean | undefined;
|
|
81
82
|
sourceMap?: boolean | undefined;
|
|
82
83
|
vitePlugins?: readonly PluginOption[] | undefined;
|
|
83
84
|
clientNavigation?: boolean | undefined;
|
|
@@ -2632,9 +2633,18 @@ export function withHydrationMarkers(options: {
|
|
|
2632
2633
|
}
|
|
2633
2634
|
|
|
2634
2635
|
export function withRouteMarkers(options: { html: string; routePath: string }): string {
|
|
2635
|
-
const
|
|
2636
|
+
const marker = routeMarkerParts(options.routePath);
|
|
2636
2637
|
|
|
2637
|
-
return
|
|
2638
|
+
return `${marker.prefix}${options.html}${marker.suffix}`;
|
|
2639
|
+
}
|
|
2640
|
+
|
|
2641
|
+
export function routeMarkerParts(routePath: string): { prefix: string; suffix: string } {
|
|
2642
|
+
const routeId = routeIdForPath(routePath);
|
|
2643
|
+
|
|
2644
|
+
return {
|
|
2645
|
+
prefix: `<div ${routeHydrationContract.routeMarkerAttribute}="${escapeHtmlAttribute(routeId)}">`,
|
|
2646
|
+
suffix: "</div>",
|
|
2647
|
+
};
|
|
2638
2648
|
}
|
|
2639
2649
|
|
|
2640
2650
|
export function hydrationMarkerParts(options: {
|
|
@@ -2872,6 +2882,8 @@ export async function buildClientRouteEntrySource(
|
|
|
2872
2882
|
);
|
|
2873
2883
|
|
|
2874
2884
|
const routeId = routeIdForPath(options.routePath);
|
|
2885
|
+
const restoreRequestUrl =
|
|
2886
|
+
options.restoreRequestUrl ?? (options.debugLabels === true || /\brequest\b/.test(options.code));
|
|
2875
2887
|
const routeUsesCells = detectRouteCellStateHint(compiled.code);
|
|
2876
2888
|
const routeUsesReactiveEffect = detectRouteReactiveEffectHint(compiled.code);
|
|
2877
2889
|
const routeUsesDomRefs = compiled.metadata.imports.some(
|
|
@@ -3401,6 +3413,7 @@ ${routeOutOfOrderFragmentHydration} const __mreactMarker = document.querySelect
|
|
|
3401
3413
|
const __mreactProps = __mreactPropsText === undefined
|
|
3402
3414
|
? {}
|
|
3403
3415
|
: JSON.parse(__mreactPropsText);
|
|
3416
|
+
${restoreRequestUrl ? " if (__mreactProps.request) __mreactProps.request.url = document.URL;\n" : ""}
|
|
3404
3417
|
const __mreactClientReferences = __mreactClientReferencesElement?.textContent === undefined
|
|
3405
3418
|
? []
|
|
3406
3419
|
: JSON.parse(__mreactClientReferencesElement.textContent);
|
|
@@ -3924,7 +3937,7 @@ function __mreactFetchNavigationHtml(href) {
|
|
|
3924
3937
|
? { "x-mreact-navigation": "1", "x-mreact-navigation-cache": "reload" }
|
|
3925
3938
|
: { "x-mreact-navigation": "1" };
|
|
3926
3939
|
|
|
3927
|
-
return fetch(href, {
|
|
3940
|
+
return fetch(__mreactStaticNavigationRequestHref(href), {
|
|
3928
3941
|
headers,
|
|
3929
3942
|
}).then((response) => {
|
|
3930
3943
|
__mreactApplyRevalidationHeader(response);
|
|
@@ -3936,6 +3949,37 @@ function __mreactFetchNavigationHtml(href) {
|
|
|
3936
3949
|
});
|
|
3937
3950
|
}
|
|
3938
3951
|
|
|
3952
|
+
function __mreactStaticNavigationRequestHref(href) {
|
|
3953
|
+
if (typeof document === "undefined" || typeof location === "undefined") {
|
|
3954
|
+
return href;
|
|
3955
|
+
}
|
|
3956
|
+
|
|
3957
|
+
const base = document
|
|
3958
|
+
.querySelector('meta[name="mreact-static-navigation"]')
|
|
3959
|
+
?.getAttribute("content")
|
|
3960
|
+
?.trim();
|
|
3961
|
+
if (base === undefined || base === "") {
|
|
3962
|
+
return href;
|
|
3963
|
+
}
|
|
3964
|
+
|
|
3965
|
+
try {
|
|
3966
|
+
const destination = new URL(href, location.href);
|
|
3967
|
+
if (destination.origin !== location.origin) {
|
|
3968
|
+
return href;
|
|
3969
|
+
}
|
|
3970
|
+
const routePath = destination.pathname.replace(/^\\/+|\\/+$/g, "");
|
|
3971
|
+
const artifactPath = routePath === "" ? "index.html" : routePath + "/index.html";
|
|
3972
|
+
const artifact = new URL(
|
|
3973
|
+
base.replace(/\\/+$/g, "") + "/" + artifactPath,
|
|
3974
|
+
location.origin,
|
|
3975
|
+
);
|
|
3976
|
+
artifact.search = destination.search;
|
|
3977
|
+
return artifact.href;
|
|
3978
|
+
} catch {
|
|
3979
|
+
return href;
|
|
3980
|
+
}
|
|
3981
|
+
}
|
|
3982
|
+
|
|
3939
3983
|
function __mreactNavigationResponseRequiresDocumentReload(response) {
|
|
3940
3984
|
return response.status === 204 || response.headers.get("x-mreact-navigation") === "reload";
|
|
3941
3985
|
}
|