@farm.js/plugin 0.1.0-beta.1 → 0.1.0-beta.100

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 (54) hide show
  1. package/dist/api/index.d.ts +2 -0
  2. package/dist/api/index.d.ts.map +1 -1
  3. package/dist/api/index.js +25 -95
  4. package/dist/dev-static.d.ts +8 -0
  5. package/dist/dev-static.d.ts.map +1 -0
  6. package/dist/dev-static.js +37 -0
  7. package/dist/index.d.ts +1 -1
  8. package/dist/index.d.ts.map +1 -1
  9. package/dist/index.js +1 -1
  10. package/dist/middleware/index.d.ts.map +1 -1
  11. package/dist/middleware/index.js +28 -53
  12. package/dist/middleware/matching.d.ts +29 -0
  13. package/dist/middleware/matching.d.ts.map +1 -0
  14. package/dist/middleware/matching.js +79 -0
  15. package/dist/rsc/automatic-optimized-boundary.d.ts +11 -0
  16. package/dist/rsc/automatic-optimized-boundary.d.ts.map +1 -0
  17. package/dist/rsc/automatic-optimized-boundary.js +162 -0
  18. package/dist/rsc/compatibility.d.ts +8 -0
  19. package/dist/rsc/compatibility.d.ts.map +1 -0
  20. package/dist/rsc/compatibility.js +46 -0
  21. package/dist/rsc/dev-response.d.ts +3 -0
  22. package/dist/rsc/dev-response.d.ts.map +1 -0
  23. package/dist/rsc/dev-response.js +10 -0
  24. package/dist/rsc/entries/client.d.ts +1 -0
  25. package/dist/rsc/entries/client.d.ts.map +1 -1
  26. package/dist/rsc/entries/client.js +42 -5
  27. package/dist/rsc/entries/error-fallback.d.ts +3 -0
  28. package/dist/rsc/entries/error-fallback.d.ts.map +1 -0
  29. package/dist/rsc/entries/error-fallback.js +12 -0
  30. package/dist/rsc/entries/rsc.d.ts.map +1 -1
  31. package/dist/rsc/entries/rsc.js +187 -189
  32. package/dist/rsc/entries/ssr.d.ts.map +1 -1
  33. package/dist/rsc/entries/ssr.js +44 -74
  34. package/dist/rsc/index.d.ts +15 -2
  35. package/dist/rsc/index.d.ts.map +1 -1
  36. package/dist/rsc/index.js +167 -69
  37. package/dist/rsc/nitro-build.d.ts.map +1 -1
  38. package/dist/rsc/nitro-build.js +28 -8
  39. package/dist/rsc/optimized-boundary.d.ts +10 -1
  40. package/dist/rsc/optimized-boundary.d.ts.map +1 -1
  41. package/dist/rsc/optimized-boundary.js +229 -1
  42. package/dist/rsc/route-action-transform.d.ts +11 -0
  43. package/dist/rsc/route-action-transform.d.ts.map +1 -0
  44. package/dist/rsc/route-action-transform.js +369 -0
  45. package/dist/rsc/server-fn-transform.js +1 -1
  46. package/dist/rsc/test-fixture-dependencies.d.ts +2 -0
  47. package/dist/rsc/test-fixture-dependencies.d.ts.map +1 -0
  48. package/dist/rsc/test-fixture-dependencies.js +23 -0
  49. package/dist/rsc/types.d.ts +9 -0
  50. package/dist/rsc/types.d.ts.map +1 -1
  51. package/dist/rsc/vite-plugin-nitro.d.ts +20 -7
  52. package/dist/rsc/vite-plugin-nitro.d.ts.map +1 -1
  53. package/dist/rsc/vite-plugin-nitro.js +51 -66
  54. package/package.json +15 -8
