@rsc-kit/core 0.30.21 → 0.30.23

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/served.js ADDED
@@ -0,0 +1,80 @@
1
+ /**
2
+ * Run the handler and call onRequest once the response it answers is over.
3
+ *
4
+ * The body is passed through rather than read, so streaming is untouched: each
5
+ * chunk goes out as it is produced. onRequest is never awaited and a throw
6
+ * from it is reported, not raised - a logger that fails must not fail the page.
7
+ */
8
+ export async function observeServed(request, serve, onRequest, headerNames = []) {
9
+ const started = performance.now();
10
+ let reported = false;
11
+ const report = (status, aborted, error) => {
12
+ if (reported)
13
+ return;
14
+ reported = true;
15
+ const served = {
16
+ method: request.method,
17
+ pathname: new URL(request.url).pathname,
18
+ status,
19
+ durationMs: performance.now() - started,
20
+ aborted,
21
+ headers: pickHeaders(request.headers, headerNames),
22
+ ...(error === undefined ? {} : { error }),
23
+ };
24
+ try {
25
+ Promise.resolve(onRequest(served)).catch(failed);
26
+ }
27
+ catch (thrown) {
28
+ failed(thrown);
29
+ }
30
+ };
31
+ let response;
32
+ try {
33
+ response = await serve(request);
34
+ }
35
+ catch (error) {
36
+ report(500, false, error);
37
+ throw error;
38
+ }
39
+ if (!response.body) {
40
+ report(response.status, false);
41
+ return response;
42
+ }
43
+ const reader = response.body.getReader();
44
+ const body = new ReadableStream({
45
+ async pull(controller) {
46
+ try {
47
+ const { done, value } = await reader.read();
48
+ if (done) {
49
+ controller.close();
50
+ report(response.status, false);
51
+ }
52
+ else {
53
+ controller.enqueue(value);
54
+ }
55
+ }
56
+ catch (error) {
57
+ controller.error(error);
58
+ report(response.status, false, error);
59
+ }
60
+ },
61
+ cancel(reason) {
62
+ report(response.status, true);
63
+ return reader.cancel(reason);
64
+ },
65
+ });
66
+ return new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers });
67
+ }
68
+ function pickHeaders(headers, names) {
69
+ const picked = {};
70
+ for (const name of names) {
71
+ const value = headers.get(name);
72
+ if (value !== null)
73
+ picked[name.toLowerCase()] = value;
74
+ }
75
+ return Object.freeze(picked);
76
+ }
77
+ function failed(error) {
78
+ console.error("[rsc-kit] instrumentation.ts onRequest() failed:", error);
79
+ }
80
+ //# sourceMappingURL=served.js.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"served.js","sourceRoot":"","sources":["../src/served.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * What instrumentation.ts's onRequest() is told, once a response is over.\n *\n * \"Over\" is the part an app cannot see for itself: a page streams, so the\n * handler has long returned when the last byte leaves, and a visitor who\n * closes the tab mid-stream ends it early. Only the engine holds the body, so\n * only the engine can say which happened, and when.\n */\nexport interface ServedRequest {\n method: string;\n /** The url's path, never its query string: a query carries what people typed. */\n pathname: string;\n status: number;\n /** From the request arriving to the last byte sent, or to the stream ending early. */\n durationMs: number;\n /** The client went away before the body was sent - a closed tab, a navigation elsewhere. */\n aborted: boolean;\n /** The render failed: the handler threw, or the body errored partway. */\n error?: unknown;\n /**\n * Only the headers instrumentation.ts names in `onRequestHeaders`, by\n * lowercase name, and only those the request had. Nothing else is passed:\n * not cookies, not authorization.\n */\n headers: Readonly<Record<string, string>>;\n}\n\nexport type OnRequest = (served: ServedRequest) => unknown;\n\n/**\n * Run the handler and call onRequest once the response it answers is over.\n *\n * The body is passed through rather than read, so streaming is untouched: each\n * chunk goes out as it is produced. onRequest is never awaited and a throw\n * from it is reported, not raised - a logger that fails must not fail the page.\n */\nexport async function observeServed(\n request: Request,\n serve: (request: Request) => Promise<Response>,\n onRequest: OnRequest,\n headerNames: readonly string[] = [],\n): Promise<Response> {\n const started = performance.now();\n let reported = false;\n\n const report = (status: number, aborted: boolean, error?: unknown) => {\n if (reported) return;\n reported = true;\n\n const served: ServedRequest = {\n method: request.method,\n pathname: new URL(request.url).pathname,\n status,\n durationMs: performance.now() - started,\n aborted,\n headers: pickHeaders(request.headers, headerNames),\n ...(error === undefined ? {} : { error }),\n };\n\n try {\n Promise.resolve(onRequest(served)).catch(failed);\n } catch (thrown) {\n failed(thrown);\n }\n };\n\n let response: Response;\n\n try {\n response = await serve(request);\n } catch (error) {\n report(500, false, error);\n throw error;\n }\n\n if (!response.body) {\n report(response.status, false);\n return response;\n }\n\n const reader = response.body.getReader();\n const body = new ReadableStream<Uint8Array>({\n async pull(controller) {\n try {\n const { done, value } = await reader.read();\n\n if (done) {\n controller.close();\n report(response.status, false);\n } else {\n controller.enqueue(value);\n }\n } catch (error) {\n controller.error(error);\n report(response.status, false, error);\n }\n },\n cancel(reason) {\n report(response.status, true);\n return reader.cancel(reason);\n },\n });\n\n return new Response(body, { status: response.status, statusText: response.statusText, headers: response.headers });\n}\n\nfunction pickHeaders(headers: Headers, names: readonly string[]): Readonly<Record<string, string>> {\n const picked: Record<string, string> = {};\n\n for (const name of names) {\n const value = headers.get(name);\n if (value !== null) picked[name.toLowerCase()] = value;\n }\n\n return Object.freeze(picked);\n}\n\nfunction failed(error: unknown): void {\n console.error(\"[rsc-kit] instrumentation.ts onRequest() failed:\", error);\n}\n"]}
package/dist/vite.d.ts CHANGED
@@ -321,6 +321,17 @@ export interface RscKitOptions {
321
321
  * whole app, or its own endpoints, to the backend.
322
322
  */
323
323
  export declare function normaliseBackendPaths(paths: readonly string[] | undefined): string[];
324
+ /**
325
+ * The prefixes a backend owns, as the route patterns the browser already reads
326
+ * for a route.ts: the prefix, and everything under it.
327
+ *
328
+ * The router never fetches these as pages. A backend url that answers a
329
+ * payload request with a redirect to another site - an OAuth start - is a hop
330
+ * a fetch cannot make, so the navigation threw and the visitor stayed where
331
+ * they were. Listed with the route.ts patterns it is a document load, with no
332
+ * prefetch, wherever the router meets one: a click, a redirect, a hover.
333
+ */
334
+ export declare function backendPatterns(paths: readonly string[]): string[];
324
335
  /**
325
336
  * The service worker, written into the client output at build time.
326
337
  *
@@ -454,6 +465,26 @@ export declare function compileStep(root: string): {
454
465
  compile?: string;
455
466
  binary?: string;
456
467
  };
468
+ /**
469
+ * The app's process bootstrap, if it has one: `instrumentation.ts` beside
470
+ * `app/`, the name Next uses so a reader arriving from there knows what it
471
+ * is.
472
+ *
473
+ * Two things make it a framework concern rather than an import the app
474
+ * adds. Module evaluation order: a page that configures a shared package at
475
+ * import time runs before any module the app could put first, and only the
476
+ * generated entry can import something before the pages. And "before the
477
+ * first request": an async `register()` — connecting, validating env — has
478
+ * to be awaited by every entry point a host or a prerender can call, which
479
+ * is a list the app cannot see.
480
+ */
481
+ type InstrumentationFile = {
482
+ file: string;
483
+ hasRegister: boolean;
484
+ hasShutdown: boolean;
485
+ hasOnRequest?: boolean;
486
+ hasOnRequestHeaders?: boolean;
487
+ };
457
488
  /**
458
489
  * How the generated entry imports instrumentation.ts, as `__instrumentation`.
459
490
  *
@@ -463,11 +494,7 @@ export declare function compileStep(root: string): {
463
494
  * "Import `shutdown` will always be undefined") - the app told off for a file
464
495
  * written exactly as documented. The rest are simply absent from the object.
465
496
  */
466
- export declare function instrumentationImport(instrumentation: {
467
- file: string;
468
- hasRegister: boolean;
469
- hasShutdown: boolean;
470
- } | null): string;
497
+ export declare function instrumentationImport(instrumentation: InstrumentationFile | null): string;
471
498
  /**
472
499
  * The worker the dev server serves at /sw.js: it removes the one a production
473
500
  * run left on this origin, and the caches with it, then reloads each page it
@@ -565,3 +592,4 @@ export declare function publicVariablesSet(mode: string, envDir?: string | false
565
592
  where: string;
566
593
  }[];
567
594
  export declare function rscKit(options?: RscKitOptions): PluginOption[];
595
+ export {};
package/dist/vite.js CHANGED
@@ -420,14 +420,28 @@ export function normaliseBackendPaths(paths) {
420
420
  if (path === "/_rsc" || path.startsWith("/_rsc/")) {
421
421
  throw new Error(`[rsc-kit] backendPaths: ${JSON.stringify(raw)} is this package's own endpoint, which the renderer answers.`);
422
422
  }
423
- if (/[?#*]/.test(path)) {
424
- throw new Error(`[rsc-kit] backendPaths: ${JSON.stringify(raw)} is a prefix, not a pattern: no query, fragment or wildcard.`);
423
+ // A bracket is a dynamic segment to the client, which reads these as route patterns.
424
+ if (/[?#*[\]]/.test(path)) {
425
+ throw new Error(`[rsc-kit] backendPaths: ${JSON.stringify(raw)} is a prefix, not a pattern: no query, fragment, wildcard or bracket.`);
425
426
  }
426
427
  if (!out.includes(path))
427
428
  out.push(path);
428
429
  }
429
430
  return out;
430
431
  }
432
+ /**
433
+ * The prefixes a backend owns, as the route patterns the browser already reads
434
+ * for a route.ts: the prefix, and everything under it.
435
+ *
436
+ * The router never fetches these as pages. A backend url that answers a
437
+ * payload request with a redirect to another site - an OAuth start - is a hop
438
+ * a fetch cannot make, so the navigation threw and the visitor stayed where
439
+ * they were. Listed with the route.ts patterns it is a document load, with no
440
+ * prefetch, wherever the router meets one: a click, a redirect, a hover.
441
+ */
442
+ export function backendPatterns(paths) {
443
+ return paths.flatMap((path) => [path, `${path}/[...rest]`]);
444
+ }
431
445
  function resolvePaths(options) {
432
446
  projectRoot = resolve(options.projectRoot || process.env.RSC_PROJECT_ROOT || process.cwd());
433
447
  sourceDir = resolve(options.sourceDir || process.env.RSC_SOURCE_DIR || join(projectRoot, "src"));
@@ -2171,6 +2185,14 @@ function renderRouteTypes(manifest) {
2171
2185
  // Api routes are a separate union, so Link refuses an api url and apiUrl()
2172
2186
  // refuses a page. Linking to an api route navigates the browser away to a
2173
2187
  // json document, which is the mistake worth catching.
2188
+ // The prefixes rscKit({ backendPaths }) named, so a link or a redirect to
2189
+ // one of the backend's urls compiles without a cast.
2190
+ " interface RegisterBackend {",
2191
+ backendPaths.length > 0
2192
+ ? " paths:\n" +
2193
+ backendPaths.map((p) => " | " + JSON.stringify(p)).join("\n")
2194
+ : " // No backendPaths named.\n paths: never",
2195
+ " }",
2174
2196
  // Regions: every section('name', …) the build read, and every @slot
2175
2197
  // directory. This is what types revalidate().
2176
2198
  " interface RegisterRegions {",
@@ -3099,19 +3121,6 @@ function installHostCallsOnce(): void {
3099
3121
  }
3100
3122
 
3101
3123
  `;
3102
- /**
3103
- * The app's process bootstrap, if it has one: `instrumentation.ts` beside
3104
- * `app/`, the name Next uses so a reader arriving from there knows what it
3105
- * is.
3106
- *
3107
- * Two things make it a framework concern rather than an import the app
3108
- * adds. Module evaluation order: a page that configures a shared package at
3109
- * import time runs before any module the app could put first, and only the
3110
- * generated entry can import something before the pages. And "before the
3111
- * first request": an async `register()` — connecting, validating env — has
3112
- * to be awaited by every entry point a host or a prerender can call, which
3113
- * is a list the app cannot see.
3114
- */
3115
3124
  function instrumentationFile() {
3116
3125
  for (const ext of ["ts", "tsx", "mts", "js", "mjs"]) {
3117
3126
  const file = join(sourceDir, `instrumentation.${ext}`);
@@ -3124,7 +3133,13 @@ function instrumentationFile() {
3124
3133
  // a file written exactly as documented.
3125
3134
  const source = readFileSync(file, "utf-8");
3126
3135
  const exports = (name) => new RegExp(`export\\s+(?:async\\s+)?(?:function\\s+${name}\\b|const\\s+${name}\\b|let\\s+${name}\\b|\\{[^}]*\\b${name}\\b[^}]*\\})`).test(source);
3127
- return { file, hasRegister: exports("register"), hasShutdown: exports("shutdown") };
3136
+ return {
3137
+ file,
3138
+ hasRegister: exports("register"),
3139
+ hasShutdown: exports("shutdown"),
3140
+ hasOnRequest: exports("onRequest"),
3141
+ hasOnRequestHeaders: exports("onRequestHeaders"),
3142
+ };
3128
3143
  }
3129
3144
  return null;
3130
3145
  }
@@ -3138,12 +3153,14 @@ function instrumentationFile() {
3138
3153
  * written exactly as documented. The rest are simply absent from the object.
3139
3154
  */
3140
3155
  export function instrumentationImport(instrumentation) {
3141
- const type = "{ register?: () => unknown; shutdown?: () => unknown }";
3156
+ const type = "{ register?: () => unknown; shutdown?: () => unknown; onRequest?: (served: never) => unknown; onRequestHeaders?: readonly string[] }";
3142
3157
  if (!instrumentation)
3143
3158
  return `const __instrumentation: ${type} = {}`;
3144
3159
  const hooks = [
3145
3160
  ...(instrumentation.hasRegister ? ["register"] : []),
3146
3161
  ...(instrumentation.hasShutdown ? ["shutdown"] : []),
3162
+ ...(instrumentation.hasOnRequest ? ["onRequest"] : []),
3163
+ ...(instrumentation.hasOnRequestHeaders ? ["onRequestHeaders"] : []),
3147
3164
  ];
3148
3165
  const from = JSON.stringify(instrumentation.file);
3149
3166
  // No hooks: still imported, first, for what its import does.
@@ -3248,6 +3265,7 @@ import { httpHostCalls } from ${JSON.stringify(join(packageDir, "hostCalls"))}
3248
3265
  import { backendVersions, installBackendVersionSource } from ${JSON.stringify(join(packageDir, "changed"))}
3249
3266
  import { RefreshOn } from ${JSON.stringify(join(packageDir, "js/refreshOn"))}
3250
3267
  import { prerenderedBeside } from ${JSON.stringify(join(packageDir, "files"))}
3268
+ import { observeServed } from ${JSON.stringify(join(packageDir, "served"))}
3251
3269
  import { renderToReadableStream, decodeReply, decodeAction, decodeFormState, loadServerAction } from '@vitejs/plugin-rsc/rsc'
3252
3270
  import { isQuery, queryCacheControl, isQueryValidationError, queryRefusal } from ${JSON.stringify(join(packageDir, "query"))}
3253
3271
  import { isActionValidationError, isActionRefusal, isClientBuilt } from ${JSON.stringify(join(packageDir, "action"))}
@@ -5914,6 +5932,16 @@ ${backendPathConsts()}${fallbackOrigin === false ? "" : FALLBACK_CONSTS.replace(
5914
5932
  let devHandler: ((request: Request) => Promise<Response | null>) | null = null
5915
5933
 
5916
5934
  export default async function handler(request: Request): Promise<Response> {
5935
+ // instrumentation.ts's onRequest(), once each response is over - streamed to
5936
+ // the end, or cut off. Not for the startup probe: no visitor asked for it.
5937
+ if (__instrumentation.onRequest && request.headers.get('x-rsc-kit-startup') === null) {
5938
+ return observeServed(request, served, __instrumentation.onRequest as never, __instrumentation.onRequestHeaders)
5939
+ }
5940
+
5941
+ return served(request)
5942
+ }
5943
+
5944
+ async function served(request: Request): Promise<Response> {
5917
5945
  try {
5918
5946
  return await serve(request)
5919
5947
  } catch (error) {
@@ -6393,8 +6421,12 @@ createViteRscApp(document, ${JSON.stringify(interceptManifest())}, ${JSON.string
6393
6421
  staticPayloads: staticPayloads || null,
6394
6422
  routes: routesForClient,
6395
6423
  // Every route.ts, as a url pattern, so a link to one is treated as an
6396
- // anchor rather than prefetched and fetched as a page.
6397
- apiRoutes: (routeManifest().apis ?? []).map((api) => patternOf(api.segments)),
6424
+ // anchor rather than prefetched and fetched as a page - and so is
6425
+ // anything under a prefix the app named as the backend's.
6426
+ apiRoutes: [
6427
+ ...(routeManifest().apis ?? []).map((api) => patternOf(api.segments)),
6428
+ ...backendPatterns(backendPaths),
6429
+ ],
6398
6430
  })})
6399
6431
 
6400
6432
  // A server component is not a module the browser has, so Vite cannot replace