@rangojs/router 0.24.4 → 0.25.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/README.md +1 -0
- package/dist/bin/rango.js +363 -39
- package/dist/testing/vitest.js +37 -4
- package/dist/types/browser/event-controller.d.ts +24 -5
- package/dist/types/browser/intercept-utils.d.ts +5 -2
- package/dist/types/browser/merge-segment-loaders.d.ts +0 -23
- package/dist/types/browser/navigation-transaction.d.ts +5 -5
- package/dist/types/browser/partial-update.d.ts +21 -13
- package/dist/types/browser/prefetch/cache.d.ts +8 -0
- package/dist/types/browser/rango-marker.d.ts +7 -0
- package/dist/types/browser/react/filter-segment-order.d.ts +10 -0
- package/dist/types/browser/react/use-handle.d.ts +20 -0
- package/dist/types/browser/react/use-href.d.ts +6 -0
- package/dist/types/browser/react/use-router.d.ts +6 -1
- package/dist/types/browser/response-adapter.d.ts +5 -3
- package/dist/types/browser/scroll-restoration.d.ts +9 -3
- package/dist/types/browser/segment-reconciler.d.ts +10 -13
- package/dist/types/browser/types.d.ts +38 -8
- package/dist/types/build/route-types/per-module-writer.d.ts +12 -0
- package/dist/types/cache/cf/cf-shell-frame.d.ts +2 -0
- package/dist/types/cache/types.d.ts +8 -0
- package/dist/types/client-urls/client-urls.d.ts +6 -2
- package/dist/types/client-urls/revalidate-registry.d.ts +1 -1
- package/dist/types/client-urls/server-projection.d.ts +9 -5
- package/dist/types/client-urls/types.d.ts +29 -16
- package/dist/types/client.d.ts +2 -2
- package/dist/types/client.rsc.d.ts +2 -2
- package/dist/types/decode-loader-results.d.ts +28 -7
- package/dist/types/href-client.d.ts +3 -1
- package/dist/types/index.rsc.d.ts +1 -1
- package/dist/types/loader-redirect.d.ts +2 -3
- package/dist/types/outlet-context.d.ts +7 -6
- package/dist/types/prefetch-markers.d.ts +14 -5
- package/dist/types/route-content-wrapper.d.ts +17 -14
- package/dist/types/route-definition/helpers-types.d.ts +7 -2
- package/dist/types/route-definition/resolve-handler-use.d.ts +6 -1
- package/dist/types/route-definition/rewrite.d.ts +48 -5
- package/dist/types/route-types.d.ts +2 -2
- package/dist/types/router/handler-context.d.ts +14 -2
- package/dist/types/router/intercept-resolution.d.ts +19 -46
- package/dist/types/router/lazy-includes.d.ts +7 -0
- package/dist/types/router/match-middleware/intercept-resolution.d.ts +2 -2
- package/dist/types/router/middleware.d.ts +0 -16
- package/dist/types/router/navigation-snapshot.d.ts +2 -0
- package/dist/types/router/prefetch-cache-ttl.d.ts +8 -11
- package/dist/types/router/revalidation.d.ts +8 -1
- package/dist/types/router/router-context.d.ts +4 -16
- package/dist/types/router/router-interfaces.d.ts +0 -6
- package/dist/types/router/router-options.d.ts +4 -7
- package/dist/types/router/segment-resolution/prefetch-deferral.d.ts +31 -8
- package/dist/types/router/segment-resolution/revalidation.d.ts +38 -4
- package/dist/types/router/segment-resolution.d.ts +1 -1
- package/dist/types/router/segment-wrappers.d.ts +3 -5
- package/dist/types/router/substitute-pattern-params.d.ts +3 -2
- package/dist/types/router/tracing.d.ts +2 -2
- package/dist/types/rsc/full-payload.d.ts +14 -2
- package/dist/types/rsc/helpers.d.ts +8 -0
- package/dist/types/rsc/manifest-init.d.ts +20 -0
- package/dist/types/rsc/prefetch-render-error.d.ts +22 -0
- package/dist/types/rsc/progressive-enhancement.d.ts +20 -0
- package/dist/types/rsc/rewrite-passes.d.ts +12 -2
- package/dist/types/rsc/server-action.d.ts +6 -0
- package/dist/types/rsc/shell-capture-constants.d.ts +10 -1
- package/dist/types/rsc/shell-capture.d.ts +7 -0
- package/dist/types/rsc/types.d.ts +5 -0
- package/dist/types/segment-system.d.ts +15 -0
- package/dist/types/server/context.d.ts +64 -4
- package/dist/types/server/request-context.d.ts +41 -4
- package/dist/types/server/request-headers.d.ts +12 -0
- package/dist/types/server/rewrite-trail.d.ts +8 -1
- package/dist/types/testing/e2e/index.d.ts +1 -1
- package/dist/types/testing/flight.entry.d.ts +1 -1
- package/dist/types/testing/index.d.ts +1 -1
- package/dist/types/testing/internal/client-urls-projection.d.ts +4 -3
- package/dist/types/testing/render-route.d.ts +21 -11
- package/dist/types/testing/run-loader.d.ts +7 -0
- package/dist/types/testing/run-middleware.d.ts +7 -0
- package/dist/types/testing/run-revalidate.d.ts +5 -4
- package/dist/types/testing/serve-shell-request.d.ts +41 -4
- package/dist/types/testing/shell-status.d.ts +17 -0
- package/dist/types/testing/vitest-stubs/version.d.ts +1 -0
- package/dist/types/testing/vitest.d.ts +10 -1
- package/dist/types/thenable-status.d.ts +0 -6
- package/dist/types/types/handler-context.d.ts +12 -1
- package/dist/types/types/loader-types.d.ts +4 -3
- package/dist/types/types/route-entry.d.ts +2 -1
- package/dist/types/urls/path-helper-types.d.ts +4 -2
- package/dist/types/vite/discovery/client-urls-projection.d.ts +55 -5
- package/dist/types/vite/discovery/state.d.ts +20 -0
- package/dist/types/vite/plugins/client-urls-revalidate-hoist.d.ts +28 -16
- package/dist/types/vite/plugins/hoist/source.d.ts +8 -7
- package/dist/types/vite/plugins/virtual-entries.d.ts +6 -0
- package/dist/types/vite/utils/prerender-utils.d.ts +7 -0
- package/dist/vite/index.js +6816 -6547
- package/package.json +1 -1
- package/skills/app-structure/SKILL.md +255 -0
- package/skills/app-structure/content-site.md +333 -0
- package/skills/app-structure/e-commerce.md +435 -0
- package/skills/app-structure/large-app.md +885 -0
- package/skills/app-structure/multi-app.md +280 -0
- package/skills/breadcrumbs/SKILL.md +6 -1
- package/skills/caching/SKILL.md +3 -1
- package/skills/catalog.json +12 -0
- package/skills/client-urls/SKILL.md +185 -32
- package/skills/composability/SKILL.md +10 -8
- package/skills/handler-use/SKILL.md +48 -9
- package/skills/hooks/handle-and-actions.md +104 -0
- package/skills/hooks/navigation.md +24 -10
- package/skills/hooks/urls.md +1 -1
- package/skills/i18n/SKILL.md +2 -1
- package/skills/intercept/SKILL.md +60 -29
- package/skills/layout/SKILL.md +6 -3
- package/skills/links/SKILL.md +151 -17
- package/skills/loader/SKILL.md +32 -10
- package/skills/middleware/SKILL.md +111 -15
- package/skills/observability/SKILL.md +11 -0
- package/skills/parallel/SKILL.md +5 -5
- package/skills/ppr/SKILL.md +43 -8
- package/skills/prerender/SKILL.md +1 -2
- package/skills/rango/SKILL.md +3 -0
- package/skills/response-routes/SKILL.md +4 -1
- package/skills/scripts/SKILL.md +12 -1
- package/skills/server-actions/SKILL.md +60 -20
- package/skills/testing/SKILL.md +1 -0
- package/skills/testing/client-components.md +13 -10
- package/skills/testing/loader.md +25 -24
- package/skills/testing/middleware.md +11 -11
- package/skills/testing/response-routes.md +4 -1
- package/skills/testing/serve-shell-request.md +85 -21
- package/skills/ux-architecture/SKILL.md +980 -0
- package/skills/view-transitions/SKILL.md +43 -10
- package/src/__augment-tests__/augmented.check.ts +28 -0
- package/src/browser/event-controller.ts +44 -18
- package/src/browser/intercept-utils.ts +3 -2
- package/src/browser/merge-segment-loaders.ts +0 -85
- package/src/browser/navigation-bridge.ts +112 -84
- package/src/browser/navigation-client.ts +20 -15
- package/src/browser/navigation-store.ts +4 -2
- package/src/browser/navigation-transaction.ts +9 -8
- package/src/browser/partial-update.ts +90 -32
- package/src/browser/prefetch/cache.ts +8 -0
- package/src/browser/prefetch/fetch.ts +13 -3
- package/src/browser/rango-marker.ts +13 -0
- package/src/browser/react/NavigationProvider.tsx +25 -7
- package/src/browser/react/filter-segment-order.ts +13 -0
- package/src/browser/react/use-action.ts +9 -15
- package/src/browser/react/use-handle.ts +63 -71
- package/src/browser/react/use-href.tsx +24 -5
- package/src/browser/react/use-link-status.ts +1 -1
- package/src/browser/react/use-navigation.ts +30 -40
- package/src/browser/react/use-router.ts +37 -6
- package/src/browser/react/use-segments.ts +8 -12
- package/src/browser/response-adapter.ts +11 -5
- package/src/browser/rsc-router.tsx +3 -1
- package/src/browser/scroll-restoration.ts +34 -20
- package/src/browser/segment-reconciler.ts +11 -29
- package/src/browser/server-action-bridge.ts +52 -16
- package/src/browser/types.ts +40 -8
- package/src/build/generate-manifest.ts +12 -1
- package/src/build/route-types/codegen.ts +6 -2
- package/src/build/route-types/per-module-writer.ts +94 -61
- package/src/cache/cache-runtime.ts +9 -0
- package/src/cache/cache-scope.ts +2 -1
- package/src/cache/cf/cf-cache-store.ts +2 -0
- package/src/cache/cf/cf-shell-frame.ts +3 -0
- package/src/cache/types.ts +8 -0
- package/src/cache/vercel/vercel-cache-store.ts +6 -1
- package/src/client-urls/client-urls.ts +202 -153
- package/src/client-urls/revalidate-registry.ts +12 -3
- package/src/client-urls/server-projection.ts +137 -66
- package/src/client-urls/types.ts +30 -15
- package/src/client.rsc.tsx +11 -1
- package/src/client.tsx +9 -1
- package/src/decode-loader-results.ts +64 -31
- package/src/href-client.ts +3 -1
- package/src/index.rsc.ts +1 -0
- package/src/index.ts +12 -4
- package/src/loader-redirect.tsx +2 -3
- package/src/outlet-context.ts +7 -6
- package/src/prefetch-markers.ts +15 -5
- package/src/reverse.ts +3 -3
- package/src/route-content-wrapper.tsx +19 -17
- package/src/route-definition/dsl-helpers.ts +27 -7
- package/src/route-definition/helpers-types.ts +7 -2
- package/src/route-definition/resolve-handler-use.ts +23 -10
- package/src/route-definition/rewrite.ts +86 -19
- package/src/route-types.ts +1 -2
- package/src/router/find-match.ts +37 -1
- package/src/router/handler-context.ts +85 -40
- package/src/router/intercept-resolution.ts +161 -307
- package/src/router/lazy-includes.ts +43 -27
- package/src/router/loader-resolution.ts +21 -1
- package/src/router/manifest.ts +6 -8
- package/src/router/match-api.ts +11 -1
- package/src/router/match-middleware/background-revalidation.ts +2 -4
- package/src/router/match-middleware/cache-lookup.ts +16 -20
- package/src/router/match-middleware/intercept-resolution.ts +41 -60
- package/src/router/match-result.ts +31 -2
- package/src/router/middleware.ts +6 -147
- package/src/router/navigation-snapshot.ts +38 -2
- package/src/router/prefetch-cache-ttl.ts +10 -18
- package/src/router/prerender-match.ts +2 -1
- package/src/router/revalidation.ts +24 -3
- package/src/router/router-context.ts +5 -18
- package/src/router/router-interfaces.ts +0 -7
- package/src/router/router-options.ts +4 -7
- package/src/router/segment-resolution/fresh.ts +6 -2
- package/src/router/segment-resolution/helpers.ts +7 -1
- package/src/router/segment-resolution/prefetch-deferral.ts +62 -12
- package/src/router/segment-resolution/revalidation.ts +75 -35
- package/src/router/segment-resolution.ts +0 -1
- package/src/router/segment-wrappers.ts +8 -13
- package/src/router/substitute-pattern-params.ts +3 -2
- package/src/router/tracing.ts +2 -2
- package/src/router.ts +2 -9
- package/src/rsc/full-payload.ts +26 -4
- package/src/rsc/handler.ts +83 -41
- package/src/rsc/helpers.ts +13 -0
- package/src/rsc/manifest-init.ts +56 -3
- package/src/rsc/prefetch-render-error.ts +38 -0
- package/src/rsc/progressive-enhancement.ts +161 -211
- package/src/rsc/rewrite-passes.ts +56 -9
- package/src/rsc/rsc-rendering.ts +79 -23
- package/src/rsc/server-action.ts +25 -2
- package/src/rsc/shell-capture-constants.ts +22 -0
- package/src/rsc/shell-capture.ts +11 -0
- package/src/rsc/types.ts +5 -0
- package/src/segment-system.tsx +125 -119
- package/src/server/context.ts +96 -5
- package/src/server/handle-store.ts +3 -2
- package/src/server/request-context.ts +42 -3
- package/src/server/request-headers.ts +32 -0
- package/src/server/rewrite-trail.ts +55 -9
- package/src/ssr/ssr-root.tsx +6 -10
- package/src/testing/dispatch.ts +4 -5
- package/src/testing/e2e/index.ts +1 -0
- package/src/testing/flight.entry.ts +1 -0
- package/src/testing/index.ts +1 -0
- package/src/testing/internal/client-urls-projection.ts +31 -11
- package/src/testing/render-route.tsx +83 -40
- package/src/testing/run-loader.ts +26 -2
- package/src/testing/run-middleware.ts +10 -0
- package/src/testing/run-revalidate.ts +13 -6
- package/src/testing/serve-shell-request.ts +77 -4
- package/src/testing/shell-status.ts +40 -0
- package/src/testing/vitest-stubs/version.ts +1 -0
- package/src/testing/vitest.ts +74 -7
- package/src/thenable-status.ts +0 -11
- package/src/types/handler-context.ts +12 -1
- package/src/types/loader-types.ts +4 -3
- package/src/types/route-entry.ts +2 -1
- package/src/urls/include-helper.ts +38 -11
- package/src/urls/path-helper-types.ts +4 -2
- package/src/vite/discovery/client-urls-projection.ts +205 -16
- package/src/vite/discovery/discover-routers.ts +2 -2
- package/src/vite/discovery/prerender-collection.ts +2 -6
- package/src/vite/discovery/state.ts +26 -0
- package/src/vite/plugins/client-urls-revalidate-hoist.ts +190 -77
- package/src/vite/plugins/expose-internal-ids.ts +46 -40
- package/src/vite/plugins/hoist/source.ts +8 -7
- package/src/vite/plugins/transition-when-hoist.ts +1 -0
- package/src/vite/plugins/version-plugin.ts +7 -0
- package/src/vite/plugins/version.d.ts +7 -0
- package/src/vite/plugins/virtual-entries.ts +20 -0
- package/src/vite/rango.ts +4 -1
- package/src/vite/router-discovery.ts +39 -7
- package/src/vite/utils/prerender-utils.ts +17 -0
|
@@ -0,0 +1,280 @@
|
|
|
1
|
+
# Several apps under createHostRouter()
|
|
2
|
+
|
|
3
|
+
One deployment, several independently routed apps: marketing on the apex
|
|
4
|
+
(`example.com`, `www.example.com`), the product on `app.`, the staff tools
|
|
5
|
+
on `admin.`, the docs on `docs.`. Each app is an ordinary `createRouter()`
|
|
6
|
+
app with its own `urls()`, Document and generated route map; the host router
|
|
7
|
+
only picks which one handles a request.
|
|
8
|
+
|
|
9
|
+
Reach for this when parts of the product want different documents, chrome,
|
|
10
|
+
deploy cadence or hostnames. Sections that share one document belong in one
|
|
11
|
+
router as `include()`s ([`./large-app.md`](./large-app.md)): a navigation
|
|
12
|
+
between apps is always a document load.
|
|
13
|
+
|
|
14
|
+
References in the router repository: `examples/cloudflare-multi-router` (a
|
|
15
|
+
Cloudflare host entry, path-mounted and host-mounted apps, a shared cache)
|
|
16
|
+
and `packages/rangojs-router/e2e/test-app/.host-fixture` (the Node preset,
|
|
17
|
+
the host-override cookie, two apps on one store).
|
|
18
|
+
|
|
19
|
+
## File tree
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
src/
|
|
23
|
+
worker.rsc.tsx the host router (Cloudflare: you own the entry)
|
|
24
|
+
env.ts bindings shared by every app
|
|
25
|
+
shared/
|
|
26
|
+
document.tsx "use client" Document the apps pass to createRouter()
|
|
27
|
+
styles.css one stylesheet (e.g. @import "tailwindcss")
|
|
28
|
+
theme.ts one ThemeConfig
|
|
29
|
+
cache.ts one cache store factory
|
|
30
|
+
origins.ts each app's origin, per environment
|
|
31
|
+
top-nav.tsx components every app renders
|
|
32
|
+
apps/
|
|
33
|
+
site/ router.tsx handler.ts urls.tsx router.named-routes.gen.ts tsconfig.json
|
|
34
|
+
app/ router.tsx handler.ts urls.tsx router.named-routes.gen.ts tsconfig.json …
|
|
35
|
+
admin/ …
|
|
36
|
+
docs/ …
|
|
37
|
+
tsconfig.json the host entry's program only
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
## The host router
|
|
41
|
+
|
|
42
|
+
```tsx
|
|
43
|
+
// src/worker.rsc.tsx
|
|
44
|
+
import {
|
|
45
|
+
createHostRouter,
|
|
46
|
+
defineHosts,
|
|
47
|
+
isNoRouteMatchError,
|
|
48
|
+
} from "@rangojs/router/host";
|
|
49
|
+
import type { AppBindings } from "./env";
|
|
50
|
+
|
|
51
|
+
export const hosts = defineHosts({
|
|
52
|
+
site: [".", "www.*"],
|
|
53
|
+
app: "app.*",
|
|
54
|
+
admin: "admin.*",
|
|
55
|
+
docs: "docs.*",
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
// Dev: every app answers on localhost, and the x-rango-host cookie picks one
|
|
59
|
+
// (x-rango-host=admin.example.com). The request is forwarded unchanged.
|
|
60
|
+
const hostRouter = createHostRouter({
|
|
61
|
+
hostOverride: { cookieName: "x-rango-host", allowedHosts: ["localhost"] },
|
|
62
|
+
});
|
|
63
|
+
|
|
64
|
+
// Lazy mounts: an app's module graph loads on its first request, and each app
|
|
65
|
+
// keeps its own cache version.
|
|
66
|
+
hostRouter.host(hosts.admin).lazy(() => import("./apps/admin/handler"));
|
|
67
|
+
hostRouter.host(hosts.app).lazy(() => import("./apps/app/handler"));
|
|
68
|
+
hostRouter.host(hosts.docs).lazy(() => import("./apps/docs/handler"));
|
|
69
|
+
hostRouter.host(hosts.site).lazy(() => import("./apps/site/handler"));
|
|
70
|
+
// localhost has one label: neither "." nor "*." matches it.
|
|
71
|
+
hostRouter.host(["localhost"]).lazy(() => import("./apps/site/handler"));
|
|
72
|
+
|
|
73
|
+
export default {
|
|
74
|
+
async fetch(request, env, ctx) {
|
|
75
|
+
try {
|
|
76
|
+
return await hostRouter.match(request, { env, ctx });
|
|
77
|
+
} catch (err) {
|
|
78
|
+
if (isNoRouteMatchError(err))
|
|
79
|
+
return new Response("Not Found", { status: 404 });
|
|
80
|
+
throw err;
|
|
81
|
+
}
|
|
82
|
+
},
|
|
83
|
+
} satisfies ExportedHandler<AppBindings>;
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
Patterns are tried in registration order, first match wins; "apex" means
|
|
87
|
+
exactly two labels (no public-suffix list), so `example.co.uk` needs its own
|
|
88
|
+
pattern (`/host-router` → "Pattern Syntax").
|
|
89
|
+
|
|
90
|
+
On the `node` and `vercel` presets rango owns the entry: export the instance
|
|
91
|
+
(`export default hostRouter`), drop the `fetch` wrapper, and an unmatched host
|
|
92
|
+
answers 404 on its own. Point `rango({ hostRouter: "./src/worker.rsc.tsx" })`
|
|
93
|
+
at the module when auto-detection cannot pick it.
|
|
94
|
+
|
|
95
|
+
## Each app
|
|
96
|
+
|
|
97
|
+
```ts
|
|
98
|
+
// src/apps/app/handler.ts: what the host router's lazy mount imports
|
|
99
|
+
import { router } from "./router";
|
|
100
|
+
|
|
101
|
+
export default (request: Request, input: any) => router.fetch(request, input);
|
|
102
|
+
```
|
|
103
|
+
|
|
104
|
+
```tsx
|
|
105
|
+
// src/apps/app/router.tsx
|
|
106
|
+
import { createRouter } from "@rangojs/router";
|
|
107
|
+
import { Document } from "../../shared/document";
|
|
108
|
+
import { appCache } from "../../shared/cache";
|
|
109
|
+
import { theme } from "../../shared/theme";
|
|
110
|
+
import type { AppBindings } from "../../env";
|
|
111
|
+
import { loadSession } from "./session";
|
|
112
|
+
import { appPatterns } from "./urls";
|
|
113
|
+
|
|
114
|
+
export const router = createRouter<AppBindings>({
|
|
115
|
+
document: Document,
|
|
116
|
+
cache: appCache,
|
|
117
|
+
theme,
|
|
118
|
+
})
|
|
119
|
+
.use(loadSession)
|
|
120
|
+
.routes(appPatterns);
|
|
121
|
+
```
|
|
122
|
+
|
|
123
|
+
```tsx
|
|
124
|
+
// src/apps/app/urls.tsx
|
|
125
|
+
import { urls } from "@rangojs/router";
|
|
126
|
+
import { Outlet } from "@rangojs/router/client";
|
|
127
|
+
import { TopNav } from "../../shared/top-nav";
|
|
128
|
+
import { DashboardPage, LoginPage } from "./pages";
|
|
129
|
+
import { requireUser } from "./session";
|
|
130
|
+
|
|
131
|
+
function Shell() {
|
|
132
|
+
return (
|
|
133
|
+
<>
|
|
134
|
+
<TopNav />
|
|
135
|
+
<Outlet />
|
|
136
|
+
</>
|
|
137
|
+
);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
export const appPatterns = urls(({ path, layout, middleware }) => [
|
|
141
|
+
path("/login", LoginPage, { name: "login" }),
|
|
142
|
+
layout(Shell, () => [
|
|
143
|
+
middleware(requireUser),
|
|
144
|
+
path("/", DashboardPage, { name: "dashboard" }),
|
|
145
|
+
// … the app's sections as includes, as in ./large-app.md
|
|
146
|
+
]),
|
|
147
|
+
]);
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
Inside an app, everything in the other recipes applies unchanged: the `app.`
|
|
151
|
+
app is [`./large-app.md`](./large-app.md) without its marketing pages, the
|
|
152
|
+
`docs.` app is [`./content-site.md`](./content-site.md), and `site` is a
|
|
153
|
+
handful of `ppr` pages:
|
|
154
|
+
|
|
155
|
+
```tsx
|
|
156
|
+
// src/apps/site/urls.tsx (excerpt)
|
|
157
|
+
export const sitePatterns = urls(({ path, layout }) => [
|
|
158
|
+
layout(SiteLayout, () => [
|
|
159
|
+
path("/", Home, { name: "home", ppr: true }),
|
|
160
|
+
path("/pricing", Pricing, { name: "pricing", ppr: true }),
|
|
161
|
+
]),
|
|
162
|
+
]);
|
|
163
|
+
```
|
|
164
|
+
|
|
165
|
+
## What is shared, and how
|
|
166
|
+
|
|
167
|
+
| Shared | How | Notes |
|
|
168
|
+
| ------------- | ---------------------------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
169
|
+
| Components | ordinary imports from `src/shared/` | One Vite build and one client manifest: a shared client component ships as the same hashed asset to every app. Keep route-name-typed calls (`ctx.reverse("…")`, `href()`) out of shared code; each app's names are its own. |
|
|
170
|
+
| The Document | one `"use client"` component every `createRouter({ document })` passes, or one per app | Each document load is that app's own; apps may differ freely |
|
|
171
|
+
| CSS | `import styles from "../../shared/styles.css?url"` in the Document, `<link precedence>` | Compiles to one hashed asset, cached across apps. Apps need not agree on `precedence`: a cross-app navigation is a document load (`/css`) |
|
|
172
|
+
| Theme | one `ThemeConfig` object passed as `theme` to every router | The stored choice is a host-only cookie (`src/theme/ThemeProvider.tsx`, no `Domain`), so a theme picked on `app.` does not follow the user to `docs.` |
|
|
173
|
+
| Cache store | one factory (`cache: appCache`), one KV namespace | Keys holding one router's output start with that router's id, so two apps never read each other's shells, records or loader entries. Keep `createRouter({ ... })` an inline object literal (or pass `id`) so ids are stable |
|
|
174
|
+
| Session | a cookie on the parent domain, `cookies().set("session", token, { domain: "example.com", … })` | Set by the app that signs users in; every app resolves it in its own `router.use()` |
|
|
175
|
+
| Client caches | not shared | The state cookie is `rango-state_{routerId}`, one per app, so sibling apps on one origin do not collide |
|
|
176
|
+
|
|
177
|
+
## TypeScript: one program per app
|
|
178
|
+
|
|
179
|
+
Every router's `router.named-routes.gen.ts` augments the one global
|
|
180
|
+
`Rango.GeneratedRouteMap`, so two apps with overlapping route names
|
|
181
|
+
(`home`, `login`) collide in one program. Give each app its own
|
|
182
|
+
`tsconfig.json`, and keep the root program to the host entry:
|
|
183
|
+
|
|
184
|
+
```jsonc
|
|
185
|
+
// tsconfig.json: the host entry only
|
|
186
|
+
{ "extends": "./tsconfig.base.json", "include": ["src/env.ts", "src/worker.rsc.tsx"] }
|
|
187
|
+
|
|
188
|
+
// src/apps/app/tsconfig.json: one app, its generated map, the shared code
|
|
189
|
+
{ "extends": "../../../tsconfig.base.json", "include": ["**/*", "../../shared/**/*", "../../env.ts"] }
|
|
190
|
+
```
|
|
191
|
+
|
|
192
|
+
The root program still type-checks the apps' source through the lazy
|
|
193
|
+
`import()`s, but not their generated maps, as long as no app source imports
|
|
194
|
+
its own `*.named-routes.gen.ts`. Run `tsc -p` for each program in CI.
|
|
195
|
+
Generated and committed: one `router.named-routes.gen.ts` per app (plus
|
|
196
|
+
per-module `*.gen.ts` where an app uses `useReverse(routes)`). See
|
|
197
|
+
`/typesafety` → multi-project setup.
|
|
198
|
+
|
|
199
|
+
## Dev: the host-override cookie
|
|
200
|
+
|
|
201
|
+
`vite dev` serves every app on one origin, `localhost`. The
|
|
202
|
+
`hostOverride` option picks the app from a cookie when the request's real
|
|
203
|
+
host matches `allowedHosts`:
|
|
204
|
+
|
|
205
|
+
```js
|
|
206
|
+
// browser devtools, on http://localhost:5173
|
|
207
|
+
document.cookie = "x-rango-host=admin.example.com; path=/";
|
|
208
|
+
```
|
|
209
|
+
|
|
210
|
+
Reload and the admin app answers; delete the cookie for the site. Without a
|
|
211
|
+
`fallback()`, a cookie the router refuses (a malformed hostname, a host not
|
|
212
|
+
in `allowedHosts`) answers 400 and clears the cookie. Playwright sets it with
|
|
213
|
+
`context.addCookies([{ name: "x-rango-host", value: "admin.example.com", url }])`,
|
|
214
|
+
which is how both reference apps run their dev and production e2e.
|
|
215
|
+
|
|
216
|
+
All apps answer under one origin in this mode, so links to another app's
|
|
217
|
+
production host leave the dev server; switch apps by changing the cookie.
|
|
218
|
+
|
|
219
|
+
## Links between apps
|
|
220
|
+
|
|
221
|
+
A navigation from one app to another is a full document load, by design. A
|
|
222
|
+
soft in-place swap between apps shipped once (#476) and was replaced by a
|
|
223
|
+
document navigation (#555): a stylesheet two apps share by `href` could drop
|
|
224
|
+
on the swap (React dedups stylesheets by `href`), and the theme, warmup and
|
|
225
|
+
prefetch settings, which are document-lifetime, would remount or never take
|
|
226
|
+
effect. A document load re-establishes the target app's whole document.
|
|
227
|
+
|
|
228
|
+
| The other app is | Link with | What happens |
|
|
229
|
+
| -------------------------------------------------------------- | ------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
|
|
230
|
+
| On another host (`app.example.com`) | an absolute URL: `<Link to="https://app.example.com/">` or `<a href>` | Another origin is external to `<Link>` (`src/browser/react/Link.tsx`) and to the plain-anchor interceptor: the browser navigates, nothing is prefetched |
|
|
231
|
+
| On the same origin under a path (`host(["example.com/docs"])`) | `<Link to="/docs" reloadDocument prefetch="none">` or `<a href="/docs" data-no-intercept="true">` | A document navigation at the click. A plain same-origin `<Link>` also ends in a document load, after a wasted round trip: the server answers the partial request with `X-RSC-Reload` |
|
|
232
|
+
|
|
233
|
+
```tsx
|
|
234
|
+
// src/shared/origins.ts
|
|
235
|
+
export const ORIGINS = {
|
|
236
|
+
site: import.meta.env.VITE_SITE_ORIGIN ?? "https://example.com",
|
|
237
|
+
app: import.meta.env.VITE_APP_ORIGIN ?? "https://app.example.com",
|
|
238
|
+
docs: import.meta.env.VITE_DOCS_ORIGIN ?? "https://docs.example.com",
|
|
239
|
+
} as const;
|
|
240
|
+
|
|
241
|
+
// src/shared/top-nav.tsx: a server component every app renders
|
|
242
|
+
import { Link } from "@rangojs/router/client";
|
|
243
|
+
import { ORIGINS } from "./origins";
|
|
244
|
+
|
|
245
|
+
export function TopNav() {
|
|
246
|
+
return (
|
|
247
|
+
<nav>
|
|
248
|
+
<Link to={`${ORIGINS.site}/`}>Home</Link>
|
|
249
|
+
<Link to={`${ORIGINS.docs}/`}>Docs</Link>
|
|
250
|
+
<Link to={`${ORIGINS.app}/`}>Open the app</Link>
|
|
251
|
+
</nav>
|
|
252
|
+
);
|
|
253
|
+
}
|
|
254
|
+
```
|
|
255
|
+
|
|
256
|
+
`reloadDocument` alone does not stop the prefetch: without
|
|
257
|
+
`prefetch="none"`, a visible link still sends one, which the other app
|
|
258
|
+
answers with `X-RSC-Reload` and the client drops. Make each app's entry pages
|
|
259
|
+
fast as documents (`ppr`, or `Prerender`), since every cross-app click lands
|
|
260
|
+
on one.
|
|
261
|
+
|
|
262
|
+
## Deploys
|
|
263
|
+
|
|
264
|
+
Each `createRouter()` app has its own cache version, computed from the code
|
|
265
|
+
its router can run up to a lazy mount. A deploy that changes the admin app's
|
|
266
|
+
server code clears the admin app's cached data and stored HTML and reloads
|
|
267
|
+
its open tabs; the other apps keep theirs. Code shared by statically
|
|
268
|
+
importing modules moves every app that imports it (`/host-router` →
|
|
269
|
+
"Deploys and the cache").
|
|
270
|
+
|
|
271
|
+
## Why
|
|
272
|
+
|
|
273
|
+
| Choice | Why |
|
|
274
|
+
| ---------------------------- | --------------------------------------------------------------------------------------------- |
|
|
275
|
+
| One `createRouter()` per app | Own document, routes, names, middleware, cache version |
|
|
276
|
+
| `.lazy()` mounts | An app's graph loads on its first request, and the mount is where its cache version stops |
|
|
277
|
+
| `defineHosts` | One typed table of patterns, reused by tests |
|
|
278
|
+
| The cookie override in dev | One dev origin, hydration same-origin; the reference apps' e2e drive every app through it |
|
|
279
|
+
| Absolute URLs between hosts | Another origin is another document; no request is wasted on a prefetch or a reload round trip |
|
|
280
|
+
| A program per app | Route-name augmentations of different apps cannot collide |
|
|
@@ -137,6 +137,11 @@ path("/product/:id", async (ctx) => {
|
|
|
137
137
|
Async content is a `Promise<ReactNode>`. Resolve it in your component
|
|
138
138
|
with React's `use()` hook wrapped in `<Suspense>`.
|
|
139
139
|
|
|
140
|
+
A server component that pushes a crumb after an `await` throws
|
|
141
|
+
`LateHandlePushError`. Reserve the crumb in the handler with `.defer()` and
|
|
142
|
+
resolve it from the component (see "Deferred content" below); the same works
|
|
143
|
+
for a `Meta` title.
|
|
144
|
+
|
|
140
145
|
A crumb the HANDLER pushes is handler output. In a route `cache()` entry and
|
|
141
146
|
in a `ppr` shell it is stored resolved: the cache write, or the shell capture
|
|
142
147
|
(within `ppr.captureTimeout`), waits for a `content` promise and for a
|
|
@@ -202,7 +207,7 @@ consumer sees it — `useHandle(Breadcrumbs)` returns the resolved item, never a
|
|
|
202
207
|
`Promise`, so you read it like any sync crumb (no `use()`, no thenable narrowing).
|
|
203
208
|
On a full/SSR load the value is resolved server-side; on a soft navigation the
|
|
204
209
|
breadcrumbs HOLD the previous resolved value until the deferred value lands, then
|
|
205
|
-
swap in — no blank, no pending entry. If the slot times out to `else: null`/
|
|
210
|
+
swap in — no blank, no pending entry. The hold-then-swap is deliberate: a handle never holds the page and never shows the next page's values over the old one (see [hooks](../hooks/handle-and-actions.md#when-handle-values-change-by-design)). If the slot times out to `else: null`/
|
|
206
211
|
undefined, the entry is simply dropped. Use `.defer()` only when even
|
|
207
212
|
`label`/`href` are unknown at handler time AND the producer is a component,
|
|
208
213
|
not a loader — a loader-produced crumb pushes directly from the loader body
|
package/skills/caching/SKILL.md
CHANGED
|
@@ -894,7 +894,9 @@ and a read that worked only on direct visits would break the first time a
|
|
|
894
894
|
rewritten request reached it. A PPR shell capture and a render the document
|
|
895
895
|
cache stores read it as `undefined` instead of throwing: those renders are
|
|
896
896
|
the target page as a direct visit renders it, and the rewritten visitor's
|
|
897
|
-
own render (outside them) still sees its URL. A
|
|
897
|
+
own render (outside them) still sees its URL. A shell keyed by the visitor's
|
|
898
|
+
URL (`rewrite(...).shell("visitor")`, `/ppr`) reads it, since its key covers
|
|
899
|
+
that URL. A response route under
|
|
898
900
|
`cache()` whose handler read it is not stored.
|
|
899
901
|
|
|
900
902
|
A loader body invoked from a handler with `await ctx.use(Loader)` may read
|
package/skills/catalog.json
CHANGED
|
@@ -9,6 +9,12 @@
|
|
|
9
9
|
"argumentHint": "",
|
|
10
10
|
"path": "skills/api-client/SKILL.md"
|
|
11
11
|
},
|
|
12
|
+
{
|
|
13
|
+
"name": "app-structure",
|
|
14
|
+
"description": "Concrete urls() layouts for whole Rango apps, with the file tree, router.tsx, the root urls.tsx with sync include() of section modules, and where middleware, server layouts, clientUrls() groups, response routes, caching, prerender, PPR and boundaries sit. Recipes for a large signed-in app with a protected admin area (auth, onboarding, app, settings and admin as clientUrls() groups under one server shell), several apps under createHostRouter(), a docs site or blog, and a shop. Use when starting or restructuring a Rango app, laying out urls() files, splitting an app into sections or separate apps, or setting up a dashboard, SaaS, multi-app, static or content site, or e-commerce app.",
|
|
15
|
+
"argumentHint": "[large-app|multi-app|content-site|e-commerce]",
|
|
16
|
+
"path": "skills/app-structure/SKILL.md"
|
|
17
|
+
},
|
|
12
18
|
{
|
|
13
19
|
"name": "breadcrumbs",
|
|
14
20
|
"description": "Built-in Breadcrumbs handle for accumulating breadcrumb navigation across route segments. Use when building a breadcrumb trail for nested routes, or asking how to show the current navigation path in a layout.",
|
|
@@ -273,6 +279,12 @@
|
|
|
273
279
|
"argumentHint": "[profile-name]",
|
|
274
280
|
"path": "skills/use-cache/SKILL.md"
|
|
275
281
|
},
|
|
282
|
+
{
|
|
283
|
+
"name": "ux-architecture",
|
|
284
|
+
"description": "Architect a Rango app so navigation is instant and stable — when a server urls() page or a clientUrls() group wins, where layouts, loaders, Suspense boundaries, revalidate() rules, prefetch, transition(), intercept() modals and actions go, with worked recipes (shop, dashboard tabs, settings forms, search filters, a modal over a list) and the anti-patterns behind remounts, skeleton flashes and slow clicks. Use when designing or reviewing a Rango app's routing, layout, loading or navigation UX, choosing between urls() and clientUrls(), placing a loader or a boundary, writing revalidate() rules, or debugging a remount, a re-shown fallback or a click that waits.",
|
|
285
|
+
"argumentHint": "[shop|dashboard|settings|search|modal]",
|
|
286
|
+
"path": "skills/ux-architecture/SKILL.md"
|
|
287
|
+
},
|
|
276
288
|
{
|
|
277
289
|
"name": "vercel",
|
|
278
290
|
"description": "Deploy a Rango app to Vercel Functions (Build Output API v3). Use when deploying a Rango app to Vercel, or asking how the router works with Vercel Functions and the Build Output API.",
|
|
@@ -142,6 +142,85 @@ At the top level of the builder only `path()`, `layout()` and `intercept()`
|
|
|
142
142
|
are accepted, and the builder must define at least one `path()`. Any helper
|
|
143
143
|
name not in this table throws (`clientUrls() has no route() helper`).
|
|
144
144
|
|
|
145
|
+
`loader`, `loading`, `revalidate` and `transition` are also exported from
|
|
146
|
+
`@rangojs/router/client`, the same functions the builder hands out, for a
|
|
147
|
+
component's own items (next section).
|
|
148
|
+
|
|
149
|
+
## A component's own items: `Component.use`
|
|
150
|
+
|
|
151
|
+
A page, layout or intercept component can carry its own items, as a server
|
|
152
|
+
handler does with `.use` (`/handler-use`): its loaders, its skeleton and its
|
|
153
|
+
revalidation rules travel with it instead of being repeated at every `path()`
|
|
154
|
+
that mounts it. Build them with the helpers from `@rangojs/router/client`. The
|
|
155
|
+
component's module needs no `clientUrls()`, and no `"use client"` of its own
|
|
156
|
+
when a group imports it.
|
|
157
|
+
|
|
158
|
+
```tsx
|
|
159
|
+
// src/dash/Dashboard.tsx
|
|
160
|
+
"use client";
|
|
161
|
+
import { loader, loading, revalidate, useLoader } from "@rangojs/router/client";
|
|
162
|
+
import * as StatsActions from "./stats.actions";
|
|
163
|
+
import { StatsLoader } from "./stats.loader";
|
|
164
|
+
|
|
165
|
+
export function Dashboard() {
|
|
166
|
+
const { data } = useLoader(StatsLoader);
|
|
167
|
+
return <StatsView stats={data} />;
|
|
168
|
+
}
|
|
169
|
+
Dashboard.use = () => [
|
|
170
|
+
loader(StatsLoader, () => [
|
|
171
|
+
revalidate(({ isAction }) => isAction(StatsActions)),
|
|
172
|
+
]),
|
|
173
|
+
loading(<DashboardSkeleton />),
|
|
174
|
+
];
|
|
175
|
+
```
|
|
176
|
+
|
|
177
|
+
```tsx
|
|
178
|
+
// src/urls/app.client.tsx
|
|
179
|
+
"use client";
|
|
180
|
+
import { clientUrls } from "@rangojs/router/client";
|
|
181
|
+
import { Dashboard } from "../dash/Dashboard";
|
|
182
|
+
|
|
183
|
+
export default clientUrls(({ path }) => [
|
|
184
|
+
path("/dash", Dashboard, { name: "dash" }),
|
|
185
|
+
]);
|
|
186
|
+
```
|
|
187
|
+
|
|
188
|
+
| Component mounted by | Its `.use` may return |
|
|
189
|
+
| -------------------- | ---------------------------------------------- |
|
|
190
|
+
| `path()` | `loader()`, `loading()`, `transition()` |
|
|
191
|
+
| `layout()` | `loader()`, `loading()` |
|
|
192
|
+
| `intercept()` | `loader()` (without `ssr: false`), `loading()` |
|
|
193
|
+
|
|
194
|
+
- **Merge order is the server's.** The component's items come first and the
|
|
195
|
+
mount site's own items second: an explicit `loading()` or `transition()`
|
|
196
|
+
wins, loaders accumulate (the same definition in both gives two loaders, as
|
|
197
|
+
`loader(Def)` twice does). A layout component's loaders are layout loaders,
|
|
198
|
+
shared by every route below it.
|
|
199
|
+
- **No routes inside `.use`.** `path()`, `layout()` and `intercept()` there
|
|
200
|
+
throw (`clientUrls() does not support path() inside Dashboard.use`): route
|
|
201
|
+
types and the generated `.gen.ts` are read from the builder's own items.
|
|
202
|
+
Unlike a server layout handler's `.use`, a client layout's cannot add
|
|
203
|
+
routes.
|
|
204
|
+
- **`.use` runs when the group is defined**, once per mount site (in the
|
|
205
|
+
browser, during SSR and in discovery), never at render. The error for an
|
|
206
|
+
item the site does not take names the component.
|
|
207
|
+
- **A predicate in `.use` runs on the server,** as one in the builder does:
|
|
208
|
+
the build hoists it out of the component's module with the rules below,
|
|
209
|
+
and it registers when a group whose routes use the component loads on the
|
|
210
|
+
server, wherever the module sits: behind barrels or in another workspace
|
|
211
|
+
package. A predicate in `node_modules` is not hoisted,
|
|
212
|
+
and route discovery fails naming its loader.
|
|
213
|
+
- **Import the helpers from `@rangojs/router/client`.** The same names from
|
|
214
|
+
`@rangojs/router` are the server DSL's; called in a client module they
|
|
215
|
+
throw, pointing to `@rangojs/router/client`.
|
|
216
|
+
- In dev, an edit of a component's `.use` refreshes its groups' projections
|
|
217
|
+
and predicates, as an edit of the group does.
|
|
218
|
+
- Under `isolatedDeclarations` (a library's sources), an exported function
|
|
219
|
+
cannot take an assigned property: declare the component unexported,
|
|
220
|
+
assign `.use`, and export it through a const typed `ClientUrlComponent`.
|
|
221
|
+
- A `createLoader()` definition's own `.use` (`Def.use`) is not read inside
|
|
222
|
+
`clientUrls()` yet: put its `revalidate()` on the `loader()` call.
|
|
223
|
+
|
|
145
224
|
`include`, `parallel`, `cache`, `middleware`, `errorBoundary`,
|
|
146
225
|
`notFoundBoundary` **throw** — and that is a design position, not a gap.
|
|
147
226
|
Client route groups are a PERFORMANCE surface (instant presentation, held
|
|
@@ -201,10 +280,11 @@ the arguments and type of a `urls()` loader's (`ShouldRevalidateFn` from
|
|
|
201
280
|
browser before the request goes out; there is no client-computed decision and
|
|
202
281
|
no hybrid.
|
|
203
282
|
|
|
204
|
-
You still write the predicate in the `"use client"` module
|
|
205
|
-
plugin hoists each `revalidate()` argument
|
|
206
|
-
key in the browser and SSR copies, and
|
|
207
|
-
group's module loads on the server
|
|
283
|
+
You still write the predicate in the `"use client"` module, or in a
|
|
284
|
+
component's `.use`. The rango Vite plugin hoists each `revalidate()` argument
|
|
285
|
+
into a server-only module, leaves a key in the browser and SSR copies, and
|
|
286
|
+
registers the predicate when the group's module loads on the server, with
|
|
287
|
+
those of the modules it imports (a lazy `include()` stays lazy). The cost
|
|
208
288
|
is a rule: **the argument may reference imports and globals only.** Anything
|
|
209
289
|
else is a build error naming `file:line:column`, starting
|
|
210
290
|
`[rango] clientUrls() revalidate() at ...: revalidate() runs on the server; ...`.
|
|
@@ -213,6 +293,14 @@ else is a build error naming `file:line:column`, starting
|
|
|
213
293
|
(`revalidate(isCartAction)`), a namespace member (`P.cart`), a call of an
|
|
214
294
|
imported factory (`makePredicate("cart")`), and globals such as `URL`,
|
|
215
295
|
`Boolean` and `import.meta.env`.
|
|
296
|
+
- `revalidate(true)` / `revalidate(false)`, shorthand for `() => true` /
|
|
297
|
+
`() => false`: a boolean LITERAL stays in place (nothing to hoist). Any
|
|
298
|
+
other argument is hoisted whatever it evaluates to, because the build cannot
|
|
299
|
+
tell an imported boolean from an imported predicate: the server evaluates
|
|
300
|
+
it, and a boolean result is the same constant answer
|
|
301
|
+
(`revalidate(HOLD_SESSION)` with an imported flag works). A value that is
|
|
302
|
+
neither fails when the server mounts the group, naming the route and
|
|
303
|
+
loader.
|
|
216
304
|
- A binding declared in the same `"use client"` module is rejected: move the
|
|
217
305
|
predicate to its own module without `"use client"` and import it, or write it
|
|
218
306
|
inline. A binding of an enclosing function is rejected: read it from the
|
|
@@ -223,7 +311,8 @@ else is a build error naming `file:line:column`, starting
|
|
|
223
311
|
request (a cookie or a search param) and read it from the argument, or
|
|
224
312
|
refresh with `useRefreshLoaders`.
|
|
225
313
|
- An `async` or generator function, a spread, a second argument, no argument,
|
|
226
|
-
or a
|
|
314
|
+
or a literal that is neither a function nor a boolean (object, array,
|
|
315
|
+
string, number or `null`) is rejected.
|
|
227
316
|
- An import of `client-only`, or of a module whose directive prologue is
|
|
228
317
|
`"use client"`, is rejected (on the server its exports are client
|
|
229
318
|
references). A `"use server"` import is fine: on the server it is the real
|
|
@@ -273,10 +362,10 @@ with the ternary above instead (same rule as a server-tree loader). A bare
|
|
|
273
362
|
`isAction(refs)` is a hard `false` on navigation too, so the loader stops
|
|
274
363
|
refetching when params change.
|
|
275
364
|
|
|
276
|
-
`intercept()` loaders inside `clientUrls()`
|
|
277
|
-
|
|
278
|
-
|
|
279
|
-
|
|
365
|
+
`intercept()` loaders inside `clientUrls()` take `revalidate()` the same way:
|
|
366
|
+
the build moves the predicate to the server, and it runs there with the same
|
|
367
|
+
arguments, `actionResult` included, so an action from the modal re-runs only
|
|
368
|
+
the loaders whose predicate says so.
|
|
280
369
|
|
|
281
370
|
A group `layout()` loader revalidates as a server layout's loader does. With no
|
|
282
371
|
`revalidate()` a navigation keeps it, whichever route of the layout it goes to
|
|
@@ -292,7 +381,7 @@ currentParams.slug !== nextParams.slug ? true : defaultShouldRevalidate)`.
|
|
|
292
381
|
|
|
293
382
|
Two scars worth copying:
|
|
294
383
|
|
|
295
|
-
- A blunt `()
|
|
384
|
+
- A blunt `revalidate(false)` keeps serving the OLD product on product→product
|
|
296
385
|
navigations (same route, new param). Make predicates param-sensitive:
|
|
297
386
|
return `defaultShouldRevalidate` when the identifying param changed.
|
|
298
387
|
- One action, per-loader outcomes: a cart badge loader revalidates on cart
|
|
@@ -312,8 +401,8 @@ Everything after that is decided on the server, while it handles that request:
|
|
|
312
401
|
- **The browser's caches.** When the response arrives the browser marks its
|
|
313
402
|
history cache stale and drops its prefetches, unless the action called
|
|
314
403
|
`keepClientCache()` (`/server-actions` → "Client Cache After an Action").
|
|
315
|
-
- **A loader that fails on its rerun** reaches your `onError` on the server
|
|
316
|
-
the
|
|
404
|
+
- **A loader that fails on its rerun** reaches your `onError` on the server,
|
|
405
|
+
and in the browser it throws at its read, below.
|
|
317
406
|
|
|
318
407
|
A form's React state survives an action's revalidation in a group, as on a
|
|
319
408
|
`urls()` route: the commit renders into the mounted page instead of remounting
|
|
@@ -322,17 +411,79 @@ it, so a controlled input keeps what the user typed.
|
|
|
322
411
|
### A loader that fails on its rerun
|
|
323
412
|
|
|
324
413
|
The action has already run when its loaders rerun, so a loader that throws
|
|
325
|
-
there does not undo it.
|
|
414
|
+
there does not undo it. It behaves as on a navigation, on every route (a group
|
|
415
|
+
route or a `urls()` route, with or without `loading()`), and whether the
|
|
416
|
+
action was called directly or from `<form action>`, `useActionState` or
|
|
417
|
+
`useTransition`:
|
|
418
|
+
|
|
419
|
+
- **The call resolves** with the action's return value.
|
|
420
|
+
- **The loader throws in place,** at its `useLoader` read. The nearest
|
|
421
|
+
boundary to the read catches it: your own React error boundary around the
|
|
422
|
+
read, else the `errorBoundary()` around the group's `include()` mount (or
|
|
423
|
+
the route's, in `urls()`), else the router's root error boundary.
|
|
424
|
+
- **Everything outside that boundary stays,** with its React state: a form
|
|
425
|
+
next to the failed list keeps what the user typed. Loaders that reran
|
|
426
|
+
without error commit their new data in the same commit.
|
|
427
|
+
- **The error is reported**: to your `onError` on the server (phase
|
|
428
|
+
`"loader"`), and in the browser as `revalidationError` on the action's
|
|
429
|
+
`useAction()` state. `error` stays `null` there and `result` holds the
|
|
430
|
+
action's return value, so "saved, but the list could not refresh" reads
|
|
431
|
+
apart from "the save failed".
|
|
432
|
+
- **A later rerun that succeeds clears it.** An `errorBoundary()` fallback
|
|
433
|
+
clears on the next action or navigation that reruns the loader without
|
|
434
|
+
error. Your own boundary resets by its own rule: key the reset on data that
|
|
435
|
+
reruns with the loader and is read outside the boundary, so the reset lands
|
|
436
|
+
in the same render as the new data. A reset when the action settles can run
|
|
437
|
+
before React rendered the commit and catch the old failure again.
|
|
438
|
+
|
|
439
|
+
```tsx
|
|
440
|
+
// in the group's "use client" module
|
|
441
|
+
function StockPanel() {
|
|
442
|
+
// Reruns with every save and never throws.
|
|
443
|
+
const { data: status } = useLoader(SaveStatusLoader);
|
|
444
|
+
return (
|
|
445
|
+
<StockBoundary resetKey={status.version}>
|
|
446
|
+
<StockList /> {/* useLoader(StockLoader) throws here on a failed rerun */}
|
|
447
|
+
</StockBoundary>
|
|
448
|
+
);
|
|
449
|
+
}
|
|
326
450
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
451
|
+
function SaveStock({ sku }: { sku: string }) {
|
|
452
|
+
const { revalidationError } = useAction(saveStock);
|
|
453
|
+
const [stale, setStale] = useState(false);
|
|
454
|
+
// useAction resets to idle once the action has settled: keep the notice.
|
|
455
|
+
useEffect(() => {
|
|
456
|
+
if (revalidationError) setStale(true);
|
|
457
|
+
}, [revalidationError]);
|
|
458
|
+
return (
|
|
459
|
+
<form action={saveStock.bind(null, sku)}>
|
|
460
|
+
<button>Save</button>
|
|
461
|
+
{stale && (
|
|
462
|
+
<p role="status">Saved. The stock count could not be refreshed.</p>
|
|
463
|
+
)}
|
|
464
|
+
</form>
|
|
465
|
+
);
|
|
466
|
+
}
|
|
467
|
+
```
|
|
332
468
|
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
469
|
+
`StockBoundary` is an ordinary React error boundary whose
|
|
470
|
+
`getDerivedStateFromProps` clears its error when `resetKey` changes. A loader
|
|
471
|
+
that throws `notFound()` or `redirect()` is not a failure and keeps its path:
|
|
472
|
+
a `redirect()` (a deleted record's page) replaces the route's content and
|
|
473
|
+
navigates, and the layouts above keep their instance. A no-JS form post
|
|
474
|
+
renders the whole document after the action, so a loader that throws there
|
|
475
|
+
behaves as on a page load. Every refetch the router runs for an action
|
|
476
|
+
follows the same rule (after several actions at once, after a navigation
|
|
477
|
+
during it), and the action that ran it reports the error as its
|
|
478
|
+
`revalidationError`; with several actions at once, that is the one whose
|
|
479
|
+
refetch covers them all. Not covered yet: an `intercept()`'s loaders, and a
|
|
480
|
+
parallel slot's loaders under its own `loading()`, decoded above the slot's
|
|
481
|
+
content, so a boundary inside the slot does not see the error.
|
|
482
|
+
|
|
483
|
+
For a failure the user should see in place of the data without an error
|
|
484
|
+
boundary (a flaky upstream, a
|
|
485
|
+
rate limit), return it as data instead of throwing, and render it where the
|
|
486
|
+
data goes:
|
|
336
487
|
|
|
337
488
|
```tsx
|
|
338
489
|
// stock.loader.ts
|
|
@@ -355,10 +506,7 @@ function Stock() {
|
|
|
355
506
|
}
|
|
356
507
|
```
|
|
357
508
|
|
|
358
|
-
The action
|
|
359
|
-
next action or navigation that reruns the loader replaces the message. Keep
|
|
360
|
-
throwing for a failure that should stop the page, with an `errorBoundary()`
|
|
361
|
-
around the mount for it.
|
|
509
|
+
The next action or navigation that reruns the loader replaces the message.
|
|
362
510
|
|
|
363
511
|
A failure returned as data is an ordinary value, so whatever stores the
|
|
364
512
|
loader's value stores it too: a `cache()` on the loader serves it for its TTL,
|
|
@@ -370,8 +518,9 @@ data in a loader that is not cached itself.
|
|
|
370
518
|
|
|
371
519
|
Pinned by dev + production e2e in the router repository (a group, its `urls()`
|
|
372
520
|
twin, each with and without `loading()`, and the group under an
|
|
373
|
-
`errorBoundary()`), and by
|
|
374
|
-
|
|
521
|
+
`errorBoundary()`), and by `serveShellRequest({ action })` tests whose
|
|
522
|
+
`readAction()` reads the call's answer as the browser does: the result and
|
|
523
|
+
the `revalidationError`.
|
|
375
524
|
|
|
376
525
|
## Loaders are full citizens: signals and handles
|
|
377
526
|
|
|
@@ -386,7 +535,7 @@ import { createLoader, notFound, redirect } from "@rangojs/router";
|
|
|
386
535
|
|
|
387
536
|
export const ProductLoader = createLoader(async (ctx) => {
|
|
388
537
|
const moved = LEGACY_SLUGS[ctx.params.slug];
|
|
389
|
-
if (moved) throw redirect(
|
|
538
|
+
if (moved) throw redirect(ctx.reverse("shop.product", { slug: moved }));
|
|
390
539
|
if (!(await exists(ctx.params.slug))) notFound(`No "${ctx.params.slug}"`);
|
|
391
540
|
return getProduct(ctx.params.slug);
|
|
392
541
|
});
|
|
@@ -507,7 +656,9 @@ rest of the destination; otherwise it is held. A same-route click
|
|
|
507
656
|
that layout is new to the page (entering the group from outside).
|
|
508
657
|
- `loading()` in `clientUrls()` takes no options, and a flagged `loading()`
|
|
509
658
|
on a server layout above the group defers nothing for the group's routes.
|
|
510
|
-
- `intercept()`
|
|
659
|
+
- On an `intercept()` loader the flag works as in `urls()`: an in-group
|
|
660
|
+
prefetch of the intercept's target defers it, and the click opens the modal
|
|
661
|
+
at once with one fill for it.
|
|
511
662
|
- With `ssr: false` on a `ppr` group route the flag is ignored (the bake
|
|
512
663
|
lane), as on a server route.
|
|
513
664
|
- Test it with `serveShellRequest` in the Flight project: mount the module
|
|
@@ -573,9 +724,11 @@ a hard load of the target URL renders the full route.
|
|
|
573
724
|
in the HTML once" as a guarantee — only the flagged loader is guaranteed.
|
|
574
725
|
- **Hook semantics shift inside a group.** `usePathname` is ABSOLUTE (mount
|
|
575
726
|
included) — never compare it to your own `path()` patterns; build local
|
|
576
|
-
links with `
|
|
577
|
-
`
|
|
578
|
-
|
|
727
|
+
links with `useReverse(routes)` from the group's `.gen.ts`: it composes the
|
|
728
|
+
mount and fills its params. `useHref()` (`groupHref("/items/1")`) and a
|
|
729
|
+
relative `useRouter().push("cart")` also fill `:param` mount segments from
|
|
730
|
+
the current params (a missing one throws, or rejects the push), but append
|
|
731
|
+
the rest unchecked. `push("/x")` stays
|
|
579
732
|
app-absolute by design (never auto-prefixed). `useSearchParams` carries REAL
|
|
580
733
|
values during SSR (live request seeds the SSR store; on ppr routes the
|
|
581
734
|
capture/resume renders seed the shell key's own sorted search — search is
|