@rangojs/router 0.21.0 → 0.22.0
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/dist/bin/rango.js +13 -1
- package/dist/types/browser/event-controller.d.ts +30 -13
- package/dist/types/browser/navigation-store.d.ts +9 -0
- package/dist/types/browser/partial-update.d.ts +2 -1
- package/dist/types/browser/prefetch/default-strategy.d.ts +16 -20
- package/dist/types/browser/react/NavigationProvider.d.ts +14 -1
- package/dist/types/browser/react/context.d.ts +14 -0
- package/dist/types/browser/rsc-router.d.ts +13 -2
- package/dist/types/browser/types.d.ts +10 -0
- package/dist/types/build/generate-manifest.d.ts +2 -0
- package/dist/types/cache/background-task.d.ts +22 -0
- package/dist/types/cache/cache-key-utils.d.ts +50 -5
- package/dist/types/cache/cache-policy.d.ts +17 -0
- package/dist/types/cache/cache-scope.d.ts +1 -1
- package/dist/types/cache/cf/cf-cache-store.d.ts +7 -1
- package/dist/types/cache/document-cache.d.ts +8 -5
- package/dist/types/cache/handle-snapshot.d.ts +23 -0
- package/dist/types/cache/index.d.ts +1 -1
- package/dist/types/cache/invalidation-order.d.ts +14 -0
- package/dist/types/cache/memory-segment-store.d.ts +33 -1
- package/dist/types/cache/shell-snapshot.d.ts +10 -1
- package/dist/types/cache/store-scope.d.ts +22 -0
- package/dist/types/cache/tag-invalidation.d.ts +7 -6
- package/dist/types/cache/types.d.ts +63 -14
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +7 -1
- package/dist/types/errors.d.ts +6 -0
- package/dist/types/handles/deferred-resolution.d.ts +7 -0
- package/dist/types/internal-suspense-audit.d.ts +1 -0
- package/dist/types/loader-store.d.ts +4 -8
- package/dist/types/outlet-provider.d.ts +2 -1
- package/dist/types/prerender/cloudflare.d.ts +49 -0
- package/dist/types/prerender/create-prerender-trigger.d.ts +121 -0
- package/dist/types/prerender/index.d.ts +12 -0
- package/dist/types/prerender/memory-prerender-store.d.ts +29 -0
- package/dist/types/prerender/on-demand.d.ts +272 -0
- package/dist/types/prerender/producer-guard.d.ts +33 -0
- package/dist/types/prerender/shell-manifest-key.d.ts +1 -18
- package/dist/types/prerender/warm-request.d.ts +65 -0
- package/dist/types/prerender/warm.d.ts +65 -0
- package/dist/types/prerender/writable-store.d.ts +170 -0
- package/dist/types/prerender.d.ts +27 -0
- package/dist/types/reverse.d.ts +1 -1
- package/dist/types/route-content-wrapper.d.ts +3 -1
- package/dist/types/router/error-handling.d.ts +2 -0
- package/dist/types/router/handler-context.d.ts +1 -1
- package/dist/types/router/match-middleware/cache-lookup.d.ts +14 -0
- package/dist/types/router/pattern-matching.d.ts +2 -0
- package/dist/types/router/prefetch-default.d.ts +4 -4
- package/dist/types/router/prerender-match.d.ts +23 -1
- package/dist/types/router/route-trie-builder.d.ts +8 -1
- package/dist/types/router/router-context.d.ts +1 -0
- package/dist/types/router/router-interfaces.d.ts +30 -0
- package/dist/types/router/router-options.d.ts +24 -0
- package/dist/types/router/segment-resolution/fresh.d.ts +7 -0
- package/dist/types/router/segment-resolution/loader-cache.d.ts +2 -1
- package/dist/types/router/segment-resolution/revalidation.d.ts +5 -1
- package/dist/types/router/segment-resolution/streamed-handler-boundary.d.ts +43 -0
- package/dist/types/router/trie-matching.d.ts +2 -0
- package/dist/types/rsc/shell-build-manifest.d.ts +6 -4
- package/dist/types/rsc/shell-capture-constants.d.ts +20 -5
- package/dist/types/rsc/shell-capture.d.ts +14 -1
- package/dist/types/rsc/shell-serve.d.ts +2 -2
- package/dist/types/rsc/ssr-module-loader.d.ts +15 -0
- package/dist/types/rsc/types.d.ts +8 -0
- package/dist/types/segment-boundary-content.d.ts +17 -0
- package/dist/types/segment-loader-promise.d.ts +2 -6
- package/dist/types/server/build-version-table.d.ts +9 -0
- package/dist/types/server/context.d.ts +12 -4
- package/dist/types/server/handle-store.d.ts +2 -0
- package/dist/types/server/request-context.d.ts +63 -2
- package/dist/types/suspense-audit-react.d.ts +26 -0
- package/dist/types/suspense-audit.d.ts +157 -0
- package/dist/types/testing/action-form.d.ts +34 -0
- package/dist/types/testing/flight.entry.d.ts +2 -0
- package/dist/types/testing/index.d.ts +4 -0
- package/dist/types/testing/internal/server-modules.d.ts +8 -0
- package/dist/types/testing/serve-shell-request.d.ts +34 -4
- package/dist/types/testing/shell-status.d.ts +14 -6
- package/dist/types/testing/vitest-stubs/plugin-rsc.d.ts +9 -6
- package/dist/types/thenable-status.d.ts +6 -0
- package/dist/types/types/handler-context.d.ts +7 -0
- package/dist/types/types/route-entry.d.ts +5 -0
- package/dist/types/urls/path-helper.d.ts +18 -0
- package/dist/types/vite/discovery/state.d.ts +11 -0
- package/dist/types/vite/inject-client-debug.d.ts +6 -25
- package/dist/types/vite/utils/bundle-analysis.d.ts +6 -2
- package/dist/vite/index.js +179 -496
- package/package.json +12 -2
- package/skills/cache-guide/SKILL.md +18 -17
- package/skills/caching/SKILL.md +40 -4
- package/skills/catalog.json +1 -1
- package/skills/cloudflare/SKILL.md +131 -0
- package/skills/comparison/references/framework-comparison.md +1 -1
- package/skills/deployment-caching/SKILL.md +33 -12
- package/skills/document-cache/SKILL.md +16 -14
- package/skills/hooks/SKILL.md +22 -0
- package/skills/hooks/handle-and-actions.md +3 -4
- package/skills/hooks/navigation.md +10 -0
- package/skills/hooks/state.md +5 -4
- package/skills/host-router/SKILL.md +18 -0
- package/skills/loader/SKILL.md +6 -4
- package/skills/middleware/SKILL.md +1 -1
- package/skills/migrate-nextjs/SKILL.md +82 -9
- package/skills/ppr/SKILL.md +31 -8
- package/skills/prerender/SKILL.md +645 -8
- package/skills/rango/SKILL.md +1 -0
- package/skills/response-routes/SKILL.md +5 -5
- package/skills/route/SKILL.md +29 -0
- package/skills/testing/SKILL.md +2 -1
- package/skills/testing/cache-prerender.md +18 -17
- package/skills/testing/serve-shell-request.md +104 -8
- package/skills/use-cache/SKILL.md +9 -5
- package/skills/vercel/SKILL.md +2 -0
- package/skills/view-transitions/SKILL.md +9 -4
- package/src/browser/event-controller.ts +69 -25
- package/src/browser/link-interceptor.ts +2 -2
- package/src/browser/navigation-bridge.ts +75 -35
- package/src/browser/navigation-store.ts +26 -0
- package/src/browser/navigation-transaction.ts +9 -2
- package/src/browser/network-error-handler.ts +2 -0
- package/src/browser/partial-update.ts +48 -12
- package/src/browser/prefetch/default-strategy.ts +22 -42
- package/src/browser/react/Link.tsx +37 -42
- package/src/browser/react/NavigationProvider.tsx +63 -3
- package/src/browser/react/context.ts +17 -0
- package/src/browser/react/location-state.ts +5 -19
- package/src/browser/react/use-action.ts +4 -1
- package/src/browser/react/use-handle.ts +4 -20
- package/src/browser/react/use-link-status.ts +4 -3
- package/src/browser/react/use-navigation.ts +4 -1
- package/src/browser/react/use-params.ts +4 -1
- package/src/browser/react/use-pathname.ts +4 -1
- package/src/browser/react/use-search-params.ts +5 -3
- package/src/browser/react/use-segments.ts +36 -54
- package/src/browser/rsc-router.tsx +79 -39
- package/src/browser/segment-reconciler.ts +3 -3
- package/src/browser/server-action-bridge.ts +26 -6
- package/src/browser/types.ts +11 -0
- package/src/build/generate-manifest.ts +17 -0
- package/src/build/merge-full-manifests.ts +5 -0
- package/src/build/route-trie.ts +1 -0
- package/src/cache/background-task.ts +61 -0
- package/src/cache/cache-key-utils.ts +91 -12
- package/src/cache/cache-policy.ts +11 -2
- package/src/cache/cache-runtime.ts +23 -23
- package/src/cache/cache-scope.ts +38 -36
- package/src/cache/cf/cf-cache-store.ts +16 -11
- package/src/cache/document-cache.ts +35 -14
- package/src/cache/handle-snapshot.ts +50 -0
- package/src/cache/index.ts +1 -0
- package/src/cache/invalidation-order.ts +53 -0
- package/src/cache/memory-segment-store.ts +52 -1
- package/src/cache/shell-snapshot.ts +17 -0
- package/src/cache/store-scope.ts +36 -0
- package/src/cache/tag-invalidation.ts +7 -5
- package/src/cache/types.ts +69 -14
- package/src/cache/vercel/vercel-cache-store.ts +20 -6
- package/src/client.tsx +40 -3
- package/src/errors.ts +13 -6
- package/src/handles/deferred-resolution.ts +7 -2
- package/src/internal-suspense-audit.ts +12 -0
- package/src/loader-store.ts +4 -9
- package/src/outlet-provider.tsx +25 -1
- package/src/prerender/cloudflare.ts +130 -0
- package/src/prerender/create-prerender-trigger.ts +780 -0
- package/src/prerender/index.ts +39 -0
- package/src/prerender/memory-prerender-store.ts +115 -0
- package/src/prerender/on-demand.ts +339 -0
- package/src/prerender/producer-guard.ts +66 -0
- package/src/prerender/shell-manifest-key.ts +12 -17
- package/src/prerender/warm-request.ts +154 -0
- package/src/prerender/warm.ts +193 -0
- package/src/prerender/writable-store.ts +278 -0
- package/src/prerender.ts +29 -0
- package/src/reverse.ts +1 -1
- package/src/route-content-wrapper.tsx +107 -13
- package/src/router/error-handling.ts +12 -12
- package/src/router/find-match.ts +1 -0
- package/src/router/handler-context.ts +2 -0
- package/src/router/intercept-resolution.ts +15 -8
- package/src/router/loader-resolution.ts +2 -10
- package/src/router/match-middleware/background-revalidation.ts +2 -0
- package/src/router/match-middleware/cache-lookup.ts +230 -38
- package/src/router/pattern-matching.ts +14 -0
- package/src/router/prefetch-default.ts +4 -4
- package/src/router/prerender-match.ts +460 -352
- package/src/router/route-trie-builder.ts +9 -0
- package/src/router/router-context.ts +1 -0
- package/src/router/router-interfaces.ts +33 -0
- package/src/router/router-options.ts +30 -0
- package/src/router/segment-resolution/fresh.ts +20 -19
- package/src/router/segment-resolution/loader-cache.ts +15 -17
- package/src/router/segment-resolution/revalidation.ts +34 -24
- package/src/router/segment-resolution/streamed-handler-boundary.ts +133 -0
- package/src/router/trie-matching.ts +3 -0
- package/src/router.ts +107 -1
- package/src/rsc/handler.ts +51 -4
- package/src/rsc/progressive-enhancement.ts +5 -1
- package/src/rsc/response-cache-serve.ts +10 -4
- package/src/rsc/rsc-rendering.ts +53 -11
- package/src/rsc/shell-build-manifest.ts +10 -8
- package/src/rsc/shell-capture-constants.ts +39 -7
- package/src/rsc/shell-capture.ts +108 -19
- package/src/rsc/shell-serve.ts +12 -6
- package/src/rsc/ssr-module-loader.ts +26 -0
- package/src/rsc/types.ts +8 -0
- package/src/segment-boundary-content.ts +25 -0
- package/src/segment-loader-promise.ts +28 -38
- package/src/segment-system.tsx +117 -57
- package/src/server/build-version-table.ts +15 -0
- package/src/server/context.ts +40 -17
- package/src/server/cookie-store.ts +5 -0
- package/src/server/handle-store.ts +13 -0
- package/src/server/request-context.ts +87 -1
- package/src/ssr/ssr-root.tsx +20 -13
- package/src/suspense-audit-react.tsx +98 -0
- package/src/suspense-audit.ts +1030 -0
- package/src/testing/action-form.ts +66 -0
- package/src/testing/flight-runtime.d.ts +27 -0
- package/src/testing/flight.entry.ts +3 -0
- package/src/testing/index.ts +19 -0
- package/src/testing/internal/server-modules.ts +32 -0
- package/src/testing/render-route.tsx +25 -12
- package/src/testing/serve-shell-request.ts +190 -34
- package/src/testing/shell-status.ts +17 -6
- package/src/testing/vitest-stubs/plugin-rsc.ts +37 -11
- package/src/thenable-status.ts +10 -0
- package/src/types/handler-context.ts +7 -0
- package/src/types/route-entry.ts +6 -0
- package/src/urls/path-helper.ts +69 -9
- package/src/use-loader.tsx +57 -30
- package/src/vite/discovery/bundle-postprocess.ts +4 -0
- package/src/vite/discovery/discover-routers.ts +15 -0
- package/src/vite/discovery/shell-prerender-phase.ts +5 -5
- package/src/vite/discovery/state.ts +17 -1
- package/src/vite/inject-client-debug.ts +42 -20
- package/src/vite/plugins/virtual-entries.ts +8 -1
- package/src/vite/router-discovery.ts +14 -0
- package/src/vite/utils/bundle-analysis.ts +39 -7
- package/dist/types/browser/react/subscribe-to-nothing.d.ts +0 -7
- package/dist/types/segment-content-promise.d.ts +0 -13
- package/src/browser/react/subscribe-to-nothing.ts +0 -7
- package/src/segment-content-promise.ts +0 -67
|
@@ -0,0 +1,780 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* `router.prerender()` trigger factory.
|
|
3
|
+
*
|
|
4
|
+
* Builds the `router.prerender` binder: `(runtime) => runner`, the runner being
|
|
5
|
+
* the per-target callable (plus `.many` / `.remove` / `.markStale`) from router-supplied
|
|
6
|
+
* deps. Pure and testable: all router internals (reverse, match, the producer,
|
|
7
|
+
* the request handler) arrive as injected functions, so this module has no RSC
|
|
8
|
+
* imports and can be unit-tested with fakes.
|
|
9
|
+
*
|
|
10
|
+
* One verb, dispatched by route after the match:
|
|
11
|
+
* - A `Prerender(..., { onDemand })` route takes the requestless render into
|
|
12
|
+
* the prerender store (docs/design/ondemand-prerender.md), then, when the
|
|
13
|
+
* app cache store is shared and an origin resolves, a warm request that
|
|
14
|
+
* rebuilds the runtime caches on top of the new entry.
|
|
15
|
+
* - Every other route is warmed: a cookie-free GET through the router's own
|
|
16
|
+
* handler in which every runtime cache read misses and every write replaces
|
|
17
|
+
* the entry (docs/design/prerender-every-route.md).
|
|
18
|
+
*
|
|
19
|
+
* Contract highlights:
|
|
20
|
+
* - Requestless: the producer render never inherits the caller's request state.
|
|
21
|
+
* - A refresh always renders and replaces; `onlyIfStale` is the cron-sweep
|
|
22
|
+
* opt-in and the only path that returns `already-fresh`.
|
|
23
|
+
* - A failed render/store keeps the previous entry (replace-on-success).
|
|
24
|
+
* - A `notFound()` in the render is not a failure: it stores a "removed"
|
|
25
|
+
* marker in place of the page, stamped with the route's ttl and tags so it
|
|
26
|
+
* is rechecked like a page. `prerender.remove()` stores one without
|
|
27
|
+
* rendering, which never goes stale. A Passthrough build handler that
|
|
28
|
+
* declines (`ctx.passthrough()`) stores one too, so its live handler answers.
|
|
29
|
+
* - A page and a render's marker (a `notFound()` or a decline) carry the
|
|
30
|
+
* render's start as `storedAt`, so a `markStale()` that lands during the
|
|
31
|
+
* render marks the result; `staleAt` counts the ttl from the write. A
|
|
32
|
+
* `remove()` marker renders nothing and carries its write time.
|
|
33
|
+
* - An on-demand target is path-only (its key carries no search); a warm
|
|
34
|
+
* target keeps its search params, which cache keys carry.
|
|
35
|
+
* - A warm writes only to a store shared beyond the place the call runs.
|
|
36
|
+
*/
|
|
37
|
+
|
|
38
|
+
import type { SerializedSegmentData } from "../cache/types.js";
|
|
39
|
+
import { isReservedSearchParam } from "../cache/cache-key-utils.js";
|
|
40
|
+
import { resolveWarmStoreScope } from "../cache/store-scope.js";
|
|
41
|
+
import type { HandlerCacheConfig } from "../rsc/types.js";
|
|
42
|
+
import type { ExecutionContext } from "../types/request-scope.js";
|
|
43
|
+
import { createCollectingExecutionContext } from "../cache/background-task.js";
|
|
44
|
+
import { composeWarmResult, runWarmRequest } from "./warm.js";
|
|
45
|
+
import { hashParams } from "./param-hash.js";
|
|
46
|
+
import { isPrerenderPersonalizationError } from "./producer-guard.js";
|
|
47
|
+
import { normalizeTagList } from "../cache/cache-policy.js";
|
|
48
|
+
import {
|
|
49
|
+
composeStoredEntry,
|
|
50
|
+
composeStoredTombstone,
|
|
51
|
+
isStoredEntryStale,
|
|
52
|
+
readVerifiedStoredEntry,
|
|
53
|
+
serializePrerenderKey,
|
|
54
|
+
type PrerenderKey,
|
|
55
|
+
type PrerenderStoredEntry,
|
|
56
|
+
} from "./writable-store.js";
|
|
57
|
+
import type {
|
|
58
|
+
OnDemandRouteConfig,
|
|
59
|
+
PrerenderConfig,
|
|
60
|
+
PrerenderFn,
|
|
61
|
+
PrerenderManyOptions,
|
|
62
|
+
PrerenderResult,
|
|
63
|
+
PrerenderRunner,
|
|
64
|
+
PrerenderRunOptions,
|
|
65
|
+
PrerenderRuntime,
|
|
66
|
+
PrerenderTarget,
|
|
67
|
+
} from "./on-demand.js";
|
|
68
|
+
|
|
69
|
+
/** Route metadata the trigger needs, resolved from a pathname by the router. */
|
|
70
|
+
export interface PrerenderMatchInfo {
|
|
71
|
+
routeName: string;
|
|
72
|
+
params: Record<string, string>;
|
|
73
|
+
/** True when the route opted into on-demand (Prerender(..., { onDemand })). */
|
|
74
|
+
isOnDemand: boolean;
|
|
75
|
+
/** True when the route is wrapped in Passthrough(). */
|
|
76
|
+
isPassthrough: boolean;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Raw producer output (matchForPrerender's main-variant shape). `onDemandConfig`
|
|
81
|
+
* is the route's `onDemand: { ttl, tags }` object (the `tags` callback can't
|
|
82
|
+
* ride the serialized trie), read from the loaded route entry during production.
|
|
83
|
+
*/
|
|
84
|
+
export interface ProducerOutput {
|
|
85
|
+
segments: SerializedSegmentData[];
|
|
86
|
+
handles: string;
|
|
87
|
+
routeName: string;
|
|
88
|
+
params: Record<string, string>;
|
|
89
|
+
passthrough?: true;
|
|
90
|
+
/** The render hit `notFound()` and nothing else failed: no page to store. */
|
|
91
|
+
notFound?: true;
|
|
92
|
+
onDemandConfig?: OnDemandRouteConfig;
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export interface PrerenderTriggerDeps<TEnv = any> {
|
|
96
|
+
routerId: string;
|
|
97
|
+
/**
|
|
98
|
+
* The key version: the router's data version (resolvePrerenderVersion),
|
|
99
|
+
* resolved per refresh (once per `.many()` batch), as the request handler
|
|
100
|
+
* resolves it per request.
|
|
101
|
+
*/
|
|
102
|
+
resolveVersion: () => string;
|
|
103
|
+
/** True when running under Vite dev (drives the producer context's `dev`). */
|
|
104
|
+
isDev: () => boolean;
|
|
105
|
+
/**
|
|
106
|
+
* True under the Vite dev server (isViteDevServer): the one place the
|
|
107
|
+
* memory store counts as shared, and so where the not-shared warning fires.
|
|
108
|
+
*/
|
|
109
|
+
isViteDevServer: () => boolean;
|
|
110
|
+
/** Load the per-router manifest/trie before matching, as router.fetch does. */
|
|
111
|
+
ensureManifest: () => Promise<void>;
|
|
112
|
+
/** Resolve the env-scoped prerender config (factory or object); undefined when unconfigured. */
|
|
113
|
+
resolveConfig: (
|
|
114
|
+
env: TEnv,
|
|
115
|
+
ctx: ExecutionContext | undefined,
|
|
116
|
+
) => PrerenderConfig<TEnv> | undefined;
|
|
117
|
+
/** Reverse an object target to a pathname; undefined when the route name is unknown. */
|
|
118
|
+
reverse: (
|
|
119
|
+
route: string,
|
|
120
|
+
params: Record<string, string>,
|
|
121
|
+
) => string | undefined;
|
|
122
|
+
/** Match a pathname to route metadata; null when nothing matches. */
|
|
123
|
+
matchRoute: (
|
|
124
|
+
pathname: string,
|
|
125
|
+
) => PrerenderMatchInfo | null | Promise<PrerenderMatchInfo | null>;
|
|
126
|
+
/** Run the requestless producer (matchForPrerender with onDemand). May throw. */
|
|
127
|
+
runProducer: (input: {
|
|
128
|
+
pathname: string;
|
|
129
|
+
isPassthrough: boolean;
|
|
130
|
+
env: TEnv;
|
|
131
|
+
dev: boolean;
|
|
132
|
+
}) => Promise<ProducerOutput | null>;
|
|
133
|
+
/**
|
|
134
|
+
* Resolve `createRouter({ cache })` (factory or object) for a warm;
|
|
135
|
+
* undefined when the router has none. May throw, like the prerender factory.
|
|
136
|
+
*/
|
|
137
|
+
resolveCacheConfig: (
|
|
138
|
+
env: TEnv,
|
|
139
|
+
ctx: ExecutionContext,
|
|
140
|
+
) => HandlerCacheConfig | undefined;
|
|
141
|
+
/** The router's own request handler (`router.fetch`): where a warm is dispatched. */
|
|
142
|
+
fetch: (
|
|
143
|
+
request: Request,
|
|
144
|
+
input: { env: TEnv; ctx: ExecutionContext },
|
|
145
|
+
) => Promise<Response>;
|
|
146
|
+
/** The origin of the request the runner is called from, when there is one. */
|
|
147
|
+
ambientOrigin: () => string | undefined;
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
/** Thrown by the trigger when `throwOnError: true` and the operation did not succeed. */
|
|
151
|
+
export class PrerenderError extends Error {
|
|
152
|
+
readonly result: Extract<PrerenderResult, { ok: false }>;
|
|
153
|
+
/** `operation` is the runner call that failed; the message names it. */
|
|
154
|
+
constructor(
|
|
155
|
+
result: Extract<PrerenderResult, { ok: false }>,
|
|
156
|
+
operation: "prerender" | "remove" = "prerender",
|
|
157
|
+
) {
|
|
158
|
+
const call =
|
|
159
|
+
operation === "remove" ? "prerender.remove" : "router.prerender";
|
|
160
|
+
super(
|
|
161
|
+
`${call}("${result.target}") failed: ${result.status}` +
|
|
162
|
+
(result.error instanceof Error ? ` — ${result.error.message}` : ""),
|
|
163
|
+
);
|
|
164
|
+
this.name = "PrerenderError";
|
|
165
|
+
this.result = result;
|
|
166
|
+
if (result.error !== undefined) this.cause = result.error;
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
|
|
170
|
+
interface ResolvedTarget {
|
|
171
|
+
/** Path with no search/hash, ready to match. */
|
|
172
|
+
pathname?: string;
|
|
173
|
+
/** Display form used in the result's `target` field. */
|
|
174
|
+
display: string;
|
|
175
|
+
/** Set when the target does not parse as an http(s) URL or a path. */
|
|
176
|
+
unsupported?: boolean;
|
|
177
|
+
/** The target's own origin: a full-URL string or a URL. */
|
|
178
|
+
origin?: string;
|
|
179
|
+
/** `?`-prefixed search, or "". */
|
|
180
|
+
search: string;
|
|
181
|
+
/** The target carries a hash, or a search param the router reserves. */
|
|
182
|
+
hashOrReserved: boolean;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
/** Stands in for the origin while a path target is parsed; never requested. */
|
|
186
|
+
const PATH_BASE = "http://prerender.local";
|
|
187
|
+
|
|
188
|
+
function resolveTarget<TRoutes>(
|
|
189
|
+
target: PrerenderTarget<TRoutes>,
|
|
190
|
+
reverse: PrerenderTriggerDeps["reverse"],
|
|
191
|
+
): ResolvedTarget {
|
|
192
|
+
if (typeof target === "string") {
|
|
193
|
+
return fromUrlLike(target, target);
|
|
194
|
+
}
|
|
195
|
+
if (target instanceof URL) {
|
|
196
|
+
return fromUrlLike(target.href, target.pathname);
|
|
197
|
+
}
|
|
198
|
+
// Object target { route, params? }
|
|
199
|
+
const route = (target as { route: string }).route;
|
|
200
|
+
const params = ((target as { params?: Record<string, string> }).params ??
|
|
201
|
+
{}) as Record<string, string>;
|
|
202
|
+
const pathname = reverse(route, params);
|
|
203
|
+
if (pathname === undefined) {
|
|
204
|
+
// Unknown route name — surfaced as no-match downstream.
|
|
205
|
+
return { display: route, search: "", hashOrReserved: false };
|
|
206
|
+
}
|
|
207
|
+
return fromUrlLike(pathname, pathname);
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
function fromUrlLike(raw: string, display: string): ResolvedTarget {
|
|
211
|
+
let url: URL;
|
|
212
|
+
try {
|
|
213
|
+
// A relative path resolves against the stand-in base; a full URL keeps
|
|
214
|
+
// its own origin, which a warm requests (cache keys carry the host).
|
|
215
|
+
url = new URL(raw, PATH_BASE);
|
|
216
|
+
} catch {
|
|
217
|
+
return { display, unsupported: true, search: "", hashOrReserved: false };
|
|
218
|
+
}
|
|
219
|
+
if (url.protocol !== "http:" && url.protocol !== "https:") {
|
|
220
|
+
return { display, unsupported: true, search: "", hashOrReserved: false };
|
|
221
|
+
}
|
|
222
|
+
// The params that switch the handler's mode (`_rsc*`, `__no_cache`) are not
|
|
223
|
+
// a page a visitor requests.
|
|
224
|
+
const reserved = [...url.searchParams.keys()].some(isReservedSearchParam);
|
|
225
|
+
return {
|
|
226
|
+
pathname: url.pathname,
|
|
227
|
+
display: url.pathname,
|
|
228
|
+
...(url.origin !== PATH_BASE ? { origin: url.origin } : {}),
|
|
229
|
+
search: url.search,
|
|
230
|
+
hashOrReserved: url.hash !== "" || reserved,
|
|
231
|
+
};
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
/**
|
|
235
|
+
* Create the `router.prerender` callable. See {@link PrerenderTriggerDeps}.
|
|
236
|
+
*/
|
|
237
|
+
export function createPrerenderTrigger<TEnv = any, TRoutes = {}>(
|
|
238
|
+
deps: PrerenderTriggerDeps<TEnv>,
|
|
239
|
+
): PrerenderFn<TEnv, TRoutes> {
|
|
240
|
+
/** One target: a refresh, or with `removing` a removal (`prerender.remove`). */
|
|
241
|
+
async function refresh(
|
|
242
|
+
target: PrerenderTarget<TRoutes>,
|
|
243
|
+
runtime: PrerenderRuntime<TEnv>,
|
|
244
|
+
options: PrerenderRunOptions = {},
|
|
245
|
+
version: string = deps.resolveVersion(),
|
|
246
|
+
removing = false,
|
|
247
|
+
): Promise<PrerenderResult> {
|
|
248
|
+
const result = await run(target, runtime, options, version, removing);
|
|
249
|
+
if (!result.ok && options.throwOnError) {
|
|
250
|
+
throw new PrerenderError(result, removing ? "remove" : "prerender");
|
|
251
|
+
}
|
|
252
|
+
return result;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
async function run(
|
|
256
|
+
target: PrerenderTarget<TRoutes>,
|
|
257
|
+
runtime: PrerenderRuntime<TEnv>,
|
|
258
|
+
options: PrerenderRunOptions,
|
|
259
|
+
version: string,
|
|
260
|
+
removing: boolean,
|
|
261
|
+
): Promise<PrerenderResult> {
|
|
262
|
+
const resolved = resolveTarget(target, deps.reverse);
|
|
263
|
+
const display = resolved.display;
|
|
264
|
+
|
|
265
|
+
if (resolved.unsupported) {
|
|
266
|
+
return {
|
|
267
|
+
ok: false,
|
|
268
|
+
status: "skipped-unsupported-target",
|
|
269
|
+
target: display,
|
|
270
|
+
};
|
|
271
|
+
}
|
|
272
|
+
if (resolved.pathname === undefined) {
|
|
273
|
+
return { ok: false, status: "no-match", target: display };
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
await deps.ensureManifest();
|
|
277
|
+
|
|
278
|
+
const match = await deps.matchRoute(resolved.pathname);
|
|
279
|
+
if (!match) {
|
|
280
|
+
return { ok: false, status: "no-match", target: display };
|
|
281
|
+
}
|
|
282
|
+
if (removing && !match.isOnDemand) {
|
|
283
|
+
// Only an on-demand route has a page in the prerender store. Never the
|
|
284
|
+
// warm a refresh of this route would be.
|
|
285
|
+
return {
|
|
286
|
+
ok: false,
|
|
287
|
+
path: "on-demand",
|
|
288
|
+
status: "skipped-not-on-demand",
|
|
289
|
+
target: display,
|
|
290
|
+
routeName: match.routeName,
|
|
291
|
+
};
|
|
292
|
+
}
|
|
293
|
+
if (resolved.hashOrReserved) {
|
|
294
|
+
return {
|
|
295
|
+
ok: false,
|
|
296
|
+
path: match.isOnDemand ? "on-demand" : "warm",
|
|
297
|
+
status: "skipped-unsupported-target",
|
|
298
|
+
target: display,
|
|
299
|
+
routeName: match.routeName,
|
|
300
|
+
};
|
|
301
|
+
}
|
|
302
|
+
if (!match.isOnDemand) {
|
|
303
|
+
return warm(
|
|
304
|
+
resolved,
|
|
305
|
+
match,
|
|
306
|
+
runtime,
|
|
307
|
+
options.onlyIfStale ? "fill" : "replace",
|
|
308
|
+
);
|
|
309
|
+
}
|
|
310
|
+
|
|
311
|
+
if (resolved.search) {
|
|
312
|
+
// An on-demand key is route + params: a search would silently persist
|
|
313
|
+
// (or remove the page) under the base key.
|
|
314
|
+
return {
|
|
315
|
+
ok: false,
|
|
316
|
+
path: "on-demand",
|
|
317
|
+
status: "skipped-unsupported-target",
|
|
318
|
+
target: display,
|
|
319
|
+
routeName: match.routeName,
|
|
320
|
+
};
|
|
321
|
+
}
|
|
322
|
+
const result = await renderOnDemand(
|
|
323
|
+
resolved.pathname,
|
|
324
|
+
display,
|
|
325
|
+
match,
|
|
326
|
+
runtime,
|
|
327
|
+
options,
|
|
328
|
+
version,
|
|
329
|
+
removing,
|
|
330
|
+
);
|
|
331
|
+
// Then the request handler, on top of the entry just stored: the route's
|
|
332
|
+
// loaders' own cache() and the document cache are rebuilt on it. A
|
|
333
|
+
// Passthrough param the producer declined is served by the live handler
|
|
334
|
+
// (the marker renderOnDemand stored for it keeps a page stored or baked
|
|
335
|
+
// earlier from answering the warm), whose caches are warmed the same way.
|
|
336
|
+
// Quiet: the refresh is complete without it. Never after `removed`: there
|
|
337
|
+
// is no page to rebuild a cache on, and a Passthrough route's live handler
|
|
338
|
+
// may still render the removed page from a data source that has not
|
|
339
|
+
// caught up, which the warm would then write to its caches.
|
|
340
|
+
if (
|
|
341
|
+
result.status === "rendered" ||
|
|
342
|
+
result.status === "skipped-passthrough"
|
|
343
|
+
) {
|
|
344
|
+
const warmed = await warm(resolved, match, runtime, "replace", true);
|
|
345
|
+
if (warmed.caches) return { ...result, caches: warmed.caches };
|
|
346
|
+
}
|
|
347
|
+
return result;
|
|
348
|
+
}
|
|
349
|
+
|
|
350
|
+
let warnedNotShared = false;
|
|
351
|
+
|
|
352
|
+
/**
|
|
353
|
+
* Warm one matched route: a GET of the URL a visitor requests, through the
|
|
354
|
+
* router's own handler. `followUp` is the warm after an on-demand render:
|
|
355
|
+
* its refusals are not worth a warning, the render already did its job.
|
|
356
|
+
*/
|
|
357
|
+
async function warm(
|
|
358
|
+
resolved: ResolvedTarget,
|
|
359
|
+
match: PrerenderMatchInfo,
|
|
360
|
+
runtime: PrerenderRuntime<TEnv>,
|
|
361
|
+
mode: "replace" | "fill",
|
|
362
|
+
followUp = false,
|
|
363
|
+
): Promise<PrerenderResult> {
|
|
364
|
+
const refused = (
|
|
365
|
+
status: Extract<PrerenderResult, { ok: false }>["status"],
|
|
366
|
+
target: string = resolved.display,
|
|
367
|
+
error?: unknown,
|
|
368
|
+
): PrerenderResult => ({
|
|
369
|
+
ok: false,
|
|
370
|
+
path: "warm",
|
|
371
|
+
status,
|
|
372
|
+
target,
|
|
373
|
+
routeName: match.routeName,
|
|
374
|
+
...(error !== undefined ? { error } : {}),
|
|
375
|
+
});
|
|
376
|
+
|
|
377
|
+
// Cache keys carry the host, so the warm requests the host visitors use:
|
|
378
|
+
// the target's own, the binding's, else the calling request's.
|
|
379
|
+
let origin: string | undefined;
|
|
380
|
+
try {
|
|
381
|
+
origin =
|
|
382
|
+
resolved.origin ??
|
|
383
|
+
(runtime.origin !== undefined
|
|
384
|
+
? new URL(runtime.origin).origin
|
|
385
|
+
: deps.ambientOrigin());
|
|
386
|
+
} catch (error) {
|
|
387
|
+
return refused("skipped-no-origin", resolved.display, error);
|
|
388
|
+
}
|
|
389
|
+
if (!origin) return refused("skipped-no-origin");
|
|
390
|
+
const url = new URL(resolved.pathname + resolved.search, origin);
|
|
391
|
+
|
|
392
|
+
// Resolved with the collecting context, so a store's own background
|
|
393
|
+
// writes are waited for too. A factory throw maps to no-store.
|
|
394
|
+
const ctx = createCollectingExecutionContext(runtime.ctx);
|
|
395
|
+
let cacheConfig: HandlerCacheConfig | undefined;
|
|
396
|
+
let configError: unknown;
|
|
397
|
+
try {
|
|
398
|
+
cacheConfig = deps.resolveCacheConfig(runtime.env, ctx);
|
|
399
|
+
} catch (error) {
|
|
400
|
+
configError = error;
|
|
401
|
+
}
|
|
402
|
+
if (!cacheConfig?.store || cacheConfig.enabled === false) {
|
|
403
|
+
return refused("no-store", url.href, configError);
|
|
404
|
+
}
|
|
405
|
+
|
|
406
|
+
const devServer = deps.isViteDevServer();
|
|
407
|
+
if (resolveWarmStoreScope(cacheConfig.store, devServer) === "local") {
|
|
408
|
+
if (!followUp && devServer && !warnedNotShared) {
|
|
409
|
+
warnedNotShared = true;
|
|
410
|
+
const name = cacheConfig.store.constructor?.name;
|
|
411
|
+
console.warn(
|
|
412
|
+
`[rango] router.prerender("${resolved.display}") did not warm: the ` +
|
|
413
|
+
`cache store${name && name !== "Object" ? ` (${name})` : ""} ` +
|
|
414
|
+
`declares ${
|
|
415
|
+
cacheConfig.store.scope === undefined
|
|
416
|
+
? "no scope"
|
|
417
|
+
: `scope "${String(cacheConfig.store.scope)}"`
|
|
418
|
+
}, so entries written here serve no other process, isolate or ` +
|
|
419
|
+
"edge location. Warming needs a store whose scope is " +
|
|
420
|
+
'"global" or "regional" (CFCacheStore with kv, ' +
|
|
421
|
+
"VercelCacheStore), or a custom store that declares one.",
|
|
422
|
+
);
|
|
423
|
+
}
|
|
424
|
+
return refused("skipped-store-not-shared", url.href);
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
const outcome = await runWarmRequest({
|
|
428
|
+
url,
|
|
429
|
+
mode,
|
|
430
|
+
cacheConfig,
|
|
431
|
+
env: runtime.env,
|
|
432
|
+
ctx,
|
|
433
|
+
fetch: deps.fetch,
|
|
434
|
+
});
|
|
435
|
+
return composeWarmResult(outcome, url.href, match.routeName);
|
|
436
|
+
}
|
|
437
|
+
|
|
438
|
+
/**
|
|
439
|
+
* The requestless render of an on-demand route into the prerender store
|
|
440
|
+
* (#640), or with `removing` the write of its "removed" marker (#1060).
|
|
441
|
+
*/
|
|
442
|
+
async function renderOnDemand(
|
|
443
|
+
pathname: string,
|
|
444
|
+
display: string,
|
|
445
|
+
match: PrerenderMatchInfo,
|
|
446
|
+
runtime: PrerenderRuntime<TEnv>,
|
|
447
|
+
options: PrerenderRunOptions,
|
|
448
|
+
version: string,
|
|
449
|
+
removing: boolean,
|
|
450
|
+
): Promise<PrerenderResult> {
|
|
451
|
+
// The prerender factory is user code like tags()/store.set: a throw (e.g.
|
|
452
|
+
// a missing binding tripping store construction) must map to a result, not
|
|
453
|
+
// escape run() — which would reject refresh() despite throwOnError: false
|
|
454
|
+
// and abort a many() batch with no per-target results. A throw leaves
|
|
455
|
+
// config undefined, so it funnels into the same no-store return.
|
|
456
|
+
let config: PrerenderConfig<TEnv> | undefined;
|
|
457
|
+
let configError: unknown;
|
|
458
|
+
try {
|
|
459
|
+
config = deps.resolveConfig(runtime.env, runtime.ctx);
|
|
460
|
+
} catch (err) {
|
|
461
|
+
configError = err;
|
|
462
|
+
}
|
|
463
|
+
if (!config?.store) {
|
|
464
|
+
return {
|
|
465
|
+
ok: false,
|
|
466
|
+
path: "on-demand",
|
|
467
|
+
status: "no-store",
|
|
468
|
+
target: display,
|
|
469
|
+
routeName: match.routeName,
|
|
470
|
+
...(configError !== undefined ? { error: configError } : {}),
|
|
471
|
+
};
|
|
472
|
+
}
|
|
473
|
+
|
|
474
|
+
const key: PrerenderKey = {
|
|
475
|
+
routerId: deps.routerId,
|
|
476
|
+
version,
|
|
477
|
+
routeName: match.routeName,
|
|
478
|
+
paramHash: hashParams(match.params),
|
|
479
|
+
};
|
|
480
|
+
const keyStr = serializePrerenderKey(key);
|
|
481
|
+
const store = config.store;
|
|
482
|
+
/** What the store holds for the page, as a result reports it. */
|
|
483
|
+
const held = (
|
|
484
|
+
status: "already-fresh" | "removed",
|
|
485
|
+
meta: PrerenderStoredEntry["meta"],
|
|
486
|
+
): PrerenderResult => ({
|
|
487
|
+
ok: true,
|
|
488
|
+
path: "on-demand",
|
|
489
|
+
status,
|
|
490
|
+
target: display,
|
|
491
|
+
routeName: match.routeName,
|
|
492
|
+
key: keyStr,
|
|
493
|
+
tags: meta.tags,
|
|
494
|
+
// The configured ttl, not staleAt - storedAt: storedAt is the render's
|
|
495
|
+
// start and a markStale() lowers staleAt, so the difference is neither.
|
|
496
|
+
...(meta.ttl != null ? { ttl: meta.ttl } : {}),
|
|
497
|
+
});
|
|
498
|
+
// Replace-on-success: a failed write leaves the prior durable entry (and
|
|
499
|
+
// the bundled manifest fallback) in place.
|
|
500
|
+
const storeFailed = (error: unknown): PrerenderResult => ({
|
|
501
|
+
ok: false,
|
|
502
|
+
path: "on-demand",
|
|
503
|
+
status: "store-failed",
|
|
504
|
+
target: display,
|
|
505
|
+
routeName: match.routeName,
|
|
506
|
+
error,
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
if (removing) {
|
|
510
|
+
// The "removed" marker in place of the page. Not store.delete(): that
|
|
511
|
+
// would bring a baked param's build-time entry back. No ttl and no
|
|
512
|
+
// tags: it never goes stale and markStale() cannot reach it, so only a
|
|
513
|
+
// refresh without onlyIfStale brings the page back.
|
|
514
|
+
const marker = composeStoredTombstone(
|
|
515
|
+
key,
|
|
516
|
+
{ tags: [], params: match.params },
|
|
517
|
+
Date.now(),
|
|
518
|
+
);
|
|
519
|
+
try {
|
|
520
|
+
await store.set(key, marker);
|
|
521
|
+
} catch (error) {
|
|
522
|
+
return storeFailed(error);
|
|
523
|
+
}
|
|
524
|
+
return held("removed", marker.meta);
|
|
525
|
+
}
|
|
526
|
+
|
|
527
|
+
// onlyIfStale: the cron-sweep opt-in. A CMS webhook (default) always renders,
|
|
528
|
+
// because it fires precisely when content changed. This is the only path that
|
|
529
|
+
// returns already-fresh, reported with the existing entry's ttl/tags.
|
|
530
|
+
if (options.onlyIfStale) {
|
|
531
|
+
// An unreadable or unverifiable entry is "couldn't confirm fresh":
|
|
532
|
+
// render, never throw (one result per target, many() keeps going).
|
|
533
|
+
const existing = await readVerifiedStoredEntry(store, key, match.params);
|
|
534
|
+
// A fresh marker is left like a fresh page; a stale one (a notFound()
|
|
535
|
+
// past its ttl, or marked stale) is rendered again below.
|
|
536
|
+
if (existing && !isStoredEntryStale(existing, Date.now())) {
|
|
537
|
+
return held(
|
|
538
|
+
existing.removed === true ? "removed" : "already-fresh",
|
|
539
|
+
existing.meta,
|
|
540
|
+
);
|
|
541
|
+
}
|
|
542
|
+
}
|
|
543
|
+
|
|
544
|
+
// The entry's storedAt (#1072): a markStale() while the render runs must
|
|
545
|
+
// mark it, and the stores compare their tag marker with storedAt.
|
|
546
|
+
const startedAt = Date.now();
|
|
547
|
+
let produced: ProducerOutput | null;
|
|
548
|
+
try {
|
|
549
|
+
produced = await deps.runProducer({
|
|
550
|
+
pathname,
|
|
551
|
+
isPassthrough: match.isPassthrough,
|
|
552
|
+
env: runtime.env,
|
|
553
|
+
dev: deps.isDev(),
|
|
554
|
+
});
|
|
555
|
+
} catch (err) {
|
|
556
|
+
if (isPrerenderPersonalizationError(err)) {
|
|
557
|
+
return {
|
|
558
|
+
ok: false,
|
|
559
|
+
path: "on-demand",
|
|
560
|
+
status: "skipped-personalized",
|
|
561
|
+
target: display,
|
|
562
|
+
routeName: match.routeName,
|
|
563
|
+
};
|
|
564
|
+
}
|
|
565
|
+
// #587: matchForPrerender already threads throwOnError so a render throw
|
|
566
|
+
// surfaces here rather than baking a frozen error page. Keep the old entry.
|
|
567
|
+
return {
|
|
568
|
+
ok: false,
|
|
569
|
+
path: "on-demand",
|
|
570
|
+
status: "render-failed",
|
|
571
|
+
target: display,
|
|
572
|
+
routeName: match.routeName,
|
|
573
|
+
error: err,
|
|
574
|
+
};
|
|
575
|
+
}
|
|
576
|
+
|
|
577
|
+
if (!produced) {
|
|
578
|
+
return {
|
|
579
|
+
ok: false,
|
|
580
|
+
path: "on-demand",
|
|
581
|
+
status: "no-match",
|
|
582
|
+
target: display,
|
|
583
|
+
routeName: match.routeName,
|
|
584
|
+
};
|
|
585
|
+
}
|
|
586
|
+
// No page to store: the render hit notFound() (the page no longer
|
|
587
|
+
// exists; keeping the old entry would serve a deleted item until the next
|
|
588
|
+
// deploy), or a Passthrough build handler declined the param (the live
|
|
589
|
+
// handler is to answer, which it only does once nothing in a prerender
|
|
590
|
+
// store does). Both store the "removed" marker, stamped below with the
|
|
591
|
+
// route's ttl and tags like a page, so the outcome is rechecked like one.
|
|
592
|
+
const noPage = produced.notFound === true || produced.passthrough === true;
|
|
593
|
+
|
|
594
|
+
// Resolve soft TTL + tags from the route's onDemand config (read off the
|
|
595
|
+
// loaded route entry by the producer), falling back to the router default.
|
|
596
|
+
const ttl = produced.onDemandConfig?.ttl ?? config.ttl;
|
|
597
|
+
let tags: string[] = [];
|
|
598
|
+
try {
|
|
599
|
+
// The user-supplied tags callback runs here; a throw from it must not
|
|
600
|
+
// escape run() (which would bypass the result contract and abort a many()
|
|
601
|
+
// batch) — keep it inside the store guard so it maps to store-failed.
|
|
602
|
+
const routeTags = produced.onDemandConfig?.tags;
|
|
603
|
+
const rawTags =
|
|
604
|
+
typeof routeTags === "function"
|
|
605
|
+
? routeTags({ params: match.params })
|
|
606
|
+
: (routeTags ?? []);
|
|
607
|
+
// Same normalization as cache({ tags }): trimmed, no empties, no duplicates.
|
|
608
|
+
tags =
|
|
609
|
+
normalizeTagList(
|
|
610
|
+
[...rawTags].filter((t): t is string => typeof t === "string"),
|
|
611
|
+
) ?? [];
|
|
612
|
+
const stamp = {
|
|
613
|
+
...(ttl != null ? { ttl } : {}),
|
|
614
|
+
tags,
|
|
615
|
+
params: match.params,
|
|
616
|
+
};
|
|
617
|
+
if (!noPage) {
|
|
618
|
+
// A marker stored after this render started is not overwritten by its
|
|
619
|
+
// page: a remove() (stamped with its write time) always qualifies. A
|
|
620
|
+
// render's own marker carries that render's start, so only a render
|
|
621
|
+
// that started later than this one wins; an older render's notFound()
|
|
622
|
+
// does not (this one read newer data). This narrows the race, it
|
|
623
|
+
// cannot close it: the read and the write are two store calls, and KV
|
|
624
|
+
// is eventually consistent. Otherwise last write wins.
|
|
625
|
+
const current = await readVerifiedStoredEntry(store, key, match.params);
|
|
626
|
+
if (current?.removed === true && current.meta.storedAt > startedAt) {
|
|
627
|
+
return held("removed", current.meta);
|
|
628
|
+
}
|
|
629
|
+
}
|
|
630
|
+
// The router composes the envelope; the store only persists it.
|
|
631
|
+
const writtenAt = Date.now();
|
|
632
|
+
await store.set(
|
|
633
|
+
key,
|
|
634
|
+
noPage
|
|
635
|
+
? composeStoredTombstone(key, stamp, startedAt, writtenAt)
|
|
636
|
+
: composeStoredEntry(
|
|
637
|
+
key,
|
|
638
|
+
{ segments: produced.segments, handles: produced.handles },
|
|
639
|
+
stamp,
|
|
640
|
+
startedAt,
|
|
641
|
+
writtenAt,
|
|
642
|
+
),
|
|
643
|
+
);
|
|
644
|
+
} catch (err) {
|
|
645
|
+
return storeFailed(err);
|
|
646
|
+
}
|
|
647
|
+
|
|
648
|
+
if (produced.passthrough) {
|
|
649
|
+
return {
|
|
650
|
+
ok: false,
|
|
651
|
+
path: "on-demand",
|
|
652
|
+
status: "skipped-passthrough",
|
|
653
|
+
target: display,
|
|
654
|
+
routeName: match.routeName,
|
|
655
|
+
};
|
|
656
|
+
}
|
|
657
|
+
return {
|
|
658
|
+
ok: true,
|
|
659
|
+
path: "on-demand",
|
|
660
|
+
status: produced.notFound ? "removed" : "rendered",
|
|
661
|
+
target: display,
|
|
662
|
+
routeName: match.routeName,
|
|
663
|
+
key: keyStr,
|
|
664
|
+
tags,
|
|
665
|
+
...(ttl != null ? { ttl } : {}),
|
|
666
|
+
};
|
|
667
|
+
}
|
|
668
|
+
|
|
669
|
+
let warnedNoStore = false;
|
|
670
|
+
let warnedNoRevalidate = false;
|
|
671
|
+
|
|
672
|
+
async function many(
|
|
673
|
+
runtime: PrerenderRuntime<TEnv>,
|
|
674
|
+
targets: ReadonlyArray<PrerenderTarget<TRoutes>>,
|
|
675
|
+
options: PrerenderManyOptions = {},
|
|
676
|
+
removing = false,
|
|
677
|
+
): Promise<PrerenderResult[]> {
|
|
678
|
+
// Default to 1 for any invalid concurrency (undefined, NaN, < 1). Without
|
|
679
|
+
// this, Math.floor(NaN) -> NaN spawns zero workers and many() resolves to an
|
|
680
|
+
// array of holes that reports nothing.
|
|
681
|
+
const raw = options.concurrency;
|
|
682
|
+
const concurrency =
|
|
683
|
+
typeof raw === "number" && Number.isFinite(raw) && raw >= 1
|
|
684
|
+
? Math.floor(raw)
|
|
685
|
+
: 1;
|
|
686
|
+
const version = deps.resolveVersion();
|
|
687
|
+
return runWithConcurrency(targets, concurrency, (target) =>
|
|
688
|
+
// throwOnError propagates per-item so the batch stops on the first failure;
|
|
689
|
+
// without it, every target yields a result.
|
|
690
|
+
refresh(target, runtime, options, version, removing),
|
|
691
|
+
);
|
|
692
|
+
}
|
|
693
|
+
|
|
694
|
+
async function markStale(
|
|
695
|
+
runtime: PrerenderRuntime<TEnv>,
|
|
696
|
+
tags: string[],
|
|
697
|
+
): Promise<void> {
|
|
698
|
+
if (tags.length === 0) return;
|
|
699
|
+
const config = deps.resolveConfig(runtime.env, runtime.ctx);
|
|
700
|
+
if (!config?.store.markStale) {
|
|
701
|
+
// A void return makes a misconfigured caller (no store, or a store
|
|
702
|
+
// without tag support) look like success: a dead CMS webhook.
|
|
703
|
+
if (deps.isDev() && !warnedNoStore) {
|
|
704
|
+
warnedNoStore = true;
|
|
705
|
+
console.warn(
|
|
706
|
+
"[rango] prerender.markStale() is a no-op: " +
|
|
707
|
+
(config?.store
|
|
708
|
+
? "the configured store does not implement markStale."
|
|
709
|
+
: "no prerender store is configured."),
|
|
710
|
+
);
|
|
711
|
+
}
|
|
712
|
+
return;
|
|
713
|
+
}
|
|
714
|
+
if (!config.onRevalidate && deps.isDev() && !warnedNoRevalidate) {
|
|
715
|
+
warnedNoRevalidate = true;
|
|
716
|
+
console.warn(
|
|
717
|
+
"[rango] prerender.markStale() marked entries stale, but no " +
|
|
718
|
+
"onRevalidate is configured, so a stale hit schedules nothing. The " +
|
|
719
|
+
"entries keep serving until router.prerender() re-renders them " +
|
|
720
|
+
"(for example a sweep with { onlyIfStale: true }).",
|
|
721
|
+
);
|
|
722
|
+
}
|
|
723
|
+
await config.store.markStale(deps.routerId, tags);
|
|
724
|
+
}
|
|
725
|
+
|
|
726
|
+
// Binding does no work: the config factory, version and manifest resolve per
|
|
727
|
+
// call. The warn-once flags live on the trigger, shared by every bound runner.
|
|
728
|
+
return (runtime: PrerenderRuntime<TEnv>): PrerenderRunner<TRoutes> => {
|
|
729
|
+
const runner = ((target, options) =>
|
|
730
|
+
refresh(target, runtime, options)) as PrerenderRunner<TRoutes>;
|
|
731
|
+
runner.many = (targets, options) => many(runtime, targets, options);
|
|
732
|
+
const remove = ((target, options) =>
|
|
733
|
+
refresh(
|
|
734
|
+
target,
|
|
735
|
+
runtime,
|
|
736
|
+
options,
|
|
737
|
+
undefined,
|
|
738
|
+
true,
|
|
739
|
+
)) as PrerenderRunner<TRoutes>["remove"];
|
|
740
|
+
remove.many = (targets, options) => many(runtime, targets, options, true);
|
|
741
|
+
runner.remove = remove;
|
|
742
|
+
runner.markStale = (tags) => markStale(runtime, tags);
|
|
743
|
+
return runner;
|
|
744
|
+
};
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/**
|
|
748
|
+
* Bounded-concurrency map preserving input order. Rejects on the first task
|
|
749
|
+
* error (used so `many({ throwOnError })` stops the batch). The abort flag is
|
|
750
|
+
* load-bearing: Promise.all rejects on the first worker error while the other
|
|
751
|
+
* workers are mid-await — without it they would drain the entire remaining
|
|
752
|
+
* batch (rendering and writing, results discarded) after the caller has
|
|
753
|
+
* already seen the throw. In-flight items still settle; no new item starts.
|
|
754
|
+
*/
|
|
755
|
+
async function runWithConcurrency<T, R>(
|
|
756
|
+
items: ReadonlyArray<T>,
|
|
757
|
+
concurrency: number,
|
|
758
|
+
task: (item: T) => Promise<R>,
|
|
759
|
+
): Promise<R[]> {
|
|
760
|
+
const results = new Array<R>(items.length);
|
|
761
|
+
let next = 0;
|
|
762
|
+
let aborted = false;
|
|
763
|
+
async function worker(): Promise<void> {
|
|
764
|
+
while (!aborted && next < items.length) {
|
|
765
|
+
const i = next++;
|
|
766
|
+
try {
|
|
767
|
+
results[i] = await task(items[i]);
|
|
768
|
+
} catch (err) {
|
|
769
|
+
aborted = true;
|
|
770
|
+
throw err;
|
|
771
|
+
}
|
|
772
|
+
}
|
|
773
|
+
}
|
|
774
|
+
const workers = Array.from(
|
|
775
|
+
{ length: Math.min(concurrency, items.length) },
|
|
776
|
+
() => worker(),
|
|
777
|
+
);
|
|
778
|
+
await Promise.all(workers);
|
|
779
|
+
return results;
|
|
780
|
+
}
|