@reckona/mreact-router 0.0.203 → 0.0.205
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 +53 -9
- package/dist/actions.d.ts +6 -0
- package/dist/actions.d.ts.map +1 -1
- package/dist/actions.js +70 -10
- 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 +2 -1
- package/dist/adapters/cloudflare.d.ts.map +1 -1
- package/dist/adapters/cloudflare.js +18 -10
- 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 +1 -0
- package/dist/build.d.ts.map +1 -1
- package/dist/build.js +91 -5
- 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 +3 -1
- package/dist/cache.d.ts.map +1 -1
- package/dist/cache.js +211 -13
- package/dist/cache.js.map +1 -1
- package/dist/cli.js +12 -1
- package/dist/cli.js.map +1 -1
- package/dist/client.d.ts.map +1 -1
- package/dist/client.js +32 -1
- 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 +11 -6
- 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.map +1 -1
- package/dist/navigation-marker.js +72 -9
- package/dist/navigation-marker.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 +14 -0
- package/dist/prerender-entry.d.ts.map +1 -1
- package/dist/prerender-entry.js +40 -3
- package/dist/prerender-entry.js.map +1 -1
- package/dist/render.d.ts +9 -1
- package/dist/render.d.ts.map +1 -1
- package/dist/render.js +102 -123
- package/dist/render.js.map +1 -1
- package/dist/serve.d.ts +8 -2
- package/dist/serve.d.ts.map +1 -1
- package/dist/serve.js +22 -20
- 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 +81 -11
- package/src/adapters/aws-lambda.ts +426 -115
- package/src/adapters/cloudflare.ts +29 -13
- 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 +117 -10
- package/src/built-runtime.ts +28 -0
- package/src/cache.ts +262 -15
- package/src/cli.ts +14 -1
- package/src/client.ts +32 -1
- package/src/config.ts +43 -6
- package/src/csp.ts +14 -11
- 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 +79 -9
- package/src/navigation.ts +1 -1
- package/src/node-server.ts +128 -22
- package/src/prerender-entry.ts +52 -3
- package/src/render.ts +807 -753
- package/src/serve.ts +49 -56
- 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 };
|
package/src/cli.ts
CHANGED
|
@@ -64,7 +64,16 @@ if (parsed !== undefined) {
|
|
|
64
64
|
const loaded =
|
|
65
65
|
routeArg === undefined
|
|
66
66
|
? await loadMreactRouterViteConfigDetails({ command: "build", cwd: process.cwd() })
|
|
67
|
-
: {
|
|
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
|
+
}
|
|
68
77
|
try {
|
|
69
78
|
const result = await buildApp({
|
|
70
79
|
...loaded.project,
|
|
@@ -151,12 +160,16 @@ if (parsed !== undefined) {
|
|
|
151
160
|
routeArg === undefined
|
|
152
161
|
? await loadMreactRouterViteConfigDetails({ command: "serve", cwd: process.cwd() })
|
|
153
162
|
: {
|
|
163
|
+
dehydrateOptions: undefined,
|
|
154
164
|
project: { appDir: resolve(routeArg) },
|
|
155
165
|
serverPort: undefined,
|
|
156
166
|
viteConfig: undefined,
|
|
157
167
|
};
|
|
158
168
|
const server = await startDevServer({
|
|
159
169
|
...loaded.project,
|
|
170
|
+
...(loaded.dehydrateOptions === undefined
|
|
171
|
+
? {}
|
|
172
|
+
: { dehydrateOptions: loaded.dehydrateOptions }),
|
|
160
173
|
hostname: resolveCliHost(parsed.host, process.env),
|
|
161
174
|
logger,
|
|
162
175
|
port: resolveCliDevPort(parsed.port, process.env, loaded.serverPort),
|
package/src/client.ts
CHANGED
|
@@ -3937,7 +3937,7 @@ function __mreactFetchNavigationHtml(href) {
|
|
|
3937
3937
|
? { "x-mreact-navigation": "1", "x-mreact-navigation-cache": "reload" }
|
|
3938
3938
|
: { "x-mreact-navigation": "1" };
|
|
3939
3939
|
|
|
3940
|
-
return fetch(href, {
|
|
3940
|
+
return fetch(__mreactStaticNavigationRequestHref(href), {
|
|
3941
3941
|
headers,
|
|
3942
3942
|
}).then((response) => {
|
|
3943
3943
|
__mreactApplyRevalidationHeader(response);
|
|
@@ -3949,6 +3949,37 @@ function __mreactFetchNavigationHtml(href) {
|
|
|
3949
3949
|
});
|
|
3950
3950
|
}
|
|
3951
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
|
+
|
|
3952
3983
|
function __mreactNavigationResponseRequiresDocumentReload(response) {
|
|
3953
3984
|
return response.status === 204 || response.headers.get("x-mreact-navigation") === "reload";
|
|
3954
3985
|
}
|
package/src/config.ts
CHANGED
|
@@ -37,6 +37,8 @@ export interface AppRouterProjectOptions {
|
|
|
37
37
|
buildConcurrency?: number | undefined;
|
|
38
38
|
buildTargets?: readonly AppRouterBuildTarget[] | undefined;
|
|
39
39
|
clientSourceMaps?: AppRouterClientSourceMapOption | undefined;
|
|
40
|
+
/** Project-local server module exporting a named `dehydrateOptions` policy. */
|
|
41
|
+
dehydratePolicyModule?: string | undefined;
|
|
40
42
|
/**
|
|
41
43
|
* Legacy route root. When provided without routesDir/projectRoot, this keeps
|
|
42
44
|
* the historical "appDir is the whole app boundary" behavior.
|
|
@@ -61,6 +63,7 @@ export interface ResolvedAppRouterProject {
|
|
|
61
63
|
buildTargets: readonly AppRouterBuildTarget[];
|
|
62
64
|
clientSourceMaps: AppRouterClientSourceMapMode;
|
|
63
65
|
clientConsolePureFunctions?: readonly string[] | undefined;
|
|
66
|
+
dehydratePolicyModule?: string | undefined;
|
|
64
67
|
projectRoot: string;
|
|
65
68
|
publicAssetBaseUrl?: string | undefined;
|
|
66
69
|
publicDir: string;
|
|
@@ -80,11 +83,12 @@ export function resolveAppRouterProjectOptions(
|
|
|
80
83
|
options.routesDir === undefined
|
|
81
84
|
) {
|
|
82
85
|
const appDir = resolve(options.appDir);
|
|
86
|
+
const allowedSourceDirs = (options.allowedSourceDirs ?? [appDir]).map((directory) =>
|
|
87
|
+
resolveProjectPath(appDir, directory, "allowedSourceDirs"),
|
|
88
|
+
);
|
|
83
89
|
|
|
84
90
|
return {
|
|
85
|
-
allowedSourceDirs
|
|
86
|
-
resolveProjectPath(appDir, directory, "allowedSourceDirs"),
|
|
87
|
-
),
|
|
91
|
+
allowedSourceDirs,
|
|
88
92
|
...(options.assetBaseUrl === undefined ? {} : { assetBaseUrl: options.assetBaseUrl }),
|
|
89
93
|
...(options.buildConcurrency === undefined
|
|
90
94
|
? {}
|
|
@@ -92,6 +96,15 @@ export function resolveAppRouterProjectOptions(
|
|
|
92
96
|
buildTargets: resolveBuildTargets(options.buildTargets),
|
|
93
97
|
clientSourceMaps: resolveClientSourceMapMode(options.clientSourceMaps),
|
|
94
98
|
...(clientConsolePureFunctions === undefined ? {} : { clientConsolePureFunctions }),
|
|
99
|
+
...(options.dehydratePolicyModule === undefined
|
|
100
|
+
? {}
|
|
101
|
+
: {
|
|
102
|
+
dehydratePolicyModule: resolvePolicyModule(
|
|
103
|
+
appDir,
|
|
104
|
+
options.dehydratePolicyModule,
|
|
105
|
+
allowedSourceDirs,
|
|
106
|
+
),
|
|
107
|
+
}),
|
|
95
108
|
projectRoot: appDir,
|
|
96
109
|
...(options.publicAssetBaseUrl === undefined
|
|
97
110
|
? {}
|
|
@@ -105,11 +118,12 @@ export function resolveAppRouterProjectOptions(
|
|
|
105
118
|
const routesDir = resolveProjectPath(projectRoot, options.routesDir ?? "src/app", "routesDir");
|
|
106
119
|
const defaultAllowedSourceDirs =
|
|
107
120
|
options.routesDir === undefined ? ["src"] : [defaultAllowedSourceDir(projectRoot, routesDir)];
|
|
121
|
+
const allowedSourceDirs = (options.allowedSourceDirs ?? defaultAllowedSourceDirs).map(
|
|
122
|
+
(directory) => resolveProjectPath(projectRoot, directory, "allowedSourceDirs"),
|
|
123
|
+
);
|
|
108
124
|
|
|
109
125
|
return {
|
|
110
|
-
allowedSourceDirs
|
|
111
|
-
resolveProjectPath(projectRoot, directory, "allowedSourceDirs"),
|
|
112
|
-
),
|
|
126
|
+
allowedSourceDirs,
|
|
113
127
|
...(options.assetBaseUrl === undefined ? {} : { assetBaseUrl: options.assetBaseUrl }),
|
|
114
128
|
...(options.buildConcurrency === undefined
|
|
115
129
|
? {}
|
|
@@ -117,6 +131,15 @@ export function resolveAppRouterProjectOptions(
|
|
|
117
131
|
buildTargets: resolveBuildTargets(options.buildTargets),
|
|
118
132
|
clientSourceMaps: resolveClientSourceMapMode(options.clientSourceMaps),
|
|
119
133
|
...(clientConsolePureFunctions === undefined ? {} : { clientConsolePureFunctions }),
|
|
134
|
+
...(options.dehydratePolicyModule === undefined
|
|
135
|
+
? {}
|
|
136
|
+
: {
|
|
137
|
+
dehydratePolicyModule: resolvePolicyModule(
|
|
138
|
+
projectRoot,
|
|
139
|
+
options.dehydratePolicyModule,
|
|
140
|
+
allowedSourceDirs,
|
|
141
|
+
),
|
|
142
|
+
}),
|
|
120
143
|
projectRoot,
|
|
121
144
|
...(options.publicAssetBaseUrl === undefined
|
|
122
145
|
? {}
|
|
@@ -126,6 +149,20 @@ export function resolveAppRouterProjectOptions(
|
|
|
126
149
|
};
|
|
127
150
|
}
|
|
128
151
|
|
|
152
|
+
function resolvePolicyModule(
|
|
153
|
+
projectRoot: string,
|
|
154
|
+
modulePath: string,
|
|
155
|
+
allowedSourceDirs: readonly string[],
|
|
156
|
+
): string {
|
|
157
|
+
const resolvedModule = resolveProjectPath(projectRoot, modulePath, "dehydratePolicyModule");
|
|
158
|
+
if (!allowedSourceDirs.some((directory) => isInsideDirectory(directory, resolvedModule))) {
|
|
159
|
+
throw new Error(
|
|
160
|
+
`mreactRouter dehydratePolicyModule must resolve inside allowedSourceDirs: ${resolvedModule}`,
|
|
161
|
+
);
|
|
162
|
+
}
|
|
163
|
+
return resolvedModule;
|
|
164
|
+
}
|
|
165
|
+
|
|
129
166
|
function defaultAllowedSourceDir(projectRoot: string, routesDir: string): string {
|
|
130
167
|
const sourceRoot = dirname(routesDir);
|
|
131
168
|
return basename(routesDir) === "app" && sourceRoot !== projectRoot ? sourceRoot : routesDir;
|
package/src/csp.ts
CHANGED
|
@@ -18,6 +18,17 @@ export interface ContentSecurityPolicyInput {
|
|
|
18
18
|
replace?: Record<string, readonly string[] | string>;
|
|
19
19
|
}
|
|
20
20
|
|
|
21
|
+
export function responseHeadersContainCspNonce(headers: Headers): boolean {
|
|
22
|
+
for (const name of ["content-security-policy", "content-security-policy-report-only"]) {
|
|
23
|
+
const value = headers.get(name);
|
|
24
|
+
if (value !== null && /(?:^|[\s;,])'nonce-[^']+'(?=$|[\s;,])/i.test(value)) {
|
|
25
|
+
return true;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
return false;
|
|
30
|
+
}
|
|
31
|
+
|
|
21
32
|
const VALID_NONCE = /^[A-Za-z0-9+/=_-]+$/;
|
|
22
33
|
const VALID_DIRECTIVE_NAME = /^[a-z][a-z0-9-]*$/i;
|
|
23
34
|
// One CSP "source expression". Reject anything containing `;`, quote,
|
|
@@ -46,13 +57,7 @@ function isValidDirectiveValue(value: string): boolean {
|
|
|
46
57
|
for (let index = 0; index < value.length; index += 1) {
|
|
47
58
|
const code = value.charCodeAt(index);
|
|
48
59
|
|
|
49
|
-
if (
|
|
50
|
-
code <= 0x20 ||
|
|
51
|
-
code === 0x22 ||
|
|
52
|
-
code === 0x27 ||
|
|
53
|
-
code === 0x3b ||
|
|
54
|
-
code === 0x7f
|
|
55
|
-
) {
|
|
60
|
+
if (code <= 0x20 || code === 0x22 || code === 0x27 || code === 0x3b || code === 0x7f) {
|
|
56
61
|
return false;
|
|
57
62
|
}
|
|
58
63
|
}
|
|
@@ -84,9 +89,7 @@ export function contentSecurityPolicy(
|
|
|
84
89
|
|
|
85
90
|
for (const rawValue of rawValues) {
|
|
86
91
|
if (typeof rawValue !== "string" || !isValidDirectiveValue(rawValue)) {
|
|
87
|
-
throw new TypeError(
|
|
88
|
-
`invalid CSP directive value for ${name}: ${JSON.stringify(rawValue)}`,
|
|
89
|
-
);
|
|
92
|
+
throw new TypeError(`invalid CSP directive value for ${name}: ${JSON.stringify(rawValue)}`);
|
|
90
93
|
}
|
|
91
94
|
}
|
|
92
95
|
|
|
@@ -99,5 +102,5 @@ export function contentSecurityPolicy(
|
|
|
99
102
|
serialized.push(`${name} ${values.join(" ")}`);
|
|
100
103
|
}
|
|
101
104
|
|
|
102
|
-
return serialized.join("; ");
|
|
105
|
+
return serialized.length === 0 ? undefined : serialized.join("; ");
|
|
103
106
|
}
|
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
import type { DehydrateOptions } from "@reckona/mreact-query";
|
|
2
|
+
|
|
3
|
+
export function dehydrateOptionsFromModule(
|
|
4
|
+
module: unknown,
|
|
5
|
+
label: string,
|
|
6
|
+
): DehydrateOptions {
|
|
7
|
+
if (module === null || typeof module !== "object" || !("dehydrateOptions" in module)) {
|
|
8
|
+
throw new Error(`${label} must export a named dehydrateOptions object.`);
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const dehydrateOptions = (module as { dehydrateOptions?: unknown }).dehydrateOptions;
|
|
12
|
+
if (dehydrateOptions === null || typeof dehydrateOptions !== "object") {
|
|
13
|
+
throw new Error(`${label} must export a named dehydrateOptions object.`);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const shouldDehydrateQuery = (dehydrateOptions as { shouldDehydrateQuery?: unknown })
|
|
17
|
+
.shouldDehydrateQuery;
|
|
18
|
+
if (shouldDehydrateQuery !== undefined && typeof shouldDehydrateQuery !== "function") {
|
|
19
|
+
throw new Error(`${label} dehydrateOptions.shouldDehydrateQuery must be a function.`);
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
return dehydrateOptions as DehydrateOptions;
|
|
23
|
+
}
|