@@ -0,0 +1,162 @@
1
+ import { parseAst } from "vite";
2
+ const JSX_RUNTIME_MODULES = new Set(["react/jsx-runtime", "react/jsx-dev-runtime"]);
3
+ const JSX_FACTORY_EXPORTS = new Set(["jsx", "jsxs", "jsxDEV"]);
4
+ const AUTOMATIC_BOUNDARY_TAGS = new Set([
5
+ "article",
6
+ "aside",
7
+ "div",
8
+ "footer",
9
+ "header",
10
+ "main",
11
+ "nav",
12
+ "section",
13
+ "span",
14
+ ]);
15
+ const AUTOMATIC_BOUNDARY_IMPORT = "@farm.js/plugin/rsc/optimized-boundary";
16
+ function nodeName(node) {
17
+ if (!node || typeof node !== "object")
18
+ return undefined;
19
+ const candidate = node;
20
+ if (typeof candidate.name === "string")
21
+ return candidate.name;
22
+ if (typeof candidate.value === "string")
23
+ return candidate.value;
24
+ return undefined;
25
+ }
26
+ function isClientModule(body) {
27
+ for (const statement of body) {
28
+ if (statement.type === "ImportDeclaration")
29
+ return false;
30
+ if (statement.type !== "ExpressionStatement")
31
+ return false;
32
+ const expression = statement.expression;
33
+ if (expression?.type !== "Literal")
34
+ return false;
35
+ if (expression.value === "use client")
36
+ return true;
37
+ }
38
+ return false;
39
+ }
40
+ function collectJsxFactories(body) {
41
+ const factories = new Set();
42
+ for (const statement of body) {
43
+ if (statement.type !== "ImportDeclaration")
44
+ continue;
45
+ const source = nodeName(statement.source);
46
+ if (!source || !JSX_RUNTIME_MODULES.has(source))
47
+ continue;
48
+ for (const specifier of statement.specifiers || []) {
49
+ if (specifier.type !== "ImportSpecifier")
50
+ continue;
51
+ const imported = nodeName(specifier.imported);
52
+ const local = nodeName(specifier.local);
53
+ if (imported && local && JSX_FACTORY_EXPORTS.has(imported))
54
+ factories.add(local);
55
+ }
56
+ }
57
+ return factories;
58
+ }
59
+ function collectBoundaryCalls(node, factories, calls) {
60
+ if (!node || typeof node !== "object")
61
+ return;
62
+ if (Array.isArray(node)) {
63
+ for (const child of node)
64
+ collectBoundaryCalls(child, factories, calls);
65
+ return;
66
+ }
67
+ const candidate = node;
68
+ if (candidate.type === "CallExpression") {
69
+ const callee = candidate.callee;
70
+ const args = candidate.arguments || [];
71
+ const tag = args[0];
72
+ if (callee?.type === "Identifier" &&
73
+ factories.has(nodeName(callee) || "") &&
74
+ tag?.type === "Literal" &&
75
+ AUTOMATIC_BOUNDARY_TAGS.has(String(tag.value)) &&
76
+ typeof candidate.start === "number" &&
77
+ typeof candidate.end === "number") {
78
+ calls.push(candidate);
79
+ }
80
+ }
81
+ for (const [key, value] of Object.entries(candidate)) {
82
+ if (key === "start" || key === "end" || key === "loc")
83
+ continue;
84
+ collectBoundaryCalls(value, factories, calls);
85
+ }
86
+ }
87
+ function createHelperName(code) {
88
+ const base = "__farmOptimizeBoundary";
89
+ let name = base;
90
+ let suffix = 1;
91
+ while (new RegExp(`\\b${name}\\b`).test(code))
92
+ name = `${base}${suffix++}`;
93
+ return name;
94
+ }
95
+ function importInsertionPoint(body) {
96
+ let position = 0;
97
+ for (const statement of body) {
98
+ if (statement.type === "ImportDeclaration" && typeof statement.end === "number") {
99
+ position = statement.end;
100
+ continue;
101
+ }
102
+ if (statement.type === "ExpressionStatement") {
103
+ const expression = statement.expression;
104
+ if (expression?.type === "Literal" && typeof statement.end === "number") {
105
+ position = statement.end;
106
+ continue;
107
+ }
108
+ }
109
+ break;
110
+ }
111
+ return position;
112
+ }
113
+ /**
114
+ * Wrap React JSX-runtime calls for native host boundaries with Farm's
115
+ * server-only optimizer. Eligibility is decided against the fully evaluated
116
+ * React element at runtime, so unsupported trees retain normal React behavior.
117
+ */
118
+ export function transformAutomaticOptimizedBoundaries(code, id) {
119
+ const cleanId = id.split("?", 1)[0].replace(/\\/g, "/");
120
+ if (!/\.[cm]?[jt]sx?$/.test(cleanId))
121
+ return null;
122
+ if (cleanId.includes("/node_modules/") || cleanId.includes("/.farm/"))
123
+ return null;
124
+ if (!code.includes("react/jsx-runtime") && !code.includes("react/jsx-dev-runtime"))
125
+ return null;
126
+ let ast;
127
+ try {
128
+ ast = parseAst(code);
129
+ }
130
+ catch {
131
+ return null;
132
+ }
133
+ const body = ast.body || [];
134
+ if (isClientModule(body))
135
+ return null;
136
+ const factories = collectJsxFactories(body);
137
+ if (factories.size === 0)
138
+ return null;
139
+ const calls = [];
140
+ collectBoundaryCalls(ast, factories, calls);
141
+ if (calls.length === 0)
142
+ return null;
143
+ const helperName = createHelperName(code);
144
+ const insertions = [];
145
+ for (const call of calls) {
146
+ insertions.push({ position: call.start, text: `${helperName}(`, order: 1 });
147
+ insertions.push({ position: call.end, text: ")", order: 0 });
148
+ }
149
+ insertions.push({
150
+ position: importInsertionPoint(body),
151
+ text: `${importInsertionPoint(body) === 0 ? "" : "\n"}import { _optimizeBoundary as ${helperName} } from ${JSON.stringify(AUTOMATIC_BOUNDARY_IMPORT)};\n`,
152
+ order: 2,
153
+ });
154
+ let transformed = code;
155
+ for (const insertion of insertions.sort((left, right) => right.position - left.position || left.order - right.order)) {
156
+ transformed =
157
+ transformed.slice(0, insertion.position) +
158
+ insertion.text +
159
+ transformed.slice(insertion.position);
160
+ }
161
+ return { code: transformed, boundaryCount: calls.length };
162
+ }
@@ -0,0 +1,8 @@
1
+ /**
2
+ * React's framework-level RSC APIs do not follow semver. Keep the complete
3
+ * renderer/decoder toolchain on the exact combination exercised by Farm's
4
+ * integration tests instead of accepting a potentially incompatible or
5
+ * vulnerable package resolution.
6
+ */
7
+ export declare function assertRscPackageCompatibility(root: string): void;
8
+ //# sourceMappingURL=compatibility.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"compatibility.d.ts","sourceRoot":"","sources":["../../src/rsc/compatibility.ts"],"names":[],"mappings":"AA0BA;;;;;GAKG;AACH,wBAAgB,6BAA6B,CAAC,IAAI,EAAE,MAAM,GAAG,IAAI,CAgChE"}
@@ -0,0 +1,46 @@
1
+ import { readFileSync } from "node:fs";
2
+ import { createRequire } from "node:module";
3
+ import path from "node:path";
4
+ const RSC_PACKAGE_VERSIONS = {
5
+ react: "19.2.8",
6
+ "react-dom": "19.2.8",
7
+ "react-server-dom-webpack": "19.2.8",
8
+ "@vitejs/plugin-rsc": "0.5.32",
9
+ };
10
+ function readPackageVersion(projectRequire, packageName) {
11
+ const manifestPath = projectRequire.resolve(`${packageName}/package.json`);
12
+ const manifest = JSON.parse(readFileSync(manifestPath, "utf8"));
13
+ return typeof manifest.version === "string" ? manifest.version : undefined;
14
+ }
15
+ /**
16
+ * React's framework-level RSC APIs do not follow semver. Keep the complete
17
+ * renderer/decoder toolchain on the exact combination exercised by Farm's
18
+ * integration tests instead of accepting a potentially incompatible or
19
+ * vulnerable package resolution.
20
+ */
21
+ export function assertRscPackageCompatibility(root) {
22
+ const projectRequire = createRequire(path.join(path.resolve(root), "__farm_rsc_check__.cjs"));
23
+ const problems = [];
24
+ for (const [packageName, expectedVersion] of Object.entries(RSC_PACKAGE_VERSIONS)) {
25
+ try {
26
+ const actualVersion = readPackageVersion(projectRequire, packageName);
27
+ if (actualVersion !== expectedVersion) {
28
+ problems.push(`${packageName}: expected ${expectedVersion}, found ${actualVersion ?? "unknown"}`);
29
+ }
30
+ }
31
+ catch {
32
+ problems.push(`${packageName}: expected ${expectedVersion}, package is not installed`);
33
+ }
34
+ }
35
+ if (problems.length === 0)
36
+ return;
37
+ throw new Error([
38
+ "[Farm.js] Unsupported React Server Components package combination.",
39
+ ...problems.map((problem) => `- ${problem}`),
40
+ "",
41
+ "Farm pins the framework-level RSC APIs to one supported, integration-tested combination.",
42
+ "Install the supported versions with:",
43
+ "pnpm add react@19.2.8 react-dom@19.2.8 react-server-dom-webpack@19.2.8",
44
+ "pnpm add -D @vitejs/plugin-rsc@0.5.32",
45
+ ].join("\n"));
46
+ }
@@ -0,0 +1,3 @@
1
+ import type { ServerResponse } from "node:http";
2
+ export declare function sendRscDevelopmentResponse(res: ServerResponse, response: Response): Promise<void>;
3
+ //# sourceMappingURL=dev-response.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dev-response.d.ts","sourceRoot":"","sources":["../../src/rsc/dev-response.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,cAAc,EAAE,MAAM,WAAW,CAAC;AAGhD,wBAAsB,0BAA0B,CAC9C,GAAG,EAAE,cAAc,EACnB,QAAQ,EAAE,QAAQ,GACjB,OAAO,CAAC,IAAI,CAAC,CAYf"}
@@ -0,0 +1,10 @@
1
+ import { sendWebResponse } from "@farm.js/core/server";
2
+ export async function sendRscDevelopmentResponse(res, response) {
3
+ const headers = new Headers(response.headers);
4
+ headers.delete("transfer-encoding");
5
+ await sendWebResponse(res, new Response(response.body, {
6
+ status: response.status,
7
+ statusText: response.statusText,
8
+ headers,
9
+ }));
10
+ }
@@ -1,4 +1,5 @@
1
1
  import type { EntryContext } from "../types.js";
