@rsc-kit/core 0.30.22 → 0.30.24

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":"AA6BA;;;;;;GAMG;AACH,MAAM,CAAC,KAAK,UAAU,aAAa,CACjC,OAAgB,EAChB,KAA8C,EAC9C,SAAoB,EACpB,WAAW,GAAsB,EAAE;IAEnC,MAAM,OAAO,GAAG,WAAW,CAAC,GAAG,EAAE,CAAC;IAClC,IAAI,QAAQ,GAAG,KAAK,CAAC;IAErB,MAAM,MAAM,GAAG,CAAC,MAAc,EAAE,OAAgB,EAAE,KAAe,EAAE,EAAE;QACnE,IAAI,QAAQ;YAAE,OAAO;QACrB,QAAQ,GAAG,IAAI,CAAC;QAEhB,MAAM,MAAM,GAAkB;YAC5B,MAAM,EAAE,OAAO,CAAC,MAAM;YACtB,QAAQ,EAAE,IAAI,GAAG,CAAC,OAAO,CAAC,GAAG,CAAC,CAAC,QAAQ;YACvC,MAAM;YACN,UAAU,EAAE,WAAW,CAAC,GAAG,EAAE,GAAG,OAAO;YACvC,OAAO;YACP,OAAO,EAAE,WAAW,CAAC,OAAO,CAAC,OAAO,EAAE,WAAW,CAAC;YAClD,GAAG,CAAC,KAAK,KAAK,SAAS,CAAC,CAAC,CAAC,EAAE,CAAC,CAAC,CAAC,EAAE,KAAK,EAAE,CAAC;SAC1C,CAAC;QAEF,IAAI,CAAC;YACH,OAAO,CAAC,OAAO,CAAC,SAAS,CAAC,MAAM,CAAC,CAAC,CAAC,KAAK,CAAC,MAAM,CAAC,CAAC;QACnD,CAAC;QAAC,OAAO,MAAM,EAAE,CAAC;YAChB,MAAM,CAAC,MAAM,CAAC,CAAC;QACjB,CAAC;IACH,CAAC,CAAC;IAEF,IAAI,QAAkB,CAAC;IAEvB,IAAI,CAAC;QACH,QAAQ,GAAG,MAAM,KAAK,CAAC,OAAO,CAAC,CAAC;IAClC,CAAC;IAAC,OAAO,KAAK,EAAE,CAAC;QACf,MAAM,CAAC,GAAG,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;QAC1B,MAAM,KAAK,CAAC;IACd,CAAC;IAED,IAAI,CAAC,QAAQ,CAAC,IAAI,EAAE,CAAC;QACnB,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;QAC/B,OAAO,QAAQ,CAAC;IAClB,CAAC;IAED,MAAM,MAAM,GAAG,QAAQ,CAAC,IAAI,CAAC,SAAS,EAAE,CAAC;IACzC,MAAM,IAAI,GAAG,IAAI,cAAc,CAAa;QAC1C,KAAK,CAAC,IAAI,CAAC,UAAU;YACnB,IAAI,CAAC;gBACH,MAAM,EAAE,IAAI,EAAE,KAAK,EAAE,GAAG,MAAM,MAAM,CAAC,IAAI,EAAE,CAAC;gBAE5C,IAAI,IAAI,EAAE,CAAC;oBACT,UAAU,CAAC,KAAK,EAAE,CAAC;oBACnB,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,KAAK,CAAC,CAAC;gBACjC,CAAC;qBAAM,CAAC;oBACN,UAAU,CAAC,OAAO,CAAC,KAAK,CAAC,CAAC;gBAC5B,CAAC;YACH,CAAC;YAAC,OAAO,KAAK,EAAE,CAAC;gBACf,UAAU,CAAC,KAAK,CAAC,KAAK,CAAC,CAAC;gBACxB,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,KAAK,EAAE,KAAK,CAAC,CAAC;YACxC,CAAC;QACH,CAAC;QACD,MAAM,CAAC,MAAM;YACX,MAAM,CAAC,QAAQ,CAAC,MAAM,EAAE,IAAI,CAAC,CAAC;YAC9B,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;QAC/B,CAAC;KACF,CAAC,CAAC;IAEH,OAAO,IAAI,QAAQ,CAAC,IAAI,EAAE,EAAE,MAAM,EAAE,QAAQ,CAAC,MAAM,EAAE,UAAU,EAAE,QAAQ,CAAC,UAAU,EAAE,OAAO,EAAE,QAAQ,CAAC,OAAO,EAAE,CAAC,CAAC;AACrH,CAAC;AAED,SAAS,WAAW,CAAC,OAAgB,EAAE,KAAwB;IAC7D,MAAM,MAAM,GAA2B,EAAE,CAAC;IAE1C,KAAK,MAAM,IAAI,IAAI,KAAK,EAAE,CAAC;QACzB,MAAM,KAAK,GAAG,OAAO,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QAChC,IAAI,KAAK,KAAK,IAAI;YAAE,MAAM,CAAC,IAAI,CAAC,WAAW,EAAE,CAAC,GAAG,KAAK,CAAC;IACzD,CAAC;IAED,OAAO,MAAM,CAAC,MAAM,CAAC,MAAM,CAAC,CAAC;AAC/B,CAAC;AAED,SAAS,MAAM,CAAC,KAAc;IAC5B,OAAO,CAAC,KAAK,CAAC,kDAAkD,EAAE,KAAK,CAAC,CAAC;AAC3E,CAAC","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
@@ -465,6 +465,26 @@ export declare function compileStep(root: string): {
465
465
  compile?: string;
466
466
  binary?: string;
467
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
+ };
468
488
  /**
469
489
  * How the generated entry imports instrumentation.ts, as `__instrumentation`.
470
490
  *
@@ -474,11 +494,7 @@ export declare function compileStep(root: string): {
474
494
  * "Import `shutdown` will always be undefined") - the app told off for a file
475
495
  * written exactly as documented. The rest are simply absent from the object.
476
496
  */
