@rangojs/router 0.24.4 → 0.26.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-cache-store.d.ts +0 -2
- package/dist/types/cache/cf/cf-cache-types.d.ts +0 -16
- package/dist/types/cache/cf/cf-shell-frame.d.ts +2 -0
- package/dist/types/cache/handle-snapshot.d.ts +2 -2
- package/dist/types/cache/types.d.ts +16 -6
- package/dist/types/cache/vercel/vercel-cache-store.d.ts +1 -15
- 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/handles/deferred-resolution.d.ts +3 -2
- package/dist/types/handles/meta.d.ts +5 -0
- 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/prerender/build-shell-capture.d.ts +1 -2
- package/dist/types/prerender/writable-store.d.ts +2 -3
- 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 -11
- package/dist/types/router/router-options.d.ts +4 -27
- package/dist/types/router/segment-resolution/mask-nested.d.ts +5 -3
- 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/router-versions.d.ts +8 -6
- 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 +14 -4
- package/dist/types/rsc/shell-capture.d.ts +23 -2
- package/dist/types/rsc/types.d.ts +5 -8
- package/dist/types/segment-system.d.ts +15 -0
- package/dist/types/server/build-version-table.d.ts +10 -13
- package/dist/types/server/context.d.ts +90 -6
- package/dist/types/server/handle-store.d.ts +17 -6
- package/dist/types/server/request-context.d.ts +45 -7
- package/dist/types/server/request-headers.d.ts +12 -0
- package/dist/types/server/rewrite-trail.d.ts +8 -1
- package/dist/types/testing/build-versions.d.ts +0 -3
- 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/internal/seed-vars.d.ts +4 -4
- 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 +42 -5
- 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 +10 -4
- 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 +6819 -6555
- 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/cache-guide/SKILL.md +4 -8
- package/skills/caching/SKILL.md +6 -6
- package/skills/catalog.json +12 -0
- package/skills/client-urls/SKILL.md +185 -32
- package/skills/comparison/references/framework-comparison.md +1 -1
- 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/host-router/SKILL.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 +42 -19
- 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 +103 -52
- package/skills/prerender/SKILL.md +4 -5
- package/skills/rango/SKILL.md +3 -0
- package/skills/response-routes/SKILL.md +4 -1
- package/skills/router-setup/SKILL.md +0 -2
- 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/render-handler.md +17 -17
- package/skills/testing/response-routes.md +4 -1
- package/skills/testing/serve-shell-request.md +86 -22
- package/skills/testing/server-actions.md +16 -16
- package/skills/ux-architecture/SKILL.md +980 -0
- package/skills/vercel/SKILL.md +1 -1
- 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 +6 -7
- package/src/cache/cf/cf-cache-types.ts +0 -17
- package/src/cache/cf/cf-shell-frame.ts +3 -0
- package/src/cache/handle-snapshot.ts +2 -2
- package/src/cache/types.ts +16 -6
- package/src/cache/vercel/vercel-cache-store.ts +8 -19
- 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/handles/deferred-resolution.ts +3 -2
- package/src/handles/meta.ts +5 -0
- 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/prerender/build-shell-capture.ts +1 -2
- package/src/prerender/writable-store.ts +2 -3
- package/src/reverse.ts +3 -3
- package/src/route-content-wrapper.tsx +19 -17
- package/src/route-definition/dsl-helpers.ts +41 -21
- 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 +15 -10
- 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 -13
- package/src/router/router-options.ts +4 -28
- package/src/router/segment-resolution/fresh.ts +6 -2
- package/src/router/segment-resolution/helpers.ts +7 -1
- package/src/router/segment-resolution/loader-cache.ts +4 -3
- package/src/router/segment-resolution/mask-nested.ts +5 -3
- 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-versions.ts +7 -7
- package/src/router.ts +5 -17
- package/src/rsc/full-payload.ts +26 -4
- package/src/rsc/handler.ts +86 -48
- 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 +26 -3
- package/src/rsc/shell-capture.ts +137 -51
- package/src/rsc/types.ts +5 -9
- package/src/segment-system.tsx +125 -119
- package/src/server/build-version-table.ts +12 -24
- package/src/server/context.ts +160 -7
- package/src/server/handle-store.ts +80 -12
- package/src/server/request-context.ts +46 -6
- 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/build-versions.ts +0 -3
- package/src/testing/dispatch.ts +5 -8
- 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/internal/seed-vars.ts +4 -4
- 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 +79 -9
- 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 +10 -4
- 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/shell-prerender-phase.ts +1 -5
- 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,435 @@
|
|
|
1
|
+
# A shop
|
|
2
|
+
|
|
3
|
+
A storefront: category pages with filters, product pages, a cart badge on
|
|
4
|
+
every page, a cart page and a checkout. Most of each page is the same for
|
|
5
|
+
every visitor (the product, the category grid); a few regions are per
|
|
6
|
+
visitor or change by the minute (the cart, the price, the stock).
|
|
7
|
+
|
|
8
|
+
- **Category and product pages are server routes with `ppr`.** The handler
|
|
9
|
+
renders the shared part from a tagged `"use cache"` read, and it bakes into
|
|
10
|
+
the shell; the per-visitor parts are live loaders read under their own
|
|
11
|
+
`<Suspense>`, the holes.
|
|
12
|
+
- **The cart is a loader on the store layout**, re-run after cart actions
|
|
13
|
+
only.
|
|
14
|
+
- **Checkout is server-rendered with actions**: no shell, no cache, a guard
|
|
15
|
+
that needs a cart.
|
|
16
|
+
- **Filters are search params** with a typed schema; each distinct query is
|
|
17
|
+
its own shell.
|
|
18
|
+
|
|
19
|
+
## File tree
|
|
20
|
+
|
|
21
|
+
```
|
|
22
|
+
src/
|
|
23
|
+
router.tsx cache store; tracking params out of the keys; loadCart
|
|
24
|
+
router.named-routes.gen.ts generated, committed
|
|
25
|
+
urls.tsx root: store layout, catalog, cart, checkout
|
|
26
|
+
urls/
|
|
27
|
+
catalog.tsx urls(): category and product pages (ppr)
|
|
28
|
+
checkout.tsx urls(): checkout steps (no ppr)
|
|
29
|
+
pages/store.tsx StoreLayout, CategoryPage, ProductPage
|
|
30
|
+
pages/checkout.tsx "use client": CartPage, CheckoutPage
|
|
31
|
+
components/
|
|
32
|
+
cart-badge.tsx "use client": useLoader(CartLoader)
|
|
33
|
+
buy-box.tsx "use client": price, stock, reviews, add to cart
|
|
34
|
+
loaders/shop.ts CartLoader, PriceLoader, StockLoader, ReviewsLoader
|
|
35
|
+
lib/data.ts getCategory, getProduct: "use cache" + cacheTag
|
|
36
|
+
lib/catalog.ts lib/carts.ts lib/payments.ts your data layer
|
|
37
|
+
middleware/cart.ts loadCart (global), requireCart
|
|
38
|
+
actions/cart.ts "use server": addToCart
|
|
39
|
+
actions/checkout.ts "use server": placeOrder
|
|
40
|
+
```
|
|
41
|
+
|
|
42
|
+
## router.tsx
|
|
43
|
+
|
|
44
|
+
```tsx
|
|
45
|
+
// src/router.tsx
|
|
46
|
+
import { createRouter, TRACKING_SEARCH_PARAMS } from "@rangojs/router";
|
|
47
|
+
import { MemorySegmentCacheStore } from "@rangojs/router/cache";
|
|
48
|
+
import { loadCart } from "./middleware/cart";
|
|
49
|
+
import { urlpatterns } from "./urls";
|
|
50
|
+
|
|
51
|
+
export const router = createRouter({
|
|
52
|
+
cache: {
|
|
53
|
+
store: new MemorySegmentCacheStore(), // CFCacheStore / VercelCacheStore when deployed
|
|
54
|
+
// utm_* and click ids mint no new shells or cache entries.
|
|
55
|
+
searchParams: { exclude: TRACKING_SEARCH_PARAMS },
|
|
56
|
+
},
|
|
57
|
+
})
|
|
58
|
+
.use(loadCart)
|
|
59
|
+
.routes(urlpatterns);
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
```ts
|
|
63
|
+
// src/middleware/cart.ts
|
|
64
|
+
import { cookies, createVar, redirect } from "@rangojs/router";
|
|
65
|
+
import type { Middleware } from "@rangojs/router";
|
|
66
|
+
import { carts } from "../lib/carts";
|
|
67
|
+
|
|
68
|
+
export const CartId = createVar<string>({ cache: false });
|
|
69
|
+
|
|
70
|
+
export const loadCart: Middleware = async (ctx, next) => {
|
|
71
|
+
const id = cookies().get("cart")?.value;
|
|
72
|
+
if (id) ctx.set(CartId, id);
|
|
73
|
+
return next();
|
|
74
|
+
};
|
|
75
|
+
|
|
76
|
+
export const requireCart: Middleware = async (ctx, next) => {
|
|
77
|
+
const id = ctx.get(CartId);
|
|
78
|
+
if (!id || (await carts.get(id)).length === 0)
|
|
79
|
+
return redirect(ctx.reverse("cart"));
|
|
80
|
+
return next();
|
|
81
|
+
};
|
|
82
|
+
```
|
|
83
|
+
|
|
84
|
+
## urls.tsx
|
|
85
|
+
|
|
86
|
+
```tsx
|
|
87
|
+
// src/urls.tsx
|
|
88
|
+
import { urls } from "@rangojs/router";
|
|
89
|
+
import { catalogPatterns } from "./urls/catalog";
|
|
90
|
+
import { checkoutPatterns } from "./urls/checkout";
|
|
91
|
+
import { StoreLayout } from "./pages/store";
|
|
92
|
+
import { CartPage } from "./pages/checkout";
|
|
93
|
+
import { CartLoader } from "./loaders/shop";
|
|
94
|
+
import * as CartActions from "./actions/cart";
|
|
95
|
+
import { requireCart } from "./middleware/cart";
|
|
96
|
+
|
|
97
|
+
export const urlpatterns = urls(
|
|
98
|
+
({ path, layout, include, loader, revalidate, middleware }) => [
|
|
99
|
+
layout(StoreLayout, () => [
|
|
100
|
+
// The cart badge on every page. A layout loader re-runs after every
|
|
101
|
+
// action by default: narrow it to cart actions. Navigations hold it.
|
|
102
|
+
loader(CartLoader, () => [
|
|
103
|
+
revalidate((ctx) =>
|
|
104
|
+
ctx.isAction() ? ctx.isAction(CartActions) : undefined,
|
|
105
|
+
),
|
|
106
|
+
]),
|
|
107
|
+
include("/", catalogPatterns, { name: "catalog" }),
|
|
108
|
+
path("/cart", CartPage, { name: "cart" }),
|
|
109
|
+
middleware(requireCart, () => [
|
|
110
|
+
include("/checkout", checkoutPatterns, { name: "checkout" }),
|
|
111
|
+
]),
|
|
112
|
+
]),
|
|
113
|
+
],
|
|
114
|
+
);
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
## The catalog
|
|
118
|
+
|
|
119
|
+
```ts
|
|
120
|
+
// src/lib/data.ts
|
|
121
|
+
import { cacheTag } from "@rangojs/router";
|
|
122
|
+
import { catalog } from "./catalog";
|
|
123
|
+
|
|
124
|
+
// Shared by every visitor and tagged: a catalog edit drops the entry and
|
|
125
|
+
// every shell that baked it.
|
|
126
|
+
export async function getCategory(
|
|
127
|
+
category: string,
|
|
128
|
+
sort?: string,
|
|
129
|
+
page?: number,
|
|
130
|
+
) {
|
|
131
|
+
"use cache";
|
|
132
|
+
cacheTag(`category:${category}`);
|
|
133
|
+
return catalog.list(category, { sort, page });
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
export async function getProduct(slug: string) {
|
|
137
|
+
"use cache";
|
|
138
|
+
cacheTag(`product:${slug}`);
|
|
139
|
+
return catalog.get(slug);
|
|
140
|
+
}
|
|
141
|
+
```
|
|
142
|
+
|
|
143
|
+
```tsx
|
|
144
|
+
// src/pages/store.tsx
|
|
145
|
+
import { Suspense } from "react";
|
|
146
|
+
import { notFound, type Handler } from "@rangojs/router";
|
|
147
|
+
import { Link, Outlet } from "@rangojs/router/client";
|
|
148
|
+
import { CartBadge } from "../components/cart-badge";
|
|
149
|
+
import { BuyBox } from "../components/buy-box";
|
|
150
|
+
import { getCategory, getProduct } from "../lib/data";
|
|
151
|
+
|
|
152
|
+
export function StoreLayout() {
|
|
153
|
+
return (
|
|
154
|
+
<>
|
|
155
|
+
<header>
|
|
156
|
+
{/* A badge-sized hole: the header itself bakes into every shell. */}
|
|
157
|
+
<Suspense fallback={<span>Cart</span>}>
|
|
158
|
+
<CartBadge />
|
|
159
|
+
</Suspense>
|
|
160
|
+
</header>
|
|
161
|
+
<Outlet />
|
|
162
|
+
</>
|
|
163
|
+
);
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
// Typed from the generated map: ctx.params.category, ctx.search.sort.
|
|
167
|
+
export const CategoryPage: Handler<"catalog.category"> = async (ctx) => {
|
|
168
|
+
const products = await getCategory(
|
|
169
|
+
ctx.params.category,
|
|
170
|
+
ctx.search.sort,
|
|
171
|
+
ctx.search.page,
|
|
172
|
+
);
|
|
173
|
+
return (
|
|
174
|
+
<ul>
|
|
175
|
+
{products.map((p) => (
|
|
176
|
+
<li key={p.slug}>
|
|
177
|
+
<Link to={ctx.reverse("catalog.product", { slug: p.slug })}>
|
|
178
|
+
{p.name}
|
|
179
|
+
</Link>
|
|
180
|
+
</li>
|
|
181
|
+
))}
|
|
182
|
+
</ul>
|
|
183
|
+
);
|
|
184
|
+
};
|
|
185
|
+
|
|
186
|
+
export const ProductPage: Handler<"catalog.product"> = async (ctx) => {
|
|
187
|
+
const product = await getProduct(ctx.params.slug);
|
|
188
|
+
if (!product) notFound();
|
|
189
|
+
return (
|
|
190
|
+
<article>
|
|
191
|
+
<h1>{product.name}</h1>
|
|
192
|
+
<BuyBox slug={ctx.params.slug} />
|
|
193
|
+
</article>
|
|
194
|
+
);
|
|
195
|
+
};
|
|
196
|
+
```
|
|
197
|
+
|
|
198
|
+
```tsx
|
|
199
|
+
// src/urls/catalog.tsx
|
|
200
|
+
import { urls, type Revalidate } from "@rangojs/router";
|
|
201
|
+
import { CategoryPage, ProductPage } from "../pages/store";
|
|
202
|
+
import { PriceLoader, ReviewsLoader, StockLoader } from "../loaders/shop";
|
|
203
|
+
|
|
204
|
+
// A route and its loaders re-run after every action by default. The product
|
|
205
|
+
// itself, its price and its reviews do not change with the cart; stock may.
|
|
206
|
+
const notOnActions: Revalidate = (ctx) => (ctx.isAction() ? false : undefined);
|
|
207
|
+
|
|
208
|
+
export const catalogPatterns = urls(({ path, loader, revalidate }) => [
|
|
209
|
+
path("/c/:category", CategoryPage, {
|
|
210
|
+
name: "category",
|
|
211
|
+
search: { sort: "string?", page: "number?" },
|
|
212
|
+
ppr: true,
|
|
213
|
+
}),
|
|
214
|
+
path("/p/:slug", ProductPage, { name: "product", ppr: { ttl: 3600 } }, () => [
|
|
215
|
+
revalidate(notOnActions), // the handler
|
|
216
|
+
loader(PriceLoader, () => [revalidate(notOnActions)]),
|
|
217
|
+
loader(StockLoader),
|
|
218
|
+
// Below the fold and costly: kept out of link prefetches.
|
|
219
|
+
loader(ReviewsLoader, { prefetch: false }, () => [
|
|
220
|
+
revalidate(notOnActions),
|
|
221
|
+
]),
|
|
222
|
+
]),
|
|
223
|
+
]);
|
|
224
|
+
```
|
|
225
|
+
|
|
226
|
+
```tsx
|
|
227
|
+
// src/components/buy-box.tsx
|
|
228
|
+
"use client";
|
|
229
|
+
|
|
230
|
+
import { Suspense } from "react";
|
|
231
|
+
import { useLoader } from "@rangojs/router/client";
|
|
232
|
+
import { PriceLoader, ReviewsLoader, StockLoader } from "../loaders/shop";
|
|
233
|
+
import { addToCart } from "../actions/cart";
|
|
234
|
+
|
|
235
|
+
function Price() {
|
|
236
|
+
const { data } = useLoader(PriceLoader);
|
|
237
|
+
return <p>{(data.cents / 100).toFixed(2)}</p>;
|
|
238
|
+
}
|
|
239
|
+
function Stock() {
|
|
240
|
+
const { data } = useLoader(StockLoader);
|
|
241
|
+
return <p>{data.count} left</p>;
|
|
242
|
+
}
|
|
243
|
+
function Reviews() {
|
|
244
|
+
const { data } = useLoader(ReviewsLoader);
|
|
245
|
+
return (
|
|
246
|
+
<ul>
|
|
247
|
+
{data.map((r) => (
|
|
248
|
+
<li key={r.id}>{r.text}</li>
|
|
249
|
+
))}
|
|
250
|
+
</ul>
|
|
251
|
+
);
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
// Each read under its own boundary: these are the shell's live holes.
|
|
255
|
+
export function BuyBox({ slug }: { slug: string }) {
|
|
256
|
+
return (
|
|
257
|
+
<>
|
|
258
|
+
<Suspense fallback={<p>…</p>}>
|
|
259
|
+
<Price />
|
|
260
|
+
<Stock />
|
|
261
|
+
</Suspense>
|
|
262
|
+
<button onClick={() => addToCart(slug)}>Add to cart</button>
|
|
263
|
+
<Suspense fallback={<p>Loading reviews…</p>}>
|
|
264
|
+
<Reviews />
|
|
265
|
+
</Suspense>
|
|
266
|
+
</>
|
|
267
|
+
);
|
|
268
|
+
}
|
|
269
|
+
```
|
|
270
|
+
|
|
271
|
+
What a product page request does:
|
|
272
|
+
|
|
273
|
+
| Part | Where it comes from | On a shell HIT |
|
|
274
|
+
| ------------------------- | ------------------------------------------------------ | ---------------------------------------------------- |
|
|
275
|
+
| Name, description, images | the handler, from `getProduct` (`"use cache"`, tagged) | from the shell; the handler does not run |
|
|
276
|
+
| Price, stock | live loaders under `<Suspense>` | run for this request, stream into the holes |
|
|
277
|
+
| Reviews | a live loader, kept out of prefetches | run for this request; a prefetch defers it to a fill |
|
|
278
|
+
| Cart badge | the layout's `CartLoader` | run for this request |
|
|
279
|
+
|
|
280
|
+
- Filters link with the typed search:
|
|
281
|
+
`ctx.reverse("catalog.category", { category }, { sort: "price" })`. Every
|
|
282
|
+
distinct query is its own shell, captured on its first request; keep the
|
|
283
|
+
filter set small or move the grid into a live loader when combinations
|
|
284
|
+
explode.
|
|
285
|
+
- A catalog edit is a tag: `updateTag()` with `product:<slug>` drops the
|
|
286
|
+
`"use cache"` entry and every shell that baked it; the next request
|
|
287
|
+
recaptures. A webhook route has the request context `updateTag()` needs:
|
|
288
|
+
|
|
289
|
+
```ts
|
|
290
|
+
path.json(
|
|
291
|
+
"/hooks/catalog",
|
|
292
|
+
async (ctx) => {
|
|
293
|
+
// verify the sender's signature first
|
|
294
|
+
const { slug } = (await ctx.request.json()) as { slug: string };
|
|
295
|
+
await updateTag(`product:${slug}`);
|
|
296
|
+
return { ok: true };
|
|
297
|
+
},
|
|
298
|
+
{ name: "catalogHook" },
|
|
299
|
+
);
|
|
300
|
+
```
|
|
301
|
+
|
|
302
|
+
- Every per-visitor read lives in a loader the client reads. A `cookies()`
|
|
303
|
+
read in a handler or in a loader a handler awaits refuses the shell
|
|
304
|
+
capture, and the page never serves a shell (`/ppr` → "The
|
|
305
|
+
layout-with-loaders playbook").
|
|
306
|
+
|
|
307
|
+
### Variant: product pages prerendered
|
|
308
|
+
|
|
309
|
+
For a catalog known at build time, the product page can be
|
|
310
|
+
`Prerender(getParams, handler, { onDemand })` wrapped in `Passthrough()`: the
|
|
311
|
+
build bakes the listed products, a CMS webhook refreshes one with
|
|
312
|
+
`router.prerender()`, and the live handler renders the long tail. `onDemand`
|
|
313
|
+
cannot be combined with `ppr`, so this variant stores the Flight payload but
|
|
314
|
+
no HTML shell; a plain `Prerender` (no `onDemand`) can take `ppr`, but then a
|
|
315
|
+
content change needs a deploy. The pattern, webhook included, is in
|
|
316
|
+
[`./content-site.md`](./content-site.md#the-blog-section).
|
|
317
|
+
|
|
318
|
+
## The cart
|
|
319
|
+
|
|
320
|
+
```ts
|
|
321
|
+
// src/loaders/shop.ts
|
|
322
|
+
import { createLoader } from "@rangojs/router";
|
|
323
|
+
import { carts } from "../lib/carts";
|
|
324
|
+
import { catalog } from "../lib/catalog";
|
|
325
|
+
import { CartId } from "../middleware/cart";
|
|
326
|
+
|
|
327
|
+
// Per visitor: live, never cached.
|
|
328
|
+
export const CartLoader = createLoader(async (ctx) => {
|
|
329
|
+
const id = ctx.get(CartId);
|
|
330
|
+
const items = id ? await carts.get(id) : [];
|
|
331
|
+
return { count: items.length, items };
|
|
332
|
+
});
|
|
333
|
+
|
|
334
|
+
// Change by the minute: live on every request, shell HITs included.
|
|
335
|
+
export const PriceLoader = createLoader(async (ctx) =>
|
|
336
|
+
catalog.price(ctx.params.slug!),
|
|
337
|
+
);
|
|
338
|
+
export const StockLoader = createLoader(async (ctx) =>
|
|
339
|
+
catalog.stock(ctx.params.slug!),
|
|
340
|
+
);
|
|
341
|
+
export const ReviewsLoader = createLoader(async (ctx) =>
|
|
342
|
+
catalog.reviews(ctx.params.slug!),
|
|
343
|
+
);
|
|
344
|
+
```
|
|
345
|
+
|
|
346
|
+
```ts
|
|
347
|
+
// src/actions/cart.ts
|
|
348
|
+
"use server";
|
|
349
|
+
|
|
350
|
+
import { cookies, getRequestContext } from "@rangojs/router";
|
|
351
|
+
import { carts } from "../lib/carts";
|
|
352
|
+
import { CartId } from "../middleware/cart";
|
|
353
|
+
|
|
354
|
+
export async function addToCart(slug: string) {
|
|
355
|
+
const ctx = getRequestContext();
|
|
356
|
+
let id = ctx.get(CartId);
|
|
357
|
+
if (!id) {
|
|
358
|
+
id = crypto.randomUUID();
|
|
359
|
+
cookies().set("cart", id, { httpOnly: true, path: "/" });
|
|
360
|
+
// loadCart ran before the action; the revalidation render reads this.
|
|
361
|
+
ctx.set(CartId, id);
|
|
362
|
+
}
|
|
363
|
+
await carts.add(id, slug);
|
|
364
|
+
return { ok: true as const };
|
|
365
|
+
}
|
|
366
|
+
```
|
|
367
|
+
|
|
368
|
+
One add-to-cart from a product page re-runs `CartLoader` (its
|
|
369
|
+
`revalidate()` names cart actions) and `StockLoader` (the default), and holds
|
|
370
|
+
`PriceLoader`, `ReviewsLoader` and the product handler (`notOnActions`); the
|
|
371
|
+
store layout is skipped after actions by default. Each decision is per
|
|
372
|
+
segment, in the action's own response. Without the route's own
|
|
373
|
+
`revalidate()`, the handler would re-run on every add-to-cart.
|
|
374
|
+
|
|
375
|
+
### Variant: the shop as a `clientUrls()` group
|
|
376
|
+
|
|
377
|
+
When shoppers mostly move inside one layout (filters, tabs, product to
|
|
378
|
+
product) and the pages render entirely from loaders, mount the shop as a
|
|
379
|
+
`clientUrls()` group, `include("/shop", shopUrls, { name: "shop" })`:
|
|
380
|
+
navigations inside it render at the click, and each loader's `revalidate()`
|
|
381
|
+
holds its data across them. A group has no handlers, so a `ppr` group route
|
|
382
|
+
holds its client components' HTML and its `ssr: false` loaders' data, and
|
|
383
|
+
every other loader is a hole; handler output baked from a tagged
|
|
384
|
+
`"use cache"` read, as in the catalog above, needs `urls()` routes. The router
|
|
385
|
+
repository's `tests/vite-rsc-demo/src/urls/client-shop.client.tsx` is a full
|
|
386
|
+
example (index, product, a cart badge, param-sensitive predicates). A cart
|
|
387
|
+
loader on the group's `layout()` is held across the group's routes, as on a
|
|
388
|
+
server layout. Register it on a server layout above the include instead when
|
|
389
|
+
the badge must also survive entering and leaving the group.
|
|
390
|
+
|
|
391
|
+
## Checkout
|
|
392
|
+
|
|
393
|
+
```tsx
|
|
394
|
+
// src/urls/checkout.tsx
|
|
395
|
+
import { urls } from "@rangojs/router";
|
|
396
|
+
import { CheckoutPage } from "../pages/checkout";
|
|
397
|
+
|
|
398
|
+
// Per visitor and mutating: no ppr, no cache().
|
|
399
|
+
export const checkoutPatterns = urls(({ path }) => [
|
|
400
|
+
path("/", CheckoutPage, { name: "start" }),
|
|
401
|
+
]);
|
|
402
|
+
```
|
|
403
|
+
|
|
404
|
+
```ts
|
|
405
|
+
// src/actions/checkout.ts
|
|
406
|
+
"use server";
|
|
407
|
+
|
|
408
|
+
import { redirect } from "@rangojs/router";
|
|
409
|
+
import { payments } from "../lib/payments";
|
|
410
|
+
|
|
411
|
+
export async function placeOrder(_prev: unknown, formData: FormData) {
|
|
412
|
+
const email = String(formData.get("email") ?? "");
|
|
413
|
+
if (!email.includes("@")) return { error: "Enter an email address" }; // shown by useActionState
|
|
414
|
+
const session = await payments.createSession();
|
|
415
|
+
// Off-site redirects are refused unless you opt in.
|
|
416
|
+
throw redirect(session.url, { external: true });
|
|
417
|
+
}
|
|
418
|
+
```
|
|
419
|
+
|
|
420
|
+
`requireCart` wraps the checkout's renders, not `placeOrder`: an action
|
|
421
|
+
checks what it needs itself (`/server-actions` → "Authorization in actions").
|
|
422
|
+
A form whose `action` is the dispatcher `useActionState` returns posts
|
|
423
|
+
without JavaScript too (`/server-actions` → "Progressive Enhancement").
|
|
424
|
+
|
|
425
|
+
## Why
|
|
426
|
+
|
|
427
|
+
| Choice | Why |
|
|
428
|
+
| --------------------------------------------- | -------------------------------------------------------------------------- |
|
|
429
|
+
| `ppr` on category and product pages | The shared part is HTML from the shell; only the holes render per request |
|
|
430
|
+
| `"use cache"` + `cacheTag()` for product data | One read for every visitor, and the tag reaches the shells that baked it |
|
|
431
|
+
| Live loaders for price, stock, cart | Fresh on every request, shell HITs included |
|
|
432
|
+
| `CartLoader` on the store layout | One badge on every page; held on navigations; narrowed to cart actions |
|
|
433
|
+
| `TRACKING_SEARCH_PARAMS` excluded | Campaign links do not multiply shells |
|
|
434
|
+
| `prefetch: false` on reviews | Every product card prefetches its page; few shoppers scroll to the reviews |
|
|
435
|
+
| Checkout without `ppr` or `cache()` | Nothing on it is shared |
|