2
+ export declare const serverFnTransportErrorClientRuntime = "function createFarmServerFnTransportError(value) {\n const message = value && typeof value === 'object' && typeof value.message === 'string'\n ? value.message\n : 'Server function failed';\n const error = new Error(message);\n\n if (\n value &&\n typeof value === 'object' &&\n value.name === 'ServerFnFailure' &&\n typeof value.code === 'string' &&\n Number.isInteger(value.status) &&\n value.status >= 400 &&\n value.status <= 599 &&\n Object.prototype.hasOwnProperty.call(value, 'data')\n ) {\n error.name = 'ServerFnFailure';\n error.code = value.code;\n error.status = value.status;\n error.data = value.data;\n Object.defineProperty(error, Symbol.for('farm.server-fn.failure'), {\n value: true,\n enumerable: false,\n });\n return error;\n }\n\n error.name = 'ServerActionError';\n return error;\n}";
2
3
  /**
3
4
  * Generates the browser entry file.
4
5
  *
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/rsc/entries/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,YAAY,GAAG,MAAM,CAiR7D"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/rsc/entries/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,eAAO,MAAM,mCAAmC,62BA6B9C,CAAC;AAEH;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,YAAY,GAAG,MAAM,CAwR7D"}
@@ -1,3 +1,33 @@
1
+ export const serverFnTransportErrorClientRuntime = `function createFarmServerFnTransportError(value) {
2
+ const message = value && typeof value === 'object' && typeof value.message === 'string'
3
+ ? value.message
4
+ : 'Server function failed';
5
+ const error = new Error(message);
6
+
7
+ if (
8
+ value &&
9
+ typeof value === 'object' &&
10
+ value.name === 'ServerFnFailure' &&
11
+ typeof value.code === 'string' &&
12
+ Number.isInteger(value.status) &&
13
+ value.status >= 400 &&
14
+ value.status <= 599 &&
15
+ Object.prototype.hasOwnProperty.call(value, 'data')
16
+ ) {
17
+ error.name = 'ServerFnFailure';
18
+ error.code = value.code;
19
+ error.status = value.status;
20
+ error.data = value.data;
21
+ Object.defineProperty(error, Symbol.for('farm.server-fn.failure'), {
22
+ value: true,
23
+ enumerable: false,
24
+ });
25
+ return error;
26
+ }
27
+
28
+ error.name = 'ServerActionError';
29
+ return error;
30
+ }`;
1
31
  /**
2
32
  * Generates the browser entry file.
3
33
  *
@@ -37,6 +67,7 @@ import { applyFarmCacheInvalidations } from '@farm.js/core/cache';
37
67
  import {
38
68
  beginFarmServerQueryAction,
39
69
  completeFarmServerQueryAction,
70
+ shouldApplyFarmServerQueryActionResult,
40
71
  } from '@farm.js/core/server-query/client';
41
72
  `;
42
73
  }
@@ -45,6 +76,8 @@ import {
45
76
  let actionSetup = "";
46
77
  if (ctx.actionsEnabled) {
47
78
  actionSetup = `
79
+ ${serverFnTransportErrorClientRuntime}
80
+
48
81
  // Ref for payload setter (used by server action callback and refetch)
49
82
  const setPayloadRef = { current: null };
50
83
 
@@ -92,13 +125,13 @@ setServerCallback(async (id, args) => {
92
125
  console.error('[Farm.js] Action response missing payload.root / payload.rootContent');
93
126
  return;
94
127
  }
95
- setPayloadRef.current?.(p);
96
- applyFarmCacheInvalidations(p.returnValue?.invalidations);
128
+ if (shouldApplyFarmServerQueryActionResult(serverQueryInvocation)) {
129
+ setPayloadRef.current?.(p);
130
+ applyFarmCacheInvalidations(p.returnValue?.invalidations);
131
+ }
97
132
  if (!p.returnValue || !p.returnValue.ok) {
98
133
  debug('Server action failed:', id);
99
- const error = new Error(p.returnValue?.data?.message || 'Server function failed');
100
- error.name = 'ServerActionError';
101
- throw error;
134
+ throw createFarmServerFnTransportError(p.returnValue?.data);
102
135
  }
103
136
  return completeFarmServerQueryAction(serverQueryInvocation, p.returnValue.data);
104
137
  });
@@ -110,7 +143,9 @@ const setPayloadRef = { current: null };
110
143
  `;
111
144
  }
112
145
  return `${imports}
146
+ ${ctx.clientCachePersistence?.imports ?? ""}
113
147
  ${actionSetup}
148
+ ${ctx.clientCachePersistence?.init ?? ""}
114
149
  const farmDeploymentId = ${JSON.stringify(ctx.deploymentId)};
115
150
 
116
151
  function reportDeploymentMismatch(response) {
@@ -193,6 +228,8 @@ async function main() {
193
228
  if (a.hasAttribute('data-native') || a.hasAttribute('data-reload')) {
194
229
  return;
195
230
  }
231
+ if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return;
232
+ if (e.button !== 0) return;
196
233
 
197
234
  e.preventDefault();
198
235
  history.pushState(null, '', a.href);
@@ -0,0 +1,3 @@
1
+ /** Client-owned reset callbacks must not be serialized from the RSC server. */
2
+ export declare function generateErrorFallbackEntry(): string;
3
+ //# sourceMappingURL=error-fallback.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"error-fallback.d.ts","sourceRoot":"","sources":["../../../src/rsc/entries/error-fallback.ts"],"names":[],"mappings":"AAAA,+EAA+E;AAC/E,wBAAgB,0BAA0B,IAAI,MAAM,CAUnD"}
@@ -0,0 +1,12 @@
1
+ /** Client-owned reset callbacks must not be serialized from the RSC server. */
2
+ export function generateErrorFallbackEntry() {
3
+ return `"use client";
4
+ import React from 'react';
5
+
6
+ export default function ServerErrorFallback({ Fallback, message, fallbackProps }) {
7
+ const error = new Error(message);
8
+ const reset = () => window.location.reload();
9
+ return React.createElement(Fallback, { ...fallbackProps, error, reset });
10
+ }
11
+ `;
12
+ }
@@ -1 +1 @@
1
- {"version":3,"file":"rsc.d.ts","sourceRoot":"","sources":["../../../src/rsc/entries/rsc.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,YAAY,GAAG,MAAM,CAg7B1D"}
1
+ {"version":3,"file":"rsc.d.ts","sourceRoot":"","sources":["../../../src/rsc/entries/rsc.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,YAAY,GAAG,MAAM,CA86B1D"}