477
- export declare function instrumentationImport(instrumentation: {
478
- file: string;
479
- hasRegister: boolean;
480
- hasShutdown: boolean;
481
- } | null): string;
497
+ export declare function instrumentationImport(instrumentation: InstrumentationFile | null): string;
482
498
  /**
483
499
  * The worker the dev server serves at /sw.js: it removes the one a production
484
500
  * run left on this origin, and the caches with it, then reloads each page it
@@ -576,3 +592,4 @@ export declare function publicVariablesSet(mode: string, envDir?: string | false
576
592
  where: string;
577
593
  }[];
578
594
  export declare function rscKit(options?: RscKitOptions): PluginOption[];
595
+ export {};
package/dist/vite.js CHANGED
@@ -1907,18 +1907,17 @@ async function prerenderAfterBundles(bundle, staticDir, assetsDir, knownActions
1907
1907
  console.log(" React's cache() dedupes only inside a component render; in a guard, an action or\n" +
1908
1908
  " an api route it calls straight through. Import it from @rsc-kit/core/cache instead.");
1909
1909
  }
1910
- // Server files importing a client library. Legal - a server component may
1911
- // render a client component from a package - but the shape that costs an
1912
- // afternoon is a file with no "use client" that only wraps them, so the
1913
- // library's internals run on the server. Said, with the packages and the
1914
- // importer, so the person reading can tell which it is.
1910
+ // Server files whose code, or a library's under them, reaches a hook on the
1911
+ // server (clientImports.ts). The render that calls one fails naming React,
1912
+ // not the file; this names the file, the package and the hook.
1915
1913
  if (clientLibraryImports.length > 0) {
1916
- console.log(`\n \u2139 ${clientLibraryImports.length} server ${clientLibraryImports.length === 1 ? "file imports" : "files import"} a client library: ` +
1914
+ console.log(`\n \u2139 ${clientLibraryImports.length} server ${clientLibraryImports.length === 1 ? "file reaches" : "files reach"} client-only React: ` +
1917
1915
  clientLibraryImports
1918
- .map((c) => `${c.file} (${c.packages.join(", ")}${c.from ? `; imported by ${c.from}` : ""})`)
1916
+ .map((c) => `${c.file} (${c.packages.length > 0 ? `${c.packages.join(", ")}: ` : ""}${c.calls.join(", ")}${c.from ? `; imported by ${c.from}` : ""})`)
1919
1917
  .join("; "));
1920
- console.log(' Legal for a server component. A file that only wraps client components wants "use client" -\n' +
1921
- " as shadcn ships it - so the server stops at the boundary.");
1918
+ console.log(' The server has none of these, so a render that calls one fails with "... is not a function".\n' +
1919
+ ' A file that only wraps client components wants "use client" - as shadcn ships it. A library\n' +
1920
+ " export holding a hook the page never renders is fine.");
1922
1921
  }
1923
1922
  // Written from the rows that were just printed rather than recomputed: the
1924
1923
  // report and the terminal must not be able to disagree about what happened.
@@ -3121,19 +3120,6 @@ function installHostCallsOnce(): void {
3121
3120
  }
3122
3121
 
3123
3122
  `;
3124
- /**
3125
- * The app's process bootstrap, if it has one: `instrumentation.ts` beside
3126
- * `app/`, the name Next uses so a reader arriving from there knows what it
3127
- * is.
3128
- *
3129
- * Two things make it a framework concern rather than an import the app
3130
- * adds. Module evaluation order: a page that configures a shared package at
3131
- * import time runs before any module the app could put first, and only the
3132
- * generated entry can import something before the pages. And "before the
3133
- * first request": an async `register()` — connecting, validating env — has
3134
- * to be awaited by every entry point a host or a prerender can call, which
3135
- * is a list the app cannot see.
3136
- */
3137
3123
  function instrumentationFile() {
3138
3124
  for (const ext of ["ts", "tsx", "mts", "js", "mjs"]) {
3139
3125
  const file = join(sourceDir, `instrumentation.${ext}`);
@@ -3146,7 +3132,13 @@ function instrumentationFile() {
3146
3132
  // a file written exactly as documented.
3147
3133
  const source = readFileSync(file, "utf-8");
3148
3134
  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);
3149
- return { file, hasRegister: exports("register"), hasShutdown: exports("shutdown") };
3135
+ return {
3136
+ file,
3137
+ hasRegister: exports("register"),
3138
+ hasShutdown: exports("shutdown"),
3139
+ hasOnRequest: exports("onRequest"),
3140
+ hasOnRequestHeaders: exports("onRequestHeaders"),
3141
+ };
3150
3142
  }
3151
3143
  return null;
3152
3144
  }
