@farm.js/plugin 0.1.0-beta.4 → 0.1.0-beta.40

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.
@@ -0,0 +1,8 @@
1
+ /**
2
+ * Route segments may legitimately contain dots (e.g. /kinfish/farm.js), so a
3
+ * dot alone cannot classify a dev request as a static asset. A dotted path is
4
+ * only treated as an asset when it maps to a real file under the project root
5
+ * or public dir, matching the filesystem-first behavior of production hosting.
6
+ */
7
+ export declare function devServableFileExists(pathname: string, baseDirs: Array<string | false | undefined>): boolean;
8
+ //# sourceMappingURL=dev-static.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"dev-static.d.ts","sourceRoot":"","sources":["../src/dev-static.ts"],"names":[],"mappings":"AAGA;;;;;GAKG;AACH,wBAAgB,qBAAqB,CACnC,QAAQ,EAAE,MAAM,EAChB,QAAQ,EAAE,KAAK,CAAC,MAAM,GAAG,KAAK,GAAG,SAAS,CAAC,GAC1C,OAAO,CAuBT"}
@@ -0,0 +1,37 @@
1
+ import * as fs from "node:fs";
2
+ import * as path from "node:path";
3
+ /**
4
+ * Route segments may legitimately contain dots (e.g. /kinfish/farm.js), so a
5
+ * dot alone cannot classify a dev request as a static asset. A dotted path is
6
+ * only treated as an asset when it maps to a real file under the project root
7
+ * or public dir, matching the filesystem-first behavior of production hosting.
8
+ */
9
+ export function devServableFileExists(pathname, baseDirs) {
10
+ let decodedPathname;
11
+ try {
12
+ decodedPathname = decodeURIComponent(pathname);
13
+ }
14
+ catch {
15
+ return false;
16
+ }
17
+ const relativePathname = decodedPathname.replace(/^\/+/, "");
18
+ if (!relativePathname)
19
+ return false;
20
+ for (const baseDir of baseDirs) {
21
+ if (typeof baseDir !== "string" || baseDir.length === 0)
22
+ continue;
23
+ const resolvedBase = path.resolve(baseDir);
24
+ const candidate = path.resolve(resolvedBase, relativePathname);
25
+ if (!candidate.startsWith(resolvedBase + path.sep))
26
+ continue;
27
+ try {
28
+ if (fs.existsSync(candidate) && fs.statSync(candidate).isFile()) {
29
+ return true;
30
+ }
31
+ }
32
+ catch {
33
+ // Ignore filesystem errors and keep checking other base dirs.
34
+ }
35
+ }
36
+ return false;
37
+ }
package/dist/index.d.ts CHANGED
@@ -39,5 +39,5 @@
39
39
  * })
40
40
  * ```
41
41
  */
42
- export declare const version = "0.1.0-beta.4";
42
+ export declare const version = "0.1.0-beta.40";
43
43
  //# sourceMappingURL=index.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,eAAO,MAAM,OAAO,iBAAiB,CAAC"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../src/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAwCG;AAEH,eAAO,MAAM,OAAO,kBAAkB,CAAC"}
package/dist/index.js CHANGED
@@ -39,7 +39,7 @@
39
39
  * })
40
40
  * ```
41
41
  */
42
- export const version = "0.1.0-beta.4";
42
+ export const version = "0.1.0-beta.40";
43
43
  // Note: Import plugins from their respective subpaths:
44
44
  // - @farm.js/plugin/rsc
45
45
  // - @farm.js/plugin/middleware
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/middleware/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/middleware/index.ts"],"names":[],"mappings":"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"}
@@ -15,6 +15,7 @@
15
15
  */
16
16
  import { createRequire } from "node:module";
17
17
  import { _withAfterNodeMiddleware } from "@farm.js/core/after";
18
+ import { devServableFileExists } from "../dev-static.js";
18
19
  // Create require for ESM compatibility
19
20
  const require_ = createRequire(import.meta.url);
20
21
  // Get picocolors with forced color support (to handle NO_COLOR env being set)
