@rsc-kit/core 0.16.3 → 0.18.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (107) hide show
  1. package/dist/barrelImports.d.ts +6 -0
  2. package/dist/barrelImports.js +93 -0
  3. package/dist/barrelImports.js.map +1 -0
  4. package/dist/buildReport.d.ts +6 -1
  5. package/dist/buildReport.js +16 -18
  6. package/dist/buildReport.js.map +1 -1
  7. package/dist/cache.js +8 -4
  8. package/dist/cache.js.map +1 -1
  9. package/dist/clientEntries.d.ts +32 -0
  10. package/dist/clientEntries.js +107 -0
  11. package/dist/clientEntries.js.map +1 -0
  12. package/dist/clientImports.d.ts +36 -0
  13. package/dist/clientImports.js +36 -0
  14. package/dist/clientImports.js.map +1 -0
  15. package/dist/devReload.d.ts +19 -0
  16. package/dist/devReload.js +32 -0
  17. package/dist/devReload.js.map +1 -0
  18. package/dist/events.d.ts +57 -0
  19. package/dist/events.js +113 -0
  20. package/dist/events.js.map +1 -0
  21. package/dist/host.d.ts +16 -6
  22. package/dist/host.js +219 -124
  23. package/dist/host.js.map +1 -1
  24. package/dist/hostCalls.d.ts +27 -0
  25. package/dist/hostCalls.js +127 -12
  26. package/dist/hostCalls.js.map +1 -1
  27. package/dist/hostRouting.d.ts +53 -0
  28. package/dist/hostRouting.js +100 -0
  29. package/dist/hostRouting.js.map +1 -0
  30. package/dist/js/DefaultRouteError.d.ts +3 -0
  31. package/dist/js/DefaultRouteError.js +61 -0
  32. package/dist/js/DefaultRouteError.js.map +1 -0
  33. package/dist/js/Form.js +54 -6
  34. package/dist/js/Form.js.map +1 -1
  35. package/dist/js/Link.d.ts +0 -3
  36. package/dist/js/Link.js +3 -6
  37. package/dist/js/Link.js.map +1 -1
  38. package/dist/js/SegmentBoundary.d.ts +38 -2
  39. package/dist/js/SegmentBoundary.js +51 -13
  40. package/dist/js/SegmentBoundary.js.map +1 -1
  41. package/dist/js/createViteRscApp.js +32 -17
  42. package/dist/js/createViteRscApp.js.map +1 -1
  43. package/dist/js/devNotice.d.ts +2 -2
  44. package/dist/js/devNotice.js +17 -7
  45. package/dist/js/devNotice.js.map +1 -1
  46. package/dist/js/fallbackReport.d.ts +2 -0
  47. package/dist/js/fallbackReport.js +51 -0
  48. package/dist/js/fallbackReport.js.map +1 -0
  49. package/dist/js/navigate.d.ts +1 -1
  50. package/dist/js/navigate.js +55 -23
  51. package/dist/js/navigate.js.map +1 -1
  52. package/dist/js/queryClient.d.ts +2 -1
  53. package/dist/js/queryClient.js +0 -12
  54. package/dist/js/queryClient.js.map +1 -1
  55. package/dist/js/router.d.ts +2 -2
  56. package/dist/js/router.js.map +1 -1
  57. package/dist/js/segmentStore.d.ts +2 -0
  58. package/dist/js/segmentStore.js +21 -1
  59. package/dist/js/segmentStore.js.map +1 -1
  60. package/dist/js/staleAssets.js +10 -1
  61. package/dist/js/staleAssets.js.map +1 -1
  62. package/dist/js/standardSchema.d.ts +6 -0
  63. package/dist/js/standardSchema.js +19 -1
  64. package/dist/js/standardSchema.js.map +1 -1
  65. package/dist/js/useEvents.d.ts +25 -0
  66. package/dist/js/useEvents.js +78 -0
  67. package/dist/js/useEvents.js.map +1 -0
  68. package/dist/js/useLinkStatus.d.ts +14 -1
  69. package/dist/js/useLinkStatus.js +15 -1
  70. package/dist/js/useLinkStatus.js.map +1 -1
  71. package/dist/js/usePolling.d.ts +29 -0
  72. package/dist/js/usePolling.js +141 -0
  73. package/dist/js/usePolling.js.map +1 -0
  74. package/dist/manifest.d.ts +18 -1
  75. package/dist/manifest.js.map +1 -1
  76. package/dist/metadata.d.ts +94 -15
  77. package/dist/metadata.js.map +1 -1
  78. package/dist/metadataRoutes.d.ts +61 -0
  79. package/dist/metadataRoutes.js +173 -0
  80. package/dist/metadataRoutes.js.map +1 -0
  81. package/dist/prerender.js +44 -10
  82. package/dist/prerender.js.map +1 -1
  83. package/dist/reactCache.d.ts +1 -0
  84. package/dist/reactCache.js +52 -0
  85. package/dist/reactCache.js.map +1 -0
  86. package/dist/redirect.d.ts +16 -2
  87. package/dist/redirect.js +10 -19
  88. package/dist/redirect.js.map +1 -1
  89. package/dist/request.d.ts +18 -1
  90. package/dist/request.js +91 -5
  91. package/dist/request.js.map +1 -1
  92. package/dist/revalidate.d.ts +6 -4
  93. package/dist/revalidate.js +6 -5
  94. package/dist/revalidate.js.map +1 -1
  95. package/dist/routes.d.ts +16 -3
  96. package/dist/routes.js +5 -5
  97. package/dist/routes.js.map +1 -1
  98. package/dist/routing.d.ts +2 -2
  99. package/dist/routing.js +47 -25
  100. package/dist/routing.js.map +1 -1
  101. package/dist/useSsr.d.ts +19 -0
  102. package/dist/useSsr.js +104 -0
  103. package/dist/useSsr.js.map +1 -0
  104. package/dist/vite.d.ts +86 -9
  105. package/dist/vite.js +827 -24
  106. package/dist/vite.js.map +1 -1
  107. package/package.json +13 -1