@@ -3160,12 +3152,14 @@ function instrumentationFile() {
3160
3152
  * written exactly as documented. The rest are simply absent from the object.
3161
3153
  */
3162
3154
  export function instrumentationImport(instrumentation) {
3163
- const type = "{ register?: () => unknown; shutdown?: () => unknown }";
3155
+ const type = "{ register?: () => unknown; shutdown?: () => unknown; onRequest?: (served: never) => unknown; onRequestHeaders?: readonly string[] }";
3164
3156
  if (!instrumentation)
3165
3157
  return `const __instrumentation: ${type} = {}`;
3166
3158
  const hooks = [
3167
3159
  ...(instrumentation.hasRegister ? ["register"] : []),
3168
3160
  ...(instrumentation.hasShutdown ? ["shutdown"] : []),
3161
+ ...(instrumentation.hasOnRequest ? ["onRequest"] : []),
3162
+ ...(instrumentation.hasOnRequestHeaders ? ["onRequestHeaders"] : []),
3169
3163
  ];
3170
3164
  const from = JSON.stringify(instrumentation.file);
3171
3165
  // No hooks: still imported, first, for what its import does.
@@ -3270,6 +3264,7 @@ import { httpHostCalls } from ${JSON.stringify(join(packageDir, "hostCalls"))}
3270
3264
  import { backendVersions, installBackendVersionSource } from ${JSON.stringify(join(packageDir, "changed"))}
3271
3265
  import { RefreshOn } from ${JSON.stringify(join(packageDir, "js/refreshOn"))}
3272
3266
  import { prerenderedBeside } from ${JSON.stringify(join(packageDir, "files"))}
3267
+ import { observeServed } from ${JSON.stringify(join(packageDir, "served"))}
3273
3268
  import { renderToReadableStream, decodeReply, decodeAction, decodeFormState, loadServerAction } from '@vitejs/plugin-rsc/rsc'
3274
3269
  import { isQuery, queryCacheControl, isQueryValidationError, queryRefusal } from ${JSON.stringify(join(packageDir, "query"))}
3275
3270
  import { isActionValidationError, isActionRefusal, isClientBuilt } from ${JSON.stringify(join(packageDir, "action"))}
@@ -5936,6 +5931,16 @@ ${backendPathConsts()}${fallbackOrigin === false ? "" : FALLBACK_CONSTS.replace(
5936
5931
  let devHandler: ((request: Request) => Promise<Response | null>) | null = null
5937
5932
 
5938
5933
  export default async function handler(request: Request): Promise<Response> {
5934
+ // instrumentation.ts's onRequest(), once each response is over - streamed to
5935
+ // the end, or cut off. Not for the startup probe: no visitor asked for it.
5936
+ if (__instrumentation.onRequest && request.headers.get('x-rsc-kit-startup') === null) {
5937
+ return observeServed(request, served, __instrumentation.onRequest as never, __instrumentation.onRequestHeaders)
5938
+ }
5939
+
5940
+ return served(request)
5941
+ }
5942
+
5943
+ async function served(request: Request): Promise<Response> {
5939
5944
  try {
5940
5945
  return await serve(request)
5941
5946
  } catch (error) {
@@ -7122,6 +7127,7 @@ function clientImportsAudit() {
7122
7127
  moduleIds: () => this.getModuleIds(),
7123
7128
  importedIds: (id) => this.getModuleInfo(id)?.importedIds ?? [],
7124
7129
  importers: (id) => this.getModuleInfo(id)?.importers ?? [],
7130
+ code: (id) => this.getModuleInfo(id)?.code ?? null,
7125
7131
  }, {
7126
7132
  sourceDir,
7127
7133
  clientPackages,