@rsc-kit/core 0.30.21 → 0.30.22

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.
@@ -89,25 +89,30 @@ function put(depth, key, tree, identity) {
89
89
  * Deeper segments belonged to the page being replaced; leaving them would
90
90
  * render the previous page inside the new one.
91
91
  */
92
- export function setSegment(depth, key, tree, inPlaceOf) {
92
+ export function setSegment(depth, key, tree, inPlaceOf, keepsPrevious) {
93
93
  // A guess about another page was wrong; the one about this page, if there
94
94
  // was one, is replaced by put() below - with the same tree, when the
95
95
  // prerender and the navigation read the same decoded payload, which is
96
96
  // what makes the update a reveal rather than a render.
97
97
  dropSpeculative(depth, key);
98
- // A replace to the same path with a new query: the page it replaces is gone
99
- // from history, so this is that page, moved - not a new one to mount beside
100
- // it. It takes the old entry's identity, and the old entry goes.
98
+ // The same path with a new query is the page on screen, moved - not a new
99
+ // one to mount beside it, which would show every fallback in the page. It
100
+ // takes the old entry's identity, so React updates the page it has. A
101
+ // replace has left that entry nothing to go back to, and it goes. A push
102
+ // keeps it for Back, under an identity of its own: mounted again behind the
103
+ // page, where its tree is already decoded and nothing waits.
101
104
  const state = depths.get(depth);
102
105
  const replaced = inPlaceOf && inPlaceOf !== key && !state?.entries.some((entry) => entry.key === key)
103
106
  ? state?.entries.find((entry) => entry.key === inPlaceOf)
104
107
  : undefined;
105
108
  if (replaced && state) {
106
- depths.set(depth, {
107
- ...state,
108
- entries: state.entries.filter((entry) => entry !== replaced),
109
- order: state.order.filter((k) => k !== replaced.key),
110
- });
109
+ depths.set(depth, keepsPrevious
110
+ ? { ...state, entries: state.entries.map((entry) => (entry === replaced ? { ...entry, identity: `${entry.key}#${++tick}` } : entry)) }
111
+ : {
112
+ ...state,
113
+ entries: state.entries.filter((entry) => entry !== replaced),
114
+ order: state.order.filter((k) => k !== replaced.key),
115
+ });
111
116
  }
112
117
  put(depth, key, tree, replaced ? (replaced.identity ?? replaced.key) : undefined);
113
118
  const stale = [...depths.keys()].filter((d) => d > depth);
@@ -1 +1 @@
1
- {"version":3,"file":"segmentStore.js","sourceRoot":"","sources":["../../src/js/segmentStore.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What each segment boundary is showing, and what it is keeping alive behind it.\n *\n * A navigation replaces one segment. Keeping the previous one mounted — hidden,\n * not unmounted — is what lets going back restore it with its client state:\n * the half-typed form, the open disclosure, the scrolled list. Unmounting\n * throws all of that away, which is what replacing the root used to do.\n *\n * Entries are keyed by page (the URL, or its intercept variant), so a boundary\n * can hold several and reveal one. Empty is meaningful: a boundary with nothing\n * stored renders the children the server gave it.\n *\n * A store rather than state, and not a preference — three things rule state out.\n * A boundary is inserted between every layout level, so there are several and a\n * navigation targets one by depth; they are separated by server components, so\n * no setter can be threaded down to them, because a function does not cross\n * that boundary; and navigate.ts is a plain module with no component instance\n * to call one on. Addressing a component you hold no reference to is what an\n * external store is for.\n *\n * Underneath it is the wire protocol. A partial navigation sends only the\n * segment that changed — see the X-RSC-Segments headers — so there is nothing\n * for a root to re-render with even if it held the state. Next.js keeps its\n * router in useState and derives every segment from it; that is the same trade\n * in the other direction.\n *\n * What it costs: React pins an external store's updates to synchronous\n * priority, because a store cannot be safely time-sliced. Synchronous is never\n * a transition, and anything that only runs for one — React's <ViewTransition>\n * among them — never ran for a navigation.\n *\n * Which is why SegmentBoundary does not read this with useSyncExternalStore\n * any more. The store still does the addressing, which is the part only it can\n * do; the boundary copies into state, so the render is a transition. See the\n * view transitions guide.\n */\n\ntype Tree = unknown;\ntype Listener = () => void;\n\ninterface Entry {\n key: string;\n /**\n * The key React mounts this page under, when it is not `key`.\n *\n * `key` is what a navigation files the page under - its path and query, so\n * Back to ?q=a restores that page and not ?q=b. The identity is what keeps\n * it mounted, and it has to survive two things that are not a new page: a\n * page loaded with a query, which the server rendered under its pathname\n * alone (the shell was frozen that way), and a replace to the same path with\n * a new query - the first message of a chat - which leaves nothing to go\n * back to. A new identity is a remount, and a remount shows every Suspense\n * fallback in the page: a skeleton over the chat, on every reply.\n */\n identity?: string;\n tree: Tree;\n /**\n * When this tree arrived, so a link can decide whether it is still worth\n * revealing. The back button never asks — it means \"the page I was on\",\n * however long ago that was.\n */\n at: number;\n /**\n * Its place in the order things happened, for the question \"was this\n * here before the mutation?\" - which a clock answers wrongly for a seed\n * and a write in the same millisecond, and a test hit exactly that.\n */\n seq: number;\n /**\n * Rendered before the click, hidden, on the strength of a touch or a\n * settled hover - see prerenderSegment. Outside the retention window: it\n * is a guess, and a guess must not evict a page the visitor was on. One\n * per depth; the next guess replaces it, and a navigation to anything\n * else drops it.\n */\n speculative?: boolean;\n}\n\n/**\n * Immutable: useSyncExternalStore compares snapshots by identity, so a new\n * object per read reads as \"changed every render\" and loops forever. Every\n * mutation replaces this wholesale; nothing edits one in place.\n */\ninterface DepthState {\n readonly entries: readonly Entry[];\n readonly activeKey: string;\n /** Most recently shown last; eviction takes from the front. */\n readonly order: readonly string[];\n}\n\n/** Pages kept alive per boundary. Four covers ordinary back-and-forth. */\nexport const RETENTION = 4;\n\nconst depths = new Map<number, DepthState>();\n/** A counter every entry takes a number from, in order. */\nlet tick = 0;\n/** The number the last mutation took: everything held before it is wrong. */\nlet invalidatedAt = 0;\nconst listeners = new Map<number, Set<Listener>>();\n\nfunction notify(depth: number): void {\n for (const listener of listeners.get(depth) ?? []) listener();\n}\n\nexport function subscribeToSegment(\n depth: number,\n listener: Listener,\n): () => void {\n let set = listeners.get(depth);\n\n if (!set) {\n set = new Set();\n listeners.set(depth, set);\n }\n\n set.add(listener);\n\n return () => {\n set!.delete(listener);\n if (set!.size === 0) listeners.delete(depth);\n };\n}\n\n/** Everything a boundary at this depth needs to render, or null for \"use children\". */\nexport function getSegmentState(depth: number): DepthState | null {\n return depths.get(depth) ?? null;\n}\n\n/** Apply the retention window to a candidate state. */\nfunction retain(\n entries: readonly Entry[],\n order: readonly string[],\n activeKey: string,\n): DepthState {\n const kept = order.slice(-RETENTION);\n\n return {\n entries: entries.filter((entry) => kept.includes(entry.key) || entry.speculative),\n order: kept,\n activeKey,\n };\n}\n\nfunction put(depth: number, key: string, tree: Tree, identity?: string): void {\n const state = depths.get(depth);\n // The same page again keeps the identity it is mounted under.\n const kept = identity ?? state?.entries.find((entry) => entry.key === key)?.identity;\n const entries = [\n ...(state?.entries ?? []).filter((entry) => entry.key !== key),\n { key, tree, at: Date.now(), seq: ++tick, ...(kept !== undefined && kept !== key ? { identity: kept } : {}) },\n ];\n const order = [...(state?.order ?? []).filter((k) => k !== key), key];\n\n depths.set(depth, retain(entries, order, key));\n}\n\n/**\n * Show `tree` at `depth` for `key`, retaining what was there.\n *\n * Deeper segments belonged to the page being replaced; leaving them would\n * render the previous page inside the new one.\n */\nexport function setSegment(depth: number, key: string, tree: Tree, inPlaceOf?: string | null): void {\n // A guess about another page was wrong; the one about this page, if there\n // was one, is replaced by put() below - with the same tree, when the\n // prerender and the navigation read the same decoded payload, which is\n // what makes the update a reveal rather than a render.\n dropSpeculative(depth, key);\n\n // A replace to the same path with a new query: the page it replaces is gone\n // from history, so this is that page, moved - not a new one to mount beside\n // it. It takes the old entry's identity, and the old entry goes.\n const state = depths.get(depth);\n const replaced =\n inPlaceOf && inPlaceOf !== key && !state?.entries.some((entry) => entry.key === key)\n ? state?.entries.find((entry) => entry.key === inPlaceOf)\n : undefined;\n\n if (replaced && state) {\n depths.set(depth, {\n ...state,\n entries: state.entries.filter((entry) => entry !== replaced),\n order: state.order.filter((k) => k !== replaced.key),\n });\n }\n\n put(depth, key, tree, replaced ? (replaced.identity ?? replaced.key) : undefined);\n\n const stale = [...depths.keys()].filter((d) => d > depth);\n for (const d of stale) depths.delete(d);\n\n notify(depth);\n for (const d of stale) notify(d);\n}\n\nfunction dropSpeculative(depth: number, except?: string): void {\n const state = depths.get(depth);\n\n if (!state?.entries.some((entry) => entry.speculative && entry.key !== except)) return;\n\n depths.set(depth, {\n ...state,\n entries: state.entries.filter((entry) => !entry.speculative || entry.key === except),\n });\n}\n\n/**\n * Render a page hidden at `depth`, before any navigation to it.\n *\n * The click then finds the work done: setSegment with the same tree is a\n * bail-out for React - same element, same props - and the Activity flips\n * from hidden to visible. What was 87 ms of rendering on a phone, after\n * the tap, is paid before the finger lifts, at idle priority, yielding to\n * the scroll. Never changes what is showing, never counts against\n * retention, and a page already held needs nothing.\n */\nexport function prerenderSegment(depth: number, key: string, tree: Tree): void {\n const state = depths.get(depth);\n\n // Nothing at this depth yet: the boundary is still showing the server's\n // children, and it has no page key to keep them under. A guess would take\n // the store over with no active entry to show. seedSegment runs on mount,\n // so this is the gap between hydration and that effect; the click will\n // render.\n if (!state) return;\n\n if (state.entries.some((entry) => entry.key === key)) return;\n\n depths.set(depth, {\n ...state,\n entries: [\n ...state.entries.filter((entry) => !entry.speculative),\n { key, tree, at: Date.now(), seq: ++tick, speculative: true },\n ],\n });\n\n notify(depth);\n}\n\n/** Whether a page is rendered hidden at `depth`, ahead of a navigation to it. */\nexport function isPrerendered(depth: number, key: string): boolean {\n return depths.get(depth)?.entries.some((entry) => entry.key === key && entry.speculative) ?? false;\n}\n\n/**\n * Give the page on screen at `depth` a new tree, under the key it has.\n *\n * For revalidate(\"all\"): the whole document rendered again, in place. The\n * root hands its layout the new tree, the layout hands its boundary new\n * children, and a boundary that holds state would otherwise keep showing\n * the store's tree and drop the new one on the floor - or, cleared first,\n * re-key its Activity to the current url and remount everything under it.\n * Neither. The entry that is showing takes the new tree and keeps its key,\n * so React reconciles the page in place, and the boundary below gets its\n * new children the same way.\n */\nexport function replaceActive(depth: number, tree: Tree): void {\n const state = depths.get(depth);\n\n if (!state) return;\n\n const active = state.entries.find((entry) => entry.key === state.activeKey);\n\n if (!active || active.tree === tree) return;\n\n // The tree changes; when the entry arrived does not. A layout's entry is\n // keyed by the page it was seeded with - \"/\" for a document loaded at\n // home - and stays active through every navigation below it. Stamped\n // with the re-render's time it read as held since the mutation, and a\n // tap on the brand link after \"Add to cart\" revealed it: the url went\n // to \"/\" and the product stayed, because the tree under that key was\n // now the product's document. See dropHidden and isHeld.\n depths.set(depth, {\n ...state,\n entries: state.entries.map((entry) => (entry === active ? { ...entry, tree } : entry)),\n });\n notify(depth);\n}\n\n/**\n * Record the children the server rendered, so the page you arrived on can be\n * returned to later. Never changes what is showing.\n */\nexport function seedSegment(depth: number, key: string, tree: Tree, identity?: string): void {\n const state = depths.get(depth);\n\n if (state?.entries.some((entry) => entry.key === key)) return;\n\n if (!state) {\n put(depth, key, tree, identity);\n notify(depth);\n\n return;\n }\n\n // Older than whatever is showing, so it goes to the front of the eviction\n // order — and crucially does not become the active page.\n depths.set(\n depth,\n retain(\n [...state.entries, { key, tree, at: Date.now(), seq: ++tick, ...(identity !== undefined && identity !== key ? { identity } : {}) }],\n [key, ...state.order.filter((k) => k !== key)],\n state.activeKey,\n ),\n );\n\n notify(depth);\n}\n\n/**\n * Reveal a page the boundaries are still holding, without asking the server.\n *\n * Restoring is anchored on the deepest boundary that can show the page. Deeper\n * ones than that belonged to the page being left — a section with its own\n * layout adds a boundary the page you are going back to never had — so they\n * are dropped, exactly as setSegment drops them. Requiring every boundary to\n * hold the key instead made any such page refuse to restore.\n *\n * Shallower boundaries need no key of their own: their trees contain the\n * deeper boundary, so they delegate to whatever it is showing. One that does\n * hold the key is switched to it, since that is a real change at its level.\n */\n/**\n * Whether a link to `key` would be answered by revealing a held page.\n *\n * The question restoreSegments answers, asked without acting on it: a\n * prefetch of a page the boundaries still hold is a request for nothing -\n * a navigation would reveal it. The page just left is the usual case, one\n * wasted payload per navigation.\n */\nexport function isHeld(key: string, maxAge?: number): boolean {\n const ages = [...depths.values()].flatMap((state) =>\n state.entries.filter((entry) => entry.key === key && !entry.speculative && entry.seq > invalidatedAt).map((entry) => entry.at),\n );\n\n if (ages.length === 0) return false;\n if (maxAge === undefined) return true;\n\n return Date.now() - Math.min(...ages) < maxAge;\n}\n\n/**\n * Drop every page held behind the one on screen.\n *\n * After a mutation: a page kept for the back button holds the data from\n * before it, and revealing it would show a row that is gone, a name that\n * changed. The page on screen stays - it was re-rendered by the action, or\n * is about to be.\n */\nexport function dropHidden(): void {\n // And the ones that stay - the active entry at each depth - are from\n // before it too. A layout's entry is keyed by the page it was seeded\n // with, and revealing that key later shows the page inside it as it was\n // then. Nothing seeded before this moment is revealed again.\n invalidatedAt = ++tick;\n\n for (const [depth, state] of depths) {\n if (state.entries.length <= 1 && !state.entries.some((entry) => entry.speculative)) continue;\n\n depths.set(depth, {\n entries: state.entries.filter((entry) => entry.key === state.activeKey),\n order: state.order.filter((key) => key === state.activeKey),\n activeKey: state.activeKey,\n });\n notify(depth);\n }\n}\n\n/**\n * Reveal a page still being held, if it is worth revealing.\n *\n * `maxAge` is what a link passes and the back button does not. Going back is\n * unambiguous — it names a moment, and the page from that moment is the right\n * answer however old. A link says \"go here\", and answering it with a tree from\n * twenty minutes ago is stale data presented as fresh, which is the objection\n * this design started with. Recent enough, and it is the same page you were\n * just on, with the form you were filling in still filled in.\n */\nexport function restoreSegments(key: string, maxAge?: number): boolean {\n // A guess is not a held page: revealing it would be showing prefetched\n // data as the page the visitor was on. The navigation takes the\n // prerendered tree through setSegment instead, where it is a reveal too.\n // Nor is a page from before a mutation - see dropHidden.\n const holding = [...depths.keys()].filter((d) =>\n depths.get(d)!.entries.some((entry) => entry.key === key && !entry.speculative && entry.seq > invalidatedAt),\n );\n\n if (holding.length === 0) return false;\n\n if (maxAge !== undefined) {\n const ages = holding.flatMap((d) =>\n depths\n .get(d)!\n .entries.filter((entry) => entry.key === key)\n .map((entry) => entry.at),\n );\n\n // The oldest layer decides: revealing a fresh page under a stale layout\n // would be a chain nobody rendered together.\n // >= rather than >, so a window of 0 means never rather than \"only within\n // the same millisecond\".\n if (Date.now() - Math.min(...ages) >= maxAge) return false;\n }\n\n const anchor = Math.max(...holding);\n\n for (const d of [...depths.keys()].filter((d) => d > anchor)) {\n depths.delete(d);\n notify(d);\n }\n\n for (const d of holding) {\n dropSpeculative(d);\n\n const state = depths.get(d)!;\n\n depths.set(\n d,\n retain(\n state.entries,\n [...state.order.filter((k) => k !== key), key],\n key,\n ),\n );\n notify(d);\n }\n\n return true;\n}\n\n/**\n * Drop everything, so boundaries fall back to their server-given children.\n *\n * A deployment invalidates them all: a segment from the previous build has no\n * claim on being correct for this one.\n */\nexport function clearSegments(): void {\n const all = [...depths.keys()];\n depths.clear();\n for (const depth of all) notify(depth);\n}\n\n/**\n * Whether a navigation has happened in this document.\n *\n * The first commit after hydration is the boundary taking over its\n * server-rendered children - the same page, re-keyed for retention. A view\n * transition for that commit fades the page into itself: the blank-then-\n * content a first load or a reload showed. Inertia transitions on visits and\n * never on load; so does this. The router notes the first navigation before\n * it updates a segment, and the boundary animates from then on.\n */\nlet navigated = false;\n\nexport function noteNavigation(): void {\n navigated = true;\n}\n\nexport function navigatedOnce(): boolean {\n return navigated;\n}\n"]}
1
+ {"version":3,"file":"segmentStore.js","sourceRoot":"","sources":["../../src/js/segmentStore.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What each segment boundary is showing, and what it is keeping alive behind it.\n *\n * A navigation replaces one segment. Keeping the previous one mounted — hidden,\n * not unmounted — is what lets going back restore it with its client state:\n * the half-typed form, the open disclosure, the scrolled list. Unmounting\n * throws all of that away, which is what replacing the root used to do.\n *\n * Entries are keyed by page (the URL, or its intercept variant), so a boundary\n * can hold several and reveal one. Empty is meaningful: a boundary with nothing\n * stored renders the children the server gave it.\n *\n * A store rather than state, and not a preference — three things rule state out.\n * A boundary is inserted between every layout level, so there are several and a\n * navigation targets one by depth; they are separated by server components, so\n * no setter can be threaded down to them, because a function does not cross\n * that boundary; and navigate.ts is a plain module with no component instance\n * to call one on. Addressing a component you hold no reference to is what an\n * external store is for.\n *\n * Underneath it is the wire protocol. A partial navigation sends only the\n * segment that changed — see the X-RSC-Segments headers — so there is nothing\n * for a root to re-render with even if it held the state. Next.js keeps its\n * router in useState and derives every segment from it; that is the same trade\n * in the other direction.\n *\n * What it costs: React pins an external store's updates to synchronous\n * priority, because a store cannot be safely time-sliced. Synchronous is never\n * a transition, and anything that only runs for one — React's <ViewTransition>\n * among them — never ran for a navigation.\n *\n * Which is why SegmentBoundary does not read this with useSyncExternalStore\n * any more. The store still does the addressing, which is the part only it can\n * do; the boundary copies into state, so the render is a transition. See the\n * view transitions guide.\n */\n\ntype Tree = unknown;\ntype Listener = () => void;\n\ninterface Entry {\n key: string;\n /**\n * The key React mounts this page under, when it is not `key`.\n *\n * `key` is what a navigation files the page under - its path and query, so\n * Back to ?q=a restores that page and not ?q=b. The identity is what keeps\n * it mounted, and it has to survive two things that are not a new page: a\n * page loaded with a query, which the server rendered under its pathname\n * alone (the shell was frozen that way), and a replace to the same path with\n * a new query - the first message of a chat - which leaves nothing to go\n * back to. A new identity is a remount, and a remount shows every Suspense\n * fallback in the page: a skeleton over the chat, on every reply.\n */\n identity?: string;\n tree: Tree;\n /**\n * When this tree arrived, so a link can decide whether it is still worth\n * revealing. The back button never asks — it means \"the page I was on\",\n * however long ago that was.\n */\n at: number;\n /**\n * Its place in the order things happened, for the question \"was this\n * here before the mutation?\" - which a clock answers wrongly for a seed\n * and a write in the same millisecond, and a test hit exactly that.\n */\n seq: number;\n /**\n * Rendered before the click, hidden, on the strength of a touch or a\n * settled hover - see prerenderSegment. Outside the retention window: it\n * is a guess, and a guess must not evict a page the visitor was on. One\n * per depth; the next guess replaces it, and a navigation to anything\n * else drops it.\n */\n speculative?: boolean;\n}\n\n/**\n * Immutable: useSyncExternalStore compares snapshots by identity, so a new\n * object per read reads as \"changed every render\" and loops forever. Every\n * mutation replaces this wholesale; nothing edits one in place.\n */\ninterface DepthState {\n readonly entries: readonly Entry[];\n readonly activeKey: string;\n /** Most recently shown last; eviction takes from the front. */\n readonly order: readonly string[];\n}\n\n/** Pages kept alive per boundary. Four covers ordinary back-and-forth. */\nexport const RETENTION = 4;\n\nconst depths = new Map<number, DepthState>();\n/** A counter every entry takes a number from, in order. */\nlet tick = 0;\n/** The number the last mutation took: everything held before it is wrong. */\nlet invalidatedAt = 0;\nconst listeners = new Map<number, Set<Listener>>();\n\nfunction notify(depth: number): void {\n for (const listener of listeners.get(depth) ?? []) listener();\n}\n\nexport function subscribeToSegment(\n depth: number,\n listener: Listener,\n): () => void {\n let set = listeners.get(depth);\n\n if (!set) {\n set = new Set();\n listeners.set(depth, set);\n }\n\n set.add(listener);\n\n return () => {\n set!.delete(listener);\n if (set!.size === 0) listeners.delete(depth);\n };\n}\n\n/** Everything a boundary at this depth needs to render, or null for \"use children\". */\nexport function getSegmentState(depth: number): DepthState | null {\n return depths.get(depth) ?? null;\n}\n\n/** Apply the retention window to a candidate state. */\nfunction retain(\n entries: readonly Entry[],\n order: readonly string[],\n activeKey: string,\n): DepthState {\n const kept = order.slice(-RETENTION);\n\n return {\n entries: entries.filter((entry) => kept.includes(entry.key) || entry.speculative),\n order: kept,\n activeKey,\n };\n}\n\nfunction put(depth: number, key: string, tree: Tree, identity?: string): void {\n const state = depths.get(depth);\n // The same page again keeps the identity it is mounted under.\n const kept = identity ?? state?.entries.find((entry) => entry.key === key)?.identity;\n const entries = [\n ...(state?.entries ?? []).filter((entry) => entry.key !== key),\n { key, tree, at: Date.now(), seq: ++tick, ...(kept !== undefined && kept !== key ? { identity: kept } : {}) },\n ];\n const order = [...(state?.order ?? []).filter((k) => k !== key), key];\n\n depths.set(depth, retain(entries, order, key));\n}\n\n/**\n * Show `tree` at `depth` for `key`, retaining what was there.\n *\n * Deeper segments belonged to the page being replaced; leaving them would\n * render the previous page inside the new one.\n */\nexport function setSegment(\n depth: number,\n key: string,\n tree: Tree,\n inPlaceOf?: string | null,\n keepsPrevious?: boolean,\n): void {\n // A guess about another page was wrong; the one about this page, if there\n // was one, is replaced by put() below - with the same tree, when the\n // prerender and the navigation read the same decoded payload, which is\n // what makes the update a reveal rather than a render.\n dropSpeculative(depth, key);\n\n // The same path with a new query is the page on screen, moved - not a new\n // one to mount beside it, which would show every fallback in the page. It\n // takes the old entry's identity, so React updates the page it has. A\n // replace has left that entry nothing to go back to, and it goes. A push\n // keeps it for Back, under an identity of its own: mounted again behind the\n // page, where its tree is already decoded and nothing waits.\n const state = depths.get(depth);\n const replaced =\n inPlaceOf && inPlaceOf !== key && !state?.entries.some((entry) => entry.key === key)\n ? state?.entries.find((entry) => entry.key === inPlaceOf)\n : undefined;\n\n if (replaced && state) {\n depths.set(\n depth,\n keepsPrevious\n ? { ...state, entries: state.entries.map((entry) => (entry === replaced ? { ...entry, identity: `${entry.key}#${++tick}` } : entry)) }\n : {\n ...state,\n entries: state.entries.filter((entry) => entry !== replaced),\n order: state.order.filter((k) => k !== replaced.key),\n },\n );\n }\n\n put(depth, key, tree, replaced ? (replaced.identity ?? replaced.key) : undefined);\n\n const stale = [...depths.keys()].filter((d) => d > depth);\n for (const d of stale) depths.delete(d);\n\n notify(depth);\n for (const d of stale) notify(d);\n}\n\nfunction dropSpeculative(depth: number, except?: string): void {\n const state = depths.get(depth);\n\n if (!state?.entries.some((entry) => entry.speculative && entry.key !== except)) return;\n\n depths.set(depth, {\n ...state,\n entries: state.entries.filter((entry) => !entry.speculative || entry.key === except),\n });\n}\n\n/**\n * Render a page hidden at `depth`, before any navigation to it.\n *\n * The click then finds the work done: setSegment with the same tree is a\n * bail-out for React - same element, same props - and the Activity flips\n * from hidden to visible. What was 87 ms of rendering on a phone, after\n * the tap, is paid before the finger lifts, at idle priority, yielding to\n * the scroll. Never changes what is showing, never counts against\n * retention, and a page already held needs nothing.\n */\nexport function prerenderSegment(depth: number, key: string, tree: Tree): void {\n const state = depths.get(depth);\n\n // Nothing at this depth yet: the boundary is still showing the server's\n // children, and it has no page key to keep them under. A guess would take\n // the store over with no active entry to show. seedSegment runs on mount,\n // so this is the gap between hydration and that effect; the click will\n // render.\n if (!state) return;\n\n if (state.entries.some((entry) => entry.key === key)) return;\n\n depths.set(depth, {\n ...state,\n entries: [\n ...state.entries.filter((entry) => !entry.speculative),\n { key, tree, at: Date.now(), seq: ++tick, speculative: true },\n ],\n });\n\n notify(depth);\n}\n\n/** Whether a page is rendered hidden at `depth`, ahead of a navigation to it. */\nexport function isPrerendered(depth: number, key: string): boolean {\n return depths.get(depth)?.entries.some((entry) => entry.key === key && entry.speculative) ?? false;\n}\n\n/**\n * Give the page on screen at `depth` a new tree, under the key it has.\n *\n * For revalidate(\"all\"): the whole document rendered again, in place. The\n * root hands its layout the new tree, the layout hands its boundary new\n * children, and a boundary that holds state would otherwise keep showing\n * the store's tree and drop the new one on the floor - or, cleared first,\n * re-key its Activity to the current url and remount everything under it.\n * Neither. The entry that is showing takes the new tree and keeps its key,\n * so React reconciles the page in place, and the boundary below gets its\n * new children the same way.\n */\nexport function replaceActive(depth: number, tree: Tree): void {\n const state = depths.get(depth);\n\n if (!state) return;\n\n const active = state.entries.find((entry) => entry.key === state.activeKey);\n\n if (!active || active.tree === tree) return;\n\n // The tree changes; when the entry arrived does not. A layout's entry is\n // keyed by the page it was seeded with - \"/\" for a document loaded at\n // home - and stays active through every navigation below it. Stamped\n // with the re-render's time it read as held since the mutation, and a\n // tap on the brand link after \"Add to cart\" revealed it: the url went\n // to \"/\" and the product stayed, because the tree under that key was\n // now the product's document. See dropHidden and isHeld.\n depths.set(depth, {\n ...state,\n entries: state.entries.map((entry) => (entry === active ? { ...entry, tree } : entry)),\n });\n notify(depth);\n}\n\n/**\n * Record the children the server rendered, so the page you arrived on can be\n * returned to later. Never changes what is showing.\n */\nexport function seedSegment(depth: number, key: string, tree: Tree, identity?: string): void {\n const state = depths.get(depth);\n\n if (state?.entries.some((entry) => entry.key === key)) return;\n\n if (!state) {\n put(depth, key, tree, identity);\n notify(depth);\n\n return;\n }\n\n // Older than whatever is showing, so it goes to the front of the eviction\n // order — and crucially does not become the active page.\n depths.set(\n depth,\n retain(\n [...state.entries, { key, tree, at: Date.now(), seq: ++tick, ...(identity !== undefined && identity !== key ? { identity } : {}) }],\n [key, ...state.order.filter((k) => k !== key)],\n state.activeKey,\n ),\n );\n\n notify(depth);\n}\n\n/**\n * Reveal a page the boundaries are still holding, without asking the server.\n *\n * Restoring is anchored on the deepest boundary that can show the page. Deeper\n * ones than that belonged to the page being left — a section with its own\n * layout adds a boundary the page you are going back to never had — so they\n * are dropped, exactly as setSegment drops them. Requiring every boundary to\n * hold the key instead made any such page refuse to restore.\n *\n * Shallower boundaries need no key of their own: their trees contain the\n * deeper boundary, so they delegate to whatever it is showing. One that does\n * hold the key is switched to it, since that is a real change at its level.\n */\n/**\n * Whether a link to `key` would be answered by revealing a held page.\n *\n * The question restoreSegments answers, asked without acting on it: a\n * prefetch of a page the boundaries still hold is a request for nothing -\n * a navigation would reveal it. The page just left is the usual case, one\n * wasted payload per navigation.\n */\nexport function isHeld(key: string, maxAge?: number): boolean {\n const ages = [...depths.values()].flatMap((state) =>\n state.entries.filter((entry) => entry.key === key && !entry.speculative && entry.seq > invalidatedAt).map((entry) => entry.at),\n );\n\n if (ages.length === 0) return false;\n if (maxAge === undefined) return true;\n\n return Date.now() - Math.min(...ages) < maxAge;\n}\n\n/**\n * Drop every page held behind the one on screen.\n *\n * After a mutation: a page kept for the back button holds the data from\n * before it, and revealing it would show a row that is gone, a name that\n * changed. The page on screen stays - it was re-rendered by the action, or\n * is about to be.\n */\nexport function dropHidden(): void {\n // And the ones that stay - the active entry at each depth - are from\n // before it too. A layout's entry is keyed by the page it was seeded\n // with, and revealing that key later shows the page inside it as it was\n // then. Nothing seeded before this moment is revealed again.\n invalidatedAt = ++tick;\n\n for (const [depth, state] of depths) {\n if (state.entries.length <= 1 && !state.entries.some((entry) => entry.speculative)) continue;\n\n depths.set(depth, {\n entries: state.entries.filter((entry) => entry.key === state.activeKey),\n order: state.order.filter((key) => key === state.activeKey),\n activeKey: state.activeKey,\n });\n notify(depth);\n }\n}\n\n/**\n * Reveal a page still being held, if it is worth revealing.\n *\n * `maxAge` is what a link passes and the back button does not. Going back is\n * unambiguous — it names a moment, and the page from that moment is the right\n * answer however old. A link says \"go here\", and answering it with a tree from\n * twenty minutes ago is stale data presented as fresh, which is the objection\n * this design started with. Recent enough, and it is the same page you were\n * just on, with the form you were filling in still filled in.\n */\nexport function restoreSegments(key: string, maxAge?: number): boolean {\n // A guess is not a held page: revealing it would be showing prefetched\n // data as the page the visitor was on. The navigation takes the\n // prerendered tree through setSegment instead, where it is a reveal too.\n // Nor is a page from before a mutation - see dropHidden.\n const holding = [...depths.keys()].filter((d) =>\n depths.get(d)!.entries.some((entry) => entry.key === key && !entry.speculative && entry.seq > invalidatedAt),\n );\n\n if (holding.length === 0) return false;\n\n if (maxAge !== undefined) {\n const ages = holding.flatMap((d) =>\n depths\n .get(d)!\n .entries.filter((entry) => entry.key === key)\n .map((entry) => entry.at),\n );\n\n // The oldest layer decides: revealing a fresh page under a stale layout\n // would be a chain nobody rendered together.\n // >= rather than >, so a window of 0 means never rather than \"only within\n // the same millisecond\".\n if (Date.now() - Math.min(...ages) >= maxAge) return false;\n }\n\n const anchor = Math.max(...holding);\n\n for (const d of [...depths.keys()].filter((d) => d > anchor)) {\n depths.delete(d);\n notify(d);\n }\n\n for (const d of holding) {\n dropSpeculative(d);\n\n const state = depths.get(d)!;\n\n depths.set(\n d,\n retain(\n state.entries,\n [...state.order.filter((k) => k !== key), key],\n key,\n ),\n );\n notify(d);\n }\n\n return true;\n}\n\n/**\n * Drop everything, so boundaries fall back to their server-given children.\n *\n * A deployment invalidates them all: a segment from the previous build has no\n * claim on being correct for this one.\n */\nexport function clearSegments(): void {\n const all = [...depths.keys()];\n depths.clear();\n for (const depth of all) notify(depth);\n}\n\n/**\n * Whether a navigation has happened in this document.\n *\n * The first commit after hydration is the boundary taking over its\n * server-rendered children - the same page, re-keyed for retention. A view\n * transition for that commit fades the page into itself: the blank-then-\n * content a first load or a reload showed. Inertia transitions on visits and\n * never on load; so does this. The router notes the first navigation before\n * it updates a segment, and the boundary animates from then on.\n */\nlet navigated = false;\n\nexport function noteNavigation(): void {\n navigated = true;\n}\n\nexport function navigatedOnce(): boolean {\n return navigated;\n}\n"]}
package/dist/routes.d.ts CHANGED
@@ -43,7 +43,19 @@ type OffRoute = `${string}://${string}` | `mailto:${string}` | `tel:${string}` |
43
43
  * `Route` is the same type under the name Next uses, so a port keeps the
44
44
  * word it already has.
45
45
  */
46
- export type Route = Unregistered extends true ? string : Filled<RoutePattern> | `${Filled<RoutePattern>}?${string}` | `${Filled<RoutePattern>}#${string}` | Filled<ApiPattern> | `${Filled<ApiPattern>}?${string}` | OffRoute;
46
+ export type Route = Unregistered extends true ? string : Filled<RoutePattern> | `${Filled<RoutePattern>}?${string}` | `${Filled<RoutePattern>}#${string}` | Filled<ApiPattern> | `${Filled<ApiPattern>}?${string}` | BackendUrl | OffRoute;
47
+ /** Augmented by the generated `rsc-routes.d.ts`, like `Register`. */
48
+ export interface RegisterBackend {
49
+ }
50
+ /** The prefixes `backendPaths` named: `'/gitlab'`. */
51
+ type BackendPrefix = RegisterBackend extends {
52
+ paths: infer P extends string;
53
+ } ? P : never;
54
+ /**
55
+ * A prefix and everything under it, at a segment boundary: `/gitlab`,
56
+ * `/gitlab/connect?next=/`, never `/gitlabs`. `never` when nothing was named.
57
+ */
58
+ type BackendUrl = BackendPrefix | `${BackendPrefix}/${string}` | `${BackendPrefix}?${string}` | `${BackendPrefix}#${string}`;
47
59
  /** Augmented by the generated `rsc-routes.d.ts`, like `Register`. */
48
60
  export interface RegisterApi {
49
61
  }
@@ -1 +1 @@
1
- {"version":3,"file":"routes.js","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"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","sourcesContent":["// Typed routes: the urls this app can actually answer, as a type.\n//\n// Laravel needs route() because the url lives in PHP and can move\n// independently of the name it is called by. Here the url *is* the file path,\n// so a name would be indirection that buys nothing. What is worth having is\n// the other half — a link to a page that does not exist should fail at build\n// time rather than in the browser.\n//\n// There is no route() builder to go with this, deliberately. A template\n// literal is checked the same way — `/posts/${slug}` compiles and\n// `/postz/${slug}` does not — so a builder would only wrap what the language\n// already does. Encoding a value that is not url-safe is `encodeURIComponent`\n// in the template, the same as anywhere else.\n//\n// The build already walks app/ and knows every route's segments, so it writes\n// one line into the app's source dir:\n//\n// declare module '@rsc-kit/core/routes' {\n// interface Register { routes: '/' | '/posts/[slug]' }\n// }\n//\n// Everything below is derived from that union. An app that never runs the\n// generator — a generic host, a Laravel app that has not rebuilt — registers\n// nothing, `RoutePattern` stays `string`, and every url-taking API is exactly\n// as permissive as it was before. That fallback is the reason this can ship\n// without a flag.\n\n/**\n * Augmented by the generated `rsc-routes.d.ts`. Empty here on purpose.\n *\n * Declaration merging rather than a generic parameter, because the routes are\n * a property of the project, not of each call site — threading them through\n * every component that renders a Link is not a thing anyone would do twice.\n */\nexport interface Register {}\n\n/** The route patterns this app declared: `'/posts/[slug]'`. */\nexport type RoutePattern = Register extends { routes: infer R extends string }\n ? R\n : string;\n\n/** Whether anything was registered. `string` means the generator never ran. */\ntype Unregistered = string extends RoutePattern ? true : false;\n\n/**\n * A pattern with its dynamic segments opened up: `/posts/[slug]` accepts\n * `/posts/anything`.\n *\n * Catch-all and single params both become `${string}`, which for a catch-all\n * also swallows the slashes — `/docs/[...path]` accepts `/docs/a/b/c`.\n */\ntype Filled<P extends string> = P extends `${infer A}[...${string}]${infer B}`\n ? `${A}${string}${Filled<B>}`\n : P extends `${infer A}[${string}]${infer B}`\n ? `${A}${string}${Filled<B>}`\n : P;\n\n/**\n * Not a route, but a legitimate href: another site, a mail client, a phone\n * number, an anchor on this page, a bare query string.\n */\ntype OffRoute =\n | `${string}://${string}`\n | `mailto:${string}`\n | `tel:${string}`\n | `#${string}`\n | `?${string}`;\n\n/**\n * A url this app can answer, or one that deliberately leaves it.\n *\n * Cast when the destination is computed rather than written:\n * `href={path as Route}`.\n */\n/**\n * A url this app answers - a page or a route.ts - or one that deliberately\n * leaves it. One type, as Next's `Route` is one type: what `<Link href>`,\n * `visit()` and `redirect()` take. A route.ts is a full navigation rather\n * than a payload fetch, and never prefetched - the client knows which urls\n * are routes and treats a link to one as the anchor it is - so the type\n * does not have to keep them apart to keep a hover from running one.\n *\n * `Route` is the same type under the name Next uses, so a port keeps the\n * word it already has.\n */\nexport type Route = Unregistered extends true\n ? string\n : | Filled<RoutePattern>\n | `${Filled<RoutePattern>}?${string}`\n | `${Filled<RoutePattern>}#${string}`\n | Filled<ApiPattern>\n | `${Filled<ApiPattern>}?${string}`\n | OffRoute;\n\n// ── Api routes ───────────────────────────────────────────────────────────────\n//\n// Their own union rather than part of Route, because they are not pages and a\n// link to one is almost always a mistake — an <a href=\"/api/orders\"> navigates\n// the browser away to a json document. Keeping them apart means `Link` refuses\n// an api url and `apiUrl()` refuses a page, which is the pair of mistakes worth\n// catching.\n\n/** Augmented by the generated `rsc-routes.d.ts`, like `Register`. */\nexport interface RegisterApi {}\n\n// ── Regions a page can re-render on its own ──────────────────────────────────\n//\n// Every `section('name', …)` the build found, and every `@slot` directory.\n// `revalidate('orders')` is checked against them, so a name that matches no\n// section - a typo, or a section renamed since - stops compiling instead of\n// being refused by the renderer at runtime.\n\n/** Augmented by the generated `rsc-routes.d.ts`, like `Register`. */\nexport interface RegisterRegions {}\n\n/** The region names the build found: section names and slot names. */\nexport type RegionName = RegisterRegions extends {\n regions: infer R extends string;\n}\n ? R\n : string;\n\ntype NoRegions = string extends RegionName ? true : false;\n\n/**\n * What `revalidate()` takes: the page, the whole document, or a region by\n * name. `string` until the build has written the types.\n */\nexport type RevalidateTarget = NoRegions extends true\n ? string\n : \"page\" | \"all\" | RegionName;\n\n/** The api route patterns this app declared: `'/api/orders/[id]'`. */\nexport type ApiPattern = RegisterApi extends { apis: infer R extends string }\n ? R\n : string;\n\ntype NoApis = string extends ApiPattern ? true : false;\n\n/**\n * A url an api route in this app answers.\n *\n * `/api/orders/[id]` accepts `/api/orders/42`, and a query string is allowed\n * because that is how a GET is parameterised.\n */\nexport type ApiRoute = NoApis extends true\n ? string\n : Filled<ApiPattern> | `${Filled<ApiPattern>}?${string}`;\n\n/**\n * An api url, checked against the routes the build found.\n *\n * await fetch(apiUrl(`/api/orders/${id}`))\n *\n * A function rather than a bare type so it can be used inline at a call site\n * that is typed `string` — `fetch` takes any string, so nothing would check the\n * argument without somewhere to put the type. It returns what it was given.\n *\n * Wrong path, and it stops compiling. Renamed the directory, and every call\n * site says so rather than one of them 404ing in production.\n */\nexport function apiUrl(href: ApiRoute): string {\n return href;\n}\n\n// ── What an events route sends ───────────────────────────────────────────────\n//\n// A route.ts whose GET is `events(...)` says, as a type, what its generator\n// yields. The generated file records that per api pattern:\n//\n// interface RegisterApi {\n// events: { '/api/orders/[id]/events': EventsExportOf<typeof import('../src/app/api/orders/[id]/events/route')> }\n// }\n//\n// and `useEvents` reads a url's messages from it, so the hook and the route\n// cannot disagree about a message's shape.\n\n/** What a route module's GET yields, when it is `events(...)`; `unknown` for any other stream. */\nexport type EventsExportOf<M> = M extends {\n GET: { readonly \"~events\"?: infer S };\n}\n ? NonNullable<S> extends { yields: infer Y }\n ? Y\n : unknown\n : unknown;\n\ntype EventsMap = RegisterApi extends { events: infer M } ? M : {};\n\ntype Split<S extends string> = S extends `${infer A}/${infer B}`\n ? [A, ...Split<B>]\n : [S];\n\n/**\n * Whether a url's segments fit a pattern's, one segment at a time. Unlike\n * `Filled`, a `[id]` takes exactly one segment, so `/api/orders/42/events`\n * is `/api/orders/[id]/events` and not also `/api/orders/[id]`.\n */\ntype Fits<H extends string[], P extends string[]> = P extends [\n infer P0 extends string,\n ...infer PR extends string[],\n]\n ? P0 extends `[...${string}]`\n ? H extends [string, ...string[]]\n ? true\n : false\n : H extends [infer H0 extends string, ...infer HR extends string[]]\n ? P0 extends `[${string}]`\n ? Fits<HR, PR>\n : H0 extends P0\n ? Fits<HR, PR>\n : false\n : false\n : H extends []\n ? true\n : false;\n\n/** The api pattern a written url belongs to: `/api/orders/42/events` is `/api/orders/[id]/events`. */\ntype ApiPatternOf<H extends string> = ApiPattern extends infer P\n ? P extends string\n ? Fits<Split<StripQuery<H>>, Split<P>> extends true\n ? P\n : never\n : never\n : never;\n\n/**\n * Everything the stream at `H` yields, named events included. `unknown` when\n * `H` is not one api route the build found - a url computed at runtime, or\n * another site's - since there is nothing to read it from.\n */\nexport type EventsAt<H extends string> = NoApis extends true\n ? unknown\n : IsUnion<H> extends true\n ? unknown\n : [ApiPatternOf<H>] extends [never]\n ? unknown\n : ApiPatternOf<H> extends keyof EventsMap\n ? EventsMap[ApiPatternOf<H>]\n : unknown;\n\n/** A url `useEvents` may open: an api route this app answers, or another site's stream. */\nexport type EventsUrl = ApiRoute | `${string}://${string}`;\n\n// ── Search params, typed per route ───────────────────────────────────────────\n//\n// A page that exports a `searchParams` schema has said what its query string\n// means. The generated file records that schema per pattern:\n//\n// interface Register {\n// search: { '/search': SearchExportOf<typeof import('../src/app/search/page')> }\n// }\n//\n// and from there a link to `/search` is checked against the same schema the\n// page parses with — a `page` that must be a number is a number on the link,\n// a `q` the page requires is required to write the link, and a key the page\n// never reads does not compile. One schema, both ends. A route that exports\n// none takes anything; an href that is not a single route (computed, cast, or\n// off-site) takes anything too, because there is nothing to check it against.\n\n/** What a page module contributes: its `searchParams` export, or nothing. For the generated file. */\nexport type SearchExportOf<M> = M extends { searchParams: infer S }\n ? S\n : undefined;\n\ntype SearchMap = Register extends { search: infer M } ? M : {};\n\ntype StripQuery<H extends string> = H extends `${infer P}?${string}`\n ? P\n : H extends `${infer P}#${string}`\n ? P\n : H;\n\n/** The pattern a written href belongs to: `/posts/hello` is `/posts/[slug]`. */\ntype PatternOf<H extends string> = RoutePattern extends infer P\n ? P extends string\n ? StripQuery<H> extends Filled<P>\n ? P\n : never\n : never\n : never;\n\ntype IsUnion<T, U = T> = T extends unknown\n ? [U] extends [T]\n ? false\n : true\n : never;\n\ntype SchemaFor<P> = P extends keyof SearchMap ? SearchMap[P] : undefined;\n\ntype InputOf<S> = S extends { \"~standard\": { types?: { input: infer I } } }\n ? I\n : never;\ntype OutputOf<S> = S extends { \"~standard\": { types?: { output: infer O } } }\n ? O\n : never;\n\ntype OptionalKeys<T> = {\n [K in keyof T]-?: {} extends Pick<T, K> ? K : never;\n}[keyof T];\ntype RequiredKeys<T> = Exclude<keyof T, OptionalKeys<T>>;\ntype Simplify<T> = { [K in keyof T]: T[K] } & {};\n\n/**\n * What a link may write for a schema: the keys the schema's input requires\n * are required, the rest optional, and every value is the schema's OUTPUT\n * type. Output rather than input because `z.coerce.number()` takes `unknown`\n * in - that is what coercion means - and a link typed by it would accept\n * `page: 'two'`. The output is the number the page will actually see.\n */\ntype LinkInputOf<S> = Simplify<\n { [K in RequiredKeys<InputOf<S>> & keyof OutputOf<S>]: OutputOf<S>[K] } & {\n [K in OptionalKeys<InputOf<S>> & keyof OutputOf<S>]?: OutputOf<S>[K];\n }\n>;\n\ntype Scalar = string | number | boolean | null | undefined;\n\n/** What a link may carry when nothing declares otherwise. */\nexport type LooseSearch = Record<string, Scalar | readonly (string | number)[]>;\n\n/**\n * The search params a link to `H` may carry.\n *\n * The page's schema input when `H` is one declared route with a schema;\n * otherwise anything. \"One route\" matters: `path as Route` is every route at\n * once, and a link that could go anywhere cannot be held to one page's schema.\n */\nexport type SearchFor<H extends string> = Unregistered extends true\n ? LooseSearch\n : IsUnion<H> extends true\n ? LooseSearch\n : [PatternOf<H>] extends [never]\n ? LooseSearch\n : SchemaFor<PatternOf<H>> extends undefined\n ? LooseSearch\n : LinkInputOf<SchemaFor<PatternOf<H>>>;\n\n/**\n * The `search` prop, required exactly when the page's schema has a required\n * key. A page that needs `q` is not reachable without one, so the link that\n * omits it is the bug — caught here rather than on the page's error boundary.\n */\nexport type SearchProp<H extends string> =\n {} extends SearchFor<H>\n ? { search?: SearchFor<H> }\n : { search: SearchFor<H> };\n\n/** A query string from an object: scalars stringified, arrays repeated, null and undefined dropped. */\nexport function searchString(search: object): string {\n const params = new URLSearchParams();\n\n for (const [key, value] of Object.entries(search)) {\n if (value === null || value === undefined) continue;\n\n if (Array.isArray(value)) {\n for (const item of value) params.append(key, String(item));\n } else {\n params.set(key, String(value));\n }\n }\n\n return params.toString();\n}\n\n/** `path` with `search` appended, keeping any query and hash already on it. */\nexport function withSearch(path: string, search: object | undefined): string {\n if (!search) return path;\n\n const hashAt = path.indexOf(\"#\");\n const hash = hashAt === -1 ? \"\" : path.slice(hashAt);\n const before = hashAt === -1 ? path : path.slice(0, hashAt);\n const queryAt = before.indexOf(\"?\");\n const base = queryAt === -1 ? before : before.slice(0, queryAt);\n const existing = queryAt === -1 ? \"\" : before.slice(queryAt + 1);\n const added = searchString(search);\n const query = [existing, added].filter(Boolean).join(\"&\");\n\n return query ? `${base}?${query}${hash}` : `${base}${hash}`;\n}\n\n/**\n * A typed url with its search params, for the places that take a string.\n *\n * visit(href('/search', { q: 'shoes', page: 2 }))\n *\n * `Link` has the same check on its own `search` prop. This is for `visit`,\n * `prefetch`, `redirect` and anything else that wants the finished string.\n */\nexport function href<H extends Route>(\n path: H,\n ...rest: {} extends SearchFor<H>\n ? [search?: SearchFor<H>]\n : [search: SearchFor<H>]\n): Route {\n return withSearch(path, rest[0] as object | undefined) as Route;\n}\n"]}
1
+ {"version":3,"file":"routes.js","sourceRoot":"","sources":["../src/routes.ts"],"names":[],"mappings":"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","sourcesContent":["// Typed routes: the urls this app can actually answer, as a type.\n//\n// Laravel needs route() because the url lives in PHP and can move\n// independently of the name it is called by. Here the url *is* the file path,\n// so a name would be indirection that buys nothing. What is worth having is\n// the other half — a link to a page that does not exist should fail at build\n// time rather than in the browser.\n//\n// There is no route() builder to go with this, deliberately. A template\n// literal is checked the same way — `/posts/${slug}` compiles and\n// `/postz/${slug}` does not — so a builder would only wrap what the language\n// already does. Encoding a value that is not url-safe is `encodeURIComponent`\n// in the template, the same as anywhere else.\n//\n// The build already walks app/ and knows every route's segments, so it writes\n// one line into the app's source dir:\n//\n// declare module '@rsc-kit/core/routes' {\n// interface Register { routes: '/' | '/posts/[slug]' }\n// }\n//\n// Everything below is derived from that union. An app that never runs the\n// generator — a generic host, a Laravel app that has not rebuilt — registers\n// nothing, `RoutePattern` stays `string`, and every url-taking API is exactly\n// as permissive as it was before. That fallback is the reason this can ship\n// without a flag.\n\n/**\n * Augmented by the generated `rsc-routes.d.ts`. Empty here on purpose.\n *\n * Declaration merging rather than a generic parameter, because the routes are\n * a property of the project, not of each call site — threading them through\n * every component that renders a Link is not a thing anyone would do twice.\n */\nexport interface Register {}\n\n/** The route patterns this app declared: `'/posts/[slug]'`. */\nexport type RoutePattern = Register extends { routes: infer R extends string }\n ? R\n : string;\n\n/** Whether anything was registered. `string` means the generator never ran. */\ntype Unregistered = string extends RoutePattern ? true : false;\n\n/**\n * A pattern with its dynamic segments opened up: `/posts/[slug]` accepts\n * `/posts/anything`.\n *\n * Catch-all and single params both become `${string}`, which for a catch-all\n * also swallows the slashes — `/docs/[...path]` accepts `/docs/a/b/c`.\n */\ntype Filled<P extends string> = P extends `${infer A}[...${string}]${infer B}`\n ? `${A}${string}${Filled<B>}`\n : P extends `${infer A}[${string}]${infer B}`\n ? `${A}${string}${Filled<B>}`\n : P;\n\n/**\n * Not a route, but a legitimate href: another site, a mail client, a phone\n * number, an anchor on this page, a bare query string.\n */\ntype OffRoute =\n | `${string}://${string}`\n | `mailto:${string}`\n | `tel:${string}`\n | `#${string}`\n | `?${string}`;\n\n/**\n * A url this app can answer, or one that deliberately leaves it.\n *\n * Cast when the destination is computed rather than written:\n * `href={path as Route}`.\n */\n/**\n * A url this app answers - a page or a route.ts - or one that deliberately\n * leaves it. One type, as Next's `Route` is one type: what `<Link href>`,\n * `visit()` and `redirect()` take. A route.ts is a full navigation rather\n * than a payload fetch, and never prefetched - the client knows which urls\n * are routes and treats a link to one as the anchor it is - so the type\n * does not have to keep them apart to keep a hover from running one.\n *\n * `Route` is the same type under the name Next uses, so a port keeps the\n * word it already has.\n */\nexport type Route = Unregistered extends true\n ? string\n : | Filled<RoutePattern>\n | `${Filled<RoutePattern>}?${string}`\n | `${Filled<RoutePattern>}#${string}`\n | Filled<ApiPattern>\n | `${Filled<ApiPattern>}?${string}`\n | BackendUrl\n | OffRoute;\n\n// ── The backend's urls ───────────────────────────────────────────────────────\n//\n// `rscKit({ backendPaths: ['/gitlab'] })` names the prefixes the backend owns.\n// They are not in the route tree, so a link or a redirect to `/gitlab/connect`\n// needed a cast to compile, for a url the app itself had declared. They are a\n// place to go and, like a route.ts, a full page load.\n\n/** Augmented by the generated `rsc-routes.d.ts`, like `Register`. */\nexport interface RegisterBackend {}\n\n/** The prefixes `backendPaths` named: `'/gitlab'`. */\ntype BackendPrefix = RegisterBackend extends { paths: infer P extends string }\n ? P\n : never;\n\n/**\n * A prefix and everything under it, at a segment boundary: `/gitlab`,\n * `/gitlab/connect?next=/`, never `/gitlabs`. `never` when nothing was named.\n */\ntype BackendUrl =\n | BackendPrefix\n | `${BackendPrefix}/${string}`\n | `${BackendPrefix}?${string}`\n | `${BackendPrefix}#${string}`;\n\n// ── Api routes ───────────────────────────────────────────────────────────────\n//\n// Their own union rather than part of Route, because they are not pages and a\n// link to one is almost always a mistake — an <a href=\"/api/orders\"> navigates\n// the browser away to a json document. Keeping them apart means `Link` refuses\n// an api url and `apiUrl()` refuses a page, which is the pair of mistakes worth\n// catching.\n\n/** Augmented by the generated `rsc-routes.d.ts`, like `Register`. */\nexport interface RegisterApi {}\n\n// ── Regions a page can re-render on its own ──────────────────────────────────\n//\n// Every `section('name', …)` the build found, and every `@slot` directory.\n// `revalidate('orders')` is checked against them, so a name that matches no\n// section - a typo, or a section renamed since - stops compiling instead of\n// being refused by the renderer at runtime.\n\n/** Augmented by the generated `rsc-routes.d.ts`, like `Register`. */\nexport interface RegisterRegions {}\n\n/** The region names the build found: section names and slot names. */\nexport type RegionName = RegisterRegions extends {\n regions: infer R extends string;\n}\n ? R\n : string;\n\ntype NoRegions = string extends RegionName ? true : false;\n\n/**\n * What `revalidate()` takes: the page, the whole document, or a region by\n * name. `string` until the build has written the types.\n */\nexport type RevalidateTarget = NoRegions extends true\n ? string\n : \"page\" | \"all\" | RegionName;\n\n/** The api route patterns this app declared: `'/api/orders/[id]'`. */\nexport type ApiPattern = RegisterApi extends { apis: infer R extends string }\n ? R\n : string;\n\ntype NoApis = string extends ApiPattern ? true : false;\n\n/**\n * A url an api route in this app answers.\n *\n * `/api/orders/[id]` accepts `/api/orders/42`, and a query string is allowed\n * because that is how a GET is parameterised.\n */\nexport type ApiRoute = NoApis extends true\n ? string\n : Filled<ApiPattern> | `${Filled<ApiPattern>}?${string}`;\n\n/**\n * An api url, checked against the routes the build found.\n *\n * await fetch(apiUrl(`/api/orders/${id}`))\n *\n * A function rather than a bare type so it can be used inline at a call site\n * that is typed `string` — `fetch` takes any string, so nothing would check the\n * argument without somewhere to put the type. It returns what it was given.\n *\n * Wrong path, and it stops compiling. Renamed the directory, and every call\n * site says so rather than one of them 404ing in production.\n */\nexport function apiUrl(href: ApiRoute): string {\n return href;\n}\n\n// ── What an events route sends ───────────────────────────────────────────────\n//\n// A route.ts whose GET is `events(...)` says, as a type, what its generator\n// yields. The generated file records that per api pattern:\n//\n// interface RegisterApi {\n// events: { '/api/orders/[id]/events': EventsExportOf<typeof import('../src/app/api/orders/[id]/events/route')> }\n// }\n//\n// and `useEvents` reads a url's messages from it, so the hook and the route\n// cannot disagree about a message's shape.\n\n/** What a route module's GET yields, when it is `events(...)`; `unknown` for any other stream. */\nexport type EventsExportOf<M> = M extends {\n GET: { readonly \"~events\"?: infer S };\n}\n ? NonNullable<S> extends { yields: infer Y }\n ? Y\n : unknown\n : unknown;\n\ntype EventsMap = RegisterApi extends { events: infer M } ? M : {};\n\ntype Split<S extends string> = S extends `${infer A}/${infer B}`\n ? [A, ...Split<B>]\n : [S];\n\n/**\n * Whether a url's segments fit a pattern's, one segment at a time. Unlike\n * `Filled`, a `[id]` takes exactly one segment, so `/api/orders/42/events`\n * is `/api/orders/[id]/events` and not also `/api/orders/[id]`.\n */\ntype Fits<H extends string[], P extends string[]> = P extends [\n infer P0 extends string,\n ...infer PR extends string[],\n]\n ? P0 extends `[...${string}]`\n ? H extends [string, ...string[]]\n ? true\n : false\n : H extends [infer H0 extends string, ...infer HR extends string[]]\n ? P0 extends `[${string}]`\n ? Fits<HR, PR>\n : H0 extends P0\n ? Fits<HR, PR>\n : false\n : false\n : H extends []\n ? true\n : false;\n\n/** The api pattern a written url belongs to: `/api/orders/42/events` is `/api/orders/[id]/events`. */\ntype ApiPatternOf<H extends string> = ApiPattern extends infer P\n ? P extends string\n ? Fits<Split<StripQuery<H>>, Split<P>> extends true\n ? P\n : never\n : never\n : never;\n\n/**\n * Everything the stream at `H` yields, named events included. `unknown` when\n * `H` is not one api route the build found - a url computed at runtime, or\n * another site's - since there is nothing to read it from.\n */\nexport type EventsAt<H extends string> = NoApis extends true\n ? unknown\n : IsUnion<H> extends true\n ? unknown\n : [ApiPatternOf<H>] extends [never]\n ? unknown\n : ApiPatternOf<H> extends keyof EventsMap\n ? EventsMap[ApiPatternOf<H>]\n : unknown;\n\n/** A url `useEvents` may open: an api route this app answers, or another site's stream. */\nexport type EventsUrl = ApiRoute | `${string}://${string}`;\n\n// ── Search params, typed per route ───────────────────────────────────────────\n//\n// A page that exports a `searchParams` schema has said what its query string\n// means. The generated file records that schema per pattern:\n//\n// interface Register {\n// search: { '/search': SearchExportOf<typeof import('../src/app/search/page')> }\n// }\n//\n// and from there a link to `/search` is checked against the same schema the\n// page parses with — a `page` that must be a number is a number on the link,\n// a `q` the page requires is required to write the link, and a key the page\n// never reads does not compile. One schema, both ends. A route that exports\n// none takes anything; an href that is not a single route (computed, cast, or\n// off-site) takes anything too, because there is nothing to check it against.\n\n/** What a page module contributes: its `searchParams` export, or nothing. For the generated file. */\nexport type SearchExportOf<M> = M extends { searchParams: infer S }\n ? S\n : undefined;\n\ntype SearchMap = Register extends { search: infer M } ? M : {};\n\ntype StripQuery<H extends string> = H extends `${infer P}?${string}`\n ? P\n : H extends `${infer P}#${string}`\n ? P\n : H;\n\n/** The pattern a written href belongs to: `/posts/hello` is `/posts/[slug]`. */\ntype PatternOf<H extends string> = RoutePattern extends infer P\n ? P extends string\n ? StripQuery<H> extends Filled<P>\n ? P\n : never\n : never\n : never;\n\ntype IsUnion<T, U = T> = T extends unknown\n ? [U] extends [T]\n ? false\n : true\n : never;\n\ntype SchemaFor<P> = P extends keyof SearchMap ? SearchMap[P] : undefined;\n\ntype InputOf<S> = S extends { \"~standard\": { types?: { input: infer I } } }\n ? I\n : never;\ntype OutputOf<S> = S extends { \"~standard\": { types?: { output: infer O } } }\n ? O\n : never;\n\ntype OptionalKeys<T> = {\n [K in keyof T]-?: {} extends Pick<T, K> ? K : never;\n}[keyof T];\ntype RequiredKeys<T> = Exclude<keyof T, OptionalKeys<T>>;\ntype Simplify<T> = { [K in keyof T]: T[K] } & {};\n\n/**\n * What a link may write for a schema: the keys the schema's input requires\n * are required, the rest optional, and every value is the schema's OUTPUT\n * type. Output rather than input because `z.coerce.number()` takes `unknown`\n * in - that is what coercion means - and a link typed by it would accept\n * `page: 'two'`. The output is the number the page will actually see.\n */\ntype LinkInputOf<S> = Simplify<\n { [K in RequiredKeys<InputOf<S>> & keyof OutputOf<S>]: OutputOf<S>[K] } & {\n [K in OptionalKeys<InputOf<S>> & keyof OutputOf<S>]?: OutputOf<S>[K];\n }\n>;\n\ntype Scalar = string | number | boolean | null | undefined;\n\n/** What a link may carry when nothing declares otherwise. */\nexport type LooseSearch = Record<string, Scalar | readonly (string | number)[]>;\n\n/**\n * The search params a link to `H` may carry.\n *\n * The page's schema input when `H` is one declared route with a schema;\n * otherwise anything. \"One route\" matters: `path as Route` is every route at\n * once, and a link that could go anywhere cannot be held to one page's schema.\n */\nexport type SearchFor<H extends string> = Unregistered extends true\n ? LooseSearch\n : IsUnion<H> extends true\n ? LooseSearch\n : [PatternOf<H>] extends [never]\n ? LooseSearch\n : SchemaFor<PatternOf<H>> extends undefined\n ? LooseSearch\n : LinkInputOf<SchemaFor<PatternOf<H>>>;\n\n/**\n * The `search` prop, required exactly when the page's schema has a required\n * key. A page that needs `q` is not reachable without one, so the link that\n * omits it is the bug — caught here rather than on the page's error boundary.\n */\nexport type SearchProp<H extends string> =\n {} extends SearchFor<H>\n ? { search?: SearchFor<H> }\n : { search: SearchFor<H> };\n\n/** A query string from an object: scalars stringified, arrays repeated, null and undefined dropped. */\nexport function searchString(search: object): string {\n const params = new URLSearchParams();\n\n for (const [key, value] of Object.entries(search)) {\n if (value === null || value === undefined) continue;\n\n if (Array.isArray(value)) {\n for (const item of value) params.append(key, String(item));\n } else {\n params.set(key, String(value));\n }\n }\n\n return params.toString();\n}\n\n/** `path` with `search` appended, keeping any query and hash already on it. */\nexport function withSearch(path: string, search: object | undefined): string {\n if (!search) return path;\n\n const hashAt = path.indexOf(\"#\");\n const hash = hashAt === -1 ? \"\" : path.slice(hashAt);\n const before = hashAt === -1 ? path : path.slice(0, hashAt);\n const queryAt = before.indexOf(\"?\");\n const base = queryAt === -1 ? before : before.slice(0, queryAt);\n const existing = queryAt === -1 ? \"\" : before.slice(queryAt + 1);\n const added = searchString(search);\n const query = [existing, added].filter(Boolean).join(\"&\");\n\n return query ? `${base}?${query}${hash}` : `${base}${hash}`;\n}\n\n/**\n * A typed url with its search params, for the places that take a string.\n *\n * visit(href('/search', { q: 'shoes', page: 2 }))\n *\n * `Link` has the same check on its own `search` prop. This is for `visit`,\n * `prefetch`, `redirect` and anything else that wants the finished string.\n */\nexport function href<H extends Route>(\n path: H,\n ...rest: {} extends SearchFor<H>\n ? [search?: SearchFor<H>]\n : [search: SearchFor<H>]\n): Route {\n return withSearch(path, rest[0] as object | undefined) as Route;\n}\n"]}
package/dist/vite.d.ts CHANGED
@@ -321,6 +321,17 @@ export interface RscKitOptions {
321
321
  * whole app, or its own endpoints, to the backend.
322
322
  */
323
323
  export declare function normaliseBackendPaths(paths: readonly string[] | undefined): string[];
324
+ /**
325
+ * The prefixes a backend owns, as the route patterns the browser already reads
326
+ * for a route.ts: the prefix, and everything under it.
327
+ *
328
+ * The router never fetches these as pages. A backend url that answers a
329
+ * payload request with a redirect to another site - an OAuth start - is a hop
330
+ * a fetch cannot make, so the navigation threw and the visitor stayed where
331
+ * they were. Listed with the route.ts patterns it is a document load, with no
332
+ * prefetch, wherever the router meets one: a click, a redirect, a hover.
333
+ */
334
+ export declare function backendPatterns(paths: readonly string[]): string[];
324
335
  /**
325
336
  * The service worker, written into the client output at build time.
326
337
  *
package/dist/vite.js CHANGED
@@ -420,14 +420,28 @@ export function normaliseBackendPaths(paths) {
420
420
  if (path === "/_rsc" || path.startsWith("/_rsc/")) {
421
421
  throw new Error(`[rsc-kit] backendPaths: ${JSON.stringify(raw)} is this package's own endpoint, which the renderer answers.`);
422
422
  }
423
- if (/[?#*]/.test(path)) {
424
- throw new Error(`[rsc-kit] backendPaths: ${JSON.stringify(raw)} is a prefix, not a pattern: no query, fragment or wildcard.`);
423
+ // A bracket is a dynamic segment to the client, which reads these as route patterns.
424
+ if (/[?#*[\]]/.test(path)) {
425
+ throw new Error(`[rsc-kit] backendPaths: ${JSON.stringify(raw)} is a prefix, not a pattern: no query, fragment, wildcard or bracket.`);
425
426
  }
426
427
  if (!out.includes(path))
427
428
  out.push(path);
428
429
  }
429
430
  return out;
430
431
  }
432
+ /**
433
+ * The prefixes a backend owns, as the route patterns the browser already reads
434
+ * for a route.ts: the prefix, and everything under it.
435
+ *
436
+ * The router never fetches these as pages. A backend url that answers a
437
+ * payload request with a redirect to another site - an OAuth start - is a hop
438
+ * a fetch cannot make, so the navigation threw and the visitor stayed where
439
+ * they were. Listed with the route.ts patterns it is a document load, with no
440
+ * prefetch, wherever the router meets one: a click, a redirect, a hover.
441
+ */
442
+ export function backendPatterns(paths) {
443
+ return paths.flatMap((path) => [path, `${path}/[...rest]`]);
444
+ }
431
445
  function resolvePaths(options) {
432
446
  projectRoot = resolve(options.projectRoot || process.env.RSC_PROJECT_ROOT || process.cwd());
433
447
  sourceDir = resolve(options.sourceDir || process.env.RSC_SOURCE_DIR || join(projectRoot, "src"));
@@ -2171,6 +2185,14 @@ function renderRouteTypes(manifest) {
2171
2185
  // Api routes are a separate union, so Link refuses an api url and apiUrl()
2172
2186
  // refuses a page. Linking to an api route navigates the browser away to a
2173
2187
  // json document, which is the mistake worth catching.
2188
+ // The prefixes rscKit({ backendPaths }) named, so a link or a redirect to
2189
+ // one of the backend's urls compiles without a cast.
2190
+ " interface RegisterBackend {",
2191
+ backendPaths.length > 0
2192
+ ? " paths:\n" +
2193
+ backendPaths.map((p) => " | " + JSON.stringify(p)).join("\n")
2194
+ : " // No backendPaths named.\n paths: never",
2195
+ " }",
2174
2196
  // Regions: every section('name', …) the build read, and every @slot
2175
2197
  // directory. This is what types revalidate().
2176
2198
  " interface RegisterRegions {",
@@ -6393,8 +6415,12 @@ createViteRscApp(document, ${JSON.stringify(interceptManifest())}, ${JSON.string
6393
6415
  staticPayloads: staticPayloads || null,
6394
6416
  routes: routesForClient,
6395
6417
  // Every route.ts, as a url pattern, so a link to one is treated as an
6396
- // anchor rather than prefetched and fetched as a page.
6397
- apiRoutes: (routeManifest().apis ?? []).map((api) => patternOf(api.segments)),
6418
+ // anchor rather than prefetched and fetched as a page - and so is
6419
+ // anything under a prefix the app named as the backend's.
6420
+ apiRoutes: [
6421
+ ...(routeManifest().apis ?? []).map((api) => patternOf(api.segments)),
6422
+ ...backendPatterns(backendPaths),
6423
+ ],
6398
6424
  })})
6399
6425
 
6400
6426
  // A server component is not a module the browser has, so Vite cannot replace