package/dist/routing.js CHANGED
@@ -15,12 +15,17 @@ function indexRoutes(routes) {
15
15
  const scored = [];
16
16
  const exact = new Map();
17
17
  for (const route of routes) {
18
- const score = route.segments.reduce((n, s) => n + (s.type === 'static' ? 2 : s.type === 'param' ? 1 : 0), 0);
18
+ const score = route.segments.reduce((n, s) => n +
19
+ (s.type === "static"
20
+ ? 2
21
+ : s.type === "param" || s.type === "host"
22
+ ? 1
23
+ : 0), 0);
19
24
  scored.push({ route, score });
20
25
  // An all-static route matches exactly one path, with the highest score any
21
26
  // route can have for that path. First one in wins, as in the scan.
22
- if (route.segments.every((s) => s.type === 'static')) {
23
- const key = route.segments.map((s) => s.value).join('/');
27
+ if (route.segments.every((s) => s.type === "static")) {
28
+ const key = route.segments.map((s) => s.value).join("/");
24
29
  if (!exact.has(key))
25
30
  exact.set(key, route);
26
31
  }
@@ -44,9 +49,9 @@ function indexedApis(manifest) {
44
49
  return found;
45
50
  }
46
51
  export function matchApiRoute(manifest, pathname) {
47
- const parts = pathname.split('/').filter(Boolean);
52
+ const parts = pathname.split("/").filter(Boolean);
48
53
  const { scored, exact } = indexedApis(manifest);
49
- const direct = exact.get(parts.join('/'));
54
+ const direct = exact.get(parts.join("/"));
50
55
  if (direct)
51
56
  return { route: direct, params: {} };
52
57
  let best = null;
@@ -68,15 +73,15 @@ export function matchApiRoute(manifest, pathname) {
68
73
  * HEAD is included whenever GET is, because it is answered by GET.
69
74
  */
70
75
  export function allowFor(route) {
71
- const methods = route.methods.includes('HEAD') || !route.methods.includes('GET')
76
+ const methods = route.methods.includes("HEAD") || !route.methods.includes("GET")
72
77
  ? route.methods
73
- : [...route.methods, 'HEAD'];
74
- return methods.join(', ');
78
+ : [...route.methods, "HEAD"];
79
+ return methods.join(", ");
75
80
  }
76
- export function matchRoute(manifest, pathname) {
77
- const parts = pathname.split('/').filter(Boolean);
81
+ export function matchRoute(manifest, pathname, host = null) {
82
+ const parts = pathname.split("/").filter(Boolean);
78
83
  const { scored, exact } = indexed(manifest);
79
- const direct = exact.get(parts.join('/'));
84
+ const direct = exact.get(parts.join("/"));
80
85
  if (direct)
81
86
  return { route: direct, params: {} };
82
87
  let best = null;
@@ -86,7 +91,7 @@ export function matchRoute(manifest, pathname) {
86
91
  for (const { route, score } of scored) {
87
92
  if (score <= bestScore)
88
93
  continue;
89
- const bound = bindSegments(route.segments, parts);
94
+ const bound = bindSegments(route.segments, parts, host);
90
95
  if (!bound)
91
96
  continue;
92
97
  best = { route, params: bound };
@@ -102,7 +107,7 @@ export function matchRoute(manifest, pathname) {
102
107
  * modal opens over.
103
108
  */
104
109
  export function matchIntercept(manifest, pathname, slot) {
105
- const parts = pathname.split('/').filter(Boolean);
110
+ const parts = pathname.split("/").filter(Boolean);
106
111
  let best = null;
107
112
  let bestScore = -1;
108
113
  for (const intercept of manifest.intercepts) {
@@ -117,7 +122,7 @@ export function matchIntercept(manifest, pathname, slot) {
117
122
  // the guard is chosen by one while the content comes from the other, so a
118
123
  // url could be checked against the unguarded route and rendered from the
119
124
  // guarded one.
120
- const score = intercept.segments.reduce((n, segment) => n + (segment.type === 'static' ? 2 : segment.type === 'param' ? 1 : 0), 0);
125
+ const score = intercept.segments.reduce((n, segment) => n + (segment.type === "static" ? 2 : segment.type === "param" ? 1 : 0), 0);
121
126
  if (score > bestScore) {
122
127
  best = { component: intercept.component, params };
123
128
  bestScore = score;
@@ -125,22 +130,35 @@ export function matchIntercept(manifest, pathname, slot) {
125
130
  }
126
131
  return best;
127
132
  }
128
- function bindSegments(segments, parts) {
133
+ function bindSegments(segments, parts, host = null) {
129
134
  const params = {};
130
135
  for (let i = 0; i < segments.length; i++) {
131
136
  const segment = segments[i];
132
- if (segment.type === 'catchAll') {
137
+ if (segment.type === "catchAll") {
133
138
  // Swallows the rest, including none of it.
134
- params[segment.value] = parts.slice(i).join('/');
139
+ params[segment.value] = parts.slice(i).join("/");
135
140
  return params;
136
141
  }
137
142
  if (i >= parts.length)
138
143
  return null;
139
- if (segment.type === 'static') {
144
+ // The leading part is the host's when one was given: a static directory
145
+ // of that name takes it (admin.example.com -> app/admin), and so does a
146
+ // host segment. A path part never binds a host segment, so
147
+ // example.com/nope is not a tenant called "nope".
148
+ const fromHost = i === 0 && host !== null;
149
+ if (segment.type === "static") {
140
150
  if (parts[i] !== segment.value)
141
151
  return null;
142
152
  continue;
143
153
  }
154
+ if (segment.type === "host") {
155
+ if (!fromHost)
156
+ return null;
157
+ params[segment.value] = parts[i];
158
+ continue;
159
+ }
160
+ if (fromHost)
161
+ return null;
144
162
  params[segment.value] = decodeURIComponent(parts[i]);
145
163
  }
146
164
  return segments.length === parts.length ? params : null;
@@ -156,9 +174,11 @@ function bindSegments(segments, parts) {
156
174
  export function sharedDepth(held, chain) {
157
175
  if (!held)
158
176
  return 0;
159
- const mounted = held.split(',').filter(Boolean);
177
+ const mounted = held.split(",").filter(Boolean);
160
178
  let shared = 0;
161
- while (shared < mounted.length && shared < chain.length && mounted[shared] === chain[shared]) {
179
+ while (shared < mounted.length &&
180
+ shared < chain.length &&
181
+ mounted[shared] === chain[shared]) {
162
182
  shared++;
163
183
  }
164
184
  return shared;
@@ -177,7 +197,9 @@ export function sharedDepth(held, chain) {
177
197
  */
178
198
  export function retentionKey(path, interceptSlot) {
179
199
  const normalised = normalisePath(path);
180
- return interceptSlot ? `__intercept:${interceptSlot}:${normalised}` : normalised;
200
+ return interceptSlot
201
+ ? `__intercept:${interceptSlot}:${normalised}`
202
+ : normalised;
181
203
  }
182
204
  /**
183
205
  * The same page written two ways is one key.
@@ -192,12 +214,12 @@ export function retentionKey(path, interceptSlot) {
192
214
  function normalisePath(path) {
193
215
  // Absolute urls reach this from the initial page, which is identified by
194
216
  // href rather than by the path a link would use.
195
- const withoutOrigin = path.replace(/^[a-z][a-z0-9+.-]*:\/\/[^/]+/i, '');
196
- const [pathname, rest = ''] = splitQuery(withoutOrigin);
197
- return (pathname.replace(/\/+$/, '') || '/') + rest;
217
+ const withoutOrigin = path.replace(/^[a-z][a-z0-9+.-]*:\/\/[^/]+/i, "");
218
+ const [pathname, rest = ""] = splitQuery(withoutOrigin);
219
+ return (pathname.replace(/\/+$/, "") || "/") + rest;
198
220
  }
199
221
  function splitQuery(url) {
200
222
  const cut = url.search(/[?#]/);
201
- return cut === -1 ? [url, ''] : [url.slice(0, cut), url.slice(cut)];
223
+ return cut === -1 ? [url, ""] : [url.slice(0, cut), url.slice(cut)];
202
224
  }
203
225
  //# sourceMappingURL=routing.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"routing.js","sourceRoot":"","sources":["../src/routing.ts"],"names":[],"mappings":"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","sourcesContent":["// Matching a url to a route, and deciding how much of the page to send.\n//\n// Pure: no Request, no Response, no filesystem. That is what lets the same\n// implementation serve three callers — the request handler, the prerenderer\n// enumerating what to build, and the generated server bundle, which embeds its\n// own route table and answers these without a manifest being passed around at\n// all.\n//\n// Every host would otherwise write this, and would get the same two things\n// wrong: answering /docs/new with [slug], and treating the layout chain as a\n// set rather than a sequence.\n\nimport type { ManifestApiRoute, ManifestRoute, RouteManifest, RouteSegment } from './manifest.js'\n\n/** A matched route and the params its url segments bound. */\nexport interface MatchedRoute {\n route: ManifestRoute\n params: Record<string, string>\n}\n\n/**\n * Match a pathname against the manifest's segments.\n *\n * The manifest stores segments rather than a pattern string, because the\n * pattern is the host's dialect — Laravel writes `{slug}`, Hono writes\n * `:slug`, and neither is the build's business. Matching them directly means\n * no dialect at all.\n *\n * Static segments beat dynamic ones at the same position: /docs/new is the\n * page called new, not the page called [slug] with slug=new.\n */\n/** A `route.ts` the url matched, and the params it bound. */\nexport interface MatchedApiRoute {\n route: ManifestApiRoute\n params: Record<string, string>\n}\n\n/**\n * The api route a url answers, if one does.\n *\n * Exact segment binding only, scored the same way pages are: more static\n * segments wins, a catch-all is weakest. Reusing that is the point — an api\n * route takes `[id]` and `[...rest]` because the matcher was already there.\n */\n/**\n * A manifest's routes, indexed once.\n *\n * Every request used to score every route with a reduce over its segments,\n * which at fifty routes is nothing and at five hundred is the request. The\n * score is a property of the route, so it is computed once; and a path that\n * is entirely static - most of them - is answered from a map without\n * touching the list at all. Keyed by the manifest object, so a host that\n * swaps manifests gets a fresh index and a dropped one is collected.\n */\ninterface Indexed<R extends { segments: RouteSegment[] }> {\n scored: { route: R; score: number }[]\n exact: Map<string, R>\n}\n\nconst indexes = new WeakMap<object, Indexed<ManifestRoute>>()\nconst apiIndexes = new WeakMap<object, Indexed<ManifestApiRoute>>()\n\nfunction indexRoutes<R extends { segments: RouteSegment[] }>(routes: R[]): Indexed<R> {\n const scored: { route: R; score: number }[] = []\n const exact = new Map<string, R>()\n\n for (const route of routes) {\n const score = route.segments.reduce(\n (n, s) => n + (s.type === 'static' ? 2 : s.type === 'param' ? 1 : 0),\n 0,\n )\n\n scored.push({ route, score })\n\n // An all-static route matches exactly one path, with the highest score any\n // route can have for that path. First one in wins, as in the scan.\n if (route.segments.every((s) => s.type === 'static')) {\n const key = route.segments.map((s) => s.value).join('/')\n\n if (!exact.has(key)) exact.set(key, route)\n }\n }\n\n return { scored, exact }\n}\n\nfunction indexed(manifest: RouteManifest): Indexed<ManifestRoute> {\n let found = indexes.get(manifest)\n\n if (!found) {\n found = indexRoutes(manifest.routes)\n indexes.set(manifest, found)\n }\n\n return found\n}\n\nfunction indexedApis(manifest: RouteManifest): Indexed<ManifestApiRoute> {\n let found = apiIndexes.get(manifest)\n\n if (!found) {\n found = indexRoutes(manifest.apis ?? [])\n apiIndexes.set(manifest, found)\n }\n\n return found\n}\n\nexport function matchApiRoute(\n manifest: RouteManifest,\n pathname: string,\n): MatchedApiRoute | null {\n const parts = pathname.split('/').filter(Boolean)\n const { scored, exact } = indexedApis(manifest)\n const direct = exact.get(parts.join('/'))\n\n if (direct) return { route: direct, params: {} }\n\n let best: MatchedApiRoute | null = null\n let bestScore = -1\n\n for (const { route, score } of scored) {\n if (score <= bestScore) continue\n\n const bound = bindSegments(route.segments, parts)\n\n if (!bound) continue\n\n best = { route, params: bound }\n bestScore = score\n }\n\n return best\n}\n\n/**\n * What a 405 should say this route does answer.\n *\n * HEAD is included whenever GET is, because it is answered by GET.\n */\nexport function allowFor(route: ManifestApiRoute): string {\n const methods = route.methods.includes('HEAD') || !route.methods.includes('GET')\n ? route.methods\n : [...route.methods, 'HEAD']\n\n return methods.join(', ')\n}\n\nexport function matchRoute(manifest: RouteManifest, pathname: string): MatchedRoute | null {\n const parts = pathname.split('/').filter(Boolean)\n const { scored, exact } = indexed(manifest)\n const direct = exact.get(parts.join('/'))\n\n if (direct) return { route: direct, params: {} }\n\n let best: MatchedRoute | null = null\n let bestScore = -1\n\n // More static segments wins; a catch-all is the weakest possible match. A\n // route that could not beat the best so far is not worth binding.\n for (const { route, score } of scored) {\n if (score <= bestScore) continue\n\n const bound = bindSegments(route.segments, parts)\n\n if (!bound) continue\n\n best = { route, params: bound }\n bestScore = score\n }\n\n return best\n}\n\n/**\n * The interceptor registered for a slot, if this url has one.\n *\n * Matched against the url being navigated *to*: `(.)posts/[slug]` intercepts\n * /posts/anything, and the params it binds are the target's, not the page the\n * modal opens over.\n */\nexport function matchIntercept(\n manifest: RouteManifest,\n pathname: string,\n slot: string,\n): { component: string; params: Record<string, string> } | null {\n const parts = pathname.split('/').filter(Boolean)\n let best: { component: string; params: Record<string, string> } | null = null\n let bestScore = -1\n\n for (const intercept of manifest.intercepts) {\n if (intercept.slot !== slot) continue\n\n const params = bindSegments(intercept.segments, parts)\n\n if (!params) continue\n\n // Scored exactly as matchRoute scores, and for the same reason: a static\n // segment beats a dynamic one at the same position. Taking the first match\n // instead made the two disagree about which route a url belongs to — and\n // the guard is chosen by one while the content comes from the other, so a\n // url could be checked against the unguarded route and rendered from the\n // guarded one.\n const score = intercept.segments.reduce(\n (n, segment) => n + (segment.type === 'static' ? 2 : segment.type === 'param' ? 1 : 0),\n 0,\n )\n\n if (score > bestScore) {\n best = { component: intercept.component, params }\n bestScore = score\n }\n }\n\n return best\n}\n\nfunction bindSegments(segments: RouteSegment[], parts: string[]): Record<string, string> | null {\n const params: Record<string, string> = {}\n\n for (let i = 0; i < segments.length; i++) {\n const segment = segments[i]\n\n if (segment.type === 'catchAll') {\n // Swallows the rest, including none of it.\n params[segment.value] = parts.slice(i).join('/')\n\n return params\n }\n\n if (i >= parts.length) return null\n\n if (segment.type === 'static') {\n if (parts[i] !== segment.value) return null\n continue\n }\n\n params[segment.value] = decodeURIComponent(parts[i])\n }\n\n return segments.length === parts.length ? params : null\n}\n\n/**\n * How much of the layout chain the client already holds.\n *\n * It sends the chain outermost-first; the shared prefix is what it can keep.\n * Answering with a whole document instead is not merely wasteful — it replaces\n * the root, and replacing the root unmounts every page retained behind the\n * current one, so a half-typed form does not survive going back.\n */\nexport function sharedDepth(held: string | null, chain: string[]): number {\n if (!held) return 0\n\n const mounted = held.split(',').filter(Boolean)\n let shared = 0\n\n while (shared < mounted.length && shared < chain.length && mounted[shared] === chain[shared]) {\n shared++\n }\n\n return shared\n}\n\n/**\n * What a page is remembered as, for retention and for the prefetch cache.\n *\n * The same url intercepted and not intercepted are two different things to go\n * back to — a modal over the feed, and the post on its own page — so they\n * cannot share a key or restoring one returns the other.\n *\n * Shared because both halves compute it: the host stores under this key and\n * the client looks under it. Written out at each of the seven places that\n * needed it, they only had to disagree once, and the symptom is a navigation\n * that silently rebuilds a page it was holding.\n */\nexport function retentionKey(path: string, interceptSlot?: string | null): string {\n const normalised = normalisePath(path)\n\n return interceptSlot ? `__intercept:${interceptSlot}:${normalised}` : normalised\n}\n\n/**\n * The same page written two ways is one key.\n *\n * A static host serves /orders as a directory, so the browser's url ends in a\n * slash while the build wrote the page down as /orders. Left unequal, the\n * entry retained for a page is never the one looked up on the way back: it\n * stays in the document, hidden, while a second copy is fetched and rendered\n * beside it — so the form you were filling in is there, and not the one you\n * are looking at.\n */\nfunction normalisePath(path: string): string {\n // Absolute urls reach this from the initial page, which is identified by\n // href rather than by the path a link would use.\n const withoutOrigin = path.replace(/^[a-z][a-z0-9+.-]*:\\/\\/[^/]+/i, '')\n const [pathname, rest = ''] = splitQuery(withoutOrigin)\n\n return (pathname.replace(/\\/+$/, '') || '/') + rest\n}\n\nfunction splitQuery(url: string): [string, string] {\n const cut = url.search(/[?#]/)\n\n return cut === -1 ? [url, ''] : [url.slice(0, cut), url.slice(cut)]\n}\n"]}
1
+ {"version":3,"file":"routing.js","sourceRoot":"","sources":["../src/routing.ts"],"names":[],"mappings":"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","sourcesContent":["// Matching a url to a route, and deciding how much of the page to send.\n//\n// Pure: no Request, no Response, no filesystem. That is what lets the same\n// implementation serve three callers — the request handler, the prerenderer\n// enumerating what to build, and the generated server bundle, which embeds its\n// own route table and answers these without a manifest being passed around at\n// all.\n//\n// Every host would otherwise write this, and would get the same two things\n// wrong: answering /docs/new with [slug], and treating the layout chain as a\n// set rather than a sequence.\n\nimport type {\n ManifestApiRoute,\n ManifestRoute,\n RouteManifest,\n RouteSegment,\n} from \"./manifest.js\";\n\n/** A matched route and the params its url segments bound. */\nexport interface MatchedRoute {\n route: ManifestRoute;\n params: Record<string, string>;\n}\n\n/**\n * Match a pathname against the manifest's segments.\n *\n * The manifest stores segments rather than a pattern string, because the\n * pattern is the host's dialect — Laravel writes `{slug}`, Hono writes\n * `:slug`, and neither is the build's business. Matching them directly means\n * no dialect at all.\n *\n * Static segments beat dynamic ones at the same position: /docs/new is the\n * page called new, not the page called [slug] with slug=new.\n */\n/** A `route.ts` the url matched, and the params it bound. */\nexport interface MatchedApiRoute {\n route: ManifestApiRoute;\n params: Record<string, string>;\n}\n\n/**\n * The api route a url answers, if one does.\n *\n * Exact segment binding only, scored the same way pages are: more static\n * segments wins, a catch-all is weakest. Reusing that is the point — an api\n * route takes `[id]` and `[...rest]` because the matcher was already there.\n */\n/**\n * A manifest's routes, indexed once.\n *\n * Every request used to score every route with a reduce over its segments,\n * which at fifty routes is nothing and at five hundred is the request. The\n * score is a property of the route, so it is computed once; and a path that\n * is entirely static - most of them - is answered from a map without\n * touching the list at all. Keyed by the manifest object, so a host that\n * swaps manifests gets a fresh index and a dropped one is collected.\n */\ninterface Indexed<R extends { segments: RouteSegment[] }> {\n scored: { route: R; score: number }[];\n exact: Map<string, R>;\n}\n\nconst indexes = new WeakMap<object, Indexed<ManifestRoute>>();\nconst apiIndexes = new WeakMap<object, Indexed<ManifestApiRoute>>();\n\nfunction indexRoutes<R extends { segments: RouteSegment[] }>(\n routes: R[],\n): Indexed<R> {\n const scored: { route: R; score: number }[] = [];\n const exact = new Map<string, R>();\n\n for (const route of routes) {\n const score = route.segments.reduce(\n (n, s) =>\n n +\n (s.type === \"static\"\n ? 2\n : s.type === \"param\" || s.type === \"host\"\n ? 1\n : 0),\n 0,\n );\n\n scored.push({ route, score });\n\n // An all-static route matches exactly one path, with the highest score any\n // route can have for that path. First one in wins, as in the scan.\n if (route.segments.every((s) => s.type === \"static\")) {\n const key = route.segments.map((s) => s.value).join(\"/\");\n\n if (!exact.has(key)) exact.set(key, route);\n }\n }\n\n return { scored, exact };\n}\n\nfunction indexed(manifest: RouteManifest): Indexed<ManifestRoute> {\n let found = indexes.get(manifest);\n\n if (!found) {\n found = indexRoutes(manifest.routes);\n indexes.set(manifest, found);\n }\n\n return found;\n}\n\nfunction indexedApis(manifest: RouteManifest): Indexed<ManifestApiRoute> {\n let found = apiIndexes.get(manifest);\n\n if (!found) {\n found = indexRoutes(manifest.apis ?? []);\n apiIndexes.set(manifest, found);\n }\n\n return found;\n}\n\nexport function matchApiRoute(\n manifest: RouteManifest,\n pathname: string,\n): MatchedApiRoute | null {\n const parts = pathname.split(\"/\").filter(Boolean);\n const { scored, exact } = indexedApis(manifest);\n const direct = exact.get(parts.join(\"/\"));\n\n if (direct) return { route: direct, params: {} };\n\n let best: MatchedApiRoute | null = null;\n let bestScore = -1;\n\n for (const { route, score } of scored) {\n if (score <= bestScore) continue;\n\n const bound = bindSegments(route.segments, parts);\n\n if (!bound) continue;\n\n best = { route, params: bound };\n bestScore = score;\n }\n\n return best;\n}\n\n/**\n * What a 405 should say this route does answer.\n *\n * HEAD is included whenever GET is, because it is answered by GET.\n */\nexport function allowFor(route: ManifestApiRoute): string {\n const methods =\n route.methods.includes(\"HEAD\") || !route.methods.includes(\"GET\")\n ? route.methods\n : [...route.methods, \"HEAD\"];\n\n return methods.join(\", \");\n}\n\nexport function matchRoute(\n manifest: RouteManifest,\n pathname: string,\n host: string | null = null,\n): MatchedRoute | null {\n const parts = pathname.split(\"/\").filter(Boolean);\n const { scored, exact } = indexed(manifest);\n const direct = exact.get(parts.join(\"/\"));\n\n if (direct) return { route: direct, params: {} };\n\n let best: MatchedRoute | null = null;\n let bestScore = -1;\n\n // More static segments wins; a catch-all is the weakest possible match. A\n // route that could not beat the best so far is not worth binding.\n for (const { route, score } of scored) {\n if (score <= bestScore) continue;\n\n const bound = bindSegments(route.segments, parts, host);\n\n if (!bound) continue;\n\n best = { route, params: bound };\n bestScore = score;\n }\n\n return best;\n}\n\n/**\n * The interceptor registered for a slot, if this url has one.\n *\n * Matched against the url being navigated *to*: `(.)posts/[slug]` intercepts\n * /posts/anything, and the params it binds are the target's, not the page the\n * modal opens over.\n */\nexport function matchIntercept(\n manifest: RouteManifest,\n pathname: string,\n slot: string,\n): { component: string; params: Record<string, string> } | null {\n const parts = pathname.split(\"/\").filter(Boolean);\n let best: { component: string; params: Record<string, string> } | null = null;\n let bestScore = -1;\n\n for (const intercept of manifest.intercepts) {\n if (intercept.slot !== slot) continue;\n\n const params = bindSegments(intercept.segments, parts);\n\n if (!params) continue;\n\n // Scored exactly as matchRoute scores, and for the same reason: a static\n // segment beats a dynamic one at the same position. Taking the first match\n // instead made the two disagree about which route a url belongs to — and\n // the guard is chosen by one while the content comes from the other, so a\n // url could be checked against the unguarded route and rendered from the\n // guarded one.\n const score = intercept.segments.reduce(\n (n, segment) =>\n n + (segment.type === \"static\" ? 2 : segment.type === \"param\" ? 1 : 0),\n 0,\n );\n\n if (score > bestScore) {\n best = { component: intercept.component, params };\n bestScore = score;\n }\n }\n\n return best;\n}\n\nfunction bindSegments(\n segments: RouteSegment[],\n parts: string[],\n host: string | null = null,\n): Record<string, string> | null {\n const params: Record<string, string> = {};\n\n for (let i = 0; i < segments.length; i++) {\n const segment = segments[i];\n\n if (segment.type === \"catchAll\") {\n // Swallows the rest, including none of it.\n params[segment.value] = parts.slice(i).join(\"/\");\n\n return params;\n }\n\n if (i >= parts.length) return null;\n\n // The leading part is the host's when one was given: a static directory\n // of that name takes it (admin.example.com -> app/admin), and so does a\n // host segment. A path part never binds a host segment, so\n // example.com/nope is not a tenant called \"nope\".\n const fromHost = i === 0 && host !== null;\n\n if (segment.type === \"static\") {\n if (parts[i] !== segment.value) return null;\n continue;\n }\n\n if (segment.type === \"host\") {\n if (!fromHost) return null;\n\n params[segment.value] = parts[i];\n continue;\n }\n\n if (fromHost) return null;\n\n params[segment.value] = decodeURIComponent(parts[i]);\n }\n\n return segments.length === parts.length ? params : null;\n}\n\n/**\n * How much of the layout chain the client already holds.\n *\n * It sends the chain outermost-first; the shared prefix is what it can keep.\n * Answering with a whole document instead is not merely wasteful — it replaces\n * the root, and replacing the root unmounts every page retained behind the\n * current one, so a half-typed form does not survive going back.\n */\nexport function sharedDepth(held: string | null, chain: string[]): number {\n if (!held) return 0;\n\n const mounted = held.split(\",\").filter(Boolean);\n let shared = 0;\n\n while (\n shared < mounted.length &&\n shared < chain.length &&\n mounted[shared] === chain[shared]\n ) {\n shared++;\n }\n\n return shared;\n}\n\n/**\n * What a page is remembered as, for retention and for the prefetch cache.\n *\n * The same url intercepted and not intercepted are two different things to go\n * back to — a modal over the feed, and the post on its own page — so they\n * cannot share a key or restoring one returns the other.\n *\n * Shared because both halves compute it: the host stores under this key and\n * the client looks under it. Written out at each of the seven places that\n * needed it, they only had to disagree once, and the symptom is a navigation\n * that silently rebuilds a page it was holding.\n */\nexport function retentionKey(\n path: string,\n interceptSlot?: string | null,\n): string {\n const normalised = normalisePath(path);\n\n return interceptSlot\n ? `__intercept:${interceptSlot}:${normalised}`\n : normalised;\n}\n\n/**\n * The same page written two ways is one key.\n *\n * A static host serves /orders as a directory, so the browser's url ends in a\n * slash while the build wrote the page down as /orders. Left unequal, the\n * entry retained for a page is never the one looked up on the way back: it\n * stays in the document, hidden, while a second copy is fetched and rendered\n * beside it — so the form you were filling in is there, and not the one you\n * are looking at.\n */\nfunction normalisePath(path: string): string {\n // Absolute urls reach this from the initial page, which is identified by\n // href rather than by the path a link would use.\n const withoutOrigin = path.replace(/^[a-z][a-z0-9+.-]*:\\/\\/[^/]+/i, \"\");\n const [pathname, rest = \"\"] = splitQuery(withoutOrigin);\n\n return (pathname.replace(/\\/+$/, \"\") || \"/\") + rest;\n}\n\nfunction splitQuery(url: string): [string, string] {\n const cut = url.search(/[?#]/);\n\n return cut === -1 ? [url, \"\"] : [url.slice(0, cut), url.slice(cut)];\n}\n"]}
@@ -0,0 +1,19 @@
1
+ export declare const SSR_GUIDE = "https://docs.rsc-kit.dev/guides/emails";
2
+ export declare function hasUseSsr(code: string): boolean;
3
+ export declare class UseSsrError extends Error {
4
+ }
5
+ /** The runtime exports a "use ssr" module can offer: named functions, and a default. */
6
+ export declare function ssrExports(code: string, file: string): {
7
+ named: string[];
8
+ hasDefault: boolean;
9
+ };
10
+ /**
11
+ * The module the server-components environment gets in place of a "use ssr"
12
+ * one: its exports, as async proxies that call the real module where it
13
+ * runs. Null when the module carries no directive.
14
+ */
15
+ export declare function ssrProxyModule(code: string, id: string, root?: string): string | null;
16
+ /** Any of react-dom's server renderer entries. */
17
+ export declare const SERVER_RENDERER: RegExp;
18
+ /** What a module gets in place of react-dom/server where server components render. */
19
+ export declare function serverRendererMessage(importer: string | null): string;
package/dist/useSsr.js ADDED
@@ -0,0 +1,104 @@
1
+ import { basename, relative } from "node:path";
2
+ /**
3
+ * "use ssr": a module that runs in the ssr environment and is called from
4
+ * where server components render.
5
+ *
6
+ * react-dom/server cannot run where server components render. React's
7
+ * server build refuses to load it, and the refusal is right: the renderer
8
+ * needs the client build's internals, and the components it would render
9
+ * import that same server `react` - no useState, no useContext. A template
10
+ * and the call that renders it have to load together somewhere React DOM's
11
+ * server renderer can: the ssr environment, which every app here already
12
+ * has. plugin-rsc exposes that as a call to write by hand,
13
+ * `import.meta.viteRsc.import('./render', { environment: 'ssr' })`, and
14
+ * nobody should have to. So a directive names the module instead, the way
15
+ * "use client" names one, and this rewrites it for the server-components
16
+ * environment into proxies of its exports that call across. Everything
17
+ * else imports it normally.
18
+ *
19
+ * The exports are functions and the calls are async, because the answer
20
+ * crosses environments as a promise. Anything else is refused with the
21
+ * export named, at build and in dev, rather than proxied into nonsense.
22
+ */
23
+ const DIRECTIVE = /^\s*(?:\/\/[^\n]*\n|\/\*[\s\S]*?\*\/\s*)*["']use ssr["']/;
24
+ export const SSR_GUIDE = "https://docs.rsc-kit.dev/guides/emails";
25
+ export function hasUseSsr(code) {
26
+ return DIRECTIVE.test(code.slice(0, 2048));
27
+ }
28
+ export class UseSsrError extends Error {
29
+ }
30
+ /** Comments blanked, so an `export` inside one is not read as one. */
31
+ function withoutComments(code) {
32
+ return code
33
+ .replace(/\/\*[\s\S]*?\*\//g, (m) => m.replace(/[^\n]/g, " "))
34
+ .replace(/(^|[^:\\])\/\/[^\n]*/g, (m, lead) => lead + " ".repeat(m.length - lead.length));
35
+ }
36
+ const NAME = "[A-Za-z_$][\\w$]*";
37
+ const NON_FUNCTION = /^\s*(?:new\b|["'`\d[{]|true\b|false\b|null\b)/;
38
+ /** The runtime exports a "use ssr" module can offer: named functions, and a default. */
39
+ export function ssrExports(code, file) {
40
+ const src = withoutComments(code);
41
+ const named = new Set();
42
+ let hasDefault = false;
43
+ for (const m of src.matchAll(new RegExp(`^[ \\t]*export\\s+(async\\s+)?function\\s*\\*?\\s*(${NAME})`, "gm"))) {
44
+ if (!m[1]) {
45
+ throw new UseSsrError(`"use ssr": \`${m[2]}\` in ${file} is called from another environment, and the answer crosses as a promise. Make it an async function.`);
46
+ }
47
+ named.add(m[2]);
48
+ }
49
+ for (const m of src.matchAll(new RegExp(`^[ \\t]*export\\s+(?:const|let|var)\\s+(${NAME})\\s*(?::[^=\\n]*)?=([^\\n]*)`, "gm"))) {
50
+ if (NON_FUNCTION.test(m[2])) {
51
+ throw new UseSsrError(`"use ssr": a module with the directive exports functions only; \`${m[1]}\` in ${file} is a value. Put it in a module without the directive.`);
52
+ }
53
+ named.add(m[1]);
54
+ }
55
+ if (/^[ \t]*export\s+(?:const|let|var)\s*[[{]/m.test(src)) {
56
+ throw new UseSsrError(`"use ssr": ${file} destructures an export. Export each function by its own name; that is what gets called across.`);
57
+ }
58
+ if (/^[ \t]*export\s+(?:abstract\s+)?(?:class|enum)\b/m.test(src)) {
59
+ throw new UseSsrError(`"use ssr": a module with the directive exports functions only; ${file} exports a class or enum.`);
60
+ }
61
+ if (/^[ \t]*export\s*(?:\{|\*)/m.test(src)) {
62
+ throw new UseSsrError(`"use ssr": ${file} re-exports. Export the functions where they are declared; \`export { … }\` and \`export *\` cannot be called across.`);
63
+ }
64
+ const dflt = /^[ \t]*export\s+default\s+(async\s+)?(function\b)?/m.exec(src);
65
+ if (dflt) {
66
+ if (dflt[2] && !dflt[1]) {
67
+ throw new UseSsrError(`"use ssr": the default export of ${file} is called from another environment. Make it an async function.`);
68
+ }
69
+ hasDefault = true;
70
+ }
71
+ return { named: [...named], hasDefault };
72
+ }
73
+ /**
74
+ * The module the server-components environment gets in place of a "use ssr"
75
+ * one: its exports, as async proxies that call the real module where it
76
+ * runs. Null when the module carries no directive.
77
+ */
78
+ export function ssrProxyModule(code, id, root = process.cwd()) {
79
+ if (!hasUseSsr(code))
80
+ return null;
81
+ const path = id.split("?")[0];
82
+ const file = relative(root, path);
83
+ const { named, hasDefault } = ssrExports(code, file);
84
+ return ([
85
+ `// "use ssr": ${file} runs in the ssr environment, where React DOM's server renderer can. These call across.`,
86
+ `const __rsc_kit_ssr = import.meta.viteRsc.import(${JSON.stringify("./" + basename(path))}, { environment: 'ssr' });`,
87
+ ...named.map((name) => `export const ${name} = async (...args) => (await __rsc_kit_ssr).${name}(...args);`),
88
+ ...(hasDefault
89
+ ? [
90
+ "export default async (...args) => (await __rsc_kit_ssr).default(...args);",
91
+ ]
92
+ : []),
93
+ ].join("\n") + "\n");
94
+ }
95
+ /** Any of react-dom's server renderer entries. */
96
+ export const SERVER_RENDERER = /^react-dom\/server(?:\.[a-z]+)?$/;
97
+ /** What a module gets in place of react-dom/server where server components render. */
98
+ export function serverRendererMessage(importer) {
99
+ return ("react-dom/server cannot run where server components render: React's server build has no client internals for it, and the components it would render import that same React." +
100
+ (importer ? ` Imported by ${importer}.` : "") +
101
+ ' Put the rendering - the template and the call - in a module that starts with "use ssr": it runs in the ssr environment, and its exports are called from here as async functions. ' +
102
+ SSR_GUIDE);
103
+ }
104
+ //# sourceMappingURL=useSsr.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"useSsr.js","sourceRoot":"","sources":["../src/useSsr.ts"],"names":[],"mappings":"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","sourcesContent":["import { basename, relative } from \"node:path\";\n\n/**\n * \"use ssr\": a module that runs in the ssr environment and is called from\n * where server components render.\n *\n * react-dom/server cannot run where server components render. React's\n * server build refuses to load it, and the refusal is right: the renderer\n * needs the client build's internals, and the components it would render\n * import that same server `react` - no useState, no useContext. A template\n * and the call that renders it have to load together somewhere React DOM's\n * server renderer can: the ssr environment, which every app here already\n * has. plugin-rsc exposes that as a call to write by hand,\n * `import.meta.viteRsc.import('./render', { environment: 'ssr' })`, and\n * nobody should have to. So a directive names the module instead, the way\n * \"use client\" names one, and this rewrites it for the server-components\n * environment into proxies of its exports that call across. Everything\n * else imports it normally.\n *\n * The exports are functions and the calls are async, because the answer\n * crosses environments as a promise. Anything else is refused with the\n * export named, at build and in dev, rather than proxied into nonsense.\n */\nconst DIRECTIVE = /^\\s*(?:\\/\\/[^\\n]*\\n|\\/\\*[\\s\\S]*?\\*\\/\\s*)*[\"']use ssr[\"']/;\n\nexport const SSR_GUIDE = \"https://docs.rsc-kit.dev/guides/emails\";\n\nexport function hasUseSsr(code: string): boolean {\n return DIRECTIVE.test(code.slice(0, 2048));\n}\n\nexport class UseSsrError extends Error {}\n\n/** Comments blanked, so an `export` inside one is not read as one. */\nfunction withoutComments(code: string): string {\n return code\n .replace(/\\/\\*[\\s\\S]*?\\*\\//g, (m) => m.replace(/[^\\n]/g, \" \"))\n .replace(\n /(^|[^:\\\\])\\/\\/[^\\n]*/g,\n (m, lead: string) => lead + \" \".repeat(m.length - lead.length),\n );\n}\n\nconst NAME = \"[A-Za-z_$][\\\\w$]*\";\nconst NON_FUNCTION = /^\\s*(?:new\\b|[\"'`\\d[{]|true\\b|false\\b|null\\b)/;\n\n/** The runtime exports a \"use ssr\" module can offer: named functions, and a default. */\nexport function ssrExports(\n code: string,\n file: string,\n): { named: string[]; hasDefault: boolean } {\n const src = withoutComments(code);\n const named = new Set<string>();\n let hasDefault = false;\n\n for (const m of src.matchAll(\n new RegExp(\n `^[ \\\\t]*export\\\\s+(async\\\\s+)?function\\\\s*\\\\*?\\\\s*(${NAME})`,\n \"gm\",\n ),\n )) {\n if (!m[1]) {\n throw new UseSsrError(\n `\"use ssr\": \\`${m[2]}\\` in ${file} is called from another environment, and the answer crosses as a promise. Make it an async function.`,\n );\n }\n\n named.add(m[2]);\n }\n\n for (const m of src.matchAll(\n new RegExp(\n `^[ \\\\t]*export\\\\s+(?:const|let|var)\\\\s+(${NAME})\\\\s*(?::[^=\\\\n]*)?=([^\\\\n]*)`,\n \"gm\",\n ),\n )) {\n if (NON_FUNCTION.test(m[2])) {\n throw new UseSsrError(\n `\"use ssr\": a module with the directive exports functions only; \\`${m[1]}\\` in ${file} is a value. Put it in a module without the directive.`,\n );\n }\n\n named.add(m[1]);\n }\n\n if (/^[ \\t]*export\\s+(?:const|let|var)\\s*[[{]/m.test(src)) {\n throw new UseSsrError(\n `\"use ssr\": ${file} destructures an export. Export each function by its own name; that is what gets called across.`,\n );\n }\n\n if (/^[ \\t]*export\\s+(?:abstract\\s+)?(?:class|enum)\\b/m.test(src)) {\n throw new UseSsrError(\n `\"use ssr\": a module with the directive exports functions only; ${file} exports a class or enum.`,\n );\n }\n\n if (/^[ \\t]*export\\s*(?:\\{|\\*)/m.test(src)) {\n throw new UseSsrError(\n `\"use ssr\": ${file} re-exports. Export the functions where they are declared; \\`export { … }\\` and \\`export *\\` cannot be called across.`,\n );\n }\n\n const dflt = /^[ \\t]*export\\s+default\\s+(async\\s+)?(function\\b)?/m.exec(src);\n\n if (dflt) {\n if (dflt[2] && !dflt[1]) {\n throw new UseSsrError(\n `\"use ssr\": the default export of ${file} is called from another environment. Make it an async function.`,\n );\n }\n\n hasDefault = true;\n }\n\n return { named: [...named], hasDefault };\n}\n\n/**\n * The module the server-components environment gets in place of a \"use ssr\"\n * one: its exports, as async proxies that call the real module where it\n * runs. Null when the module carries no directive.\n */\nexport function ssrProxyModule(\n code: string,\n id: string,\n root: string = process.cwd(),\n): string | null {\n if (!hasUseSsr(code)) return null;\n\n const path = id.split(\"?\")[0];\n const file = relative(root, path);\n const { named, hasDefault } = ssrExports(code, file);\n\n return (\n [\n `// \"use ssr\": ${file} runs in the ssr environment, where React DOM's server renderer can. These call across.`,\n `const __rsc_kit_ssr = import.meta.viteRsc.import(${JSON.stringify(\"./\" + basename(path))}, { environment: 'ssr' });`,\n ...named.map(\n (name) =>\n `export const ${name} = async (...args) => (await __rsc_kit_ssr).${name}(...args);`,\n ),\n ...(hasDefault\n ? [\n \"export default async (...args) => (await __rsc_kit_ssr).default(...args);\",\n ]\n : []),\n ].join(\"\\n\") + \"\\n\"\n );\n}\n\n/** Any of react-dom's server renderer entries. */\nexport const SERVER_RENDERER = /^react-dom\\/server(?:\\.[a-z]+)?$/;\n\n/** What a module gets in place of react-dom/server where server components render. */\nexport function serverRendererMessage(importer: string | null): string {\n return (\n \"react-dom/server cannot run where server components render: React's server build has no client internals for it, and the components it would render import that same React.\" +\n (importer ? ` Imported by ${importer}.` : \"\") +\n ' Put the rendering - the template and the call - in a module that starts with \"use ssr\": it runs in the ssr environment, and its exports are called from here as async functions. ' +\n SSR_GUIDE\n );\n}\n"]}
package/dist/vite.d.ts CHANGED
@@ -6,16 +6,41 @@ export interface RscKitOptions {
6
6
  /** Directory holding the app/ route tree. Defaults to `src`. */
7
7
  sourceDir?: string;
8
8
  /**
9
- * Animate navigations with React's `<ViewTransition>`.
9
+ * Say what built the site: `<meta name="generator" content="rsc-kit">` in
10
+ * every document and `X-Powered-By: rsc-kit` on every response. The name
11
+ * only, never the version - a version in every response is what a
12
+ * vulnerability scanner filters on. On by default; `false` for a team
13
+ * whose policy strips every framework identifier.
10
14
  *
11
- * Off by default and a build-time constant rather than a runtime setting, so
12
- * an app that does not ask for it does not carry the boundary at all.
15
+ * `X-RSC-Kit: stored | rendered | shell` - how a response was answered, the
16
+ * header a developer reads in the Network tab - is not this and is always
17
+ * sent: it names no product, and a CDN rule or a check can key on it.
18
+ */
19
+ identify?: boolean;
20
+ /**
21
+ * Unroll named imports from a barrel package on the server, in development.
22
+ *
23
+ * `import { ArrowRight } from 'lucide-react'` reaches a file that re-exports
24
+ * fifteen hundred icons, one module each; the browser gets the package
25
+ * pre-bundled, the server environments do not, so Vite loaded every icon
26
+ * module on the first request. The import is rewritten to the module that
27
+ * owns the name, read from the barrel itself. On by default; `false` to
28
+ * leave imports as written.
29
+ */
30
+ barrelImports?: boolean;
31
+ /**
32
+ * The site's own hosts, beyond the one in the root layout's metadataBase.
13
33
  *
14
- * Needs react and react-dom at 19.3 or newer, where ViewTransition is
15
- * stable. What it animates is the segment a navigation replaces; what a page
16
- * does inside itself is the app's own business and needs no flag.
34
+ * A request from any other host is routed with the host in front of the
35
+ * path: `admin.example.com/users` reaches `app/admin/users/page.tsx`, the
36
+ * file `example.com/admin/users` reaches by path; `acme.example.com/`
37
+ * reaches `app/[domain]/page.tsx` with `domain: "acme"`, and a custom
38
+ * domain `acme.com/` the same file with `domain: "acme.com"`. The site's
39
+ * own hosts keep path routing, so an app adds tenants without moving a
40
+ * file. `www.` of each is its own too. With no metadataBase and nothing
41
+ * here, every host is the site's own and nothing is prepended.
17
42
  */
18
- viewTransitions?: boolean;
43
+ hosts?: string[];
19
44
  /**
20
45
  * Serve the app from a service worker, so it survives a reload with no
21
46
  * network at all.
@@ -44,6 +69,18 @@ export interface RscKitOptions {
44
69
  * regardless, because React expects to find it in the DOM to hydrate.
45
70
  */
46
71
  inlineStylesheets?: "auto" | boolean | number;
72
+ /**
73
+ * Run the project's own typecheck as part of `vite build`, and fail the
74
+ * build on an error. On by default when the project has a `tsconfig.json`
75
+ * and `typescript` installed; `false` turns it off.
76
+ *
77
+ * A build that bundles a `<Link href="/nowhere">` and stores every page
78
+ * has produced a broken site that looks finished. The route types exist so
79
+ * that link is an error - but Vite never typechecks, so the error only
80
+ * showed for whoever ran `tsc` separately. `next build` typechecks for the
81
+ * same reason.
82
+ */
83
+ typecheck?: boolean;
47
84
  /**
48
85
  * The most a server action body may be, in bytes. 8 MB unless set.
49
86
  *
@@ -93,8 +130,12 @@ export interface RscKitOptions {
93
130
  * because a second setting that could disagree with the first is a bug
94
131
  * waiting to be filed. `false` restores the plain 404.
95
132
  *
96
- * Development only. A built deployment's server.ts decides for itself what
97
- * to do with a url it does not own.
133
+ * The built server forwards too — the entry that carries this is the one
134
+ * Nitro bundles — with the origin read at build time from the project's
135
+ * .env, while host calls read it from the process at runtime. A build on a
136
+ * machine whose .env names a different backend than production's sets
137
+ * RSC_BACKEND for the build. The name predates that: it was the dev server's
138
+ * alone before Nitro built the server from the same entry.
98
139
  */
99
140
  devFallback?: string | false;
100
141
  /**
@@ -245,4 +286,40 @@ export interface KnownAction {
245
286
  name: string;
246
287
  file: string;
247
288
  }
289
+ /**
290
+ * The worker the dev server serves at /sw.js: it removes the one a production
291
+ * run left on this origin, and the caches with it, then reloads each page it
292
+ * was controlling so they load uncontrolled. Nothing else runs in development.
293
+ */
294
+ export declare const DEV_WORKER = "self.addEventListener('install', () => self.skipWaiting())\nself.addEventListener('activate', (event) => {\n event.waitUntil((async () => {\n const keys = await caches.keys()\n await Promise.all(keys.filter((k) => k.startsWith('rsc-kit-')).map((k) => caches.delete(k)))\n await self.clients.claim()\n const pages = await self.clients.matchAll({ type: 'window' })\n await self.registration.unregister()\n for (const page of pages) page.navigate(page.url).catch(() => {})\n })())\n})\n";
295
+ /**
296
+ * The project's typecheck, as part of the build.
297
+ *
298
+ * Runs once, in the first environment to start, after the route types have
299
+ * been written by the config hook - so a link to a route that does not exist
300
+ * is an error here and not a 404 found after deploying. Skipped where the
301
+ * project is not doing this checking at all: no tsconfig.json, or no
302
+ * typescript to run. Its own output is the message, because that is what
303
+ * the developer would have read from `tsc`.
304
+ */
305
+ export type TypecheckOutcome = {
306
+ ran: false;
307
+ because: "no tsconfig" | "no typescript";
308
+ } | {
309
+ ran: true;
310
+ ok: true;
311
+ seconds: number;
312
+ } | {
313
+ ran: true;
314
+ ok: false;
315
+ output: string;
316
+ };
317
+ /**
318
+ * `tsc --noEmit` on the project's own tsconfig, the way `check` runs it.
319
+ *
320
+ * Through package.json rather than a subpath: TypeScript's exports map does
321
+ * not expose bin/tsc, and the bin field is where the name lives. Its output
322
+ * is the message, because that is what the developer would have read.
323
+ */
324
+ export declare function typecheckProject(root: string): TypecheckOutcome;
248
325
  export declare function rscKit(options?: RscKitOptions): PluginOption[];