@rsc-kit/core 0.30.23 → 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.
@@ -1,14 +1,23 @@
1
1
  /**
2
- * Server files that import a client library.
2
+ * Server files that reach client-only React on the server.
3
3
  *
4
4
  * A server component may import a client library - `<SheetClose>` from
5
5
  * base-ui rendered inside server output is legal here and in Next.js. What
6
6
  * is worth a line is the shape that cost an afternoon: a file with no
7
- * "use client" that only wraps client components (a shadcn ui/ file that
8
- * lost its directive in a port) imported from the server, so the library's
9
- * internals run in the rsc environment. The build cannot tell the two
10
- * apart; it can say which server files do it, and with which packages, so
11
- * the person reading can.
7
+ * "use client" (a shadcn ui/ file that lost its directive in a port) whose
8
+ * code, or a library's under it, calls a hook in the rsc environment, which
9
+ * fails at render as "(0, import_react_react_server.useState) is not a
10
+ * function" - naming React, not the file.
11
+ *
12
+ * This used to name every server file importing any package that depends on
13
+ * React, which on a real app was 26 files of icons, markdown and base-ui
14
+ * parts that carry their own "use client", and the one that mattered was
15
+ * lost among them. So the code is read instead: from the server file into
16
+ * the packages it imports, stopping at a client reference - a module with
17
+ * the directive, whose code never runs on the server - and a file is named
18
+ * only when what is left mentions a React API the server build does not
19
+ * have. Mentions, not calls: a library can hold a hook in an export the
20
+ * page never renders, so the line names the API for the reader to judge.
12
21
  *
13
22
  * Read off the rsc environment's module graph after it is built. A file with
14
23
  * the directive never appears: in that graph it is already a reference, and
@@ -17,8 +26,10 @@
17
26
  export interface ClientLibraryImport {
18
27
  /** The server file, relative to the source dir. */
19
28
  file: string;
20
- /** The client packages it imports, by name. */
29
+ /** The packages whose server-run code reaches a client-only API, by name. Empty when the file itself does. */
21
30
  packages: string[];
31
+ /** The client-only React APIs reached, by name. */
32
+ calls: string[];
22
33
  /** The first app file that imports it, relative to the source dir - or null for a route file. */
23
34
  from: string | null;
24
35
  }
@@ -26,7 +37,16 @@ export interface ModuleGraph {
26
37
  moduleIds(): Iterable<string>;
27
38
  importedIds(id: string): readonly string[];
28
39
  importers(id: string): readonly string[];
40
+ /** The module's code as built - for a client reference, the reference. */
41
+ code(id: string): string | null;
29
42
  }
43
+ /**
44
+ * React's APIs that its server build leaves out, as of React 19: the ones a
45
+ * render needs state or an effect for. A test reads them off React itself.
46
+ * Not Component or startTransition - the one is a word in every library's
47
+ * prose, the other runs in an event handler, never during a render.
48
+ */
49
+ export declare const CLIENT_ONLY_APIS: readonly ["createContext", "useActionState", "useContext", "useDeferredValue", "useEffect", "useEffectEvent", "useFormState", "useFormStatus", "useImperativeHandle", "useInsertionEffect", "useLayoutEffect", "useOptimistic", "useReducer", "useRef", "useState", "useSyncExternalStore", "useTransition"];
30
50
  /** The package a node_modules id belongs to, or null for anything else. */
31
51
  export declare function packageOf(id: string): string | null;
