@rsc-kit/core 0.17.0 → 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 (52) 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/events.d.ts +57 -0
  5. package/dist/events.js +113 -0
  6. package/dist/events.js.map +1 -0
  7. package/dist/host.d.ts +16 -6
  8. package/dist/host.js +219 -124
  9. package/dist/host.js.map +1 -1
  10. package/dist/hostCalls.d.ts +27 -0
  11. package/dist/hostCalls.js +127 -12
  12. package/dist/hostCalls.js.map +1 -1
  13. package/dist/hostRouting.d.ts +53 -0
  14. package/dist/hostRouting.js +100 -0
  15. package/dist/hostRouting.js.map +1 -0
  16. package/dist/js/DefaultRouteError.d.ts +3 -0
  17. package/dist/js/DefaultRouteError.js +61 -0
  18. package/dist/js/DefaultRouteError.js.map +1 -0
  19. package/dist/js/queryClient.d.ts +2 -1
  20. package/dist/js/queryClient.js +0 -12
  21. package/dist/js/queryClient.js.map +1 -1
  22. package/dist/js/router.d.ts +2 -2
  23. package/dist/js/router.js.map +1 -1
  24. package/dist/js/useEvents.d.ts +25 -0
  25. package/dist/js/useEvents.js +78 -0
  26. package/dist/js/useEvents.js.map +1 -0
  27. package/dist/js/usePolling.d.ts +29 -0
  28. package/dist/js/usePolling.js +141 -0
  29. package/dist/js/usePolling.js.map +1 -0
  30. package/dist/manifest.d.ts +18 -1
  31. package/dist/manifest.js.map +1 -1
  32. package/dist/metadataRoutes.d.ts +20 -0
  33. package/dist/metadataRoutes.js +35 -0
  34. package/dist/metadataRoutes.js.map +1 -1
  35. package/dist/prerender.js +1 -0
  36. package/dist/prerender.js.map +1 -1
  37. package/dist/redirect.d.ts +16 -2
  38. package/dist/redirect.js +10 -19
  39. package/dist/redirect.js.map +1 -1
  40. package/dist/revalidate.d.ts +6 -4
  41. package/dist/revalidate.js +6 -5
  42. package/dist/revalidate.js.map +1 -1
  43. package/dist/routes.d.ts +16 -3
  44. package/dist/routes.js +5 -5
  45. package/dist/routes.js.map +1 -1
  46. package/dist/routing.d.ts +2 -2
  47. package/dist/routing.js +47 -25
  48. package/dist/routing.js.map +1 -1
  49. package/dist/vite.d.ts +42 -2
  50. package/dist/vite.js +333 -5
  51. package/dist/vite.js.map +1 -1
  52. package/package.json +13 -1
package/dist/vite.js CHANGED
@@ -27,7 +27,9 @@ import { ASSET_BASE, allAppAssets, appAssets, headTags, typeOf, } from "./appAss
27
27
  import { reactCacheImports } from "./reactCache.js";
28
28
  import { clientEntries, clientScanPlugin, engineClientEntries, } from "./clientEntries.js";
29
29
  import { serverImportsOfClientPackages } from "./clientImports.js";
30
- import { METADATA_ROUTES, ROOT_FILES, rootFileType } from "./metadataRoutes.js";
30
+ import { automaticSitemap, METADATA_ROUTES, ROOT_FILES, rootFileType, } from "./metadataRoutes.js";
31
+ import { ownHosts } from "./hostRouting.js";
32
+ import { unrollBarrelImports } from "./barrelImports.js";
31
33
  import { serverRendererMessage, SERVER_RENDERER, ssrProxyModule, UseSsrError, } from "./useSsr.js";
32
34
  import { httpHostCalls } from "./hostCalls.js";
33
35
  // Resolved once per rscKit() call. One build runs in one process, so these are
@@ -108,6 +110,11 @@ let isWatch = false;
108
110
  let offline = false;
109
111
  let typecheck = true;
110
112
  let webManifestOptions = null;
