@rangojs/router 0.8.0 → 0.9.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/types/client-urls/server-projection.d.ts +4 -4
- package/dist/types/client-urls/types.d.ts +13 -12
- package/dist/types/route-definition/helpers-types.d.ts +6 -5
- package/dist/types/router/segment-resolution/loader-mask.d.ts +1 -1
- package/dist/types/rsc/shell-capture.d.ts +9 -0
- package/dist/types/rsc/types.d.ts +8 -0
- package/dist/types/server/context.d.ts +1 -1
- package/dist/types/server/request-context.d.ts +1 -1
- package/dist/types/ssr/index.d.ts +16 -0
- package/dist/types/ssr/ssr-root.d.ts +5 -0
- package/dist/types/types/loader-types.d.ts +24 -22
- package/dist/types/urls/path-helper-types.d.ts +8 -7
- package/dist/vite/index.js +8 -5
- package/package.json +22 -21
- package/skills/breadcrumbs/SKILL.md +2 -2
- package/skills/catalog.json +2 -2
- package/skills/client-urls/SKILL.md +9 -8
- package/skills/comparison/references/framework-comparison.md +2 -2
- package/skills/hooks/data.md +1 -1
- package/skills/hooks/handle-and-actions.md +1 -1
- package/skills/loader/SKILL.md +29 -28
- package/skills/migrate-nextjs/SKILL.md +3 -3
- package/skills/migrate-react-router/component-migration.md +1 -1
- package/skills/migrate-react-router/data-and-actions.md +1 -1
- package/skills/migrate-react-router/route-mapping.md +1 -1
- package/skills/parallel/SKILL.md +1 -1
- package/skills/ppr/SKILL.md +1 -1
- package/skills/rango/SKILL.md +20 -20
- package/skills/router-setup/SKILL.md +1 -1
- package/skills/scripts/SKILL.md +1 -1
- package/skills/shell-manifest/SKILL.md +1 -1
- package/src/browser/react/Link.tsx +27 -4
- package/src/client-urls/client-urls.ts +12 -9
- package/src/client-urls/server-projection.ts +6 -6
- package/src/client-urls/types.ts +13 -12
- package/src/route-definition/dsl-helpers.ts +7 -3
- package/src/route-definition/helpers-types.ts +6 -5
- package/src/router/loader-resolution.ts +3 -3
- package/src/router/segment-resolution/fresh.ts +2 -2
- package/src/router/segment-resolution/loader-cache.ts +1 -1
- package/src/router/segment-resolution/loader-mask.ts +1 -1
- package/src/rsc/rsc-rendering.ts +10 -0
- package/src/rsc/shell-capture.ts +10 -0
- package/src/rsc/ssr-setup.ts +4 -0
- package/src/rsc/types.ts +18 -2
- package/src/server/context.ts +1 -1
- package/src/server/request-context.ts +1 -1
- package/src/ssr/index.tsx +22 -2
- package/src/ssr/ssr-root.tsx +17 -1
- package/src/types/loader-types.ts +21 -19
- package/src/urls/path-helper-types.ts +8 -7
|
@@ -27,10 +27,10 @@ export interface ClientUrlProjectionRoute {
|
|
|
27
27
|
readonly options: ClientUrlProjectionOptions;
|
|
28
28
|
readonly loaderIds: readonly string[];
|
|
29
29
|
readonly hasLoading: boolean;
|
|
30
|
-
/** Indices into loaderIds of loaders declared loader(Def, {
|
|
31
|
-
*
|
|
32
|
-
*
|
|
33
|
-
*
|
|
30
|
+
/** Indices into loaderIds of loaders declared loader(Def, { ssr: false });
|
|
31
|
+
* materialization passes the option through to the server loader() so
|
|
32
|
+
* document renders await them before first flush. Absent (= none) in
|
|
33
|
+
* projections serialized before the option existed. */
|
|
34
34
|
readonly awaitedLoaderIndices?: readonly number[];
|
|
35
35
|
/** Data-only transition config (no `when` — server-tree only); absent in
|
|
36
36
|
* projections serialized before transition support. */
|
|
@@ -54,12 +54,12 @@ export interface ClientUrlLoaderRecord {
|
|
|
54
54
|
/** Client-run per-loader revalidation predicates; empty = locked defaults. */
|
|
55
55
|
readonly revalidate: readonly ClientRevalidateFn[];
|
|
56
56
|
/**
|
|
57
|
-
* loader(Def, {
|
|
58
|
-
*
|
|
59
|
-
*
|
|
60
|
-
*
|
|
57
|
+
* loader(Def, { ssr: false }): document renders await this loader before
|
|
58
|
+
* first flush (see {@link LoaderOptions}). Projected into the server tree,
|
|
59
|
+
* where the per-isSSR entry stamping applies — client navigations stream
|
|
60
|
+
* regardless.
|
|
61
61
|
*/
|
|
62
|
-
readonly
|
|
62
|
+
readonly ssr?: false;
|
|
63
63
|
}
|
|
64
64
|
/**
|
|
65
65
|
* The data-only subset of TransitionConfig a clientUrls() route may declare:
|
|
@@ -101,13 +101,14 @@ export interface ClientUrlHelpers {
|
|
|
101
101
|
* revalidate() only — a CLIENT-RUN per-loader predicate; its decision (not
|
|
102
102
|
* the function) is sent with the revalidation request.
|
|
103
103
|
*
|
|
104
|
-
* Pass `{
|
|
105
|
-
*
|
|
106
|
-
*
|
|
107
|
-
*
|
|
108
|
-
*
|
|
109
|
-
*
|
|
110
|
-
* live
|
|
104
|
+
* Pass `{ ssr: false }` — the same knob as loading(fallback, { ssr:
|
|
105
|
+
* false }) — to await this loader before first flush on DOCUMENT requests
|
|
106
|
+
* (see {@link LoaderOptions}): the opt-in for loaders whose data, handle
|
|
107
|
+
* pushes, or thrown notFound()/redirect() must be in the SSR'd HTML.
|
|
108
|
+
* Per-loader: a streaming sibling keeps streaming. Under a `ppr` group
|
|
109
|
+
* route the flag BAKES: the loader executes at shell capture and its
|
|
110
|
+
* settled return freezes into the shell (nested promises stay live
|
|
111
|
+
* holes).
|
|
111
112
|
*/
|
|
112
113
|
readonly loader: <TData>(definition: LoaderDefinition<TData>, optionsOrUse?: LoaderOptions | ClientUrlUse, use?: ClientUrlUse) => ClientUrlItem;
|
|
113
114
|
readonly loading: (component: ReactNode) => ClientUrlItem;
|
|
@@ -208,13 +208,14 @@ export type RouteHelpers<T extends RouteDefinition, TEnv> = {
|
|
|
208
208
|
* return <div>{data.name}</div>;
|
|
209
209
|
* }
|
|
210
210
|
* ```
|
|
211
|
-
* Pass `{
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
*
|
|
211
|
+
* Pass `{ ssr: false }` — the same knob as loading(fallback, { ssr:
|
|
212
|
+
* false }) — to await this loader before first flush on DOCUMENT requests
|
|
213
|
+
* (see {@link LoaderOptions}): the opt-in for loaders whose data, handle
|
|
214
|
+
* pushes, or thrown notFound()/redirect() must be in the SSR'd HTML.
|
|
215
|
+
* Per-loader: a streaming sibling in the same segment keeps streaming.
|
|
215
216
|
*
|
|
216
217
|
* ```typescript
|
|
217
|
-
* loader(ProductLoader, {
|
|
218
|
+
* loader(ProductLoader, { ssr: false }, () => [cache()]),
|
|
218
219
|
* loader(RecommendationsLoader), // still streams behind loading()
|
|
219
220
|
* ```
|
|
220
221
|
*
|
|
@@ -30,7 +30,7 @@ export { createMaskedLoaderPromise } from "./mask-nested.js";
|
|
|
30
30
|
/**
|
|
31
31
|
* Entry-level lane input for an entry's loaders under PPR (the loading()
|
|
32
32
|
* value; docs/design/loader-container-bake.md). The CAPTURE decision itself
|
|
33
|
-
* is per LOADER in loader-cache.ts:
|
|
33
|
+
* is per LOADER in loader-cache.ts: an `ssr: false`
|
|
34
34
|
* (awaitBeforeFlush) loader BAKES at capture regardless of this value — the
|
|
35
35
|
* flag's document promise ("data in the HTML before first flush") maps to
|
|
36
36
|
* the frozen prelude — while every other loader is LIVE (masked at capture,
|
|
@@ -283,6 +283,15 @@ export interface ShellCaptureDescriptor {
|
|
|
283
283
|
* Omitted (build-time bare-pathname captures) = search-less, same as "".
|
|
284
284
|
*/
|
|
285
285
|
searchSeed?: string;
|
|
286
|
+
/**
|
|
287
|
+
* The scheduling request's origin, seeding the capture render's SSR store
|
|
288
|
+
* location. Shell keys are host-scoped, so the resume pass's request
|
|
289
|
+
* agrees modulo protocol drift; the seed keeps origin-dependent static
|
|
290
|
+
* markup (Link's data-external) identical across capture, resume, and
|
|
291
|
+
* browser hydration. Omitted (build-time host-agnostic captures) falls
|
|
292
|
+
* back to the internal host.
|
|
293
|
+
*/
|
|
294
|
+
originSeed?: string;
|
|
286
295
|
/**
|
|
287
296
|
* The RSC handler's build version (HandlerContext.version), stamped into the
|
|
288
297
|
* stored entry as ShellCacheEntry.buildVersion — the serve-side
|
|
@@ -175,6 +175,12 @@ export interface SSRRenderOptions {
|
|
|
175
175
|
* search; see ShellCaptureOptions.search in the SSR entry).
|
|
176
176
|
*/
|
|
177
177
|
search?: string;
|
|
178
|
+
/**
|
|
179
|
+
* The live request's origin (`url.origin`), seeding the SSR store
|
|
180
|
+
* location so origin-dependent markup (Link's data-external) agrees with
|
|
181
|
+
* the browser across hydration. Absent on host-agnostic build captures.
|
|
182
|
+
*/
|
|
183
|
+
origin?: string;
|
|
178
184
|
}
|
|
179
185
|
/**
|
|
180
186
|
* SSR module interface for HTML rendering
|
|
@@ -193,6 +199,7 @@ export interface SSRModule {
|
|
|
193
199
|
quiesce: Promise<void>;
|
|
194
200
|
maxWaitMs?: number;
|
|
195
201
|
search?: string;
|
|
202
|
+
origin?: string;
|
|
196
203
|
}) => Promise<{
|
|
197
204
|
prelude: Uint8Array;
|
|
198
205
|
postponed: string | null;
|
|
@@ -209,6 +216,7 @@ export interface SSRModule {
|
|
|
209
216
|
postponed: string | null;
|
|
210
217
|
nonce?: string;
|
|
211
218
|
search?: string;
|
|
219
|
+
origin?: string;
|
|
212
220
|
}) => Promise<ReadableStream<Uint8Array>>;
|
|
213
221
|
}
|
|
214
222
|
/**
|
|
@@ -84,7 +84,7 @@ export type LoaderEntry = {
|
|
|
84
84
|
cache?: EntryCacheConfig;
|
|
85
85
|
/**
|
|
86
86
|
* Document renders await this loader before segment resolution returns
|
|
87
|
-
* (loader(Def, {
|
|
87
|
+
* (loader(Def, { ssr: false })), so its data, handle pushes, and
|
|
88
88
|
* thrown notFound()/redirect() deterministically precede first flush.
|
|
89
89
|
* Resolved at DSL-evaluation time from ctx.isSSR — entries are cached
|
|
90
90
|
* per-isSSR (router/manifest.ts cache key), so the flag is already
|
|
@@ -509,7 +509,7 @@ export interface RequestContext<TEnv = DefaultEnv, TParams = Record<string, stri
|
|
|
509
509
|
_handlerLoaderDeps?: Set<string>;
|
|
510
510
|
/**
|
|
511
511
|
* @internal Loader IDs ($$id) whose entries carry `awaitBeforeFlush`
|
|
512
|
-
* (loader(Def, {
|
|
512
|
+
* (loader(Def, { ssr: false })): segment resolution awaits these
|
|
513
513
|
* before returning, so the render barrier cannot resolve until they settle.
|
|
514
514
|
* rendered() checks this set to fail fast — a flagged loader awaiting the
|
|
515
515
|
* barrier is a guaranteed cycle, not a race. Registered by resolveLoaders
|
|
@@ -90,6 +90,13 @@ export interface SSRRenderOptions {
|
|
|
90
90
|
* only (runtime captures own the search variants, seeded per key).
|
|
91
91
|
*/
|
|
92
92
|
search?: string;
|
|
93
|
+
/**
|
|
94
|
+
* The live request's origin, seeding the SSR store location so
|
|
95
|
+
* origin-dependent markup (Link's data-external) agrees with the
|
|
96
|
+
* browser's window.location across hydration. Absent on the build-time
|
|
97
|
+
* prerender pass (host-agnostic bare captures).
|
|
98
|
+
*/
|
|
99
|
+
origin?: string;
|
|
93
100
|
}
|
|
94
101
|
/**
|
|
95
102
|
* SSR dependencies from external packages
|
|
@@ -184,6 +191,13 @@ interface ShellCaptureOptions {
|
|
|
184
191
|
* the shell's own key. MUST equal the resume pass's seed for the same key.
|
|
185
192
|
*/
|
|
186
193
|
search?: string;
|
|
194
|
+
/**
|
|
195
|
+
* The capture request's origin. Shell keys are host-scoped, so the resume
|
|
196
|
+
* pass's request agrees modulo protocol drift; seeding it keeps
|
|
197
|
+
* origin-dependent static markup (Link's data-external) identical across
|
|
198
|
+
* capture, resume, and browser hydration.
|
|
199
|
+
*/
|
|
200
|
+
origin?: string;
|
|
187
201
|
}
|
|
188
202
|
/**
|
|
189
203
|
* Result of a successful shell capture. `prelude` is the raw prelude bytes;
|
|
@@ -210,6 +224,8 @@ interface ShellResumeOptions {
|
|
|
210
224
|
* postponed holes.
|
|
211
225
|
*/
|
|
212
226
|
search?: string;
|
|
227
|
+
/** The HIT request's origin — same host as the capture's (key-scoped). */
|
|
228
|
+
origin?: string;
|
|
213
229
|
}
|
|
214
230
|
/**
|
|
215
231
|
* Create an SSR handler that converts RSC streams to HTML.
|
|
@@ -58,6 +58,11 @@ export interface SsrRootOptions {
|
|
|
58
58
|
* the resume tree matches the captured tree above the postponed holes.
|
|
59
59
|
*/
|
|
60
60
|
search?: string;
|
|
61
|
+
/**
|
|
62
|
+
* Origin seeding the SSR store location (see createSsrEventController):
|
|
63
|
+
* live fizz passes the request's origin; capture/resume pass theirs.
|
|
64
|
+
*/
|
|
65
|
+
origin?: string;
|
|
61
66
|
}
|
|
62
67
|
/**
|
|
63
68
|
* Build the closure component that deserializes the Flight payload, consumes
|
|
@@ -63,7 +63,7 @@ export type LoaderContext<TParams = Record<string, string | undefined>, TEnv = D
|
|
|
63
63
|
* later ones stream to the client and apply post-hydration (document lane)
|
|
64
64
|
* or progressively (navigation/action lanes). To guarantee a loader's
|
|
65
65
|
* handles are in the SSR'd document, register it as
|
|
66
|
-
* `loader(Def, {
|
|
66
|
+
* `loader(Def, { ssr: false })` so the document render awaits it
|
|
67
67
|
* (see {@link LoaderOptions}).
|
|
68
68
|
*
|
|
69
69
|
* @example
|
|
@@ -149,29 +149,31 @@ export type LoaderContext<TParams = Record<string, string | undefined>, TEnv = D
|
|
|
149
149
|
*/
|
|
150
150
|
export type LoaderFn<T, TParams = Record<string, string | undefined>, TEnv = DefaultEnv> = (ctx: LoaderContext<TParams, TEnv>) => Promise<T> | T;
|
|
151
151
|
/**
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
* Default (omitted): the loader streams on every render. Its data,
|
|
155
|
-
* `ctx.use(Handle)` pushes, and any `notFound()`/`redirect()` it throws
|
|
156
|
-
* AFTER the document Response is constructed, so none of them are
|
|
157
|
-
* be in the SSR'd HTML.
|
|
158
|
-
*
|
|
159
|
-
* `
|
|
160
|
-
*
|
|
161
|
-
*
|
|
162
|
-
*
|
|
163
|
-
*
|
|
164
|
-
*
|
|
165
|
-
*
|
|
166
|
-
*
|
|
167
|
-
*
|
|
168
|
-
*
|
|
169
|
-
*
|
|
170
|
-
*
|
|
171
|
-
* `
|
|
152
|
+
* SSR delivery for a DSL-registered loader: `loader(Def, { ssr })`.
|
|
153
|
+
*
|
|
154
|
+
* Default (omitted / `true`): the loader streams on every render. Its data,
|
|
155
|
+
* its `ctx.use(Handle)` pushes, and any `notFound()`/`redirect()` it throws
|
|
156
|
+
* may land AFTER the document Response is constructed, so none of them are
|
|
157
|
+
* guaranteed to be in the SSR'd HTML.
|
|
158
|
+
*
|
|
159
|
+
* `ssr: false` turns SSR streaming off for this loader — the same knob
|
|
160
|
+
* `loading(fallback, { ssr: false })` is for the fallback: on a DOCUMENT
|
|
161
|
+
* request the loader is awaited before first flush, so no fallback paints for
|
|
162
|
+
* it (`useLoader` still suspends, but on an already-settled promise). Client
|
|
163
|
+
* navigations keep streaming it. This is the SSR-completeness opt-in. Choose
|
|
164
|
+
* it when the loader feeds something that must exist in the document:
|
|
165
|
+
* `<head>` meta via a handle, or a real 404 status (an awaited `notFound()`
|
|
166
|
+
* deterministically precedes Response construction, where the streamed
|
|
167
|
+
* default only wins that race opportunistically). Under ppr it is also the
|
|
168
|
+
* BAKE lane: the loader executes at shell capture and its settled return is
|
|
169
|
+
* shell material (nested promises stay live holes) — see the ppr docs.
|
|
170
|
+
*
|
|
171
|
+
* Scoped per LOADER, not per segment: an `ssr: false` loader alongside a
|
|
172
|
+
* deliberately streaming sibling awaits only itself, and the sibling keeps
|
|
173
|
+
* streaming behind its `loading()`/Suspense boundary.
|
|
172
174
|
*/
|
|
173
175
|
export type LoaderOptions = {
|
|
174
|
-
|
|
176
|
+
ssr?: boolean;
|
|
175
177
|
};
|
|
176
178
|
/**
|
|
177
179
|
* Options for fetchable loaders
|
|
@@ -148,13 +148,14 @@ export type PathHelpers<TEnv> = {
|
|
|
148
148
|
/**
|
|
149
149
|
* Attach a data loader to the current route/layout.
|
|
150
150
|
*
|
|
151
|
-
* Pass `{
|
|
152
|
-
*
|
|
153
|
-
*
|
|
154
|
-
*
|
|
155
|
-
*
|
|
156
|
-
*
|
|
157
|
-
* holes) — the pre-flush
|
|
151
|
+
* Pass `{ ssr: false }` — the same knob as loading(fallback, { ssr:
|
|
152
|
+
* false }) — to await this loader before first flush on DOCUMENT requests
|
|
153
|
+
* (see {@link LoaderOptions}): the opt-in for loaders whose data, handle
|
|
154
|
+
* pushes, or thrown notFound()/redirect() must be in the SSR'd HTML.
|
|
155
|
+
* Per-loader: a streaming sibling keeps streaming. Under a `ppr` route the
|
|
156
|
+
* flag BAKES: the loader executes at shell capture and its settled return
|
|
157
|
+
* freezes into the shell (nested promises stay live holes) — the pre-flush
|
|
158
|
+
* promise applied to the prelude.
|
|
158
159
|
*/
|
|
159
160
|
loader: <TData>(loaderDef: LoaderDefinition<TData>, optionsOrUse?: LoaderOptions | (() => LoaderUseItem[]), use?: () => LoaderUseItem[]) => LoaderItem;
|
|
160
161
|
/**
|
package/dist/vite/index.js
CHANGED
|
@@ -3685,7 +3685,7 @@ import { resolve } from "node:path";
|
|
|
3685
3685
|
// package.json
|
|
3686
3686
|
var package_default = {
|
|
3687
3687
|
name: "@rangojs/router",
|
|
3688
|
-
version: "0.
|
|
3688
|
+
version: "0.9.0",
|
|
3689
3689
|
description: "Django-inspired RSC router with composable URL patterns",
|
|
3690
3690
|
keywords: [
|
|
3691
3691
|
"react",
|
|
@@ -8304,11 +8304,12 @@ init_loader();
|
|
|
8304
8304
|
import { jsx as jsx6 } from "react/jsx-runtime";
|
|
8305
8305
|
var IGNORE_STRATEGY_CHANGES = (_listener) => () => {
|
|
8306
8306
|
};
|
|
8307
|
-
function isExternalUrl(href2) {
|
|
8307
|
+
function isExternalUrl(href2, origin) {
|
|
8308
8308
|
if (href2.startsWith("//")) return true;
|
|
8309
8309
|
if (href2.startsWith("http://") || href2.startsWith("https://")) {
|
|
8310
|
+
if (!origin) return false;
|
|
8310
8311
|
try {
|
|
8311
|
-
return new URL(href2).origin !==
|
|
8312
|
+
return new URL(href2).origin !== origin;
|
|
8312
8313
|
} catch {
|
|
8313
8314
|
return false;
|
|
8314
8315
|
}
|
|
@@ -8332,7 +8333,9 @@ var Link = forwardRef(function Link2({
|
|
|
8332
8333
|
...props
|
|
8333
8334
|
}, ref) {
|
|
8334
8335
|
const ctx = useContext14(NavigationStoreContext);
|
|
8335
|
-
const
|
|
8336
|
+
const storeLocation = ctx?.eventController.getState().location;
|
|
8337
|
+
const origin = storeLocation?.origin ?? (typeof window !== "undefined" ? window.location.origin : void 0);
|
|
8338
|
+
const isExternal = isExternalUrl(to, origin);
|
|
8336
8339
|
const resolvedTo = useMemo7(() => {
|
|
8337
8340
|
if (isExternal) return to;
|
|
8338
8341
|
const bn = ctx?.basename;
|
|
@@ -9018,7 +9021,7 @@ function serializeRoute(route2) {
|
|
|
9018
9021
|
}
|
|
9019
9022
|
return loader2.$$id;
|
|
9020
9023
|
});
|
|
9021
|
-
const awaitedLoaderIndices = route2.loaders.map(({
|
|
9024
|
+
const awaitedLoaderIndices = route2.loaders.map(({ ssr }, index) => ssr === false ? index : -1).filter((index) => index >= 0);
|
|
9022
9025
|
const transition2 = serializeTransition(route2);
|
|
9023
9026
|
return Object.freeze({
|
|
9024
9027
|
id: route2.id,
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@rangojs/router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.9.0",
|
|
4
4
|
"description": "Django-inspired RSC router with composable URL patterns",
|
|
5
5
|
"keywords": [
|
|
6
6
|
"react",
|
|
@@ -176,6 +176,20 @@
|
|
|
176
176
|
"access": "public",
|
|
177
177
|
"tag": "latest"
|
|
178
178
|
},
|
|
179
|
+
"scripts": {
|
|
180
|
+
"build": "pnpm run build:types && pnpm exec esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.js --platform=node --packages=external && mkdir -p dist/vite/plugins && cp src/vite/plugins/cloudflare-protocol-loader-hook.mjs dist/vite/plugins/cloudflare-protocol-loader-hook.mjs && pnpm exec esbuild src/testing/vitest.ts --bundle --format=esm --outfile=dist/testing/vitest.js --platform=node --packages=external && pnpm exec esbuild src/bin/rango.ts --bundle --format=esm --outfile=dist/bin/rango.js --platform=node --packages=external --banner:js='#!/usr/bin/env node' && chmod +x dist/bin/rango.js",
|
|
181
|
+
"build:types": "rm -rf dist/types && pnpm exec tsc -p tsconfig.types.json",
|
|
182
|
+
"prepublishOnly": "pnpm build",
|
|
183
|
+
"typecheck": "tsc --noEmit && tsc -p tsconfig.strict-check.json --noEmit && tsc -p tsconfig.augment-check.json --noEmit",
|
|
184
|
+
"test": "playwright test",
|
|
185
|
+
"test:preload": "playwright test --config=playwright.preload.config.ts",
|
|
186
|
+
"test:ui": "playwright test --ui",
|
|
187
|
+
"test:hmr-local": "playwright test --project=dev-warmup --project=hmr-basename --project=hmr-prerender --no-deps --workers=1 && RANGO_E2E_ROUTE_HMR_ONLY=1 playwright test --project=hmr-routes --no-deps --workers=1",
|
|
188
|
+
"test:hmr-routes-local": "RANGO_E2E_ROUTE_HMR_ONLY=1 playwright test --project=hmr-routes --no-deps --workers=1",
|
|
189
|
+
"test:unit": "pnpm run build:types && vitest run",
|
|
190
|
+
"test:unit:watch": "vitest",
|
|
191
|
+
"test:unit:rsc": "vitest run --config vitest.rsc.config.ts"
|
|
192
|
+
},
|
|
179
193
|
"dependencies": {
|
|
180
194
|
"@types/debug": "^4.1.12",
|
|
181
195
|
"@vitejs/plugin-rsc": "^0.5.31",
|
|
@@ -192,18 +206,18 @@
|
|
|
192
206
|
"@opentelemetry/context-async-hooks": "^2.9.0",
|
|
193
207
|
"@opentelemetry/sdk-trace-base": "^2.9.0",
|
|
194
208
|
"@playwright/test": "^1.49.1",
|
|
209
|
+
"@shared/e2e": "workspace:*",
|
|
195
210
|
"@testing-library/dom": "^10.4.1",
|
|
196
211
|
"@testing-library/react": "^16.3.2",
|
|
197
212
|
"@types/node": "^24.10.1",
|
|
198
|
-
"@types/react": "
|
|
199
|
-
"@types/react-dom": "
|
|
213
|
+
"@types/react": "catalog:",
|
|
214
|
+
"@types/react-dom": "catalog:",
|
|
200
215
|
"esbuild": "^0.28.1",
|
|
201
216
|
"happy-dom": "^20.10.1",
|
|
202
217
|
"jiti": "^2.7.0",
|
|
203
|
-
"react": "
|
|
204
|
-
"react-dom": "
|
|
205
|
-
"vitest": "^4.1.9"
|
|
206
|
-
"@shared/e2e": "0.0.1"
|
|
218
|
+
"react": "catalog:",
|
|
219
|
+
"react-dom": "catalog:",
|
|
220
|
+
"vitest": "^4.1.9"
|
|
207
221
|
},
|
|
208
222
|
"peerDependencies": {
|
|
209
223
|
"@cloudflare/vite-plugin": "^1.42.1",
|
|
@@ -242,18 +256,5 @@
|
|
|
242
256
|
},
|
|
243
257
|
"engines": {
|
|
244
258
|
"node": ">=24.0.0"
|
|
245
|
-
},
|
|
246
|
-
"scripts": {
|
|
247
|
-
"build": "pnpm run build:types && pnpm exec esbuild src/vite/index.ts --bundle --format=esm --outfile=dist/vite/index.js --platform=node --packages=external && mkdir -p dist/vite/plugins && cp src/vite/plugins/cloudflare-protocol-loader-hook.mjs dist/vite/plugins/cloudflare-protocol-loader-hook.mjs && pnpm exec esbuild src/testing/vitest.ts --bundle --format=esm --outfile=dist/testing/vitest.js --platform=node --packages=external && pnpm exec esbuild src/bin/rango.ts --bundle --format=esm --outfile=dist/bin/rango.js --platform=node --packages=external --banner:js='#!/usr/bin/env node' && chmod +x dist/bin/rango.js",
|
|
248
|
-
"build:types": "rm -rf dist/types && pnpm exec tsc -p tsconfig.types.json",
|
|
249
|
-
"typecheck": "tsc --noEmit && tsc -p tsconfig.strict-check.json --noEmit && tsc -p tsconfig.augment-check.json --noEmit",
|
|
250
|
-
"test": "playwright test",
|
|
251
|
-
"test:preload": "playwright test --config=playwright.preload.config.ts",
|
|
252
|
-
"test:ui": "playwright test --ui",
|
|
253
|
-
"test:hmr-local": "playwright test --project=dev-warmup --project=hmr-basename --project=hmr-prerender --no-deps --workers=1 && RANGO_E2E_ROUTE_HMR_ONLY=1 playwright test --project=hmr-routes --no-deps --workers=1",
|
|
254
|
-
"test:hmr-routes-local": "RANGO_E2E_ROUTE_HMR_ONLY=1 playwright test --project=hmr-routes --no-deps --workers=1",
|
|
255
|
-
"test:unit": "pnpm run build:types && vitest run",
|
|
256
|
-
"test:unit:watch": "vitest",
|
|
257
|
-
"test:unit:rsc": "vitest run --config vitest.rsc.config.ts"
|
|
258
259
|
}
|
|
259
|
-
}
|
|
260
|
+
}
|
|
@@ -82,7 +82,7 @@ Two pushes from one segment accumulate in push order (`Shop › product`).
|
|
|
82
82
|
Delivery follows the loader race model: a push that beats the handler barrier
|
|
83
83
|
is in the SSR'd document; a push after a slow fetch streams and applies
|
|
84
84
|
client-side (`useHandle` re-renders when it lands). To guarantee document
|
|
85
|
-
delivery, register the loader as `loader(Def, {
|
|
85
|
+
delivery, register the loader as `loader(Def, { ssr: false })` —
|
|
86
86
|
see `/loader` → "Writing Handles from Loaders".
|
|
87
87
|
|
|
88
88
|
## Async Content
|
|
@@ -170,7 +170,7 @@ swap in — no blank, no pending entry. If the slot times out to `else: null`/
|
|
|
170
170
|
undefined, the entry is simply dropped. Use `.defer()` only when even
|
|
171
171
|
`label`/`href` are unknown at handler time AND the producer is a component,
|
|
172
172
|
not a loader — a loader-produced crumb pushes directly from the loader body
|
|
173
|
-
(add `{
|
|
173
|
+
(add `{ ssr: false }` if it must be in the SSR'd document), and if
|
|
174
174
|
you know `label`/`href` and only the `content` is async, push a concrete item
|
|
175
175
|
with a `Promise` `content` field instead (the `content` field is a nested
|
|
176
176
|
promise you resolve with `use()` in your component; no `.defer()` needed).
|
package/skills/catalog.json
CHANGED
|
@@ -35,7 +35,7 @@
|
|
|
35
35
|
},
|
|
36
36
|
{
|
|
37
37
|
"name": "client-urls",
|
|
38
|
-
"description": "Define client-component route groups with clientUrls() in a \"use client\" module — no handlers, useLoader at read sites, client-run revalidate predicates, loader-thrown notFound/redirect, loader handle writes, and the
|
|
38
|
+
"description": "Define client-component route groups with clientUrls() in a \"use client\" module — no handlers, useLoader at read sites, client-run revalidate predicates, loader-thrown notFound/redirect, loader handle writes, and the ssr:false SSR-completeness opt-in. Use when a route group's pages are client components, when building high-navigation-speed UIs (dashboards, admin panels, settings) where transitions must feel instant, when moving data reads from handler ctx.use() to useLoader at consumption sites, or when asking how routes defined in a \"use client\" file work.",
|
|
39
39
|
"argumentHint": "[setup]",
|
|
40
40
|
"path": "skills/client-urls/SKILL.md"
|
|
41
41
|
},
|
|
@@ -137,7 +137,7 @@
|
|
|
137
137
|
},
|
|
138
138
|
{
|
|
139
139
|
"name": "loader",
|
|
140
|
-
"description": "Define data loaders for fetching data in routes with createLoader. Use when pages need per-request data that stays fresh, data should stream while the page renders, client components need reactive server data, a loader should throw notFound()/redirect(), set page meta/breadcrumbs from loader data (handle writes), or loader data must be guaranteed in the SSR'd document (
|
|
140
|
+
"description": "Define data loaders for fetching data in routes with createLoader. Use when pages need per-request data that stays fresh, data should stream while the page renders, client components need reactive server data, a loader should throw notFound()/redirect(), set page meta/breadcrumbs from loader data (handle writes), or loader data must be guaranteed in the SSR'd document (ssr:false).",
|
|
141
141
|
"argumentHint": "[loader]",
|
|
142
142
|
"path": "skills/loader/SKILL.md"
|
|
143
143
|
},
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
---
|
|
2
2
|
name: client-urls
|
|
3
|
-
description: Define client-component route groups with clientUrls() in a "use client" module — no handlers, useLoader at read sites, client-run revalidate predicates, loader-thrown notFound/redirect, loader handle writes, and the
|
|
3
|
+
description: Define client-component route groups with clientUrls() in a "use client" module — no handlers, useLoader at read sites, client-run revalidate predicates, loader-thrown notFound/redirect, loader handle writes, and the ssr:false SSR-completeness opt-in. Use when a route group's pages are client components, when building high-navigation-speed UIs (dashboards, admin panels, settings) where transitions must feel instant, when moving data reads from handler ctx.use() to useLoader at consumption sites, or when asking how routes defined in a "use client" file work.
|
|
4
4
|
argument-hint: "[setup]"
|
|
5
5
|
---
|
|
6
6
|
|
|
@@ -122,7 +122,7 @@ typing work exactly as for server routes (`/typesafety`).
|
|
|
122
122
|
| -------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
123
123
|
| `path()` | Options are `name`, `search`, `trailingSlash`, `ppr` (shell caching — see /ppr skill; loader routes need `loading()` or capture refuses); no response variants |
|
|
124
124
|
| `layout()` | Must contain at least one `path()` |
|
|
125
|
-
| `loader()` | `loader(Def, use?)` or `loader(Def, {
|
|
125
|
+
| `loader()` | `loader(Def, use?)` or `loader(Def, { ssr: false }, use?)` — see below |
|
|
126
126
|
| `loading()` | Route/layout-level pending UI; inline `<Suspense>` at read sites is usually better |
|
|
127
127
|
| `revalidate()` | Valid **inside a loader() use callback only**; runs in the browser |
|
|
128
128
|
| `transition()` | Data-only ViewTransition config — no `when` (that is a server-executed predicate) |
|
|
@@ -214,7 +214,7 @@ export const ProductLoader = createLoader(async (ctx) => {
|
|
|
214
214
|
On a document load, `notFound()` streams the resolved not-found UI in the
|
|
215
215
|
envelope and _opportunistically_ sets a real 404 status — the status write only
|
|
216
216
|
wins if the rejection settles before the document Response is constructed (a
|
|
217
|
-
fast, pre-fetch existence check usually wins; see `
|
|
217
|
+
fast, pre-fetch existence check usually wins; see `ssr: false` below
|
|
218
218
|
for the deterministic version). On navigations the 404 UI swaps in with the
|
|
219
219
|
URL preserved. A loader `redirect()` is always a client-side navigate — there
|
|
220
220
|
is no document-lane 302 from loaders; pre-stream redirect authority belongs to
|
|
@@ -228,7 +228,7 @@ ride the SSR handle snapshot; later ones stream and apply post-hydration
|
|
|
228
228
|
(document loads) or progressively (navigations). See `/loader` for the full
|
|
229
229
|
contract.
|
|
230
230
|
|
|
231
|
-
## `
|
|
231
|
+
## `ssr: false` — the SSR-completeness opt-in
|
|
232
232
|
|
|
233
233
|
By default every loader streams on every render, so nothing a slow loader
|
|
234
234
|
produces is _guaranteed_ to be in the SSR'd HTML — data SSRs as the Suspense
|
|
@@ -238,12 +238,13 @@ document — `<head>` meta via a handle, or a real 404 status — flag it:
|
|
|
238
238
|
|
|
239
239
|
```ts
|
|
240
240
|
path("/product/:slug", ProductPage, { name: "product" }, () => [
|
|
241
|
-
loader(ProductLoader, {
|
|
241
|
+
loader(ProductLoader, { ssr: false }, () => [revalidate(productData)]),
|
|
242
242
|
loader(RelatedLoader), // untouched: still streams behind its boundary
|
|
243
243
|
]),
|
|
244
244
|
```
|
|
245
245
|
|
|
246
|
-
The
|
|
246
|
+
The knob mirrors `loading(fallback, { ssr: false })` — SSR delivery is off
|
|
247
|
+
for this loader, so nothing of it is left to stream in the document: document
|
|
247
248
|
renders await this loader before first flush (data settled — no fallback
|
|
248
249
|
paints; handle pushes beat the barrier snapshot; a thrown `notFound()` is a
|
|
249
250
|
deterministic real 404, no warm-up race); client navigations stream exactly as
|
|
@@ -303,7 +304,7 @@ a hard load of the target URL renders the full route.
|
|
|
303
304
|
`<Suspense>` above each `useLoader` read is still the finer-grained tool
|
|
304
305
|
when different reads on one route should wait independently.
|
|
305
306
|
- **Two parallel loaders with equal latency look "SSR'd" together.** Loaders
|
|
306
|
-
kick off in parallel, so awaiting one (`
|
|
307
|
+
kick off in parallel, so awaiting one (`ssr: false`) gives
|
|
307
308
|
same-or-faster siblings time to settle coincidentally. Do not read "it was
|
|
308
309
|
in the HTML once" as a guarantee — only the flagged loader is guaranteed.
|
|
309
310
|
- **Hook semantics shift inside a group.** `usePathname` is ABSOLUTE (mount
|
|
@@ -332,7 +333,7 @@ The router repository (not shipped in this package) carries a canonical
|
|
|
332
333
|
consumer at `tests/vite-rsc-demo/src/urls/client-shop.client.tsx`: layout +
|
|
333
334
|
index + product routes, param-sensitive predicates, action-scoped cart
|
|
334
335
|
revalidation, loader-thrown `notFound()`/`redirect()`, loader-written
|
|
335
|
-
Meta/Breadcrumbs, viewport prefetch on cards, and the `
|
|
336
|
+
Meta/Breadcrumbs, viewport prefetch on cards, and the `ssr: false`
|
|
336
337
|
fixtures — with e2e suites next to it pinning each contract in dev and
|
|
337
338
|
production. The sections above are self-contained; the app is corroboration,
|
|
338
339
|
not required reading.
|
|
@@ -621,7 +621,7 @@ boundaries, compose server-side via `ctx.use(OtherLoader)`, and carry
|
|
|
621
621
|
route-level authority: they can throw `notFound()`/`redirect()`, WRITE handles
|
|
622
622
|
(`ctx.use(Meta)({ title })` — data-derived page metadata pushed from the data's
|
|
623
623
|
producer), read handle data after the render settles (`ctx.get(handle)` behind
|
|
624
|
-
`await ctx.rendered()`), and opt into `loader(Def, {
|
|
624
|
+
`await ctx.rendered()`), and opt into `loader(Def, { ssr: false })`
|
|
625
625
|
so a document render awaits them before first flush (deterministic SSR'd data,
|
|
626
626
|
meta, and 404 status). Reads happen through `useLoader` in a client component
|
|
627
627
|
(including its SSR pass) or `useFetchLoader` for standalone client fetches. "Fetchable" loaders are callable
|
|
@@ -691,7 +691,7 @@ that no loader-before-render has:
|
|
|
691
691
|
read by many segments, composed via `ctx.use`, or exposed as a fetchable endpoint;
|
|
692
692
|
- it **streams as a hole, not a gate** — concurrent, Suspense-resolved under
|
|
693
693
|
`loading()`, so the shell never blocks on it (and gating is a deliberate
|
|
694
|
-
per-loader opt-in — `{
|
|
694
|
+
per-loader opt-in — `{ ssr: false }` awaits ONE loader on document
|
|
695
695
|
renders while the rest keep streaming — not the model's default);
|
|
696
696
|
- it is **client-addressable and refreshable** independent of navigation (`key`,
|
|
697
697
|
`refreshGroup`, `useRefreshLoaders`), behaving like a built-in, server-defined
|
package/skills/hooks/data.md
CHANGED
|
@@ -26,7 +26,7 @@ boundary (or the route's `loading()`) — it does NOT render with
|
|
|
26
26
|
`isLoading: true`. Put a boundary above every read whose loader can be slow;
|
|
27
27
|
`isLoading` covers later refetches (`load()`, key/group refreshes). Once the
|
|
28
28
|
component renders, `data` is present. (On document loads a loader registered
|
|
29
|
-
with `{
|
|
29
|
+
with `{ ssr: false }` is already settled at first paint, so its
|
|
30
30
|
reads never suspend there — see `/loader`.)
|
|
31
31
|
|
|
32
32
|
**Precondition**: Loader must be registered on route via `loader()` helper.
|
|
@@ -33,7 +33,7 @@ const lastCrumb = useHandle(Breadcrumbs, (data) => data.at(-1));
|
|
|
33
33
|
Writers: handlers AND loader bodies — `ctx.use(Handle)` returns the push
|
|
34
34
|
function in each (middleware has no handle APIs). Loader pushes follow a delivery race (settle before
|
|
35
35
|
the handler barrier → in the SSR'd document; later → applied client-side), and
|
|
36
|
-
`loader(Def, {
|
|
36
|
+
`loader(Def, { ssr: false })` makes them deterministic in the
|
|
37
37
|
document. See `/loader` → "Writing Handles from Loaders". `useHandle`
|
|
38
38
|
re-renders on every handle update, late loader pushes included.
|
|
39
39
|
|