@rsc-kit/core 0.24.3 → 0.26.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1 +1 @@
1
- {"version":3,"file":"testing.js","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"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","sourcesContent":["// The whole app as a function, for tests.\n//\n// import { createTestApp } from '@rsc-kit/core/testing'\n//\n// const app = await createTestApp()\n// const res = await app.fetch('/api/orders', { headers: { Cookie: 'user=ada' } })\n//\n// No port, no browser, no server process. The built entry already exports the\n// same Request → Response handler the dev server and the production server\n// both call, so a test can hand it a Request and read the Response — through\n// the real router, the real middleware, the real api routes, and the pages the\n// build stored.\n//\n// That is the tier between a unit test and a browser. An action or a route is\n// a function and can be imported and called; a browser test proves the page\n// works; this proves the app answers a url the way it is deployed, which is\n// where a route that renders fine and is served wrong shows up.\n\nimport { spawnSync } from 'node:child_process'\nimport { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'\nimport { extname, join, resolve, sep } from 'node:path'\nimport { pathToFileURL } from 'node:url'\nimport { httpHostCalls } from './hostCalls.js'\nimport { testHostFetch, type TestHost } from './testHost.js'\n\nexport { hostReply, HOST_MIDDLEWARE, type HostCallInput, type HostHandler, type TestHost } from './testHost.js'\n\nexport interface TestApp {\n /** A path, not a url. The origin is whatever the app was told it is. */\n fetch(path: string, init?: RequestInit): Promise<Response>\n /** Where the build was read from, for a test that wants to look. */\n bundle: string\n}\n\nexport interface TestAppOptions {\n /** The project. Defaults to the working directory. */\n root?: string\n /**\n * Whether to build first.\n *\n * `true` builds when the source is newer than the last build, which is what\n * a test run wants: the first run pays for it, the rest do not, and an edit\n * is picked up. `false` never builds and fails loudly if there is nothing to\n * read — for a ci step that already built.\n */\n build?: boolean\n /** The origin requests are made against. Nothing reads it; it is a url. */\n origin?: string\n /**\n * The backend, answered in the test: rpc() calls and host guards by name.\n * The app reaches it through the same client it uses against a real one,\n * so nothing listens and RSC_BACKEND is not called. See `hostReply`.\n */\n host?: TestHost\n /**\n * What answers a url this app does not own - a Go route, a Laravel page,\n * /login - in place of the backend the app forwards it to. Handed the\n * request as the backend would receive it. Without one, such a url is\n * forwarded to RSC_BACKEND, and in a test that is usually a 502.\n */\n backend?: (request: Request) => Response | Promise<Response>\n}\n\n/**\n * Where the build put the server bundle.\n *\n * Two layouts, because two ways of building. Under Nitro — every scaffolded\n * app — it is inside Nitro's own directory. On its own the plugin writes\n * <outDir>/dist/rsc. The first that exists wins.\n */\nfunction findBundle(root: string): string | null {\n const candidates = [\n join(root, 'node_modules/.nitro/vite/services/rsc/index.js'),\n join(root, '.rsc/dist/rsc/index.js'),\n join(root, 'build/dist/rsc/index.js'),\n ]\n\n return candidates.find((path) => existsSync(path)) ?? null\n}\n\n/** The newest mtime under a directory, for deciding whether a build is stale. */\nfunction newest(dir: string): number {\n if (!existsSync(dir)) return 0\n\n let latest = 0\n\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue\n\n const path = join(dir, entry.name)\n const time = entry.isDirectory() ? newest(path) : statSync(path).mtimeMs\n\n if (time > latest) latest = time\n }\n\n return latest\n}\n\n/** One build per process per root, however many test files ask. */\nconst built = new Map<string, Promise<string>>()\n\n/** The content types a test is likely to assert on; anything else is octet-stream. */\nconst CONTENT_TYPES: Record<string, string> = {\n '.html': 'text/html; charset=utf-8',\n '.js': 'text/javascript; charset=utf-8',\n '.mjs': 'text/javascript; charset=utf-8',\n '.css': 'text/css; charset=utf-8',\n '.json': 'application/json; charset=utf-8',\n '.webmanifest': 'application/manifest+json',\n '.xml': 'application/xml',\n '.txt': 'text/plain; charset=utf-8',\n '.svg': 'image/svg+xml',\n '.png': 'image/png',\n '.jpg': 'image/jpeg',\n '.jpeg': 'image/jpeg',\n '.webp': 'image/webp',\n '.avif': 'image/avif',\n '.gif': 'image/gif',\n '.ico': 'image/x-icon',\n '.woff': 'font/woff',\n '.woff2': 'font/woff2',\n '.wasm': 'application/wasm',\n '.map': 'application/json',\n}\n\n/**\n * A file the build put in .output/public, answered the way Nitro's static\n * layer would - which the handler this harness loads does not include. The\n * handler is the rsc service: routing, pages, actions, api routes. Assets,\n * the service worker, the manifest and the icons are files Nitro serves in\n * production from .output/public, and a test that asked for /sw.js used to\n * get the router's 404 for a file the deployment serves fine - the one\n * shape this harness exists to catch, in the other direction.\n *\n * Files only, for GET and HEAD, within the directory: a path that escapes it\n * is a request for the handler, not a file.\n */\n/** @internal Exported for its test. */\nexport function staticFile(root: string, request: Request): Response | null {\n if (request.method !== 'GET' && request.method !== 'HEAD') return null\n\n const publicDir = join(root, '.output/public')\n const pathname = decodeURIComponent(new URL(request.url).pathname)\n const file = resolve(publicDir, '.' + pathname)\n\n if (!file.startsWith(publicDir + sep) || !existsSync(file) || !statSync(file).isFile()) return null\n\n const type = CONTENT_TYPES[extname(file).toLowerCase()] ?? 'application/octet-stream'\n const body = request.method === 'HEAD' ? null : readFileSync(file)\n\n return new Response(body, {\n status: 200,\n headers: { 'Content-Type': type, 'Cache-Control': 'no-store', 'X-RSC-Kit-Test': 'static' },\n })\n}\n\n/**\n * How this project builds: its own `build` script, run by the package manager\n * this test is running under. Without a script, Vite directly - on Bun's\n * runtime when the tests are, since a bin's node shebang would otherwise\n * start it under Node.\n */\n/** @internal Exported for its test. */\nexport function buildCommand(root: string): [string, string[]] {\n const onBun = typeof process.versions.bun === 'string'\n\n let scripts: Record<string, string> = {}\n\n try {\n scripts = JSON.parse(readFileSync(join(root, 'package.json'), 'utf-8')).scripts ?? {}\n } catch {\n // No package.json, or not JSON: there is no script to run.\n }\n\n if (typeof scripts.build === 'string') {\n return onBun ? ['bun', ['run', 'build']] : ['npm', ['run', 'build']]\n }\n\n return onBun ? ['bun', ['--bun', 'vite', 'build']] : ['npx', ['vite', 'build']]\n}\n\nasync function ensureBuilt(root: string, build: boolean): Promise<string> {\n const existing = findBundle(root)\n\n if (!build) {\n if (!existing) {\n throw new Error(\n `[rsc-kit] No build to test against under ${root}. Run the build first, or let createTestApp() do it by leaving \\`build\\` on.`,\n )\n }\n\n return existing\n }\n\n const fresh = existing && statSync(existing).mtimeMs > newest(join(root, 'src'))\n\n if (fresh) return existing\n\n // The user's own build command, so what is tested is what ships. A test\n // that built some other way would pass against a bundle nobody deploys -\n // and used to: this ran `npx vite build` whatever package.json said, which\n // on a Bun project built under Node and failed on the first `import 'bun'`.\n const [command, args] = buildCommand(root)\n const run = spawnSync(command, args, {\n cwd: root,\n stdio: ['ignore', 'pipe', 'pipe'],\n env: { ...process.env, NODE_ENV: 'production' },\n })\n\n if (run.status !== 0) {\n throw new Error(`[rsc-kit] The build failed, so there is nothing to test:\\n${run.stderr}`)\n }\n\n const bundle = findBundle(root)\n\n if (!bundle) {\n throw new Error(\n `[rsc-kit] The build finished but no server bundle was found under ${root}. Is rscKit() in vite.config?`,\n )\n }\n\n return bundle\n}\n\n/**\n * Build the app if it needs it, load it, and hand back something to fetch from.\n *\n * Memoised per root: every test file in a run shares one build and one loaded\n * module, which is both the fast path and the correct one — two copies of the\n * server bundle in one process would be two client-reference registries.\n */\nexport async function createTestApp(options: TestAppOptions = {}): Promise<TestApp> {\n const root = resolve(options.root ?? process.cwd())\n const origin = options.origin ?? 'https://app.test'\n\n built.set(root, built.get(root) ?? ensureBuilt(root, options.build ?? true))\n\n const bundle = await built.get(root)!\n const entry = (await import(pathToFileURL(bundle).href)) as {\n default: (request: Request) => Promise<Response>\n installHostFn?: (fn: (name: string, ...args: unknown[]) => Promise<unknown>) => () => void\n installBackendForward?: (fn: ((request: Request) => Response | Promise<Response>) | null) => void\n }\n\n if (typeof entry.default !== 'function') {\n throw new Error(`[rsc-kit] ${bundle} does not export a request handler.`)\n }\n\n const host = options.host\n ? httpHostCalls({ endpoint: 'http://test-host/__rsc/host-call', secret: 'test', fetch: testHostFetch(options.host) })\n : null\n\n if (host && !entry.installHostFn) {\n throw new Error(`[rsc-kit] ${bundle} cannot take a host. Rebuild against the current @rsc-kit/core.`)\n }\n\n return {\n bundle,\n fetch: async (path, init) => {\n const request = new Request(new URL(path, origin), init)\n\n // Per request, not once: the loaded bundle is shared by every app a\n // run creates, and the host is this app's.\n if (host) entry.installHostFn!(host)\n if (options.backend && !entry.installBackendForward) {\n throw new Error(`[rsc-kit] ${bundle} cannot take a backend. Rebuild against the current @rsc-kit/core.`)\n }\n entry.installBackendForward?.(options.backend ?? null)\n\n return staticFile(root, request) ?? entry.default(request)\n },\n }\n}\n"]}
1
+ {"version":3,"file":"testing.js","sourceRoot":"","sources":["../src/testing.ts"],"names":[],"mappings":"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","sourcesContent":["// The whole app as a function, for tests.\n//\n// import { createTestApp } from '@rsc-kit/core/testing'\n//\n// const app = await createTestApp()\n// const res = await app.fetch('/api/orders', { headers: { Cookie: 'user=ada' } })\n//\n// No port, no browser, no server process. The built entry already exports the\n// same Request → Response handler the dev server and the production server\n// both call, so a test can hand it a Request and read the Response — through\n// the real router, the real middleware, the real api routes, and the pages the\n// build stored.\n//\n// That is the tier between a unit test and a browser. An action or a route is\n// a function and can be imported and called; a browser test proves the page\n// works; this proves the app answers a url the way it is deployed, which is\n// where a route that renders fine and is served wrong shows up.\n\nimport { spawnSync } from 'node:child_process'\nimport { existsSync, readFileSync, readdirSync, statSync } from 'node:fs'\nimport { extname, join, resolve, sep } from 'node:path'\nimport { pathToFileURL } from 'node:url'\nimport { httpHostCalls } from './hostCalls.js'\nimport { testHostFetch, type TestHost } from './testHost.js'\n\nexport { hostReply, testChanges, HOST_MIDDLEWARE, type HostCallInput, type HostHandler, type TestHost } from './testHost.js'\n\nexport interface TestApp {\n /** A path, not a url. The origin is whatever the app was told it is. */\n fetch(path: string, init?: RequestInit): Promise<Response>\n /** Where the build was read from, for a test that wants to look. */\n bundle: string\n}\n\nexport interface TestAppOptions {\n /** The project. Defaults to the working directory. */\n root?: string\n /**\n * Whether to build first.\n *\n * `true` builds when the source is newer than the last build, which is what\n * a test run wants: the first run pays for it, the rest do not, and an edit\n * is picked up. `false` never builds and fails loudly if there is nothing to\n * read — for a ci step that already built.\n */\n build?: boolean\n /** The origin requests are made against. Nothing reads it; it is a url. */\n origin?: string\n /**\n * The backend, answered in the test: rpc() calls and host guards by name.\n * The app reaches it through the same client it uses against a real one,\n * so nothing listens and RSC_BACKEND is not called. See `hostReply`.\n */\n host?: TestHost\n /**\n * What answers a url this app does not own - a Go route, a Laravel page,\n * /login - in place of the backend the app forwards it to. Handed the\n * request as the backend would receive it. Without one, such a url is\n * forwarded to RSC_BACKEND, and in a test that is usually a 502.\n */\n backend?: (request: Request) => Response | Promise<Response>\n}\n\n/**\n * Where the build put the server bundle.\n *\n * Two layouts, because two ways of building. Under Nitro — every scaffolded\n * app — it is inside Nitro's own directory. On its own the plugin writes\n * <outDir>/dist/rsc. The first that exists wins.\n */\nfunction findBundle(root: string): string | null {\n const candidates = [\n join(root, 'node_modules/.nitro/vite/services/rsc/index.js'),\n join(root, '.rsc/dist/rsc/index.js'),\n join(root, 'build/dist/rsc/index.js'),\n ]\n\n return candidates.find((path) => existsSync(path)) ?? null\n}\n\n/** The newest mtime under a directory, for deciding whether a build is stale. */\nfunction newest(dir: string): number {\n if (!existsSync(dir)) return 0\n\n let latest = 0\n\n for (const entry of readdirSync(dir, { withFileTypes: true })) {\n if (entry.name === 'node_modules' || entry.name.startsWith('.')) continue\n\n const path = join(dir, entry.name)\n const time = entry.isDirectory() ? newest(path) : statSync(path).mtimeMs\n\n if (time > latest) latest = time\n }\n\n return latest\n}\n\n/** One build per process per root, however many test files ask. */\nconst built = new Map<string, Promise<string>>()\n\n/** The content types a test is likely to assert on; anything else is octet-stream. */\nconst CONTENT_TYPES: Record<string, string> = {\n '.html': 'text/html; charset=utf-8',\n '.js': 'text/javascript; charset=utf-8',\n '.mjs': 'text/javascript; charset=utf-8',\n '.css': 'text/css; charset=utf-8',\n '.json': 'application/json; charset=utf-8',\n '.webmanifest': 'application/manifest+json',\n '.xml': 'application/xml',\n '.txt': 'text/plain; charset=utf-8',\n '.svg': 'image/svg+xml',\n '.png': 'image/png',\n '.jpg': 'image/jpeg',\n '.jpeg': 'image/jpeg',\n '.webp': 'image/webp',\n '.avif': 'image/avif',\n '.gif': 'image/gif',\n '.ico': 'image/x-icon',\n '.woff': 'font/woff',\n '.woff2': 'font/woff2',\n '.wasm': 'application/wasm',\n '.map': 'application/json',\n}\n\n/**\n * A file the build put in .output/public, answered the way Nitro's static\n * layer would - which the handler this harness loads does not include. The\n * handler is the rsc service: routing, pages, actions, api routes. Assets,\n * the service worker, the manifest and the icons are files Nitro serves in\n * production from .output/public, and a test that asked for /sw.js used to\n * get the router's 404 for a file the deployment serves fine - the one\n * shape this harness exists to catch, in the other direction.\n *\n * Files only, for GET and HEAD, within the directory: a path that escapes it\n * is a request for the handler, not a file.\n */\n/** @internal Exported for its test. */\nexport function staticFile(root: string, request: Request): Response | null {\n if (request.method !== 'GET' && request.method !== 'HEAD') return null\n\n const publicDir = join(root, '.output/public')\n const pathname = decodeURIComponent(new URL(request.url).pathname)\n const file = resolve(publicDir, '.' + pathname)\n\n if (!file.startsWith(publicDir + sep) || !existsSync(file) || !statSync(file).isFile()) return null\n\n const type = CONTENT_TYPES[extname(file).toLowerCase()] ?? 'application/octet-stream'\n const body = request.method === 'HEAD' ? null : readFileSync(file)\n\n return new Response(body, {\n status: 200,\n headers: { 'Content-Type': type, 'Cache-Control': 'no-store', 'X-RSC-Kit-Test': 'static' },\n })\n}\n\n/**\n * How this project builds: its own `build` script, run by the package manager\n * this test is running under. Without a script, Vite directly - on Bun's\n * runtime when the tests are, since a bin's node shebang would otherwise\n * start it under Node.\n */\n/** @internal Exported for its test. */\nexport function buildCommand(root: string): [string, string[]] {\n const onBun = typeof process.versions.bun === 'string'\n\n let scripts: Record<string, string> = {}\n\n try {\n scripts = JSON.parse(readFileSync(join(root, 'package.json'), 'utf-8')).scripts ?? {}\n } catch {\n // No package.json, or not JSON: there is no script to run.\n }\n\n if (typeof scripts.build === 'string') {\n return onBun ? ['bun', ['run', 'build']] : ['npm', ['run', 'build']]\n }\n\n return onBun ? ['bun', ['--bun', 'vite', 'build']] : ['npx', ['vite', 'build']]\n}\n\nasync function ensureBuilt(root: string, build: boolean): Promise<string> {\n const existing = findBundle(root)\n\n if (!build) {\n if (!existing) {\n throw new Error(\n `[rsc-kit] No build to test against under ${root}. Run the build first, or let createTestApp() do it by leaving \\`build\\` on.`,\n )\n }\n\n return existing\n }\n\n const fresh = existing && statSync(existing).mtimeMs > newest(join(root, 'src'))\n\n if (fresh) return existing\n\n // The user's own build command, so what is tested is what ships. A test\n // that built some other way would pass against a bundle nobody deploys -\n // and used to: this ran `npx vite build` whatever package.json said, which\n // on a Bun project built under Node and failed on the first `import 'bun'`.\n const [command, args] = buildCommand(root)\n const run = spawnSync(command, args, {\n cwd: root,\n stdio: ['ignore', 'pipe', 'pipe'],\n env: { ...process.env, NODE_ENV: 'production' },\n })\n\n if (run.status !== 0) {\n throw new Error(`[rsc-kit] The build failed, so there is nothing to test:\\n${run.stderr}`)\n }\n\n const bundle = findBundle(root)\n\n if (!bundle) {\n throw new Error(\n `[rsc-kit] The build finished but no server bundle was found under ${root}. Is rscKit() in vite.config?`,\n )\n }\n\n return bundle\n}\n\n/**\n * Build the app if it needs it, load it, and hand back something to fetch from.\n *\n * Memoised per root: every test file in a run shares one build and one loaded\n * module, which is both the fast path and the correct one — two copies of the\n * server bundle in one process would be two client-reference registries.\n */\nexport async function createTestApp(options: TestAppOptions = {}): Promise<TestApp> {\n const root = resolve(options.root ?? process.cwd())\n const origin = options.origin ?? 'https://app.test'\n\n built.set(root, built.get(root) ?? ensureBuilt(root, options.build ?? true))\n\n const bundle = await built.get(root)!\n const entry = (await import(pathToFileURL(bundle).href)) as {\n default: (request: Request) => Promise<Response>\n installHostFn?: (fn: (name: string, ...args: unknown[]) => Promise<unknown>) => () => void\n installBackendForward?: (fn: ((request: Request) => Response | Promise<Response>) | null) => void\n }\n\n if (typeof entry.default !== 'function') {\n throw new Error(`[rsc-kit] ${bundle} does not export a request handler.`)\n }\n\n const host = options.host\n ? httpHostCalls({ endpoint: 'http://test-host/__rsc/host-call', secret: 'test', fetch: testHostFetch(options.host) })\n : null\n\n if (host && !entry.installHostFn) {\n throw new Error(`[rsc-kit] ${bundle} cannot take a host. Rebuild against the current @rsc-kit/core.`)\n }\n\n return {\n bundle,\n fetch: async (path, init) => {\n const request = new Request(new URL(path, origin), init)\n\n // Per request, not once: the loaded bundle is shared by every app a\n // run creates, and the host is this app's.\n if (host) entry.installHostFn!(host)\n if (options.backend && !entry.installBackendForward) {\n throw new Error(`[rsc-kit] ${bundle} cannot take a backend. Rebuild against the current @rsc-kit/core.`)\n }\n entry.installBackendForward?.(options.backend ?? null)\n\n return staticFile(root, request) ?? entry.default(request)\n },\n }\n}\n"]}
package/dist/vite.js CHANGED
@@ -1942,6 +1942,19 @@ function renderRouteTypes(manifest) {
1942
1942
  const target = relative(typesDir, join(sourceDir, route.component)).replace(/\\/g, "/");
1943
1943
  search.set(pattern, target.startsWith(".") ? target : "./" + target);
1944
1944
  }
1945
+ // Each GET api route to its module, so useEvents can read what an
1946
+ // events() route yields off its type - the same type-only import as a
1947
+ // page's searchParams. A route that is not events() resolves to unknown.
1948
+ const streams = new Map();
1949
+ for (const api of manifest.apis ?? []) {
1950
+ const route = apiRoutes.get(api.name);
1951
+ if (!route || route.generated || !api.methods.includes("GET"))
1952
+ continue;
1953
+ const pattern = patternOf(api.segments);
1954
+ const target = relative(typesDir, route.absPath.replace(/\.[cm]?[jt]sx?$/, "")).replace(/\\/g, "/");
1955
+ if (!streams.has(pattern))
1956
+ streams.set(pattern, target.startsWith(".") ? target : "./" + target);
1957
+ }
1945
1958
  // Section names are read from the source - section('orders', …) - the way
1946
1959
  // generateStaticParams is detected, because this runs before any bundle
1947
1960
  // exists. A name computed at runtime is not seen and revalidate() falls
@@ -2005,6 +2018,16 @@ function renderRouteTypes(manifest) {
2005
2018
  ? " apis:\n" +
2006
2019
  apis.map((p) => " | " + JSON.stringify(p)).join("\n")
2007
2020
  : " // No route.ts files found under the source directory.\n apis: never",
2021
+ // What each GET route streams, read off its module. This types useEvents.
2022
+ " events: {",
2023
+ ...[...streams]
2024
+ .sort()
2025
+ .map(([pattern, target]) => " " +
2026
+ JSON.stringify(pattern) +
2027
+ ": EventsExportOf<typeof import(" +
2028
+ JSON.stringify(target) +
2029
+ ")>"),
2030
+ " }",
2008
2031
  " }",
2009
2032
  "}",
2010
2033
  "",
@@ -2517,6 +2540,9 @@ function urlSchemaExports(absPath) {
2517
2540
  return {
2518
2541
  params: /export\s+const\s+params\s*[=:]/.test(src),
2519
2542
  searchParams: /export\s+const\s+searchParams\s*[=:]/.test(src),
2543
+ // What the page itself refreshes on, beside its url schemas: a change to
2544
+ // one of its names refreshes the page, as refresh('page') does.
2545
+ refreshOn: /export\s+(const|function|async\s+function)\s+refreshOn\s*[=:(]/.test(src),
2520
2546
  };
2521
2547
  }
2522
2548
  // ── Codegen ──────────────────────────────────────────────────────────────────
@@ -2817,13 +2843,14 @@ function generateEntryRsc(fallbackOrigin = "") {
2817
2843
  paramEntries.push(` ${JSON.stringify(c.name)}: ${c.alias}_params.generateStaticParams,`);
2818
2844
  }
2819
2845
  const urlSchemas = urlSchemaExports(c.absPath);
2820
- if (urlSchemas.params || urlSchemas.searchParams) {
2846
+ if (urlSchemas.params || urlSchemas.searchParams || urlSchemas.refreshOn) {
2821
2847
  imports.push(`import * as ${c.alias}_schema from ${JSON.stringify(c.absPath)}`);
2822
2848
  const fields = [
2823
2849
  urlSchemas.params ? `params: ${c.alias}_schema.params` : null,
2824
2850
  urlSchemas.searchParams
2825
2851
  ? `searchParams: ${c.alias}_schema.searchParams`
2826
2852
  : null,
2853
+ urlSchemas.refreshOn ? `refreshOn: ${c.alias}_schema.refreshOn` : null,
2827
2854
  ].filter(Boolean);
2828
2855
  schemaEntries.push(` ${JSON.stringify(c.name)}: { ${fields.join(", ")} },`);
2829
2856
  }
@@ -2849,11 +2876,13 @@ import { DefaultRouteError } from ${JSON.stringify(join(packageDir, "js/DefaultR
2849
2876
  import { searchParams as requestSearchParams, withUrl } from ${JSON.stringify(join(packageDir, "request"))}
2850
2877
  import { parseParams, parseSearchParams, parseBody, isSearchParamsError, isBodyError } from ${JSON.stringify(join(packageDir, "routeSchema"))}
2851
2878
  import { notFoundDigest, isNotFoundSignal } from ${JSON.stringify(join(packageDir, "notFound"))}
2852
- import { noteRequestRead, urlOf } from ${JSON.stringify(join(packageDir, "request"))}
2879
+ import { noteRequestRead, notePageProps, urlOf } from ${JSON.stringify(join(packageDir, "request"))}
2853
2880
  import { redirectDigest } from ${JSON.stringify(join(packageDir, "redirectDigest"))}
2854
2881
  import { cancelledByConsumer } from ${JSON.stringify(join(packageDir, "js/fallbackReport"))}
2855
2882
  import { createRscHandler, pageSaidNotFound, queryAndParams } from ${JSON.stringify(join(packageDir, "host"))}
2856
2883
  import { httpHostCalls } from ${JSON.stringify(join(packageDir, "hostCalls"))}
2884
+ import { backendVersions, installVersionSource } from ${JSON.stringify(join(packageDir, "changed"))}
2885
+ import { RefreshOn } from ${JSON.stringify(join(packageDir, "js/refreshOn"))}
2857
2886
  import { prerenderedBeside } from ${JSON.stringify(join(packageDir, "files"))}
2858
2887
  import { renderToReadableStream, decodeReply, decodeAction, decodeFormState, loadServerAction } from '@vitejs/plugin-rsc/rsc'
2859
2888
  import { isQuery, queryCacheControl, isQueryValidationError } from ${JSON.stringify(join(packageDir, "query"))}
@@ -2899,7 +2928,7 @@ const WEB_MANIFEST: { href: string; themeColor?: string } | null = ${JSON.string
2899
2928
  }
2900
2929
  : null)}
2901
2930
 
2902
- const urlSchemas: Record<string, { params?: any; searchParams?: any }> = {
2931
+ const urlSchemas: Record<string, { params?: any; searchParams?: any; refreshOn?: any }> = {
2903
2932
  ${schemaEntries.join("\n")}
2904
2933
  }
2905
2934
 
@@ -3316,8 +3345,14 @@ export function installBackendForward(fn: ((request: Request) => Response | Prom
3316
3345
 
3317
3346
  export function installHostFn(fn: HostFn) {
3318
3347
  currentHost = fn
3348
+ // The host keeps the versions a page refreshes on, read through it.
3349
+ // Uninstalled, they are this process's own again.
3350
+ installVersionSource(backendVersions(fn))
3319
3351
  return () => {
3320
- if (currentHost === fn) currentHost = null
3352
+ if (currentHost === fn) {
3353
+ currentHost = null
3354
+ installVersionSource(null)
3355
+ }
3321
3356
  }
3322
3357
  }
3323
3358
 
@@ -3572,11 +3607,27 @@ function buildElement(
3572
3607
  // invented value, right for nothing — which is why such a route could only
3573
3608
  // ever be rendered per request.
3574
3609
  const schemas = urlSchemas[component]
3575
-
3576
- let element = createElement(Component, {
3610
+ const pageProps = {
3577
3611
  params: checkedParams(schemas, params),
3578
3612
  searchParams: checkedSearchParams(schemas, pageSearchParams()),
3579
- })
3613
+ }
3614
+
3615
+ // For what a section refreshes on, which is a function of the page's
3616
+ // params whether the section is rendered by the page or alone.
3617
+ notePageProps(pageProps)
3618
+
3619
+ let element = createElement(Component, pageProps)
3620
+
3621
+ // What the page itself refreshes on, rendered beside it: when one of its
3622
+ // names is said to have changed, the tab asks for the page again.
3623
+ if (schemas?.refreshOn) {
3624
+ element = createElement(
3625
+ Fragment,
3626
+ null,
3627
+ createElement(RefreshOn, { key: 'refreshOn', target: 'page', refreshOn: schemas.refreshOn, props: pageProps }),
3628
+ createElement(Fragment, { key: 'page' }, element),
3629
+ )
3630
+ }
3580
3631
 
3581
3632
  // Through LoadingBoundary when the runtime is shipped, so a server render
3582
3633
  // can tell the engine's boundary from one the developer wrote by name -
@@ -4510,6 +4561,8 @@ async function renderRevalidated(target: string, page: PageContext, skipElsewher
4510
4561
  // The component, not the wrapper section() returned. The client replaces
4511
4562
  // what is inside the boundary, so sending the wrapper would nest a new
4512
4563
  // boundary inside the old one on every refresh.
4564
+ notePageProps(page.props)
4565
+
4513
4566
  return createElement(Section, page.props)
4514
4567
  }
4515
4568