113
+ /** The site's own hosts, from the root layout's metadataBase and rscKit({ hosts }). */
114
+ let siteHosts = [];
115
+ let hostsOption = [];
116
+ let barrelImports = true;
117
+ let identify = true;
111
118
  let foundAssets = {
112
119
  favicon: null,
113
120
  icons: [],
@@ -270,6 +277,9 @@ function resolvePaths(options) {
270
277
  prerenderAfterBuild = process.env.RSC_PRERENDER !== "0";
271
278
  offline = options.offline === true;
272
279
  typecheck = options.typecheck !== false;
280
+ hostsOption = options.hosts ?? [];
281
+ barrelImports = options.barrelImports !== false;
282
+ identify = options.identify !== false;
273
283
  inlineStylesheets = options.inlineStylesheets ?? "auto";
274
284
  maxActionBody = options.maxActionBody;
275
285
  // One place, and it is the file. A plugin option as well would be the same
@@ -323,7 +333,12 @@ function urlSegments(componentName) {
323
333
  continue;
324
334
  }
325
335
  if (part.startsWith("[") && part.endsWith("]")) {
326
- segments.push({ type: "param", value: part.slice(1, -1) });
336
+ // At the top of app/ - the first segment the url has - a parameter is
337
+ // the host's: bound from acme.example.com, never from example.com/acme.
338
+ segments.push({
339
+ type: segments.length === 0 ? "host" : "param",
340
+ value: part.slice(1, -1),
341
+ });
327
342
  continue;
328
343
  }
329
344
  // An interception marker says which url this replaces, not what it is
@@ -395,7 +410,11 @@ function routeManifest() {
395
410
  * to whoever knows what they mean.
396
411
  */
397
412
  const middlewareIn = (absDir) => {
398
- for (const file of ["route.ts", "route.tsx"]) {
413
+ // middleware.ts first: it is the file named for what this is. A guard
414
+ // the engine runs is its default export; the names a host runs are its
415
+ // `middleware` export, and a file may carry either or both. route.ts is
416
+ // read too, for the apps written before middleware.ts could.
417
+ for (const file of ["middleware.ts", "middleware.tsx", "route.ts", "route.tsx"]) {
399
418
  const path = join(absDir, file);
400
419
  if (!existsSync(path))
401
420
  continue;
@@ -481,7 +500,13 @@ function routeManifest() {
481
500
  // writing the site out, and knowing which filename the client will ask for.
482
501
  return {
483
502
  version: 1,
484
- build: { output, exportPath, payloadName: staticPayloads },
503
+ build: {
504
+ output,
505
+ exportPath,
506
+ payloadName: staticPayloads,
507
+ hosts: siteHosts,
508
+ identify,
509
+ },
485
510
  routes,
486
511
  intercepts,
487
512
  apis: [...apiRoutes.values()].map(({ name, methods, generated }) => ({
@@ -1236,6 +1261,41 @@ async function prerenderAfterBundles(bundle, staticDir, assetsDir, knownActions
1236
1261
  // places to look for one fact.
1237
1262
  const manifest = engine.manifest();
1238
1263
  const apis = await prerenderApiRoutes(engine, manifest, writeTo(staticDir));
1264
+ // A sitemap the build writes itself, when the app wrote none: every url
1265
+ // it stored or was told about, minus the guarded ones. It needs the site's
1266
+ // host, which the root layout's metadataBase is for; without one there is
1267
+ // nothing to write and the report says so once.
1268
+ const writesSitemap = manifest.apis?.some((api) => api.name === "app/sitemap.xml/route");
1269
+ const hasSitemapFile = rootFiles.includes("sitemap.xml");
1270
+ if (!writesSitemap && !hasSitemapFile) {
1271
+ const base = rootMetadataBase(join(sourceDir, "app"));
1272
+ if (base) {
1273
+ const xml = automaticSitemap(results, manifest.routes, base);
1274
+ const stored = JSON.stringify({
1275
+ status: 200,
1276
+ headers: [["content-type", "application/xml; charset=utf-8"]],
1277
+ body: xml,
1278
+ varies: false,
1279
+ });
1280
+ await writeTo(staticDir)("sitemap.xml.api.json", stored);
1281
+ pending.push({
1282
+ line: " ○ /sitemap.xml",
1283
+ bytes: null,
1284
+ extra: [
1285
+ ` written by the build: ${xml.split("<url>").length - 1} urls; a sitemap.ts beside the root layout replaces it`,
1286
+ ],
1287
+ });
1288
+ }
1289
+ else {
1290
+ pending.push({
1291
+ line: " - /sitemap.xml",
1292
+ bytes: null,
1293
+ extra: [
1294
+ " not written: the root layout has no metadataBase to make the urls absolute",
1295
+ ],
1296
+ });
1297
+ }
1298
+ }
1239
1299
  for (const api of apis) {
1240
1300
  pending.push({
1241
1301
  line: ` ${api.type === "frozen" ? "○" : "ƒ"} ${api.url}`,
@@ -1480,6 +1540,23 @@ function renderRouteTypes(manifest) {
1480
1540
  const target = relative(typesDir, join(sourceDir, route.component)).replace(/\\/g, "/");
1481
1541
  search.set(pattern, target.startsWith(".") ? target : "./" + target);
1482
1542
  }
1543
+ // Section names are read from the source - section('orders', …) - the way
1544
+ // generateStaticParams is detected, because this runs before any bundle
1545
+ // exists. A name computed at runtime is not seen and revalidate() falls
1546
+ // back to refusing it at the renderer, as before.
1547
+ const regions = [
1548
+ ...new Set([
1549
+ ...[...components.values()]
1550
+ .filter((c) => SECTION_FILE.test(c.absPath))
1551
+ .flatMap((c) => [
1552
+ ...readFileSync(c.absPath, "utf-8").matchAll(/\bsection\(\s*['"`]([^'"`]+)['"`]/g),
1553
+ ].map((m) => m[1])),
1554
+ ...[...components.keys()].flatMap((name) => {
1555
+ const slot = name.split("/").find((part) => part.startsWith("@"));
1556
+ return slot ? [slot.slice(1)] : [];
1557
+ }),
1558
+ ]),
1559
+ ].sort();
1483
1560
  return [
1484
1561
  "// @generated — do not edit. Written by the RSC build from the route tree.",
1485
1562
  "//",
@@ -1513,6 +1590,14 @@ function renderRouteTypes(manifest) {
1513
1590
  // Api routes are a separate union, so Link refuses an api url and apiUrl()
1514
1591
  // refuses a page. Linking to an api route navigates the browser away to a
1515
1592
  // json document, which is the mistake worth catching.
1593
+ // Regions: every section('name', …) the build read, and every @slot
1594
+ // directory. This is what types revalidate().
1595
+ " interface RegisterRegions {",
1596
+ regions.length > 0
1597
+ ? " regions:\n" +
1598
+ regions.map((r) => " | " + JSON.stringify(r)).join("\n")
1599
+ : " // No sections or slots found under the source directory.\n regions: never",
1600
+ " }",
1516
1601
  " interface RegisterApi {",
1517
1602
  apis.length > 0
1518
1603
  ? " apis:\n" +
@@ -1714,10 +1799,20 @@ function registerApiRoute(absPath) {
1714
1799
  }
1715
1800
  apiRoutes.set(name, { name, absPath, methods });
1716
1801
  }
1802
+ /**
1803
+ * Whether a middleware.ts is a guard the engine runs, or only names guards
1804
+ * the host runs. The engine imports a guard's default export; a file with
1805
+ * none - `export const middleware = ['auth']` and nothing else - has nothing
1806
+ * to import, and registering it would make the build fail on an export that
1807
+ * was never meant to exist.
1808
+ */
1809
+ function isEngineGuard(absPath) {
1810
+ return /export\s+default\b/.test(readFileSync(absPath, "utf-8"));
1811
+ }
1717
1812
  function discover(dir) {
1718
1813
  for (const base of ROUTE_FILES) {
1719
1814
  const p = findRouteFile(dir, base);
1720
- if (p)
1815
+ if (p && (base !== "middleware" || isEngineGuard(p)))
1721
1816
  register(p);
1722
1817
  }
1723
1818
  // route.ts — an api endpoint, colocated with the pages it sits among. Read
@@ -1748,6 +1843,21 @@ function discover(dir) {
1748
1843
  * pages export only the static object, so referencing both meant that warning
1749
1844
  * for almost every route in an app.
1750
1845
  */
1846
+ /**
1847
+ * The root layout's metadataBase, read from the source. Wanted while the
1848
+ * manifest is built, before there is a bundle to execute - the same reason
1849
+ * generateStaticParams is detected by reading. A `new URL('…')` or a string
1850
+ * literal; anything computed is not seen, and rscKit({ hosts }) says it.
1851
+ */
1852
+ function rootMetadataBase(appDir) {
1853
+ const layout = ["layout.tsx", "layout.jsx", "layout.ts", "layout.js"]
1854
+ .map((name) => join(appDir, name))
1855
+ .find((file) => existsSync(file));
1856
+ if (!layout)
1857
+ return null;
1858
+ const match = /metadataBase\s*:\s*(?:new\s+URL\(\s*)?["'`](https?:\/\/[^"'`]+)["'`]/.exec(readFileSync(layout, "utf-8"));
1859
+ return match ? match[1] : null;
1860
+ }
1751
1861
  function metadataExports(absPath) {
1752
1862
  const src = readFileSync(absPath, "utf-8");
1753
1863
  return {
@@ -1942,7 +2052,29 @@ function installHostCallsOnce(): void {
1942
2052
  }
1943
2053
 
1944
2054
  `;
2055
+ /**
2056
+ * The app's process bootstrap, if it has one: `instrumentation.ts` beside
2057
+ * `app/`, the name Next uses so a reader arriving from there knows what it
2058
+ * is.
2059
+ *
2060
+ * Two things make it a framework concern rather than an import the app
2061
+ * adds. Module evaluation order: a page that configures a shared package at
2062
+ * import time runs before any module the app could put first, and only the
2063
+ * generated entry can import something before the pages. And "before the
2064
+ * first request": an async `register()` — connecting, validating env — has
2065
+ * to be awaited by every entry point a host or a prerender can call, which
2066
+ * is a list the app cannot see.
2067
+ */
2068
+ function instrumentationFile() {
2069
+ for (const ext of ["ts", "tsx", "mts", "js", "mjs"]) {
2070
+ const file = join(sourceDir, `instrumentation.${ext}`);
2071
+ if (existsSync(file))
2072
+ return file;
2073
+ }
2074
+ return null;
2075
+ }
1945
2076
  function generateEntryRsc(fallbackOrigin = "") {
2077
+ const instrumentation = instrumentationFile();
1946
2078
  // The 404 page, if the app has one, and the layouts it renders inside.
1947
2079
  // Computed here rather than looked up at runtime: not-found is not a route,
1948
2080
  // so the manifest has no entry to read its chain from.
@@ -1999,12 +2131,20 @@ function generateEntryRsc(fallbackOrigin = "") {
1999
2131
  // from src/ in its own repo and from dist/ once published, and Vite resolves
2000
2132
  // either. Naming .tsx here builds fine from source and fails after publish.
2001
2133
  return `// GENERATED by rscKit() — do not edit.
2134
+ ${
2135
+ // First, before any page: an import's side effects run in import order,
2136
+ // and a package configured here has to be configured before a page module
2137
+ // that reads it at evaluation time.
2138
+ instrumentation
2139
+ ? `import * as __instrumentation from ${JSON.stringify(instrumentation)}`
2140
+ : "const __instrumentation: { register?: () => unknown } = {}"}
2002
2141
  import { SegmentBoundary } from ${JSON.stringify(join(packageDir, "js/SegmentBoundary"))}
2003
2142
  import { DocumentTitle } from ${JSON.stringify(join(packageDir, "js/DocumentTitle"))}
2004
2143
  import { SlotBoundary } from ${JSON.stringify(join(packageDir, "js/SlotBoundary"))}
2005
2144
  import { RouteErrorBoundary } from ${JSON.stringify(join(packageDir, "js/RouteErrorBoundary"))}
2006
2145
  import { sectionComponent } from ${JSON.stringify(join(packageDir, "js/section"))}
2007
2146
  import { PathnameProvider } from ${JSON.stringify(join(packageDir, "js/PathnameProvider"))}
2147
+ import { DefaultRouteError } from ${JSON.stringify(join(packageDir, "js/DefaultRouteError"))}
2008
2148
  import { searchParams as requestSearchParams } from ${JSON.stringify(join(packageDir, "request"))}
2009
2149
  import { parseParams, parseSearchParams, parseBody, isSearchParamsError, isBodyError } from ${JSON.stringify(join(packageDir, "routeSchema"))}
2010
2150
  import { notFoundDigest, isNotFoundSignal } from ${JSON.stringify(join(packageDir, "notFound"))}
@@ -2144,6 +2284,7 @@ export async function handleApiRoute(
2144
2284
  params: Record<string, string>,
2145
2285
  allow: string,
2146
2286
  ): Promise<Response> {
2287
+ await instrumented()
2147
2288
  applyHost()
2148
2289
 
2149
2290
  const mod = apiRoutes[name]
@@ -2251,6 +2392,8 @@ export async function auditActions(ids: string[]): Promise<{ id: string; client:
2251
2392
  * that asked for everything, or the reverse.
2252
2393
  */
2253
2394
  export async function getStaticParams(component: string): Promise<Record<string, string>[] | null> {
2395
+ await instrumented()
2396
+
2254
2397
  const generate = staticParamsMap[component]
2255
2398
 
2256
2399
  if (!generate) return null
@@ -2274,6 +2417,57 @@ const HOST_GLOBAL = ${JSON.stringify(hostGlobal)}
2274
2417
  */
2275
2418
  const HOST_MIDDLEWARE_FN = '__rsc.middleware'
2276
2419
 
2420
+ /**
2421
+ * instrumentation.ts's register(), once.
2422
+ *
2423
+ * On a long-lived server it runs at startup: the entry is evaluated when the
2424
+ * process starts, and this begins then, so env that fails validation fails
2425
+ * the boot rather than the first visitor, and the first request does not
2426
+ * pay for it. On a Worker there is no startup — an isolate is created for a
2427
+ * request, and a binding is only readable once one has arrived — so it runs
2428
+ * at the first request of each isolate instead. Every entry point awaits it
2429
+ * either way, which is what makes "before the first render" hold whether the
2430
+ * caller is the built server, the dev server or the prerender.
2431
+ *
2432
+ * A rejection is not memoised. Env that fails validation fails the same way
2433
+ * on every request, which is right; a database that was not up yet gets
2434
+ * asked again rather than leaving the process permanently refusing. In
2435
+ * production a startup failure is reported and the process exits, because a
2436
+ * server that could not bootstrap has nothing correct to serve; the dev
2437
+ * server stays up and reports it on the page instead.
2438
+ */
2439
+ let __instrumented: Promise<void> | null = null
2440
+
2441
+ function instrumented(): Promise<void> {
2442
+ if (!__instrumented) {
2443
+ __instrumented = Promise.resolve()
2444
+ .then(() => __instrumentation.register?.())
2445
+ .then(
2446
+ () => undefined,
2447
+ (error) => {
2448
+ __instrumented = null
2449
+ throw error
2450
+ },
2451
+ )
2452
+ }
2453
+
2454
+ return __instrumented
2455
+ }
2456
+
2457
+ // Workers name themselves; nothing else does. Where there is a process to
2458
+ // start, start now.
2459
+ const isolateRuntime = typeof navigator !== 'undefined' && navigator.userAgent === 'Cloudflare-Workers'
2460
+
2461
+ if (__instrumentation.register && !isolateRuntime) {
2462
+ instrumented().catch((error) => {
2463
+ console.error('[rsc-kit] instrumentation.ts register() failed:', error)
2464
+
2465
+ if (!import.meta.env.DEV && typeof process !== 'undefined' && typeof process.exit === 'function') {
2466
+ process.exit(1)
2467
+ }
2468
+ })
2469
+ }
2470
+
2277
2471
  let currentHost: HostFn | null = null
2278
2472
 
2279
2473
  export function installHostFn(fn: HostFn) {
@@ -2507,6 +2701,19 @@ function buildElement(
2507
2701
  )
2508
2702
  }
2509
2703
 
2704
+ // Outermost, for a throw no error.tsx covers - including one in a layout,
2705
+ // which the boundary inside that layout cannot see. Without it React
2706
+ // unmounted the document on hydration: a black page with the cause nowhere
2707
+ // near it. Only with the runtime: it is a client component, and a route
2708
+ // shipping none has nothing to catch with.
2709
+ if (bootstrap) {
2710
+ element = createElement(
2711
+ RouteErrorBoundary,
2712
+ { fallback: DefaultRouteError as never, resetKey: pageKey || component },
2713
+ element,
2714
+ )
2715
+ }
2716
+
2510
2717
  // <title>/<meta> go OUTSIDE the Suspense boundaries so they reach the shell
2511
2718
  // immediately — inside, they would be withheld until the page's data
2512
2719
  // resolves, delaying the whole document on a slow page.
@@ -2660,6 +2867,8 @@ async function renderTree(
2660
2867
  if (bootstrap) head.push(createElement(DocumentTitle, { key: '__ts', title: String(md.title) }))
2661
2868
  }
2662
2869
  if (md.description != null) head.push(createElement('meta', { key: '__d', name: 'description', content: String(md.description) }))
2870
+ // The name only, never the version - see the identify option.
2871
+ if (manifest().build?.identify) head.push(createElement('meta', { key: '__g', name: 'generator', content: 'rsc-kit' }))
2663
2872
 
2664
2873
  // robots is a string, or the object Next takes: index and follow as
2665
2874
  // their no- forms, the flags by name, the limits as name:value. googleBot
@@ -2961,6 +3170,7 @@ async function runMiddleware(component: string, props: Record<string, unknown> =
2961
3170
  * throws, exactly as it does mid-render.
2962
3171
  */
2963
3172
  export async function runRouteMiddleware(component: string, props: Record<string, unknown> = {}): Promise<void> {
3173
+ await instrumented()
2964
3174
  applyHost()
2965
3175
 
2966
3176
  return runMiddleware(component, props)
@@ -2976,6 +3186,7 @@ export async function handleRscStream(
2976
3186
  from = 0,
2977
3187
  pageKey = '',
2978
3188
  ): Promise<{ stream: ReadableStream; clientChunks: unknown; segmentDepth: number }> {
3189
+ await instrumented()
2979
3190
  applyHost()
2980
3191
 
2981
3192
  // The host proposes how much the client already has; the engine decides what
@@ -3031,6 +3242,7 @@ export async function handleRscHtmlStream(
3031
3242
  pageKey = '',
3032
3243
  bootstrap = true,
3033
3244
  ): Promise<{ htmlStream: ReadableStream; rscPayloadPromise: Promise<string>; clientChunks: unknown }> {
3245
+ await instrumented()
3034
3246
  applyHost()
3035
3247
  await runMiddleware(component, props)
3036
3248
  const flight = renderToReadableStream(
@@ -3066,6 +3278,7 @@ export async function handleRscResume(
3066
3278
  nonce?: string,
3067
3279
  pageKey = '',
3068
3280
  ): Promise<{ htmlStream: ReadableStream }> {
3281
+ await instrumented()
3069
3282
  applyHost()
3070
3283
  await runMiddleware(component, props)
3071
3284
 
@@ -3216,6 +3429,7 @@ export async function handleQuery(
3216
3429
  | { status: number; message: string; errors?: Record<string, string[]> }
3217
3430
  | null
3218
3431
  > {
3432
+ await instrumented()
3219
3433
  applyHost()
3220
3434
 
3221
3435
  let fn: unknown
@@ -3264,6 +3478,7 @@ export async function handleAction(
3264
3478
  page?: PageContext,
3265
3479
  takeRevalidated?: () => string[],
3266
3480
  ): Promise<{ stream: ReadableStream }> {
3481
+ await instrumented()
3267
3482
  applyHost()
3268
3483
 
3269
3484
  // Every body arrives as bytes on its own socket frame — an upload because it
@@ -3356,6 +3571,8 @@ export async function resolveMetadata(
3356
3571
  props: Record<string, unknown> = {},
3357
3572
  layouts: LayoutEntry[] = [],
3358
3573
  ): Promise<Record<string, unknown> | null> {
3574
+ await instrumented()
3575
+
3359
3576
  const pageEntry = metadataMap[component]
3360
3577
  const page: Record<string, unknown> = pageEntry
3361
3578
  ? (pageEntry.generate
@@ -3425,6 +3642,7 @@ export async function handleRsc(
3425
3642
  bootstrap = true,
3426
3643
  canReachHost = true,
3427
3644
  ): Promise<{ body: string; rscPayload: string; clientChunks: unknown; usedDynamicApis: boolean; dynamicBecause: string[]; clientComponents: string[]; serverReferences: boolean }> {
3645
+ await instrumented()
3428
3646
  applyHost()
3429
3647
 
3430
3648
  // A build renders this with no host installed, so every rpc() has to suspend
@@ -3538,6 +3756,7 @@ export async function handleRscRevalidate(
3538
3756
  target: string,
3539
3757
  page: PageContext,
3540
3758
  ): Promise<{ rscPayload: string }> {
3759
+ await instrumented()
3541
3760
  applyHost()
3542
3761
 
3543
3762
  const flight = renderToReadableStream(await renderRevalidated(target, page))
@@ -3554,6 +3773,7 @@ export async function handleRscPayload(
3554
3773
  from = 0,
3555
3774
  pageKey = '',
3556
3775
  ): Promise<{ rscPayload: string }> {
3776
+ await instrumented()
3557
3777
  applyHost()
3558
3778
 
3559
3779
  const flight = renderToReadableStream(
@@ -3621,6 +3841,7 @@ export async function handleRscPprShell(
3621
3841
  // and make every guarded route dynamic for the wrong reason.
3622
3842
  let usedDynamicApis = false
3623
3843
 
3844
+ await instrumented()
3624
3845
  applyHost()
3625
3846
 
3626
3847
  const probe = (..._args: unknown[]) => {
@@ -3790,6 +4011,17 @@ export default async function handler(request: Request): Promise<Response> {
3790
4011
  }
3791
4012
 
3792
4013
  async function serve(request: Request): Promise<Response> {
4014
+ // The startup plugin's probe. Nitro loads this module at the first request
4015
+ // rather than when the server starts, so a plugin that runs at startup
4016
+ // sends one request to load it; the module's evaluation began register(),
4017
+ // and this waits for it so a failure is reported as the startup failure it
4018
+ // is. No page is rendered and nothing is disclosed.
4019
+ if (request.headers.get('x-rsc-kit-startup') !== null) {
4020
+ await instrumented()
4021
+
4022
+ return new Response(null, { status: 204 })
4023
+ }
4024
+
3793
4025
  installHostCallsOnce()
3794
4026
 
3795
4027
  devHandler ??= createRscHandler({
@@ -4304,6 +4536,46 @@ self.addEventListener('activate', (event) => {
4304
4536
  })())
4305
4537
  })
4306
4538
  `;
4539
+ /**
4540
+ * Named imports from a barrel package, unrolled on the server environments.
4541
+ *
4542
+ * The browser gets lucide pre-bundled; the server environments do not, so
4543
+ * `import { ArrowRight } from 'lucide-react'` loaded and transformed every
4544
+ * icon module - 3,700 transforms, twenty-five seconds before the first
4545
+ * document, for thirteen icons. See barrelImports. Development only: a
4546
+ * build tree-shakes the barrel itself.
4547
+ */
4548
+ function barrelImportsPlugin() {
4549
+ const entries = new Map();
4550
+ return {
4551
+ name: "rsc-kit:barrel-imports",
4552
+ apply: () => barrelImports && process.env.NODE_ENV !== "production",
4553
+ enforce: "pre",
4554
+ applyToEnvironment: (environment) => environment.name === "rsc" || environment.name === "ssr",
4555
+ async transform(code, id) {
4556
+ if (id.includes("/node_modules/") ||
4557
+ !/\.[cm]?[jt]sx?$/.test(id.split("?")[0]))
4558
+ return;
4559
+ if (!code.includes("from"))
4560
+ return;
4561
+ const pending = [];
4562
+ const wanted = new Set();
4563
+ for (const m of code.matchAll(/import\s*(?:type\s+)?\{[^}]*\}\s*from\s*['"]([^'"./][^'"]*)['"]/g)) {
4564
+ wanted.add(m[1]);
4565
+ }
4566
+ for (const specifier of wanted) {
4567
+ if (entries.has(specifier))
4568
+ continue;
4569
+ pending.push(this.resolve(specifier, id).then((resolved) => {
4570
+ entries.set(specifier, resolved && !resolved.external ? resolved.id.split("?")[0] : null);
4571
+ }));
4572
+ }
4573
+ await Promise.all(pending);
4574
+ const out = unrollBarrelImports(code, (specifier) => entries.get(specifier) ?? null);
4575
+ return out === null ? undefined : { code: out, map: null };
4576
+ },
4577
+ };
4578
+ }
4307
4579
  /**
4308
4580
  * "use ssr" modules, rewritten for the server-components environment into
4309
4581
  * proxies that call the real module in the ssr environment. See useSsr.ts.
@@ -4478,10 +4750,64 @@ function clientImportsAudit() {
4478
4750
  },
4479
4751
  };
4480
4752
  }
4753
+ /**
4754
+ * A Nitro runtime plugin, so instrumentation.ts runs when the server starts.
4755
+ *
4756
+ * Nitro loads the rsc service at the first request, not at startup - so
4757
+ * without this, register() would run when the first visitor arrived, and a
4758
+ * bad environment would take down a server that had already reported itself
4759
+ * up. The plugin runs at app init and sends the service one request, which
4760
+ * loads the module; loading it begins register(), and the request waits for
4761
+ * it. Not on a Worker: there is no startup, and the first request is the
4762
+ * first chance to read a binding.
4763
+ */
4764
+ const STARTUP_PLUGIN = `import { viteServices } from "#nitro/virtual/vite-services"
4765
+
4766
+ export default function rscKitStartup() {
4767
+ if (typeof navigator !== 'undefined' && navigator.userAgent === 'Cloudflare-Workers') return
4768
+
4769
+ // The ssr service is the one Nitro's renderer asks; its fetch imports the
4770
+ // rsc entry and hands the request on. The rsc service's own export is the
4771
+ // handler function, not a { fetch }, so it cannot be probed directly.
4772
+ const ssr = viteServices.ssr
4773
+ if (!ssr) return
4774
+
4775
+ // Two ways to fail, one outcome. register() rejecting is reported by the
4776
+ // module itself. instrumentation.ts throwing at import - an env schema
4777
+ // refusing - means the module never evaluated, so nothing in it can
4778
+ // report; the load rejects here instead. Either way a server that could
4779
+ // not bootstrap has nothing correct to serve, and says so and stops
4780
+ // rather than reporting itself up.
4781
+ ssr
4782
+ .fetch(new Request('http://rsc-kit.internal/_rsc/startup', { headers: { 'x-rsc-kit-startup': '1' } }))
4783
+ .then((response) => {
4784
+ if (!response.ok) throw new Error('the startup probe answered ' + response.status)
4785
+ })
4786
+ .catch((error) => {
4787
+ console.error('[rsc-kit] instrumentation.ts failed at startup:', error)
4788
+
4789
+ if (typeof process !== 'undefined' && typeof process.exit === 'function') process.exit(1)
4790
+ })
4791
+ }
4792
+ `;
4481
4793
  export function rscKit(options = {}) {
4482
4794
  resolvePaths(options);
4483
4795
  const routesPlugin = {
4484
4796
  name: "rsc-kit",
4797
+ // A Nitro module, which Nitro's Vite plugin collects from any plugin
4798
+ // that carries one. Only for a built server: the dev server evaluates
4799
+ // the entry through Vite's runner when it is first asked for, and there
4800
+ // is no earlier moment to offer.
4801
+ nitro: {
4802
+ name: "rsc-kit",
4803
+ setup(nitro) {
4804
+ if (nitro.options.dev || !instrumentationFile())
4805
+ return;
4806
+ nitro.options.virtual ??= {};
4807
+ nitro.options.virtual["#rsc-kit/startup"] = STARTUP_PLUGIN;
4808
+ nitro.options.plugins = [...(nitro.options.plugins ?? []), "#rsc-kit/startup"];
4809
+ },
4810
+ },
4485
4811
  config(_config, env) {
4486
4812
  if (!existsSync(appDir)) {
4487
4813
  throw new Error(`[rsc-kit] No app directory at ${appDir} — nothing to build.`);
@@ -4493,6 +4819,7 @@ export function rscKit(options = {}) {
4493
4819
  apiRoutes.clear();
4494
4820
  discover(appDir);
4495
4821
  registerMetadataRoutes(appDir);
4822
+ siteHosts = ownHosts(rootMetadataBase(appDir), hostsOption);
4496
4823
  // Silent when it worked. The names were printed on every dev start and
4497
4824
  // every build — thirty of them for a middling app, above the output that
4498
4825
  // actually says something, and the build's classification table lists
@@ -4976,6 +5303,7 @@ export function rscKit(options = {}) {
4976
5303
  clientChunks: (meta) => meta.normalizedId,
4977
5304
  ...actionEncryptionKey(),
4978
5305
  }),
5306
+ barrelImportsPlugin(),
4979
5307
  useSsrModules(),
4980
5308
  serverRendererInRsc(),
4981
5309
  metadataRoutesPlugin(),