@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.
Files changed (51) hide show
  1. package/dist/types/client-urls/server-projection.d.ts +4 -4
  2. package/dist/types/client-urls/types.d.ts +13 -12
  3. package/dist/types/route-definition/helpers-types.d.ts +6 -5
  4. package/dist/types/router/segment-resolution/loader-mask.d.ts +1 -1
  5. package/dist/types/rsc/shell-capture.d.ts +9 -0
  6. package/dist/types/rsc/types.d.ts +8 -0
  7. package/dist/types/server/context.d.ts +1 -1
  8. package/dist/types/server/request-context.d.ts +1 -1
  9. package/dist/types/ssr/index.d.ts +16 -0
  10. package/dist/types/ssr/ssr-root.d.ts +5 -0
  11. package/dist/types/types/loader-types.d.ts +24 -22
  12. package/dist/types/urls/path-helper-types.d.ts +8 -7
  13. package/dist/vite/index.js +8 -5
  14. package/package.json +22 -21
  15. package/skills/breadcrumbs/SKILL.md +2 -2
  16. package/skills/catalog.json +2 -2
  17. package/skills/client-urls/SKILL.md +9 -8
  18. package/skills/comparison/references/framework-comparison.md +2 -2
  19. package/skills/hooks/data.md +1 -1
  20. package/skills/hooks/handle-and-actions.md +1 -1
  21. package/skills/loader/SKILL.md +29 -28
  22. package/skills/migrate-nextjs/SKILL.md +3 -3
  23. package/skills/migrate-react-router/component-migration.md +1 -1
  24. package/skills/migrate-react-router/data-and-actions.md +1 -1
  25. package/skills/migrate-react-router/route-mapping.md +1 -1
  26. package/skills/parallel/SKILL.md +1 -1
  27. package/skills/ppr/SKILL.md +1 -1
  28. package/skills/rango/SKILL.md +20 -20
  29. package/skills/router-setup/SKILL.md +1 -1
  30. package/skills/scripts/SKILL.md +1 -1
  31. package/skills/shell-manifest/SKILL.md +1 -1
  32. package/src/browser/react/Link.tsx +27 -4
  33. package/src/client-urls/client-urls.ts +12 -9
  34. package/src/client-urls/server-projection.ts +6 -6
  35. package/src/client-urls/types.ts +13 -12
  36. package/src/route-definition/dsl-helpers.ts +7 -3
  37. package/src/route-definition/helpers-types.ts +6 -5
  38. package/src/router/loader-resolution.ts +3 -3
  39. package/src/router/segment-resolution/fresh.ts +2 -2
  40. package/src/router/segment-resolution/loader-cache.ts +1 -1
  41. package/src/router/segment-resolution/loader-mask.ts +1 -1
  42. package/src/rsc/rsc-rendering.ts +10 -0
  43. package/src/rsc/shell-capture.ts +10 -0
  44. package/src/rsc/ssr-setup.ts +4 -0
  45. package/src/rsc/types.ts +18 -2
  46. package/src/server/context.ts +1 -1
  47. package/src/server/request-context.ts +1 -1
  48. package/src/ssr/index.tsx +22 -2
  49. package/src/ssr/ssr-root.tsx +17 -1
  50. package/src/types/loader-types.ts +21 -19
  51. 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, { stream:
31
- * "navigation" }); materialization passes the option through to the server
32
- * loader() so document renders await them before first flush. Absent (=
33
- * none) in projections serialized before stream support. */
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, { stream: "navigation" }): document renders await this loader
58
- * before first flush (see {@link LoaderOptions}). Projected into the server
59
- * tree, where the per-isSSR entry stamping applies — client navigations
60
- * stream regardless.
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 stream?: "navigation";
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 `{ stream: "navigation" }` to await this loader before first flush
105
- * on DOCUMENT requests (see {@link LoaderOptions}) the opt-in for loaders
106
- * whose data, handle pushes, or thrown notFound()/redirect() must be in the
107
- * SSR'd HTML. Per-loader: a dynamic sibling keeps streaming. Under a
108
- * `ppr` group route the flag BAKES: the loader executes at shell capture
109
- * and its settled return freezes into the shell (nested promises stay
110
- * live holes).
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 `{ stream: "navigation" }` to await this loader before first flush on
212
- * DOCUMENT requests (see {@link LoaderOptions}) — the opt-in for loaders whose
213
- * data, handle pushes, or thrown notFound()/redirect() must be in the SSR'd
214
- * HTML. Per-loader: a dynamic sibling in the same segment keeps streaming.
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, { stream: "navigation" }, () => [cache()]),
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: a `stream: "navigation"`
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, { stream: "navigation" })), so its data, handle pushes, and
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, { stream: "navigation" })): segment resolution awaits these
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, { stream: "navigation" })` so the document render awaits it
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
- * Delivery mode for a DSL-registered loader: `loader(Def, { stream })`.
153
- *
154
- * Default (omitted): the loader streams on every render. Its data, its
155
- * `ctx.use(Handle)` pushes, and any `notFound()`/`redirect()` it throws may land
156
- * AFTER the document Response is constructed, so none of them are guaranteed to
157
- * be in the SSR'd HTML.
158
- *
159
- * `"navigation"` narrows streaming to client navigations only: on a DOCUMENT
160
- * request the loader is awaited before first flush. `useLoader` still suspends,
161
- * but on an already-settled promise, so no fallback paints. This is the
162
- * SSR-completeness opt-in the name is about WHERE streaming still applies, not
163
- * about disabling it. Choose it when the loader feeds something that must exist
164
- * in the document: `<head>` meta via a handle, or a real 404 status (an awaited
165
- * `notFound()` deterministically precedes Response construction, where the
166
- * streamed default only wins that race opportunistically). It does NOT change
167
- * PPR capture behavior: capture renders mask loaders and skip this await.
168
- *
169
- * Scoped per LOADER, not per segment: a baked loader alongside a deliberately
170
- * dynamic sibling awaits only itself, and the sibling keeps streaming behind its
171
- * `loading()`/Suspense boundary.
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
- stream?: "navigation";
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 `{ stream: "navigation" }` to await this loader before first flush
152
- * on DOCUMENT requests (see {@link LoaderOptions}) the opt-in for loaders
153
- * whose data, handle pushes, or thrown notFound()/redirect() must be in the
154
- * SSR'd HTML. Per-loader: a dynamic sibling keeps streaming. Under a
155
- * `ppr` route the flag BAKES: the loader executes at shell capture and
156
- * its settled return freezes into the shell (nested promises stay live
157
- * holes) — the pre-flush promise applied to the prelude.
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
  /**
@@ -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.8.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 !== window.location.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 isExternal = isExternalUrl(to);
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(({ stream }, index) => stream === "navigation" ? index : -1).filter((index) => index >= 0);
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.8.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": "^19.2.7",
199
- "@types/react-dom": "^19.2.3",
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": "^19.2.8",
204
- "react-dom": "^19.2.8",
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, { stream: "navigation" })` —
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 `{ stream: "navigation" }` if it must be in the SSR'd document), and if
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).
@@ -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 stream:\"navigation\" 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.",
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 (stream:\"navigation\").",
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 stream:"navigation" 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.
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, { stream: "navigation" }, use?)` — see below |
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 `stream: "navigation"` below
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
- ## `stream: "navigation"` — the SSR-completeness opt-in
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, { stream: "navigation" }, () => [revalidate(productData)]),
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 name says WHERE streaming still applies, not that it is disabled: document
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 (`stream: "navigation"`) gives
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 `stream: "navigation"`
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, { stream: "navigation" })`
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 — `{ stream: "navigation" }` awaits ONE loader on document
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
@@ -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 `{ stream: "navigation" }` is already settled at first paint, so its
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, { stream: "navigation" })` makes them deterministic in the
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