@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
|
@@ -17,10 +17,15 @@
|
|
|
17
17
|
*
|
|
18
18
|
* Without the condition React's server build refuses to load, so the codec
|
|
19
19
|
* functions throw and a "use cache" call runs uncached. Any other load failure
|
|
20
|
-
* (a moved vendored path, a missing peer) throws.
|
|
21
|
-
*
|
|
20
|
+
* (a moved vendored path, a missing peer) throws. decodeAction and
|
|
21
|
+
* decodeFormState decode a no-JS form POST under the condition, resolving its
|
|
22
|
+
* action from the modules createActionForm() registered, as do
|
|
23
|
+
* `loadServerAction` and `decodeReply` for a plain form action. Without the
|
|
24
|
+
* condition they stay inert.
|
|
22
25
|
*/
|
|
23
26
|
|
|
27
|
+
import { loadServerModule } from "../internal/server-modules.js";
|
|
28
|
+
|
|
24
29
|
/** React's server build throws this on load without the react-server condition. */
|
|
25
30
|
const MISSING_REACT_SERVER_CONDITION =
|
|
26
31
|
/"react-server" condition must be enabled/;
|
|
@@ -52,11 +57,7 @@ async function loadFlightRuntime() {
|
|
|
52
57
|
// Resolves the `$$decode-client:` / `$$preserve:` ids a cached value's client
|
|
53
58
|
// and server references decode to; any other id would load a server module.
|
|
54
59
|
core.setRequireModule({
|
|
55
|
-
load: (id) =>
|
|
56
|
-
throw new Error(
|
|
57
|
-
`plugin-rsc stub: server reference module "${id}" is not loadable in a test`,
|
|
58
|
-
);
|
|
59
|
-
},
|
|
60
|
+
load: (id) => loadServerModule(id),
|
|
60
61
|
});
|
|
61
62
|
return { server, client, core };
|
|
62
63
|
}
|
|
@@ -121,7 +122,32 @@ export const encodeReply = (
|
|
|
121
122
|
: unavailable("encodeReply");
|
|
122
123
|
export const createClientTemporaryReferenceSet = (): unknown =>
|
|
123
124
|
runtime ? runtime.client.createTemporaryReferenceSet() : {};
|
|
124
|
-
export const loadServerAction = (): undefined =>
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
125
|
+
export const loadServerAction = (id: string): Promise<unknown> | undefined => {
|
|
126
|
+
if (!runtime) return undefined;
|
|
127
|
+
const [file, name] = id.split("#");
|
|
128
|
+
return Promise.resolve(loadServerModule(file!)[name!]);
|
|
129
|
+
};
|
|
130
|
+
export const decodeReply = (
|
|
131
|
+
body: string | FormData,
|
|
132
|
+
options?: object,
|
|
133
|
+
): Promise<unknown> | undefined =>
|
|
134
|
+
runtime
|
|
135
|
+
? runtime.server.decodeReply(body, referenceManifest("$$server:"), options)
|
|
136
|
+
: undefined;
|
|
137
|
+
export const decodeAction = (
|
|
138
|
+
body: FormData,
|
|
139
|
+
): Promise<unknown> | null | undefined =>
|
|
140
|
+
runtime
|
|
141
|
+
? runtime.server.decodeAction(body, referenceManifest("$$server:"))
|
|
142
|
+
: undefined;
|
|
143
|
+
export const decodeFormState = (
|
|
144
|
+
actionResult: unknown,
|
|
145
|
+
body: FormData,
|
|
146
|
+
): Promise<unknown> | undefined =>
|
|
147
|
+
runtime
|
|
148
|
+
? runtime.server.decodeFormState(
|
|
149
|
+
actionResult,
|
|
150
|
+
body,
|
|
151
|
+
referenceManifest("$$server:"),
|
|
152
|
+
)
|
|
153
|
+
: undefined;
|
package/src/thenable-status.ts
CHANGED
|
@@ -14,3 +14,13 @@ export function unwrapsSynchronously(stream: Promise<unknown>): boolean {
|
|
|
14
14
|
status === "resolved_module"
|
|
15
15
|
);
|
|
16
16
|
}
|
|
17
|
+
|
|
18
|
+
/**
|
|
19
|
+
* A resolved thenable for a use() whose result is ignored: React reads the
|
|
20
|
+
* "fulfilled" stamp and returns at once. `_debugInfo` keeps React DevTools from
|
|
21
|
+
* listing a 0 ms promise on every reader that calls it.
|
|
22
|
+
*/
|
|
23
|
+
export const SETTLED_THENABLE: Promise<undefined> = Object.assign(
|
|
24
|
+
Promise.resolve(undefined),
|
|
25
|
+
{ status: "fulfilled", value: undefined, _debugInfo: [] },
|
|
26
|
+
);
|
|
@@ -449,6 +449,13 @@ export type InternalHandlerContext<
|
|
|
449
449
|
_responseType?: string;
|
|
450
450
|
/** Route name for cache key scoping (prevents cross-route collisions). */
|
|
451
451
|
_routeName?: string;
|
|
452
|
+
/**
|
|
453
|
+
* @internal The on-demand overlay holds a "removed" marker for this request
|
|
454
|
+
* (tryPrerenderLookup, cache-lookup.ts); gateOnDemandProducer reads it. On
|
|
455
|
+
* this context, not the request's: withCacheLookup documents that the
|
|
456
|
+
* request ALS can be lost mid-pipeline on workerd.
|
|
457
|
+
*/
|
|
458
|
+
_prerenderRemoved?: true;
|
|
452
459
|
/**
|
|
453
460
|
* @internal Loader-cache override table: loaderId -> memoized data promise.
|
|
454
461
|
* A single stable ctx.use interceptor consults this instead of chaining one
|
package/src/types/route-entry.ts
CHANGED
|
@@ -82,6 +82,12 @@ export interface RouteEntry<TEnv = any> {
|
|
|
82
82
|
*/
|
|
83
83
|
passthroughRouteKeys?: Set<string>;
|
|
84
84
|
|
|
85
|
+
/**
|
|
86
|
+
* Route keys in this entry that opted into on-demand prerender.
|
|
87
|
+
* Used by the non-trie match path to set the `od` flag.
|
|
88
|
+
*/
|
|
89
|
+
onDemandRouteKeys?: Set<string>;
|
|
90
|
+
|
|
85
91
|
// === Lazy evaluation fields ===
|
|
86
92
|
|
|
87
93
|
/**
|
package/src/urls/path-helper.ts
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { ReactNode } from "react";
|
|
2
|
-
import type { Handler } from "../types.js";
|
|
2
|
+
import type { Handler, InternalHandlerContext } from "../types.js";
|
|
3
3
|
import type { RouteItem, RouteUseItem, UseItems } from "../route-types.js";
|
|
4
4
|
import {
|
|
5
5
|
getUrlPrefix,
|
|
@@ -8,7 +8,7 @@ import {
|
|
|
8
8
|
requireDslContext,
|
|
9
9
|
stampStaticDefScope,
|
|
10
10
|
} from "../server/context";
|
|
11
|
-
import { invariant, DataNotFoundError } from "../errors";
|
|
11
|
+
import { invariant, DataNotFoundError, isDevEnvironment } from "../errors";
|
|
12
12
|
import { validateUserRouteName } from "../route-name.js";
|
|
13
13
|
import {
|
|
14
14
|
isPrerenderHandler,
|
|
@@ -60,6 +60,41 @@ function resolveResponseType(
|
|
|
60
60
|
return options?.[RESPONSE_TYPE];
|
|
61
61
|
}
|
|
62
62
|
|
|
63
|
+
const NO_PRERENDER_DATA_MESSAGE = "No prerender data found for this route";
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* A retained on-demand producer must never render on a live production
|
|
67
|
+
* request: bundle eviction is skipped for onDemand routes (the producer body
|
|
68
|
+
* stays available for router.prerender()), so without this gate an
|
|
69
|
+
* overlay+manifest miss on a plain Prerender(..., { onDemand }) route would
|
|
70
|
+
* execute the producer in-request — uncached per-request renders with the
|
|
71
|
+
* personalization guard unarmed. Keep the pre-retention miss contract instead
|
|
72
|
+
* (DataNotFoundError -> 404). Producer runs (build-time bake and
|
|
73
|
+
* router.prerender()) arrive with ctx.build true (createPrerenderContext);
|
|
74
|
+
* Vite dev keeps the documented live fall-through on a dev-store miss, except
|
|
75
|
+
* for a page the overlay marks removed (`_prerenderRemoved`): a live render
|
|
76
|
+
* would show in dev a page production answers with a 404.
|
|
77
|
+
* Pass-through when the route is not onDemand.
|
|
78
|
+
*
|
|
79
|
+
* @internal Exported for the unit test.
|
|
80
|
+
*/
|
|
81
|
+
export function gateOnDemandProducer<TEnv>(
|
|
82
|
+
producer: Handler<any, any, TEnv>,
|
|
83
|
+
isOnDemand: boolean,
|
|
84
|
+
): Handler<any, any, TEnv> {
|
|
85
|
+
if (!isOnDemand) return producer;
|
|
86
|
+
return (ctx) => {
|
|
87
|
+
if (
|
|
88
|
+
!ctx.build &&
|
|
89
|
+
(!isDevEnvironment() ||
|
|
90
|
+
(ctx as InternalHandlerContext<any, TEnv>)._prerenderRemoved)
|
|
91
|
+
) {
|
|
92
|
+
throw new DataNotFoundError(NO_PRERENDER_DATA_MESSAGE);
|
|
93
|
+
}
|
|
94
|
+
return producer(ctx);
|
|
95
|
+
};
|
|
96
|
+
}
|
|
97
|
+
|
|
63
98
|
export function createPathHelper<TEnv>(): PathFn<TEnv> {
|
|
64
99
|
return ((
|
|
65
100
|
pattern: string,
|
|
@@ -117,6 +152,31 @@ export function createPathHelper<TEnv>(): PathFn<TEnv> {
|
|
|
117
152
|
|
|
118
153
|
const namespace = `${ctx.namespace}.${store.getNextIndex("route")}.${routeName}`;
|
|
119
154
|
|
|
155
|
+
// On-demand opt-in lives on the (inner) Prerender def's options for both
|
|
156
|
+
// plain Prerender and Passthrough-wrapped routes. Presence (truthy, not
|
|
157
|
+
// false) marks the route ISR-eligible; the trie od flag + producer retention
|
|
158
|
+
// key off it.
|
|
159
|
+
const onDemandDef = isPassthroughHandler(handler)
|
|
160
|
+
? handler.prerenderDef
|
|
161
|
+
: isPrerenderHandler(handler)
|
|
162
|
+
? handler
|
|
163
|
+
: undefined;
|
|
164
|
+
const onDemandOpt = onDemandDef?.options?.onDemand;
|
|
165
|
+
const isOnDemand = onDemandOpt != null && onDemandOpt !== false;
|
|
166
|
+
|
|
167
|
+
// onDemand routes are excluded from the PPR shell lane (a writable refresh
|
|
168
|
+
// cannot atomically replace the captured shell), so ppr has no effect there.
|
|
169
|
+
// Thrown ahead of the forRoute early return so lazy evaluation of another
|
|
170
|
+
// route cannot skip it.
|
|
171
|
+
if (isOnDemand && options?.ppr !== undefined && options.ppr !== false) {
|
|
172
|
+
throw new Error(
|
|
173
|
+
`[rango] Route "${routeName}" sets both ppr and onDemand: onDemand ` +
|
|
174
|
+
`routes are excluded from the PPR shell lane, so ppr has no ` +
|
|
175
|
+
`effect here and the route serves without the shell fast path. ` +
|
|
176
|
+
`Drop one of the two options.`,
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
|
|
120
180
|
if (ctx.forRoute && routeName !== ctx.forRoute) {
|
|
121
181
|
store.getShortCode("route");
|
|
122
182
|
return { type: "route" } as RouteItem;
|
|
@@ -129,17 +189,16 @@ export function createPathHelper<TEnv>(): PathFn<TEnv> {
|
|
|
129
189
|
? typeof handler.prerenderDef.handler === "function"
|
|
130
190
|
? (handler.prerenderDef.handler as Handler<any, any, TEnv>)
|
|
131
191
|
: () => {
|
|
132
|
-
throw new DataNotFoundError(
|
|
133
|
-
"No prerender data found for this route",
|
|
134
|
-
);
|
|
192
|
+
throw new DataNotFoundError(NO_PRERENDER_DATA_MESSAGE);
|
|
135
193
|
}
|
|
136
194
|
: isPrerenderHandler(handler)
|
|
137
195
|
? typeof handler.handler === "function"
|
|
138
|
-
? (
|
|
196
|
+
? gateOnDemandProducer(
|
|
197
|
+
handler.handler as Handler<any, any, TEnv>,
|
|
198
|
+
isOnDemand,
|
|
199
|
+
)
|
|
139
200
|
: () => {
|
|
140
|
-
throw new DataNotFoundError(
|
|
141
|
-
"No prerender data found for this route",
|
|
142
|
-
);
|
|
201
|
+
throw new DataNotFoundError(NO_PRERENDER_DATA_MESSAGE);
|
|
143
202
|
}
|
|
144
203
|
: isStaticHandler(handler)
|
|
145
204
|
? (handler.handler as Handler<any, any, TEnv>)
|
|
@@ -168,6 +227,7 @@ export function createPathHelper<TEnv>(): PathFn<TEnv> {
|
|
|
168
227
|
prerenderDef: handler as PrerenderHandlerDefinition,
|
|
169
228
|
}
|
|
170
229
|
: {}),
|
|
230
|
+
...(isOnDemand ? { isOnDemand: true as const } : {}),
|
|
171
231
|
...(isStaticHandler(handler)
|
|
172
232
|
? {
|
|
173
233
|
isStaticPrerender: true as const,
|
package/src/use-loader.tsx
CHANGED
|
@@ -14,9 +14,17 @@ import {
|
|
|
14
14
|
type ReactNode,
|
|
15
15
|
} from "react";
|
|
16
16
|
import { OutletContext, type OutletContextValue } from "./outlet-context.js";
|
|
17
|
-
import {
|
|
17
|
+
import {
|
|
18
|
+
EMPTY_LOADER_SNAPSHOT,
|
|
19
|
+
type LoaderEntry,
|
|
20
|
+
type LoaderStore,
|
|
21
|
+
} from "./loader-store.js";
|
|
22
|
+
import { NavigationStoreContext } from "./browser/react/context.js";
|
|
18
23
|
import { decodeLoaderEntry } from "./decode-loader-results.js";
|
|
19
24
|
import { warnAwaitedSsrSuspension } from "./ssr-suspension-warning.js";
|
|
25
|
+
import { SETTLED_THENABLE } from "./thenable-status.js";
|
|
26
|
+
import { useReadAudit } from "./suspense-audit-react.js";
|
|
27
|
+
import { INTERNAL_RANGO_SUSPENSE_AUDIT } from "./internal-suspense-audit.js";
|
|
20
28
|
import type { LoaderDefinition, LoadOptions } from "./types.js";
|
|
21
29
|
|
|
22
30
|
function isShareableGet(options: LoadOptions | undefined): boolean {
|
|
@@ -202,15 +210,18 @@ function useLoaderInternal<T>(
|
|
|
202
210
|
}
|
|
203
211
|
return { contextData: undefined, hasContextData: false };
|
|
204
212
|
}, [context, loader.$$id]);
|
|
205
|
-
|
|
206
213
|
// SPIKE (streaming useLoader): a pending per-loader stream suspends HERE —
|
|
207
|
-
// the implicit-suspense read.
|
|
208
|
-
// conditional call is legal; on replay after resolution it returns
|
|
214
|
+
// the implicit-suspense read. On replay after resolution use() returns
|
|
209
215
|
// synchronously and the decoded value takes the contextData slot. Hooks
|
|
210
216
|
// below never run in a suspended render, so their order is stable across
|
|
211
217
|
// every COMPLETED render.
|
|
212
218
|
const { hasContextData } = walk;
|
|
213
219
|
let contextData = walk.contextData;
|
|
220
|
+
// Dev only (suspense-audit.ts): the stream this reader is handed. The test
|
|
221
|
+
// is a build-time constant, so the hook is called on every render or never.
|
|
222
|
+
if (process.env.NODE_ENV !== "production" && INTERNAL_RANGO_SUSPENSE_AUDIT) {
|
|
223
|
+
useReadAudit(loader.$$id, walk.pendingStream);
|
|
224
|
+
}
|
|
214
225
|
if (walk.pendingStream) {
|
|
215
226
|
warnAwaitedSsrSuspension(
|
|
216
227
|
loader.$$id,
|
|
@@ -218,6 +229,10 @@ function useLoaderInternal<T>(
|
|
|
218
229
|
walk.pendingStream,
|
|
219
230
|
);
|
|
220
231
|
contextData = decodeLoaderEntry(use(walk.pendingStream)) as T;
|
|
232
|
+
} else if (hasContextData) {
|
|
233
|
+
// React logs a render that finishes without use() at a place whose reader
|
|
234
|
+
// suspended in use() on mount, so every read with route context calls it.
|
|
235
|
+
use(SETTLED_THENABLE);
|
|
221
236
|
}
|
|
222
237
|
|
|
223
238
|
const loaderId = loader.$$id;
|
|
@@ -245,10 +260,15 @@ function useLoaderInternal<T>(
|
|
|
245
260
|
const bucketKey =
|
|
246
261
|
effectiveKey === undefined ? loaderId : `${loaderId}::${effectiveKey}`;
|
|
247
262
|
|
|
263
|
+
// The router store's loader data (loader-store.ts). None on the server,
|
|
264
|
+
// where the hook reads the route's loader data only.
|
|
265
|
+
const loaders: LoaderStore | undefined = useContext(NavigationStoreContext)
|
|
266
|
+
?.store?.loaders;
|
|
267
|
+
|
|
248
268
|
const groupRefetch = useCallback(async (): Promise<void> => {
|
|
249
|
-
if (!loaderId) return;
|
|
250
|
-
const requestId =
|
|
251
|
-
|
|
269
|
+
if (!loaderId || !loaders) return;
|
|
270
|
+
const requestId = loaders.reserveRequestId(bucketKey);
|
|
271
|
+
loaders.beginRequest(bucketKey, requestId);
|
|
252
272
|
try {
|
|
253
273
|
const url = new URL(window.location.href);
|
|
254
274
|
url.searchParams.set("_rsc_loader", loaderId);
|
|
@@ -261,15 +281,15 @@ function useLoaderInternal<T>(
|
|
|
261
281
|
if (payload.loaderError) {
|
|
262
282
|
throw new Error(payload.loaderError.message);
|
|
263
283
|
}
|
|
264
|
-
|
|
284
|
+
loaders.finishData(bucketKey, requestId, payload.loaderResult);
|
|
265
285
|
} catch (e) {
|
|
266
286
|
const err = e instanceof Error ? e : new Error(String(e));
|
|
267
|
-
|
|
287
|
+
loaders.finishError(bucketKey, requestId, err);
|
|
268
288
|
throw err;
|
|
269
289
|
} finally {
|
|
270
|
-
|
|
290
|
+
loaders.setLoading(bucketKey, requestId, false);
|
|
271
291
|
}
|
|
272
|
-
}, [loaderId, bucketKey]);
|
|
292
|
+
}, [loaderId, bucketKey, loaders]);
|
|
273
293
|
|
|
274
294
|
// Held-navigation pin: a transition commit whose stream for this loader is
|
|
275
295
|
// still pending announces it (loader-store.ts announcePendingStreams, which
|
|
@@ -283,14 +303,17 @@ function useLoaderInternal<T>(
|
|
|
283
303
|
snapshot: LoaderEntry;
|
|
284
304
|
}>(() => ({
|
|
285
305
|
bucketKey,
|
|
286
|
-
snapshot:
|
|
306
|
+
snapshot: loaders?.getSnapshot(bucketKey) ?? EMPTY_LOADER_SNAPSHOT,
|
|
287
307
|
}));
|
|
308
|
+
// A new bucket (its key changed) reads nothing from the store during
|
|
309
|
+
// render: it renders empty until the effect below delivers its snapshot.
|
|
288
310
|
const sharedSnapshot =
|
|
289
311
|
sharedState.bucketKey === bucketKey
|
|
290
312
|
? sharedState.snapshot
|
|
291
|
-
:
|
|
313
|
+
: EMPTY_LOADER_SNAPSHOT;
|
|
292
314
|
useEffect(() => {
|
|
293
|
-
|
|
315
|
+
if (!loaders) return;
|
|
316
|
+
const initial = loaders.getSnapshot(bucketKey);
|
|
294
317
|
if (initial !== sharedSnapshot) {
|
|
295
318
|
startTransition(() => {
|
|
296
319
|
setSharedState({ bucketKey, snapshot: initial });
|
|
@@ -299,10 +322,10 @@ function useLoaderInternal<T>(
|
|
|
299
322
|
// ephemeral: a reader with no route context has no route-context reset
|
|
300
323
|
// trigger, so its keyed bucket is reference-counted by the store. A
|
|
301
324
|
// route-registered reader makes the bucket sticky (reset via clearFamily).
|
|
302
|
-
return
|
|
325
|
+
return loaders.subscribe(
|
|
303
326
|
bucketKey,
|
|
304
327
|
() => {
|
|
305
|
-
const next =
|
|
328
|
+
const next = loaders.getSnapshot(bucketKey);
|
|
306
329
|
startTransition(() => {
|
|
307
330
|
setSharedState({ bucketKey, snapshot: next });
|
|
308
331
|
});
|
|
@@ -323,7 +346,7 @@ function useLoaderInternal<T>(
|
|
|
323
346
|
// groupKey, and groupRefetch are the only inputs that require a fresh
|
|
324
347
|
// subscription (groupList is memoized on groupKey; groupRefetch is stable
|
|
325
348
|
// per bucketKey).
|
|
326
|
-
}, [bucketKey, hasContextData, groupKey, groupRefetch]);
|
|
349
|
+
}, [bucketKey, hasContextData, groupKey, groupRefetch, loaders]);
|
|
327
350
|
|
|
328
351
|
// Local state holds the result of:
|
|
329
352
|
// - parameterized / mutation `load()` calls (load({ params }), POST,
|
|
@@ -369,10 +392,10 @@ function useLoaderInternal<T>(
|
|
|
369
392
|
lastSharedRequestIdRef.current = null;
|
|
370
393
|
// Reset every sticky bucket of this loader (keyed or not). Ephemeral
|
|
371
394
|
// (unregistered keyed) buckets are left to their refcount lifecycle.
|
|
372
|
-
|
|
395
|
+
loaders?.clearFamily(loaderId);
|
|
373
396
|
prevContextDataRef.current = contextData;
|
|
374
397
|
}
|
|
375
|
-
}, [contextData, loaderId]);
|
|
398
|
+
}, [contextData, loaderId, loaders]);
|
|
376
399
|
|
|
377
400
|
// Read priority: a committed parameterized load() result overrides the shared
|
|
378
401
|
// snapshot; a committed shared snapshot overrides the server-seeded context.
|
|
@@ -408,15 +431,17 @@ function useLoaderInternal<T>(
|
|
|
408
431
|
const bucket = bucketKeyRef.current;
|
|
409
432
|
const hasDedicatedBucket = bucket !== id;
|
|
410
433
|
|
|
411
|
-
const shared =
|
|
412
|
-
|
|
413
|
-
|
|
434
|
+
const shared =
|
|
435
|
+
loaders !== undefined &&
|
|
436
|
+
(hasDedicatedBucket
|
|
437
|
+
? isShareableGet(loadOptions)
|
|
438
|
+
: isPlainRefetch(loadOptions) && hasContextDataRef.current);
|
|
414
439
|
let sharedRequestId = -1;
|
|
415
440
|
let localRequestId = -1;
|
|
416
441
|
if (shared) {
|
|
417
|
-
sharedRequestId =
|
|
442
|
+
sharedRequestId = loaders.reserveRequestId(bucket);
|
|
418
443
|
lastSharedRequestIdRef.current = sharedRequestId;
|
|
419
|
-
|
|
444
|
+
loaders.beginRequest(bucket, sharedRequestId);
|
|
420
445
|
} else {
|
|
421
446
|
localRequestId = ++localRequestIdRef.current;
|
|
422
447
|
setLocalIsLoading(true);
|
|
@@ -503,7 +528,7 @@ function useLoaderInternal<T>(
|
|
|
503
528
|
|
|
504
529
|
const result = payload.loaderResult;
|
|
505
530
|
if (shared) {
|
|
506
|
-
|
|
531
|
+
loaders.finishData(bucket, sharedRequestId, result);
|
|
507
532
|
} else if (localRequestId === localRequestIdRef.current) {
|
|
508
533
|
startTransition(() => {
|
|
509
534
|
setLocalFetchedData({ has: true, value: result });
|
|
@@ -514,7 +539,7 @@ function useLoaderInternal<T>(
|
|
|
514
539
|
} catch (e) {
|
|
515
540
|
const err = e instanceof Error ? e : new Error(String(e));
|
|
516
541
|
if (shared) {
|
|
517
|
-
|
|
542
|
+
loaders.finishError(bucket, sharedRequestId, err);
|
|
518
543
|
} else if (localRequestId === localRequestIdRef.current) {
|
|
519
544
|
setLocalError(err);
|
|
520
545
|
setLocalIsLoading(false);
|
|
@@ -525,11 +550,11 @@ function useLoaderInternal<T>(
|
|
|
525
550
|
return dataRef.current as T;
|
|
526
551
|
} finally {
|
|
527
552
|
if (shared) {
|
|
528
|
-
|
|
553
|
+
loaders.setLoading(bucket, sharedRequestId, false);
|
|
529
554
|
}
|
|
530
555
|
}
|
|
531
556
|
},
|
|
532
|
-
[throwOnError],
|
|
557
|
+
[throwOnError, loaders],
|
|
533
558
|
);
|
|
534
559
|
|
|
535
560
|
if (throwOnError) {
|
|
@@ -708,8 +733,10 @@ export function useFetchLoader<T>(
|
|
|
708
733
|
export function useRefreshLoaders(): (
|
|
709
734
|
groups: string | string[],
|
|
710
735
|
) => Promise<void> {
|
|
736
|
+
const loaders = useContext(NavigationStoreContext)?.store?.loaders;
|
|
711
737
|
return useCallback(
|
|
712
|
-
(groups: string | string[]) =>
|
|
713
|
-
|
|
738
|
+
(groups: string | string[]) =>
|
|
739
|
+
loaders ? loaders.refreshGroups(groups) : Promise.resolve(),
|
|
740
|
+
[loaders],
|
|
714
741
|
);
|
|
715
742
|
}
|
|
@@ -33,6 +33,10 @@ export function postprocessBundle(state: DiscoveryState): void {
|
|
|
33
33
|
);
|
|
34
34
|
|
|
35
35
|
// 1. Evict handler code from whichever chunks contain handler exports.
|
|
36
|
+
// onDemand retention needs no cross-check here: the chunk scan marks
|
|
37
|
+
// onDemand exports directly from state.onDemandHandlerIds (the evaluated
|
|
38
|
+
// source's $$id set), so retention and the runtime od trie flag share one
|
|
39
|
+
// derivation and cannot disagree.
|
|
36
40
|
// handlerChunkInfoMap/staticHandlerChunkInfoMap are populated by generateBundle
|
|
37
41
|
// after the production RSC build. In Vite 6 multi-environment builds, the
|
|
38
42
|
// RSC build runs twice (analysis + production). The maps are cleared at the
|
|
@@ -337,6 +337,20 @@ export async function discoverRouters(
|
|
|
337
337
|
(performance.now() - trieStart).toFixed(1),
|
|
338
338
|
);
|
|
339
339
|
|
|
340
|
+
// handlerId -> route name for onDemand routes, from the evaluated source —
|
|
341
|
+
// the authoritative side of postprocessBundle's retention cross-check.
|
|
342
|
+
const newOnDemandHandlerIds = new Map<string, string>();
|
|
343
|
+
for (const { manifest } of allManifests) {
|
|
344
|
+
if (!manifest.onDemandRoutes) continue;
|
|
345
|
+
const defs = manifest._prerenderDefs || {};
|
|
346
|
+
for (const routeName of manifest.onDemandRoutes) {
|
|
347
|
+
const handlerId = defs[routeName]?.$$id;
|
|
348
|
+
if (typeof handlerId === "string") {
|
|
349
|
+
newOnDemandHandlerIds.set(handlerId, routeName);
|
|
350
|
+
}
|
|
351
|
+
}
|
|
352
|
+
}
|
|
353
|
+
|
|
340
354
|
// Commit all local state to the shared discovery state atomically.
|
|
341
355
|
// This ensures a failed re-discovery (e.g. from a transient module
|
|
342
356
|
// evaluation error) preserves the last known-good state.
|
|
@@ -345,6 +359,7 @@ export async function discoverRouters(
|
|
|
345
359
|
state.perRouterManifestDataMap = newPerRouterManifestDataMap;
|
|
346
360
|
state.perRouterPrecomputedMap = newPerRouterPrecomputedMap;
|
|
347
361
|
state.perRouterTrieMap = newPerRouterTrieMap;
|
|
362
|
+
state.onDemandHandlerIds = newOnDemandHandlerIds;
|
|
348
363
|
|
|
349
364
|
// Install the route tries into the RSC realm BEFORE prerender collection.
|
|
350
365
|
// matchForPrerender resolves each enumerated URL via findMatch, and without
|
|
@@ -206,10 +206,10 @@ export async function runShellPrerenderPhase(
|
|
|
206
206
|
const staged: Array<{ key: string; value: string }> = [];
|
|
207
207
|
let skipCount = 0;
|
|
208
208
|
|
|
209
|
-
//
|
|
210
|
-
//
|
|
211
|
-
// would be ambiguous
|
|
212
|
-
// entries rather than bake a wrong-router shell.
|
|
209
|
+
// A candidate names no router (the sweep below gives it to the first one
|
|
210
|
+
// whose match lands on its route): two candidates on one pathname (two
|
|
211
|
+
// routers prerendering the same path) would be ambiguous — decline ALL of
|
|
212
|
+
// that pathname's entries rather than bake a wrong-router shell.
|
|
213
213
|
const pathCounts = new Map<string, number>();
|
|
214
214
|
for (const cand of candidates) {
|
|
215
215
|
pathCounts.set(cand.urlPath, (pathCounts.get(cand.urlPath) ?? 0) + 1);
|
|
@@ -282,7 +282,7 @@ export async function runShellPrerenderPhase(
|
|
|
282
282
|
tags: res.tags,
|
|
283
283
|
};
|
|
284
284
|
staged.push({
|
|
285
|
-
key: buildShellManifestKey(cand.urlPath),
|
|
285
|
+
key: buildShellManifestKey(routerId, cand.urlPath),
|
|
286
286
|
value: JSON.stringify(value),
|
|
287
287
|
});
|
|
288
288
|
console.log(
|
|
@@ -61,7 +61,12 @@ export interface PrecomputedEntry {
|
|
|
61
61
|
|
|
62
62
|
export interface ChunkInfo {
|
|
63
63
|
fileName: string;
|
|
64
|
-
exports: Array<{
|
|
64
|
+
exports: Array<{
|
|
65
|
+
name: string;
|
|
66
|
+
handlerId: string;
|
|
67
|
+
passthrough: boolean;
|
|
68
|
+
onDemand: boolean;
|
|
69
|
+
}>;
|
|
65
70
|
}
|
|
66
71
|
|
|
67
72
|
export interface PerRouterManifestEntry {
|
|
@@ -163,6 +168,16 @@ export interface DiscoveryState {
|
|
|
163
168
|
buildData: import("./build-versions.js").BuildDataRecord[];
|
|
164
169
|
handlerChunkInfoMap: Map<string, ChunkInfo>;
|
|
165
170
|
staticHandlerChunkInfoMap: Map<string, ChunkInfo>;
|
|
171
|
+
/**
|
|
172
|
+
* handlerId ($$id) -> route name for every route the evaluated source marks
|
|
173
|
+
* onDemand. Written during discovery (buildStart), consumed by the
|
|
174
|
+
* generateBundle chunk scan (extractHandlerExportsFromChunk) as the
|
|
175
|
+
* authoritative onDemand set driving producer retention — the same source
|
|
176
|
+
* that sets the runtime od trie flag, so any truthy onDemand spelling
|
|
177
|
+
* (literal, spread, imported const) both marks the route and retains its
|
|
178
|
+
* producer.
|
|
179
|
+
*/
|
|
180
|
+
onDemandHandlerIds: Map<string, string>;
|
|
166
181
|
rscEntryFileName: string | null;
|
|
167
182
|
resolvedPrerenderModules: Map<string, string[]> | undefined;
|
|
168
183
|
resolvedStaticModules: Map<string, string[]> | undefined;
|
|
@@ -225,6 +240,7 @@ export function createDiscoveryState(
|
|
|
225
240
|
buildData: [],
|
|
226
241
|
handlerChunkInfoMap: new Map(),
|
|
227
242
|
staticHandlerChunkInfoMap: new Map(),
|
|
243
|
+
onDemandHandlerIds: new Map(),
|
|
228
244
|
rscEntryFileName: null,
|
|
229
245
|
resolvedPrerenderModules: undefined,
|
|
230
246
|
resolvedStaticModules: undefined,
|
|
@@ -12,37 +12,59 @@ import type { Connect } from "vite";
|
|
|
12
12
|
* runs on the module regardless of how (or whether) the define is delivered, in
|
|
13
13
|
* both dev and build and for every environment, so the discovery plugin uses this
|
|
14
14
|
* to replace the module with the resolved literal.
|
|
15
|
+
*
|
|
16
|
+
* internal-suspense-audit.ts takes the same path for INTERNAL_RANGO_SUSPENSE_AUDIT,
|
|
17
|
+
* the repo-only switch of the dev suspense audit.
|
|
15
18
|
*/
|
|
16
19
|
|
|
20
|
+
// The router's flag modules, by file name: each exports one boolean named after
|
|
21
|
+
// the env var it is baked from.
|
|
22
|
+
const FLAG_MODULE =
|
|
23
|
+
/\/(internal-debug|internal-suspense-audit)\.[cm]?[jt]sx?(\?|$)/;
|
|
24
|
+
const FLAG_NAMES: Record<string, string> = {
|
|
25
|
+
"internal-debug": "INTERNAL_RANGO_DEBUG",
|
|
26
|
+
"internal-suspense-audit": "INTERNAL_RANGO_SUSPENSE_AUDIT",
|
|
27
|
+
};
|
|
28
|
+
|
|
17
29
|
/**
|
|
18
|
-
*
|
|
19
|
-
* (`/@rangojs/router/`, incl.
|
|
20
|
-
* (`/packages/rangojs-router/`).
|
|
21
|
-
* consumer file that merely sits
|
|
22
|
-
* Accepts module ids and dev-server
|
|
30
|
+
* The env var a router flag module is baked from, or undefined. Scoped to the
|
|
31
|
+
* router's own modules: the published package (`/@rangojs/router/`, incl.
|
|
32
|
+
* pnpm's nested layout) or the monorepo workspace (`/packages/rangojs-router/`).
|
|
33
|
+
* The package-anchored path avoids matching a consumer file that merely sits
|
|
34
|
+
* under a directory named `rangojs-router`. Accepts module ids and dev-server
|
|
35
|
+
* URLs (`/@fs/...internal-debug.ts?v=abc`).
|
|
23
36
|
*/
|
|
24
|
-
|
|
25
|
-
if (!id.includes("internal-
|
|
37
|
+
function routerFlagName(id: string): string | undefined {
|
|
38
|
+
if (!id.includes("internal-")) return undefined;
|
|
26
39
|
const norm = id.replace(/\\/g, "/");
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
40
|
+
if (
|
|
41
|
+
!norm.includes("/@rangojs/router/") &&
|
|
42
|
+
!norm.includes("/packages/rangojs-router/")
|
|
43
|
+
) {
|
|
44
|
+
return undefined;
|
|
45
|
+
}
|
|
46
|
+
const match = FLAG_MODULE.exec(norm);
|
|
47
|
+
return match ? FLAG_NAMES[match[1]!] : undefined;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/** A router flag module: internal-debug.ts or internal-suspense-audit.ts. */
|
|
51
|
+
export function isRouterInternalDebugId(id: string): boolean {
|
|
52
|
+
return routerFlagName(id) !== undefined;
|
|
32
53
|
}
|
|
33
54
|
|
|
34
55
|
/**
|
|
35
|
-
* Transform: replace
|
|
36
|
-
* Returns null for any
|
|
56
|
+
* Transform: replace a router flag module with the resolved literal.
|
|
57
|
+
* Returns null for any other module.
|
|
37
58
|
*/
|
|
38
59
|
export function injectClientDebugFlag(
|
|
39
60
|
id: string,
|
|
40
61
|
): { code: string; map: null } | null {
|
|
41
62
|
// Cheap early-out: this hook runs on every module in every environment.
|
|
42
|
-
|
|
43
|
-
|
|
63
|
+
const name = routerFlagName(id);
|
|
64
|
+
if (name === undefined) return null;
|
|
65
|
+
// Emit the whole module: each flag module has a single export, kept in sync.
|
|
44
66
|
return {
|
|
45
|
-
code: `export const
|
|
67
|
+
code: `export const ${name} = ${!!process.env[name]};\n`,
|
|
46
68
|
map: null,
|
|
47
69
|
};
|
|
48
70
|
}
|
|
@@ -63,9 +85,9 @@ export function injectClientDebugFlag(
|
|
|
63
85
|
* immune -- workspace source is outside node_modules and served no-cache --
|
|
64
86
|
* which is why this bit only npm consumers.
|
|
65
87
|
*
|
|
66
|
-
*
|
|
67
|
-
* are byte-identical across flag states), so forcing revalidation for
|
|
68
|
-
*
|
|
88
|
+
* The two flag modules are the only flag-varying modules in the graph (their
|
|
89
|
+
* importers are byte-identical across flag states), so forcing revalidation for
|
|
90
|
+
* them is sufficient and costs a conditional request each per session. The
|
|
69
91
|
* middleware is unconditional (not gated on the flag) so an already-poisoned
|
|
70
92
|
* cache heals in both directions. Alternatives that do not work: a plugin
|
|
71
93
|
* `resolveId` appending a flag query never fires in Vite 8 dev for
|
|
@@ -28,6 +28,12 @@ async function initializeApp() {
|
|
|
28
28
|
// is the default; createRouter({ strictMode: false }) ships the opt-out in the
|
|
29
29
|
// payload metadata. StrictMode emits no DOM, so toggling never changes markup.
|
|
30
30
|
const { strictMode, initialPayload } = await initBrowserApp({ rscStream, deps });
|
|
31
|
+
// Set only on a document a no-JS useActionState POST rendered: React needs
|
|
32
|
+
// the server's form state to hydrate the hook without resetting it. A state
|
|
33
|
+
// Flight could not carry rejects: hydrate without it, as before.
|
|
34
|
+
const formState = await Promise.resolve(initialPayload.formState).catch(
|
|
35
|
+
() => undefined
|
|
36
|
+
);
|
|
31
37
|
|
|
32
38
|
if (import.meta.hot) {
|
|
33
39
|
const { startDevDiscoveryHandshake } = await import(
|
|
@@ -42,7 +48,8 @@ async function initializeApp() {
|
|
|
42
48
|
const app = createElement(Rango);
|
|
43
49
|
hydrateRoot(
|
|
44
50
|
document,
|
|
45
|
-
strictMode === false ? app : createElement(StrictMode, null, app)
|
|
51
|
+
strictMode === false ? app : createElement(StrictMode, null, app),
|
|
52
|
+
formState ? { formState } : undefined
|
|
46
53
|
);
|
|
47
54
|
}
|
|
48
55
|
|