@@ -384,11 +385,15 @@ export default function farmMiddleware(options = {}) {
384
385
  server.middlewares.use(_withAfterNodeMiddleware(async (req, res, next) => {
385
386
  const url = req.url || "/";
386
387
  const pathname = url.split("?")[0];
387
- // Skip Vite internal requests
388
+ // Skip Vite internal requests and real static assets. Dotted
389
+ // paths that don't map to a file on disk are app routes (route
390
+ // segments may contain dots) and must still run middleware.
388
391
  if (pathname.startsWith("/@") ||
389
392
  pathname.startsWith("/__") ||
390
393
  pathname.startsWith("/node_modules") ||
391
- (pathname.includes(".") && !pathname.endsWith("/"))) {
394
+ (pathname.includes(".") &&
395
+ !pathname.endsWith("/") &&
396
+ devServableFileExists(pathname, [server.config.publicDir, server.config.root]))) {
392
397
  return next();
393
398
  }
394
399
  try {
@@ -0,0 +1,11 @@
1
+ export interface AutomaticOptimizedBoundaryTransform {
2
+ code: string;
3
+ boundaryCount: number;
4
+ }
5
+ /**
6
+ * Wrap React JSX-runtime calls for native host boundaries with Farm's
7
+ * server-only optimizer. Eligibility is decided against the fully evaluated
8
+ * React element at runtime, so unsupported trees retain normal React behavior.
9
+ */
10
+ export declare function transformAutomaticOptimizedBoundaries(code: string, id: string): AutomaticOptimizedBoundaryTransform | null;
11
+ //# sourceMappingURL=automatic-optimized-boundary.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"automatic-optimized-boundary.d.ts","sourceRoot":"","sources":["../../src/rsc/automatic-optimized-boundary.ts"],"names":[],"mappings":"AAwBA,MAAM,WAAW,mCAAmC;IAClD,IAAI,EAAE,MAAM,CAAC;IACb,aAAa,EAAE,MAAM,CAAC;CACvB;AAiGD;;;;GAIG;AACH,wBAAgB,qCAAqC,CACnD,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,GACT,mCAAmC,GAAG,IAAI,CA8C5C"}
@@ -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
+ }
@@ -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,CAiR7D"}
@@ -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
  *
@@ -45,6 +75,8 @@ import {
45
75
  let actionSetup = "";
46
76
  if (ctx.actionsEnabled) {
47
77
  actionSetup = `
78
+ ${serverFnTransportErrorClientRuntime}
79
+
48
80
  // Ref for payload setter (used by server action callback and refetch)
49
81
  const setPayloadRef = { current: null };
50
82
 
@@ -96,9 +128,7 @@ setServerCallback(async (id, args) => {
96
128
  applyFarmCacheInvalidations(p.returnValue?.invalidations);
97
129
  if (!p.returnValue || !p.returnValue.ok) {
98
130
  debug('Server action failed:', id);
99
- const error = new Error(p.returnValue?.data?.message || 'Server function failed');
100
- error.name = 'ServerActionError';
101
- throw error;
131
+ throw createFarmServerFnTransportError(p.returnValue?.data);
102
132
  }
103
133
  return completeFarmServerQueryAction(serverQueryInvocation, p.returnValue.data);
104
134
  });
@@ -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,CAs7B1D"}
@@ -642,8 +642,11 @@ async function handleFarmRequest(request) {
642
642
  if (request.signal.aborted) {
643
643
  return new Response(null, { status: 499, headers: { 'Cache-Control': 'no-store' } });
644
644
  }
645
- console.error('[Farm.js] Server action failed:', e);
646
- returnValue = { ok: false, data: sanitizeServerActionError(e) };
645
+ const actionError = sanitizeServerActionError(e);
646
+ if (actionError.name === 'ServerActionError') {
647
+ console.error('[Farm.js] Server action failed:', e);
648
+ }
649
+ returnValue = { ok: false, data: actionError };
647
650
  debug('Server action failed:', actionId, e);
648
651
  }
649
652
  } else {
@@ -674,10 +677,13 @@ async function handleFarmRequest(request) {
674
677
  if (request.signal.aborted) {
675
678
  return new Response(null, { status: 499, headers: { 'Cache-Control': 'no-store' } });
676
679
  }
677
- console.error('[Farm.js] Form action failed:', e);
678
- debug('Form action failed:', e);
679
- return new Response('Server function failed', {
680
- status: 500,
680
+ const actionError = sanitizeServerActionError(e);
681
+ if (actionError.name === 'ServerActionError') {
682
+ console.error('[Farm.js] Form action failed:', e);
683
+ }
684
+ debug('Form action failed:', actionError);
685
+ return new Response(actionError.message, {
686
+ status: actionError.name === 'ServerFnFailure' ? actionError.status : 500,
681
687
  headers: {
682
688
  'Cache-Control': 'no-store',
683
689
  'Content-Type': 'text/plain; charset=utf-8',
@@ -49,8 +49,9 @@ export interface FarmRscConfig {
49
49
  */
50
50
  serverActions?: boolean;
51
51
  /**
52
- * Enable server-only optimized boundaries through
53
- * `@farm.js/plugin/rsc/optimized-boundary`.
52
+ * Automatically optimize eligible server-only host-element subtrees with
53
+ * the native Strata renderer. Unsupported trees keep normal React
54
+ * rendering; application components do not need a boundary import.
54
55
  *
55
56
  * @experimental
56
57
  * @default false
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/rsc/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAA4B,KAAK,MAAM,EAAE,KAAK,UAAU,EAAE,MAAM,MAAM,CAAC;AAE9E,OAAO,KAAK,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AACrE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,sCAAsC,CAAC;AACpF,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AA2B9E,YAAY,EAAE,oBAAoB,EAAE,YAAY,EAAE,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAExF,YAAY,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAC7D,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,wBAAwB,CAAC;AAC1D,YAAY,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAGjE,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;IACpC,MAAM,CAAC,EAAE,SAAS,iBAAiB,EAAE,CAAC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE;QACb;;;WAGG;QACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;QAC3B;;;WAGG;QACH,aAAa,CAAC,EAAE,OAAO,CAAC;QACxB;;;;;;WAMG;QACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;KAC7B,CAAC;IACF,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,uBAAuB,CAAC;IACxC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACjD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE;QACR,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,gHAAgH;IAChH,OAAO,CAAC,EAAE,GAAG,EAAE,CAAC;CACjB;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,GAAE,aAAkB,GAAG,UAAU,CAiDnE;AA8TD;;;;;GAKG;AACH,MAAM,CAAC,OAAO,UAAU,OAAO,CAAC,OAAO,GAAE,oBAAyB,GAAG,MAAM,EAAE,CAsnC5E"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/rsc/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAA4B,KAAK,MAAM,EAAE,KAAK,UAAU,EAAE,MAAM,MAAM,CAAC;AAE9E,OAAO,KAAK,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AACrE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,sCAAsC,CAAC;AACpF,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AA8B9E,YAAY,EAAE,oBAAoB,EAAE,YAAY,EAAE,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAExF,YAAY,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAC7D,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,wBAAwB,CAAC;AAC1D,YAAY,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAGjE,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;IACpC,MAAM,CAAC,EAAE,SAAS,iBAAiB,EAAE,CAAC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE;QACb;;;WAGG;QACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;QAC3B;;;WAGG;QACH,aAAa,CAAC,EAAE,OAAO,CAAC;QACxB;;;;;;;WAOG;QACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;KAC7B,CAAC;IACF,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,uBAAuB,CAAC;IACxC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACjD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE;QACR,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,gHAAgH;IAChH,OAAO,CAAC,EAAE,GAAG,EAAE,CAAC;CACjB;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,GAAE,aAAkB,GAAG,UAAU,CAiDnE;AA8TD;;;;;GAKG;AACH,MAAM,CAAC,OAAO,UAAU,OAAO,CAAC,OAAO,GAAE,oBAAyB,GAAG,MAAM,EAAE,CAgqC5E"}
package/dist/rsc/index.js CHANGED
@@ -26,12 +26,15 @@ import { init as initModuleLexer, parse as parseModuleImports } from "es-module-
26
26
  import { farmEnvironmentFunctionsPlugin } from "@farm.js/core/environment/vite";
27
27
  import { generateRscEntry } from "./entries/rsc.js";
28
28
  import { generateSsrEntry } from "./entries/ssr.js";
29
- import { generateClientEntry } from "./entries/client.js";
29
+ import { generateClientEntry, serverFnTransportErrorClientRuntime } from "./entries/client.js";
30
+ import { transformFarmRouteActionClients } from "./route-action-transform.js";
30
31
  import { transformFarmServerFns } from "./server-fn-transform.js";
32
+ import { transformAutomaticOptimizedBoundaries } from "./automatic-optimized-boundary.js";
31
33
  import { resolveRscBuildOutputPath } from "./build-paths.js";
32
34
  import { assertRscPackageCompatibility } from "./compatibility.js";
33
35
  import fs from "fs/promises";
34
36
  import path from "path";
37
+ import { devServableFileExists } from "../dev-static.js";
35
38
  import { pathToFileURL } from "node:url";
36
39
  import { createRequire } from "node:module";
37
40
  // Use API and middleware plugins from @farm.js/core (require so CJS build resolves when ESM .mjs is missing)
@@ -710,6 +713,42 @@ export default function farmRsc(options = {}) {
710
713
  return null;
711
714
  },
712
715
  },
716
+ {
717
+ name: "@farm.js/plugin/rsc:automatic-optimized-boundary",
718
+ enforce: "post",
719
+ transform(code, id) {
720
+ if (!rscEnabled || !optimizedBoundaryEnabled)
721
+ return null;
722
+ const environmentName = this.environment?.name;
723
+ if (environmentName !== "rsc")
724
+ return null;
725
+ const result = transformAutomaticOptimizedBoundaries(code, id);
726
+ if (!result)
727
+ return null;
728
+ if (debug) {
729
+ console.log(`[Farm.js] Added ${result.boundaryCount} automatic optimized-boundary candidate${result.boundaryCount === 1 ? "" : "s"} in ${id}`);
730
+ }
731
+ return { code: result.code, map: null };
732
+ },
733
+ },
734
+ {
735
+ name: "@farm.js/plugin/rsc:route-action-clients",
736
+ enforce: "pre",
737
+ transform(code, id) {
738
+ if (!rscEnabled || !actionsEnabled)
739
+ return null;
740
+ const environmentName = this.environment?.name;
741
+ if (environmentName !== "client")
742
+ return null;
743
+ const result = transformFarmRouteActionClients(code, id);
744
+ if (!result)
745
+ return null;
746
+ return {
747
+ code: result.code,
748
+ map: null,
749
+ };
750
+ },
751
+ },
713
752
  {
714
753
  name: "@farm.js/plugin/rsc:server-fn-actions",
715
754
  enforce: "pre",
@@ -829,6 +868,7 @@ import {
829
868
  createFarmDeploymentRequestHeaders,
830
869
  isFarmDeploymentMismatchResponse,
831
870
  } from '@farm.js/core/deployment';
871
+ ${serverFnTransportErrorClientRuntime}
832
872
  const farmDeploymentId = ${JSON.stringify(entryContext.deploymentId)};
833
873
  setServerCallback(async (id, args) => {
834
874
  const refs = createTemporaryReferenceSet();
@@ -855,9 +895,7 @@ setServerCallback(async (id, args) => {
855
895
  if (!res.ok) throw new Error('Server action failed: ' + res.status);
856
896
  const p = await createFromReadableStream(res.body, { temporaryReferences: refs });
857
897
  if (p?.returnValue?.ok) return p.returnValue.data;
858
- const error = new Error(p?.returnValue?.data?.message || 'Server function failed');
859
- error.name = 'ServerActionError';
860
- throw error;
898
+ throw createFarmServerFnTransportError(p?.returnValue?.data);
861
899
  });
862
900
  `
863
901
  : "";
@@ -925,12 +963,16 @@ if (document.readyState === 'loading') {
925
963
  const url = req.url || "/";
926
964
  const pathname = url.split("?")[0];
927
965
  const method = req.method || "GET";
966
+ // Dotted paths only skip RSC rendering when they map to a real
967
+ // file on disk — route segments may legitimately contain dots.
928
968
  if (pathname.startsWith("/@") ||
929
969
  pathname.startsWith("/__") ||
930
970
  pathname.startsWith("/node_modules") ||
931
971
  pathname.startsWith("/src/") ||
932
972
  pathname.startsWith("/api/") ||
933
- (pathname.includes(".") && !pathname.endsWith("/"))) {
973
+ (pathname.includes(".") &&
974
+ !pathname.endsWith("/") &&
975
+ devServableFileExists(pathname, [server.config.publicDir, server.config.root]))) {
934
976
  return next();
935
977
  }
936
978
  const startTime = Date.now();
@@ -1,5 +1,6 @@
1
1
  import { type RenderOptions, type StrataDocument } from "@farming-labs/strata";
2
2
  import { type StaticFragmentBoundary, type StaticFragmentProps } from "@farming-labs/strata/react-server";
3
+ import { type ReactElement } from "react";
3
4
  export type { RenderOptions, StrataDocument, StrataElement, StrataNode, StrataTag, StrataText, } from "@farming-labs/strata";
4
5
  export interface OptimizedBoundaryProps extends Omit<StaticFragmentProps, "content"> {
5
6
  /**
@@ -9,12 +10,20 @@ export interface OptimizedBoundaryProps extends Omit<StaticFragmentProps, "conte
9
10
  document: StrataDocument;
10
11
  renderOptions?: RenderOptions;
11
12
  }
13
+ /**
14
+ * Internal compiler target for automatic optimized boundaries. This helper is
15
+ * deliberately fail-open: any unsupported or unsafe tree returns the original
16
+ * React element unchanged.
17
+ *
18
+ * @internal
19
+ */
20
+ export declare function _optimizeBoundary(element: ReactElement): ReactElement;
12
21
  /**
13
22
  * Render a typed host-only document with the optimized native runtime and
14
23
  * place it inside an opaque React-owned boundary.
15
24
  *
16
25
  * Enable `experimental.optimizedBoundary` before importing this component.
17
26
  */
18
- export declare function OptimizedBoundary({ document, renderOptions, ...props }: OptimizedBoundaryProps): import("react").ReactElement<any, string | import("react").JSXElementConstructor<any>>;
27
+ export declare function OptimizedBoundary({ document, renderOptions, ...props }: OptimizedBoundaryProps): ReactElement;
19
28
  export type OptimizedBoundaryTag = StaticFragmentBoundary;
20
29
  //# sourceMappingURL=optimized-boundary.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"optimized-boundary.d.ts","sourceRoot":"","sources":["../../src/rsc/optimized-boundary.ts"],"names":[],"mappings":"AAAA,OAAO,EAAU,KAAK,aAAa,EAAE,KAAK,cAAc,EAAE,MAAM,sBAAsB,CAAC;AACvF,OAAO,EAEL,KAAK,sBAAsB,EAC3B,KAAK,mBAAmB,EACzB,MAAM,mCAAmC,CAAC;AAE3C,YAAY,EACV,aAAa,EACb,cAAc,EACd,aAAa,EACb,UAAU,EACV,SAAS,EACT,UAAU,GACX,MAAM,sBAAsB,CAAC;AAE9B,MAAM,WAAW,sBAAuB,SAAQ,IAAI,CAAC,mBAAmB,EAAE,SAAS,CAAC;IAClF;;;OAGG;IACH,QAAQ,EAAE,cAAc,CAAC;IACzB,aAAa,CAAC,EAAE,aAAa,CAAC;CAC/B;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,EAAE,QAAQ,EAAE,aAAa,EAAE,GAAG,KAAK,EAAE,EAAE,sBAAsB,0FAM9F;AAED,MAAM,MAAM,oBAAoB,GAAG,sBAAsB,CAAC"}
1
+ {"version":3,"file":"optimized-boundary.d.ts","sourceRoot":"","sources":["../../src/rsc/optimized-boundary.ts"],"names":[],"mappings":"AAAA,OAAO,EAEL,KAAK,aAAa,EAClB,KAAK,cAAc,EAIpB,MAAM,sBAAsB,CAAC;AAC9B,OAAO,EAEL,KAAK,sBAAsB,EAC3B,KAAK,mBAAmB,EACzB,MAAM,mCAAmC,CAAC;AAC3C,OAAO,EAA2C,KAAK,YAAY,EAAE,MAAM,OAAO,CAAC;AAEnF,YAAY,EACV,aAAa,EACb,cAAc,EACd,aAAa,EACb,UAAU,EACV,SAAS,EACT,UAAU,GACX,MAAM,sBAAsB,CAAC;AAE9B,MAAM,WAAW,sBAAuB,SAAQ,IAAI,CAAC,mBAAmB,EAAE,SAAS,CAAC;IAClF;;;OAGG;IACH,QAAQ,EAAE,cAAc,CAAC;IACzB,aAAa,CAAC,EAAE,aAAa,CAAC;CAC/B;AAiND;;;;;;GAMG;AACH,wBAAgB,iBAAiB,CAAC,OAAO,EAAE,YAAY,GAAG,YAAY,CAsBrE;AAED;;;;;GAKG;AACH,wBAAgB,iBAAiB,CAAC,EAChC,QAAQ,EACR,aAAa,EACb,GAAG,KAAK,EACT,EAAE,sBAAsB,GAAG,YAAY,CAMvC;AAED,MAAM,MAAM,oBAAoB,GAAG,sBAAsB,CAAC"}
@@ -1,5 +1,233 @@
1
- import { render } from "@farming-labs/strata";
1
+ import { render, } from "@farming-labs/strata";
2
2
  import { StaticFragment, } from "@farming-labs/strata/react-server";
3
+ import { createElement, Fragment, isValidElement } from "react";
4
+ const AUTOMATIC_MIN_NODES = 8;
5
+ const AUTOMATIC_MIN_BYTES = 256;
6
+ const AUTOMATIC_BOUNDARIES = new Set([
7
+ "article",
8
+ "aside",
9
+ "div",
10
+ "footer",
11
+ "header",
12
+ "main",
13
+ "nav",
14
+ "section",
15
+ "span",
16
+ ]);
17
+ const STRATA_TAGS = new Set([
18
+ "a",
19
+ "abbr",
20
+ "address",
21
+ "article",
22
+ "aside",
23
+ "b",
24
+ "blockquote",
25
+ "br",
26
+ "caption",
27
+ "cite",
28
+ "code",
29
+ "col",
30
+ "colgroup",
31
+ "dd",
32
+ "del",
33
+ "details",
34
+ "dfn",
35
+ "div",
36
+ "dl",
37
+ "dt",
38
+ "em",
39
+ "figcaption",
40
+ "figure",
41
+ "footer",
42
+ "h1",
43
+ "h2",
44
+ "h3",
45
+ "h4",
46
+ "h5",
47
+ "h6",
48
+ "header",
49
+ "hr",
50
+ "i",
51
+ "img",
52
+ "ins",
53
+ "kbd",
54
+ "li",
55
+ "main",
56
+ "mark",
57
+ "nav",
58
+ "ol",
59
+ "p",
60
+ "pre",
61
+ "q",
62
+ "s",
63
+ "samp",
64
+ "section",
65
+ "small",
66
+ "span",
67
+ "strong",
68
+ "sub",
69
+ "summary",
70
+ "sup",
71
+ "table",
72
+ "tbody",
73
+ "td",
74
+ "tfoot",
75
+ "th",
76
+ "thead",
77
+ "time",
78
+ "tr",
79
+ "u",
80
+ "ul",
81
+ "var",
82
+ "wbr",
83
+ ]);
84
+ const BOOLEAN_ATTRIBUTES = new Set(["hidden", "reversed"]);
85
+ const ATTRIBUTE_NAMES = {
86
+ className: "class",
87
+ colSpan: "colspan",
88
+ dateTime: "datetime",
89
+ rowSpan: "rowspan",
90
+ };
91
+ function AutomaticOptimizedBoundary({ document, fallback, ...props }) {
92
+ let content;
93
+ try {
94
+ content = render(document);
95
+ }
96
+ catch {
97
+ return fallback;
98
+ }
99
+ if (content.nodeCount < AUTOMATIC_MIN_NODES && content.bytes < AUTOMATIC_MIN_BYTES) {
100
+ return fallback;
101
+ }
102
+ return StaticFragment({ ...props, content });
103
+ }
104
+ function attributeValue(name, value) {
105
+ if (value === null || value === undefined)
106
+ return undefined;
107
+ if (typeof value === "string" || typeof value === "number" || typeof value === "bigint") {
108
+ return String(value);
109
+ }
110
+ if (typeof value !== "boolean")
111
+ return null;
112
+ if (name.startsWith("aria-") || name.startsWith("data-"))
113
+ return String(value);
114
+ if (BOOLEAN_ATTRIBUTES.has(name))
115
+ return value ? "" : undefined;
116
+ return null;
117
+ }
118
+ function convertAttributes(tag, props) {
119
+ const attributes = {};
120
+ for (const [reactName, rawValue] of Object.entries(props)) {
121
+ if (reactName === "children")
122
+ continue;
123
+ if (reactName === "dangerouslySetInnerHTML" ||
124
+ reactName === "ref" ||
125
+ reactName === "style" ||
126
+ reactName === "suppressHydrationWarning" ||
127
+ /^on[A-Z]/.test(reactName)) {
128
+ return null;
129
+ }
130
+ const name = ATTRIBUTE_NAMES[reactName] || reactName;
131
+ const value = attributeValue(name, rawValue);
132
+ if (value === null)
133
+ return null;
134
+ if (value !== undefined)
135
+ attributes[name] = value;
136
+ }
137
+ return Object.keys(attributes).length > 0 ? attributes : {};
138
+ }
139
+ function automaticDocumentFromElement(element) {
140
+ const rootProps = element.props;
141
+ const children = [];
142
+ if (!appendStrataNodes(rootProps.children, children))
143
+ return null;
144
+ return { type: "document", ...(children.length > 0 ? { children } : {}) };
145
+ }
146
+ function appendStrataNodes(value, output) {
147
+ if (value === null || value === undefined || typeof value === "boolean")
148
+ return true;
149
+ if (typeof value === "string" || typeof value === "number" || typeof value === "bigint") {
150
+ output.push({ type: "text", value: String(value) });
151
+ return true;
152
+ }
153
+ if (Array.isArray(value)) {
154
+ for (const child of value) {
155
+ if (!appendStrataNodes(child, output))
156
+ return false;
157
+ }
158
+ return true;
159
+ }
160
+ if (!isValidElement(value))
161
+ return false;
162
+ const element = value;
163
+ const props = element.props;
164
+ if (element.type === Fragment)
165
+ return appendStrataNodes(props.children, output);
166
+ if (element.type === OptimizedBoundary || element.type === AutomaticOptimizedBoundary) {
167
+ const boundary = props.as ?? "div";
168
+ if (typeof boundary !== "string" || !STRATA_TAGS.has(boundary))
169
+ return false;
170
+ const { as: _as, document: _document, fallback: _fallback, ...boundaryProps } = props;
171
+ const attributes = convertAttributes(boundary, boundaryProps);
172
+ if (!attributes)
173
+ return false;
174
+ const document = props.document;
175
+ if (!document || document.type !== "document")
176
+ return false;
177
+ output.push({
178
+ type: "element",
179
+ tag: boundary,
180
+ ...(Object.keys(attributes).length > 0 ? { attributes } : {}),
181
+ ...(document.children?.length ? { children: document.children } : {}),
182
+ });
183
+ return true;
184
+ }
185
+ if (typeof element.type !== "string" || !STRATA_TAGS.has(element.type)) {
186
+ return false;
187
+ }
188
+ const tag = element.type;
189
+ const attributes = convertAttributes(tag, props);
190
+ if (!attributes)
191
+ return false;
192
+ const children = [];
193
+ if (!appendStrataNodes(props.children, children))
194
+ return false;
195
+ output.push({
196
+ type: "element",
197
+ tag,
198
+ ...(Object.keys(attributes).length > 0 ? { attributes } : {}),
199
+ ...(children.length > 0 ? { children } : {}),
200
+ });
201
+ return true;
202
+ }
203
+ /**
204
+ * Internal compiler target for automatic optimized boundaries. This helper is
205
+ * deliberately fail-open: any unsupported or unsafe tree returns the original
206
+ * React element unchanged.
207
+ *
208
+ * @internal
209
+ */
210
+ export function _optimizeBoundary(element) {
211
+ if (!isValidElement(element) ||
212
+ typeof element.type !== "string" ||
213
+ !AUTOMATIC_BOUNDARIES.has(element.type)) {
214
+ return element;
215
+ }
216
+ const props = element.props;
217
+ if (props.dangerouslySetInnerHTML !== undefined || props.ref !== undefined)
218
+ return element;
219
+ const document = automaticDocumentFromElement(element);
220
+ if (!document)
221
+ return element;
222
+ const { children: _children, ...boundaryProps } = props;
223
+ return createElement(AutomaticOptimizedBoundary, {
224
+ ...boundaryProps,
225
+ ...(element.key !== null ? { key: element.key } : {}),
226
+ as: element.type,
227
+ document,
228
+ fallback: element,
229
+ });
230
+ }
3
231
  /**
4
232
  * Render a typed host-only document with the optimized native runtime and
5
233
  * place it inside an opaque React-owned boundary.
@@ -0,0 +1,11 @@
1
+ export type FarmRouteActionClientTransformResult = {
2
+ code: string;
3
+ routes: string[];
4
+ };
5
+ /**
6
+ * Replace a programmatic route imported by the browser with a small action-only
7
+ * proxy. Route loaders, components, database imports, and other server code do
8
+ * not enter the client graph.
9
+ */
10
+ export declare function transformFarmRouteActionClients(code: string, id: string): FarmRouteActionClientTransformResult | null;
11
+ //# sourceMappingURL=route-action-transform.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"route-action-transform.d.ts","sourceRoot":"","sources":["../../src/rsc/route-action-transform.ts"],"names":[],"mappings":"AAQA,MAAM,MAAM,oCAAoC,GAAG;IACjD,IAAI,EAAE,MAAM,CAAC;IACb,MAAM,EAAE,MAAM,EAAE,CAAC;CAClB,CAAC;AAcF;;;;GAIG;AACH,wBAAgB,+BAA+B,CAC7C,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,GACT,oCAAoC,GAAG,IAAI,CAqC7C"}
@@ -0,0 +1,369 @@
1
+ const ROUTE_FACTORY_IMPORT_RE = /\bimport\s+(?:type\s+)?(?:[A-Za-z_$][\w$]*\s*,\s*)?(\{[^}]*\})\s+from\s*["']@farm.js\/core(?:\/routes)?["']\s*;?/g;
2
+ const EXPORTED_ROUTE_RE = /\bexport\s+(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*([A-Za-z_$][\w$]*)\s*\(/g;
3
+ const IMPORT_RE = /\bimport\s+(?!type\b)([\s\S]*?)\s+from\s*(["'][^"']+["'])\s*;?/g;
4
+ const IDENTIFIER_RE = /^[A-Za-z_$][\w$]*$/;
5
+ const MODULE_EXT_RE = /\.[cm]?[jt]sx?$/;
6
+ /**
7
+ * Replace a programmatic route imported by the browser with a small action-only
8
+ * proxy. Route loaders, components, database imports, and other server code do
9
+ * not enter the client graph.
10
+ */
11
+ export function transformFarmRouteActionClients(code, id) {
12
+ if (!isTransformableModule(id) || !code.includes("createRoute") || !code.includes("actions")) {
13
+ return null;
14
+ }
15
+ const routeFactories = findRouteFactoryImportNames(code);
16
+ if (routeFactories.size === 0)
17
+ return null;
18
+ const routes = findActionRoutes(code, routeFactories, id);
19
+ if (routes.length === 0)
20
+ return null;
21
+ const importStatements = findRuntimeImports(code);
22
+ const requiredImports = new Set();
23
+ for (const route of routes) {
24
+ for (const action of route.actions) {
25
+ const statement = importStatements.get(action.local);
26
+ if (!statement) {
27
+ throw new Error(`[Farm.js] Route "${route.exportName}" action "${action.name}" in ${id} must reference ` +
28
+ "a server function imported from another module. Define and export createServerFn in an actions module, then import it into the route.");
29
+ }
30
+ requiredImports.add(statement);
31
+ }
32
+ }
33
+ const output = [
34
+ ...requiredImports,
35
+ ...routes.map((route) => generateRouteActionProxy(route)),
36
+ ...findRouteDefaultExports(code, routes),
37
+ ].join("\n");
38
+ return {
39
+ code: `${output}\n`,
40
+ routes: routes.map((route) => route.exportName),
41
+ };
42
+ }
43
+ function findRouteFactoryImportNames(code) {
44
+ const names = new Set();
45
+ for (const match of code.matchAll(ROUTE_FACTORY_IMPORT_RE)) {
46
+ const namedImports = (match[1] ?? "").slice(1, -1).split(",");
47
+ for (const specifier of namedImports) {
48
+ const normalized = specifier.trim().replace(/^type\s+/, "");
49
+ const specifierMatch = normalized.match(/^createRoute(?:\s+as\s+([A-Za-z_$][\w$]*))?$/);
50
+ if (specifierMatch)
51
+ names.add(specifierMatch[1] ?? "createRoute");
52
+ }
53
+ }
54
+ return names;
55
+ }
56
+ function findActionRoutes(code, factories, id) {
57
+ const routes = [];
58
+ for (const match of code.matchAll(EXPORTED_ROUTE_RE)) {
59
+ const exportName = match[1];
60
+ const calleeName = match[2];
61
+ if (!exportName || !calleeName || !factories.has(calleeName))
62
+ continue;
63
+ const openParen = (match.index ?? 0) + match[0].length - 1;
64
+ const closeParen = findMatchingDelimiter(code, openParen, "(", ")");
65
+ if (closeParen === -1)
66
+ continue;
67
+ const args = splitTopLevel(code, openParen + 1, closeParen);
68
+ if (args.length < 2)
69
+ continue;
70
+ const pathSource = readSegment(code, args[0]);
71
+ if (!isQuotedString(pathSource)) {
72
+ throw new Error(`[Farm.js] Route "${exportName}" in ${id} must use a static string path before its actions can be imported by a Client Component.`);
73
+ }
74
+ const options = readSegment(code, args[1]);
75
+ if (!options.startsWith("{") || !options.endsWith("}"))
76
+ continue;
77
+ const properties = parseObjectProperties(options.slice(1, -1));
78
+ const actionsSource = properties.get("actions");
79
+ if (!actionsSource)
80
+ continue;
81
+ if (!actionsSource.startsWith("{") || !actionsSource.endsWith("}")) {
82
+ throw new Error(`[Farm.js] Route "${exportName}" actions in ${id} must be an object literal so Farm can generate its client proxy.`);
83
+ }
84
+ const actions = parseActionProperties(actionsSource.slice(1, -1), exportName, id);
85
+ if (actions.length === 0)
86
+ continue;
87
+ const defaultSource = properties.get("defaultAction");
88
+ const defaultAction = defaultSource
89
+ ? parseStringValue(defaultSource, exportName, id)
90
+ : actions[0].name;
91
+ if (!actions.some((action) => action.name === defaultAction)) {
92
+ throw new Error(`[Farm.js] Route "${exportName}" defaultAction "${defaultAction}" in ${id} does not match a declared action.`);
93
+ }
94
+ routes.push({ exportName, pathSource, actions, defaultAction });
95
+ }
96
+ return routes;
97
+ }
98
+ function parseActionProperties(source, routeName, id) {
99
+ const properties = parseObjectProperties(source, true);
100
+ const actions = [];
101
+ for (const [name, value] of properties) {
102
+ const local = value || name;
103
+ if (!IDENTIFIER_RE.test(name) || !IDENTIFIER_RE.test(local)) {
104
+ throw new Error(`[Farm.js] Route "${routeName}" actions in ${id} must use identifier entries such as { update } or { update: updateProduct }.`);
105
+ }
106
+ actions.push({ name, local });
107
+ }
108
+ return actions;
109
+ }
110
+ function parseObjectProperties(source, allowShorthand = false) {
111
+ const properties = new Map();
112
+ for (const segment of splitTopLevel(source, 0, source.length)) {
113
+ const entry = readSegment(source, segment);
114
+ if (!entry)
115
+ continue;
116
+ if (entry.startsWith("...")) {
117
+ properties.set(entry, "");
118
+ continue;
119
+ }
120
+ const colon = findTopLevelColon(entry);
121
+ if (colon === -1) {
122
+ if (allowShorthand)
123
+ properties.set(entry, "");
124
+ continue;
125
+ }
126
+ const keySource = entry.slice(0, colon).trim();
127
+ const value = entry.slice(colon + 1).trim();
128
+ const key = isQuotedString(keySource) ? unquote(keySource) : keySource;
129
+ properties.set(key, value);
130
+ }
131
+ return properties;
132
+ }
133
+ function findRuntimeImports(code) {
134
+ const imports = new Map();
135
+ for (const match of code.matchAll(IMPORT_RE)) {
136
+ const clause = (match[1] ?? "").trim();
137
+ const statement = match[0];
138
+ const namedStart = clause.indexOf("{");
139
+ const namedEnd = clause.lastIndexOf("}");
140
+ if (namedStart > -1 && namedEnd > namedStart) {
141
+ for (const specifier of clause.slice(namedStart + 1, namedEnd).split(",")) {
142
+ const normalized = specifier.trim().replace(/^type\s+/, "");
143
+ if (!normalized || specifier.trim().startsWith("type "))
144
+ continue;
145
+ const parts = normalized.split(/\s+as\s+/);
146
+ const local = parts[1] ?? parts[0];
147
+ if (local && IDENTIFIER_RE.test(local))
148
+ imports.set(local, statement);
149
+ }
150
+ }
151
+ const defaultImport = clause
152
+ .slice(0, namedStart === -1 ? clause.length : namedStart)
153
+ .replace(/,$/, "")
154
+ .trim();
155
+ if (IDENTIFIER_RE.test(defaultImport))
156
+ imports.set(defaultImport, statement);
157
+ }
158
+ return imports;
159
+ }
160
+ function generateRouteActionProxy(route) {
161
+ const actions = route.actions
162
+ .map((action) => `${JSON.stringify(action.name)}: ${action.local}`)
163
+ .join(", ");
164
+ const defaultAction = route.actions.find((action) => action.name === route.defaultAction);
165
+ return `export const ${route.exportName} = Object.freeze({
166
+ __farmRouteActionTarget: true,
167
+ path: ${route.pathSource},
168
+ actions: Object.freeze({ ${actions} }),
169
+ defaultAction: ${JSON.stringify(route.defaultAction)},
170
+ action: ${defaultAction.local},
171
+ });`;
172
+ }
173
+ function findRouteDefaultExports(code, routes) {
174
+ const exports = [];
175
+ for (const route of routes) {
176
+ const defaultExport = new RegExp(`\\bexport\\s+default\\s+${escapeRegExp(route.exportName)}\\s*;?`);
177
+ if (defaultExport.test(code))
178
+ exports.push(`export default ${route.exportName};`);
179
+ }
180
+ return exports;
181
+ }
182
+ function splitTopLevel(source, start, end) {
183
+ const segments = [];
184
+ let segmentStart = start;
185
+ let parens = 0;
186
+ let braces = 0;
187
+ let brackets = 0;
188
+ let quote = null;
189
+ let escaped = false;
190
+ let lineComment = false;
191
+ let blockComment = false;
192
+ for (let index = start; index < end; index++) {
193
+ const char = source[index];
194
+ const next = source[index + 1];
195
+ if (lineComment) {
196
+ if (char === "\n" || char === "\r")
197
+ lineComment = false;
198
+ continue;
199
+ }
200
+ if (blockComment) {
201
+ if (char === "*" && next === "/") {
202
+ blockComment = false;
203
+ index++;
204
+ }
205
+ continue;
206
+ }
207
+ if (quote) {
208
+ if (escaped)
209
+ escaped = false;
210
+ else if (char === "\\")
211
+ escaped = true;
212
+ else if (char === quote)
213
+ quote = null;
214
+ continue;
215
+ }
216
+ if (char === "/" && next === "/") {
217
+ lineComment = true;
218
+ index++;
219
+ continue;
220
+ }
221
+ if (char === "/" && next === "*") {
222
+ blockComment = true;
223
+ index++;
224
+ continue;
225
+ }
226
+ if (char === "'" || char === '"' || char === "`") {
227
+ quote = char;
228
+ continue;
229
+ }
230
+ if (char === "(")
231
+ parens++;
232
+ else if (char === ")")
233
+ parens--;
234
+ else if (char === "{")
235
+ braces++;
236
+ else if (char === "}")
237
+ braces--;
238
+ else if (char === "[")
239
+ brackets++;
240
+ else if (char === "]")
241
+ brackets--;
242
+ else if (char === "," && parens === 0 && braces === 0 && brackets === 0) {
243
+ segments.push([segmentStart, index]);
244
+ segmentStart = index + 1;
245
+ }
246
+ }
247
+ segments.push([segmentStart, end]);
248
+ return segments;
249
+ }
250
+ function findTopLevelColon(source) {
251
+ const segments = splitTopLevel(source, 0, source.length);
252
+ if (segments.length !== 1)
253
+ return -1;
254
+ let parens = 0;
255
+ let braces = 0;
256
+ let brackets = 0;
257
+ let quote = null;
258
+ let escaped = false;
259
+ for (let index = 0; index < source.length; index++) {
260
+ const char = source[index];
261
+ if (quote) {
262
+ if (escaped)
263
+ escaped = false;
264
+ else if (char === "\\")
265
+ escaped = true;
266
+ else if (char === quote)
267
+ quote = null;
268
+ continue;
269
+ }
270
+ if (char === "'" || char === '"' || char === "`") {
271
+ quote = char;
272
+ continue;
273
+ }
274
+ if (char === "(")
275
+ parens++;
276
+ else if (char === ")")
277
+ parens--;
278
+ else if (char === "{")
279
+ braces++;
280
+ else if (char === "}")
281
+ braces--;
282
+ else if (char === "[")
283
+ brackets++;
284
+ else if (char === "]")
285
+ brackets--;
286
+ else if (char === ":" && parens === 0 && braces === 0 && brackets === 0)
287
+ return index;
288
+ }
289
+ return -1;
290
+ }
291
+ function findMatchingDelimiter(source, openIndex, open, close) {
292
+ let depth = 0;
293
+ let quote = null;
294
+ let lineComment = false;
295
+ let blockComment = false;
296
+ let escaped = false;
297
+ for (let index = openIndex; index < source.length; index++) {
298
+ const char = source[index];
299
+ const next = source[index + 1];
300
+ if (lineComment) {
301
+ if (char === "\n" || char === "\r")
302
+ lineComment = false;
303
+ continue;
304
+ }
305
+ if (blockComment) {
306
+ if (char === "*" && next === "/") {
307
+ blockComment = false;
308
+ index++;
309
+ }
310
+ continue;
311
+ }
312
+ if (quote) {
313
+ if (escaped)
314
+ escaped = false;
315
+ else if (char === "\\")
316
+ escaped = true;
317
+ else if (char === quote)
318
+ quote = null;
319
+ continue;
320
+ }
321
+ if (char === "/" && next === "/") {
322
+ lineComment = true;
323
+ index++;
324
+ continue;
325
+ }
326
+ if (char === "/" && next === "*") {
327
+ blockComment = true;
328
+ index++;
329
+ continue;
330
+ }
331
+ if (char === "'" || char === '"' || char === "`") {
332
+ quote = char;
333
+ continue;
334
+ }
335
+ if (char === open)
336
+ depth++;
337
+ else if (char === close && --depth === 0)
338
+ return index;
339
+ }
340
+ return -1;
341
+ }
342
+ function readSegment(source, segment) {
343
+ return source.slice(segment[0], segment[1]).trim();
344
+ }
345
+ function parseStringValue(source, routeName, id) {
346
+ if (!isQuotedString(source)) {
347
+ throw new Error(`[Farm.js] Route "${routeName}" defaultAction in ${id} must be a static string.`);
348
+ }
349
+ return unquote(source);
350
+ }
351
+ function isQuotedString(value) {
352
+ return (value.length >= 2 &&
353
+ ((value.startsWith('"') && value.endsWith('"')) ||
354
+ (value.startsWith("'") && value.endsWith("'"))));
355
+ }
356
+ function unquote(value) {
357
+ const quote = value[0];
358
+ const body = value.slice(1, -1);
359
+ if (quote === '"')
360
+ return JSON.parse(value);
361
+ return body.replace(/\\'/g, "'").replace(/\\\\/g, "\\");
362
+ }
363
+ function isTransformableModule(id) {
364
+ const cleanId = id.split("?", 1)[0];
365
+ return MODULE_EXT_RE.test(cleanId) && !/\.d\.[cm]?ts$/.test(cleanId);
366
+ }
367
+ function escapeRegExp(value) {
368
+ return value.replace(/[.*+?^${}()|[\]\\]/g, "\\$&");
369
+ }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farm.js/plugin",
3
- "version": "0.1.0-beta.4",
3
+ "version": "0.1.0-beta.40",
4
4
  "description": "Official plugins for Farm.js framework",
5
5
  "license": "MIT",
6
6
  "author": "Farm.js Team",
@@ -61,7 +61,7 @@
61
61
  "nitro": "3.0.1-alpha.0",
62
62
  "picocolors": "^1.0.0",
63
63
  "rsc-html-stream": "^0.0.4",
64
- "@farm.js/core": "0.1.0-beta.4"
64
+ "@farm.js/core": "0.1.0-beta.40"
65
65
  },
66
66
  "devDependencies": {
67
67
  "@types/node": "^20.19.21",
@@ -82,7 +82,7 @@
82
82
  "react-dom": "19.2.8",
83
83
  "react-server-dom-webpack": "19.2.8",
84
84
  "vite": ">=6.0.0",
85
- "@farm.js/core": "0.1.0-beta.4"
85
+ "@farm.js/core": "0.1.0-beta.40"
86
86
  },
87
87
  "peerDependenciesMeta": {
88
88
  "@vitejs/plugin-rsc": {