32
52
  export declare function serverImportsOfClientPackages(graph: ModuleGraph, options: {
@@ -1,3 +1,38 @@
1
+ /**
2
+ * React's APIs that its server build leaves out, as of React 19: the ones a
3
+ * render needs state or an effect for. A test reads them off React itself.
4
+ * Not Component or startTransition - the one is a word in every library's
5
+ * prose, the other runs in an event handler, never during a render.
6
+ */
7
+ export const CLIENT_ONLY_APIS = [
8
+ "createContext",
9
+ "useActionState",
10
+ "useContext",
11
+ "useDeferredValue",
12
+ "useEffect",
13
+ "useEffectEvent",
14
+ "useFormState",
15
+ "useFormStatus",
16
+ "useImperativeHandle",
17
+ "useInsertionEffect",
18
+ "useLayoutEffect",
19
+ "useOptimistic",
20
+ "useReducer",
21
+ "useRef",
22
+ "useState",
23
+ "useSyncExternalStore",
24
+ "useTransition",
25
+ ];
26
+ const CLIENT_ONLY = new RegExp(`\\b(${CLIENT_ONLY_APIS.join("|")})\\b`, "g");
27
+ const COMMENT = /\/\*[\s\S]*?\*\/|(^|[^:])\/\/[^\n]*/g;
28
+ /** plugin-rsc's reference for a "use client" module: its code stays on the client. */
29
+ const isReference = (code) => code.includes("registerClientReference");
30
+ /** The client-only APIs a module's own code mentions, outside its comments. */
31
+ function clientOnlyIn(code) {
32
+ if (!code || isReference(code))
33
+ return [];
34
+ return [...new Set([...code.replace(COMMENT, "$1").matchAll(CLIENT_ONLY)].map((m) => m[1]))];
35
+ }
1
36
  const PACKAGE = /\/node_modules\/(@[^/]+\/[^/]+|[^/@][^/]*)/;
2
37
  /** The package a node_modules id belongs to, or null for anything else. */
3
38
  export function packageOf(id) {
@@ -13,21 +48,54 @@ export function serverImportsOfClientPackages(graph, options) {
13
48
  !id.startsWith("\0");
14
49
  const relative = (id) => (id.split("?")[0] ?? id).slice(root.length);
15
50
  const out = [];
51
+ const inClientPackage = (id) => {
52
+ const name = packageOf(id);
53
+ return name !== null && client.has(name) && !ignore.has(name);
54
+ };
55
+ // What a package module and everything it runs on the server reach,
56
+ // memoised: one barrel is imported by many files. A cycle reads as nothing
57
+ // more on its second visit, which is all a union needs.
58
+ const reached = new Map();
59
+ const reach = (id) => {
60
+ const known = reached.get(id);
61
+ if (known)
62
+ return known;
63
+ const apis = new Set();
64
+ reached.set(id, apis);
65
+ const code = graph.code(id);
66
+ if (code !== null && isReference(code))
67
+ return apis;
68
+ for (const api of clientOnlyIn(code))
69
+ apis.add(api);
70
+ for (const next of graph.importedIds(id)) {
71
+ if (inClientPackage(next))
72
+ for (const api of reach(next))
73
+ apis.add(api);
74
+ }
75
+ return apis;
76
+ };
16
77
  for (const id of graph.moduleIds()) {
17
78
  if (!isApp(id))
18
79
  continue;
80
+ const calls = new Set(clientOnlyIn(graph.code(id)));
19
81
  const packages = new Set();
20
82
  for (const imported of graph.importedIds(id)) {
21
- const name = packageOf(imported);
22
- if (name && client.has(name) && !ignore.has(name))
23
- packages.add(name);
83
+ if (!inClientPackage(imported))
84
+ continue;
85
+ const apis = reach(imported);
86
+ if (apis.size === 0)
87
+ continue;
88
+ packages.add(packageOf(imported));
89
+ for (const api of apis)
90
+ calls.add(api);
24
91
  }
25
- if (packages.size === 0)
92
+ if (calls.size === 0)
26
93
  continue;
27
94
  const from = graph.importers(id).find(isApp);
28
95
  out.push({
29
96
  file: relative(id),
30
97
  packages: [...packages].sort(),
98
+ calls: [...calls].sort(),
31
99
  from: from ? relative(from) : null,
32
100
  });
33
101
  }
@@ -1 +1 @@
1
- {"version":3,"file":"clientImports.js","sourceRoot":"","sources":["../src/clientImports.ts"],"names":[],"mappings":"AA+BA,MAAM,OAAO,GAAG,4CAA4C,CAAC;AAE7D,2EAA2E;AAC3E,MAAM,UAAU,SAAS,CAAC,EAAU;IAClC,MAAM,KAAK,GAAG,OAAO,CAAC,IAAI,CAAC,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC;IAEnD,OAAO,KAAK,CAAC,CAAC,CAAC,KAAK,CAAC,CAAC,CAAE,CAAC,CAAC,CAAC,IAAI,CAAC;AAClC,CAAC;AAED,MAAM,UAAU,6BAA6B,CAC3C,KAAkB,EAClB,OAIC;IAED,MAAM,IAAI,GAAG,OAAO,CAAC,SAAS,CAAC,OAAO,CAAC,MAAM,EAAE,EAAE,CAAC,GAAG,GAAG,CAAC;IACzD,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,cAAc,CAAC,CAAC;IAC/C,MAAM,MAAM,GAAG,IAAI,GAAG,CAAC,OAAO,CAAC,MAAM,IAAI,EAAE,CAAC,CAAC;IAC7C,MAAM,KAAK,GAAG,CAAC,EAAU,EAAE,EAAE,CAC3B,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC;QACnB,CAAC,EAAE,CAAC,QAAQ,CAAC,gBAAgB,CAAC;QAC9B,CAAC,EAAE,CAAC,UAAU,CAAC,IAAI,CAAC,CAAC;IACvB,MAAM,QAAQ,GAAG,CAAC,EAAU,EAAE,EAAE,CAAC,CAAC,EAAE,CAAC,KAAK,CAAC,GAAG,CAAC,CAAC,CAAC,CAAC,IAAI,EAAE,CAAC,CAAC,KAAK,CAAC,IAAI,CAAC,MAAM,CAAC,CAAC;IAC7E,MAAM,GAAG,GAA0B,EAAE,CAAC;IAEtC,KAAK,MAAM,EAAE,IAAI,KAAK,CAAC,SAAS,EAAE,EAAE,CAAC;QACnC,IAAI,CAAC,KAAK,CAAC,EAAE,CAAC;YAAE,SAAS;QAEzB,MAAM,QAAQ,GAAG,IAAI,GAAG,EAAU,CAAC;QAEnC,KAAK,MAAM,QAAQ,IAAI,KAAK,CAAC,WAAW,CAAC,EAAE,CAAC,EAAE,CAAC;YAC7C,MAAM,IAAI,GAAG,SAAS,CAAC,QAAQ,CAAC,CAAC;YAEjC,IAAI,IAAI,IAAI,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC,IAAI,CAAC,MAAM,CAAC,GAAG,CAAC,IAAI,CAAC;gBAAE,QAAQ,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACxE,CAAC;QAED,IAAI,QAAQ,CAAC,IAAI,KAAK,CAAC;YAAE,SAAS;QAElC,MAAM,IAAI,GAAG,KAAK,CAAC,SAAS,CAAC,EAAE,CAAC,CAAC,IAAI,CAAC,KAAK,CAAC,CAAC;QAE7C,GAAG,CAAC,IAAI,CAAC;YACP,IAAI,EAAE,QAAQ,CAAC,EAAE,CAAC;YAClB,QAAQ,EAAE,CAAC,GAAG,QAAQ,CAAC,CAAC,IAAI,EAAE;YAC9B,IAAI,EAAE,IAAI,CAAC,CAAC,CAAC,QAAQ,CAAC,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI;SACnC,CAAC,CAAC;IACL,CAAC;IAED,OAAO,GAAG,CAAC,IAAI,CAAC,CAAC,CAAC,EAAE,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,IAAI,CAAC,aAAa,CAAC,CAAC,CAAC,IAAI,CAAC,CAAC,CAAC;AAC1D,CAAC","sourcesContent":["/**\n * Server files that import a client library.\n *\n * A server component may import a client library - `<SheetClose>` from\n * base-ui rendered inside server output is legal here and in Next.js. What\n * is worth a line is the shape that cost an afternoon: a file with no\n * \"use client\" that only wraps client components (a shadcn ui/ file that\n * lost its directive in a port) imported from the server, so the library's\n * internals run in the rsc environment. The build cannot tell the two\n * apart; it can say which server files do it, and with which packages, so\n * the person reading can.\n *\n * Read off the rsc environment's module graph after it is built. A file with\n * the directive never appears: in that graph it is already a reference, and\n * a reference imports nothing.\n */\nexport interface ClientLibraryImport {\n /** The server file, relative to the source dir. */\n file: string;\n /** The client packages it imports, by name. */\n packages: string[];\n /** The first app file that imports it, relative to the source dir - or null for a route file. */\n from: string | null;\n}\n\nexport interface ModuleGraph {\n moduleIds(): Iterable<string>;\n importedIds(id: string): readonly string[];\n importers(id: string): readonly string[];\n}\n\nconst PACKAGE = /\\/node_modules\\/(@[^/]+\\/[^/]+|[^/@][^/]*)/;\n\n/** The package a node_modules id belongs to, or null for anything else. */\nexport function packageOf(id: string): string | null {\n const match = PACKAGE.exec(id.split(\"?\")[0] ?? id);\n\n return match ? match[1]! : null;\n}\n\nexport function serverImportsOfClientPackages(\n graph: ModuleGraph,\n options: {\n sourceDir: string;\n clientPackages: Iterable<string>;\n ignore?: Iterable<string>;\n },\n): ClientLibraryImport[] {\n const root = options.sourceDir.replace(/\\/+$/, \"\") + \"/\";\n const client = new Set(options.clientPackages);\n const ignore = new Set(options.ignore ?? []);\n const isApp = (id: string) =>\n id.startsWith(root) &&\n !id.includes(\"/node_modules/\") &&\n !id.startsWith(\"\\0\");\n const relative = (id: string) => (id.split(\"?\")[0] ?? id).slice(root.length);\n const out: ClientLibraryImport[] = [];\n\n for (const id of graph.moduleIds()) {\n if (!isApp(id)) continue;\n\n const packages = new Set<string>();\n\n for (const imported of graph.importedIds(id)) {\n const name = packageOf(imported);\n\n if (name && client.has(name) && !ignore.has(name)) packages.add(name);\n }\n\n if (packages.size === 0) continue;\n\n const from = graph.importers(id).find(isApp);\n\n out.push({\n file: relative(id),\n packages: [...packages].sort(),\n from: from ? relative(from) : null,\n });\n }\n\n return out.sort((a, b) => a.file.localeCompare(b.file));\n}\n"]}
1
+ {"version":3,"file":"clientImports.js","sourceRoot":"","sources":["../src/clientImports.ts"],"names":[],"mappings":"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","sourcesContent":["/**\n * Server files that reach client-only React on the server.\n *\n * A server component may import a client library - `<SheetClose>` from\n * base-ui rendered inside server output is legal here and in Next.js. What\n * is worth a line is the shape that cost an afternoon: a file with no\n * \"use client\" (a shadcn ui/ file that lost its directive in a port) whose\n * code, or a library's under it, calls a hook in the rsc environment, which\n * fails at render as \"(0, import_react_react_server.useState) is not a\n * function\" - naming React, not the file.\n *\n * This used to name every server file importing any package that depends on\n * React, which on a real app was 26 files of icons, markdown and base-ui\n * parts that carry their own \"use client\", and the one that mattered was\n * lost among them. So the code is read instead: from the server file into\n * the packages it imports, stopping at a client reference - a module with\n * the directive, whose code never runs on the server - and a file is named\n * only when what is left mentions a React API the server build does not\n * have. Mentions, not calls: a library can hold a hook in an export the\n * page never renders, so the line names the API for the reader to judge.\n *\n * Read off the rsc environment's module graph after it is built. A file with\n * the directive never appears: in that graph it is already a reference, and\n * a reference imports nothing.\n */\nexport interface ClientLibraryImport {\n /** The server file, relative to the source dir. */\n file: string;\n /** The packages whose server-run code reaches a client-only API, by name. Empty when the file itself does. */\n packages: string[];\n /** The client-only React APIs reached, by name. */\n calls: string[];\n /** The first app file that imports it, relative to the source dir - or null for a route file. */\n from: string | null;\n}\n\nexport interface ModuleGraph {\n moduleIds(): Iterable<string>;\n importedIds(id: string): readonly string[];\n importers(id: string): readonly string[];\n /** The module's code as built - for a client reference, the reference. */\n code(id: string): string | null;\n}\n\n/**\n * React's APIs that its server build leaves out, as of React 19: the ones a\n * render needs state or an effect for. A test reads them off React itself.\n * Not Component or startTransition - the one is a word in every library's\n * prose, the other runs in an event handler, never during a render.\n */\nexport const CLIENT_ONLY_APIS = [\n \"createContext\",\n \"useActionState\",\n \"useContext\",\n \"useDeferredValue\",\n \"useEffect\",\n \"useEffectEvent\",\n \"useFormState\",\n \"useFormStatus\",\n \"useImperativeHandle\",\n \"useInsertionEffect\",\n \"useLayoutEffect\",\n \"useOptimistic\",\n \"useReducer\",\n \"useRef\",\n \"useState\",\n \"useSyncExternalStore\",\n \"useTransition\",\n] as const;\n\nconst CLIENT_ONLY = new RegExp(`\\\\b(${CLIENT_ONLY_APIS.join(\"|\")})\\\\b`, \"g\");\nconst COMMENT = /\\/\\*[\\s\\S]*?\\*\\/|(^|[^:])\\/\\/[^\\n]*/g;\n\n/** plugin-rsc's reference for a \"use client\" module: its code stays on the client. */\nconst isReference = (code: string) => code.includes(\"registerClientReference\");\n\n/** The client-only APIs a module's own code mentions, outside its comments. */\nfunction clientOnlyIn(code: string | null): string[] {\n if (!code || isReference(code)) return [];\n\n return [...new Set([...code.replace(COMMENT, \"$1\").matchAll(CLIENT_ONLY)].map((m) => m[1]!))];\n}\n\nconst PACKAGE = /\\/node_modules\\/(@[^/]+\\/[^/]+|[^/@][^/]*)/;\n\n/** The package a node_modules id belongs to, or null for anything else. */\nexport function packageOf(id: string): string | null {\n const match = PACKAGE.exec(id.split(\"?\")[0] ?? id);\n\n return match ? match[1]! : null;\n}\n\nexport function serverImportsOfClientPackages(\n graph: ModuleGraph,\n options: {\n sourceDir: string;\n clientPackages: Iterable<string>;\n ignore?: Iterable<string>;\n },\n): ClientLibraryImport[] {\n const root = options.sourceDir.replace(/\\/+$/, \"\") + \"/\";\n const client = new Set(options.clientPackages);\n const ignore = new Set(options.ignore ?? []);\n const isApp = (id: string) =>\n id.startsWith(root) &&\n !id.includes(\"/node_modules/\") &&\n !id.startsWith(\"\\0\");\n const relative = (id: string) => (id.split(\"?\")[0] ?? id).slice(root.length);\n const out: ClientLibraryImport[] = [];\n const inClientPackage = (id: string) => {\n const name = packageOf(id);\n return name !== null && client.has(name) && !ignore.has(name);\n };\n\n // What a package module and everything it runs on the server reach,\n // memoised: one barrel is imported by many files. A cycle reads as nothing\n // more on its second visit, which is all a union needs.\n const reached = new Map<string, Set<string>>();\n const reach = (id: string): Set<string> => {\n const known = reached.get(id);\n if (known) return known;\n\n const apis = new Set<string>();\n reached.set(id, apis);\n\n const code = graph.code(id);\n if (code !== null && isReference(code)) return apis;\n\n for (const api of clientOnlyIn(code)) apis.add(api);\n\n for (const next of graph.importedIds(id)) {\n if (inClientPackage(next)) for (const api of reach(next)) apis.add(api);\n }\n\n return apis;\n };\n\n for (const id of graph.moduleIds()) {\n if (!isApp(id)) continue;\n\n const calls = new Set(clientOnlyIn(graph.code(id)));\n const packages = new Set<string>();\n\n for (const imported of graph.importedIds(id)) {\n if (!inClientPackage(imported)) continue;\n\n const apis = reach(imported);\n if (apis.size === 0) continue;\n\n packages.add(packageOf(imported)!);\n for (const api of apis) calls.add(api);\n }\n\n if (calls.size === 0) continue;\n\n const from = graph.importers(id).find(isApp);\n\n out.push({\n file: relative(id),\n packages: [...packages].sort(),\n calls: [...calls].sort(),\n from: from ? relative(from) : null,\n });\n }\n\n return out.sort((a, b) => a.file.localeCompare(b.file));\n}\n"]}
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.
@@ -7128,6 +7127,7 @@ function clientImportsAudit() {
7128
7127
  moduleIds: () => this.getModuleIds(),
7129
7128
  importedIds: (id) => this.getModuleInfo(id)?.importedIds ?? [],
7130
7129
  importers: (id) => this.getModuleInfo(id)?.importers ?? [],
7130
+ code: (id) => this.getModuleInfo(id)?.code ?? null,
7131
7131
  }, {
7132
7132
  sourceDir,
7133
7133
  clientPackages,