@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
@@ -40,43 +40,57 @@ const cssLinkTag = resolvedCssHref
40
40
  ? '<link rel="stylesheet" href="' + escapeHtmlAttribute(resolvedCssHref) + '" as="style" data-precedence="default">'
41
41
  : '';
42
42
 
43
- function injectIntoHeadStream(tag) {
43
+ // Work on bytes: chunk boundaries need not align with UTF-8 code points.
44
+ function injectBeforeStream(markerText, tag) {
44
45
  const encoder = new TextEncoder();
45
- const decoder = new TextDecoder();
46
- const marker = '</head>';
47
- let buffer = '';
46
+ const marker = encoder.encode(markerText);
47
+ const insertion = encoder.encode(tag);
48
+ let buffer = new Uint8Array(0);
48
49
  let injected = false;
49
50
 
50
51
  return new TransformStream({
51
52
  transform(chunk, controller) {
52
- const str = typeof chunk === "string" ? chunk : decoder.decode(chunk);
53
+ const bytes = typeof chunk === 'string' ? encoder.encode(chunk) : chunk;
53
54
  if (injected || !tag) {
54
- controller.enqueue(typeof chunk === "string" ? encoder.encode(str) : chunk);
55
+ controller.enqueue(bytes);
55
56
  return;
56
57
  }
57
58
 
58
- buffer += str;
59
- const index = buffer.indexOf(marker);
59
+ const pending = new Uint8Array(buffer.length + bytes.length);
60
+ pending.set(buffer);
61
+ pending.set(bytes, buffer.length);
62
+ let index = -1;
63
+ outer: for (let i = 0; i <= pending.length - marker.length; i++) {
64
+ for (let j = 0; j < marker.length; j++) {
65
+ if (pending[i + j] !== marker[j]) continue outer;
66
+ }
67
+ index = i;
68
+ break;
69
+ }
60
70
  if (index >= 0) {
61
- const before = buffer.slice(0, index);
62
- const after = buffer.slice(index + marker.length);
63
- controller.enqueue(encoder.encode(before + tag + marker + after));
64
- buffer = '';
71
+ if (index) controller.enqueue(pending.subarray(0, index));
72
+ controller.enqueue(insertion);
73
+ controller.enqueue(pending.subarray(index));
74
+ buffer = new Uint8Array(0);
65
75
  injected = true;
66
76
  return;
67
77
  }
68
78
 
69
- if (buffer.length > marker.length) {
70
- controller.enqueue(encoder.encode(buffer.slice(0, buffer.length - marker.length)));
71
- buffer = buffer.slice(-marker.length);
72
- }
79
+ // Retain only a possible marker prefix, never the whole response.
80
+ const emitLength = Math.max(0, pending.length - marker.length + 1);
81
+ if (emitLength) controller.enqueue(pending.subarray(0, emitLength));
82
+ buffer = pending.slice(emitLength);
73
83
  },
74
84
  flush(controller) {
75
- if (buffer) controller.enqueue(encoder.encode(buffer));
85
+ if (buffer.length) controller.enqueue(buffer);
76
86
  },
77
87
  });
78
88
  }
79
89
 
90
+ function injectIntoHeadStream(tag) {
91
+ return injectBeforeStream('</head>', tag);
92
+ }
93
+
80
94
  /** Collect Node stream (from renderToPipeableStream) into a single string. Supports Suspense. */
81
95
  function pipeableStreamToString(stream) {
82
96
  return new Promise((resolve, reject) => {
@@ -125,13 +139,19 @@ export async function renderHTML(firstArg, options = {}) {
125
139
  if (!bootstrap && PROD_BOOTSTRAP !== "__FARM_BOOTSTRAP_SCRIPT__") bootstrap = PROD_BOOTSTRAP;
126
140
 
127
141
  // Use renderToPipeableStream so Suspense streams: fallback (loading.tsx) first, then resolved content.
128
- const { pipe } = renderToPipeableStream(React.createElement(Root), {
129
- ...(bootstrap && { bootstrapScriptContent: bootstrap }),
130
- formState: options.formState,
131
- });
132
142
  const { PassThrough, Readable } = await import('stream');
133
143
  const passThrough = new PassThrough();
134
- pipe(passThrough);
144
+ // Do not commit an empty response when the shell cannot render. Reject back
145
+ // to the request handler so it can render error.tsx through RSC and SSR.
146
+ await new Promise((resolve, reject) => {
147
+ const { pipe } = renderToPipeableStream(React.createElement(Root), {
148
+ ...(bootstrap && { bootstrapScriptContent: bootstrap }),
149
+ formState: options.formState,
150
+ onShellReady() { pipe(passThrough); resolve(); },
151
+ onShellError: reject,
152
+ onError(error) { console.error('[RSC] SSR render error:', error); },
153
+ });
154
+ });
135
155
 
136
156
  const CLIENT_ENTRY_HREF = "__FARM_CLIENT_ENTRY_HREF__";
137
157
  const PLACEHOLDER_STR = "__FARM_CLIENT_ENTRY_HREF__";
@@ -141,34 +161,7 @@ export async function renderHTML(firstArg, options = {}) {
141
161
  if (hasPayload) {
142
162
  // Stream HTML to the client while preserving the RSC payload for hydration.
143
163
  debug('Streaming HTML (loading fallback then content)');
144
- const BODY_HTML_END = '</body></html>';
145
- const TAG_LEN = BODY_HTML_END.length;
146
- const encoder = new TextEncoder();
147
- let streamBuf = '';
148
- const streamingClientScriptInjector = new TransformStream({
149
- transform(chunk, controller) {
150
- const str = typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk);
151
- if (!injectClientScriptInStream || !clientScriptTag) {
152
- controller.enqueue(typeof chunk === "string" ? encoder.encode(str) : chunk);
153
- return;
154
- }
155
- streamBuf += str;
156
- while (streamBuf.length > TAG_LEN) {
157
- const tail = streamBuf.slice(-TAG_LEN);
158
- if (tail === BODY_HTML_END) {
159
- const before = streamBuf.slice(0, streamBuf.length - TAG_LEN);
160
- controller.enqueue(encoder.encode(before + clientScriptTag + BODY_HTML_END));
161
- streamBuf = '';
162
- return;
163
- }
164
- controller.enqueue(encoder.encode(streamBuf.slice(0, streamBuf.length - TAG_LEN)));
165
- streamBuf = streamBuf.slice(-TAG_LEN);
166
- }
167
- },
168
- flush(controller) {
169
- if (streamBuf) controller.enqueue(encoder.encode(streamBuf));
170
- }
171
- });
164
+ const streamingClientScriptInjector = injectBeforeStream('</body></html>', clientScriptTag);
172
165
  const webStream = typeof Readable.toWeb === 'function'
173
166
  ? Readable.toWeb(passThrough)
174
167
  : new ReadableStream({
@@ -207,30 +200,7 @@ export async function renderHTML(firstArg, options = {}) {
207
200
  controller.close();
208
201
  }
209
202
  });
210
- let clientBuffer = "";
211
- let clientScriptInjected = false;
212
- const injectClientScriptStream = new TransformStream({
213
- transform(chunk, controller) {
214
- const str = typeof chunk === "string" ? chunk : new TextDecoder().decode(chunk);
215
- if (!injectClientScriptInStream || clientScriptInjected) {
216
- controller.enqueue(chunk);
217
- return;
218
- }
219
- clientBuffer += str;
220
- if (clientBuffer.includes("</body></html>")) {
221
- const href = (typeof CLIENT_ENTRY_HREF === "string" && CLIENT_ENTRY_HREF.length > 0 && CLIENT_ENTRY_HREF.indexOf(PLACEHOLDER_STR) < 0) ? CLIENT_ENTRY_HREF : "";
222
- const out = href ? clientBuffer.replace("</body></html>", '<script type="module" src="' + href + '"></script></body></html>') : clientBuffer;
223
- controller.enqueue(new TextEncoder().encode(out));
224
- clientBuffer = "";
225
- clientScriptInjected = true;
226
- }
227
- },
228
- flush(controller) {
229
- if (clientBuffer) {
230
- controller.enqueue(new TextEncoder().encode(clientBuffer));
231
- }
232
- }
233
- });
203
+ const injectClientScriptStream = injectBeforeStream('</body></html>', clientScriptTag);
234
204
 
235
205
  let out = streamFromHtml.pipeThrough(injectRSCPayload(s2));
236
206
  out = out.pipeThrough(injectClientScriptStream);
@@ -24,6 +24,8 @@
24
24
  import { type Plugin, type UserConfig } from "vite";
25
25
  import type { FarmRscPluginOptions, EntryContext } from "./types.js";
26
26
  import type { FarmServerActionsConfig } from "@farm.js/core/server-action-security";
27
+ import type { FarmAPIConfig } from "@farm.js/core/api";
28
+ import type { FarmServerConfig } from "@farm.js/core/internal/production-runtime";
27
29
  import type { FarmLayerEntry, ResolvedFarmLayer } from "@farm.js/core/server";
28
30
  export type { FarmRscPluginOptions, EntryContext };
29
31
  export { buildRscNitro, waitForRscManifest, waitForRscOutputs } from "./nitro-build.js";
@@ -36,13 +38,24 @@ export interface FarmRscConfig {
36
38
  layers?: readonly ResolvedFarmLayer[];
37
39
  outDir?: string;
38
40
  basePath?: string;
41
+ api?: FarmAPIConfig;
42
+ server?: FarmServerConfig;
39
43
  port?: number;
40
44
  experimental?: {
45
+ /**
46
+ * Enable React Server Components rendering.
47
+ * @default true
48
+ */
41
49
  serverComponents?: boolean;
50
+ /**
51
+ * Enable React Server Actions and their public POST decoder.
52
+ * @default false
53
+ */
42
54
  serverActions?: boolean;
43
55
  /**
44
- * Enable server-only optimized boundaries through
45
- * `@farm.js/plugin/rsc/optimized-boundary`.
56
+ * Automatically optimize eligible server-only host-element subtrees with
57
+ * the native Strata renderer. Unsupported trees keep normal React
58
+ * rendering; application components do not need a boundary import.
46
59
  *
47
60
  * @experimental
48
61
  * @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;AA0B9E,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,gBAAgB,CAAC,EAAE,OAAO,CAAC;QAC3B,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,CAqnC5E"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/rsc/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAIL,KAAK,MAAM,EACX,KAAK,UAAU,EAChB,MAAM,MAAM,CAAC;AAEd,OAAO,KAAK,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AACrE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,sCAAsC,CAAC;AACpF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2CAA2C,CAAC;AAClF,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAwD9E,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,GAAG,CAAC,EAAE,aAAa,CAAC;IACpB,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,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,CAuDnE;AA8TD;;;;;GAKG;AACH,MAAM,CAAC,OAAO,UAAU,OAAO,CAAC,OAAO,GAAE,oBAAyB,GAAG,MAAM,EAAE,CAgvC5E"}
package/dist/rsc/index.js CHANGED
@@ -21,24 +21,35 @@
21
21
  * })
22
22
  * ```
23
23
  */
24
- import { parseAst } from "vite";
24
+ import { parseAst, version as viteVersion, } from "vite";
25
25
  import { init as initModuleLexer, parse as parseModuleImports } from "es-module-lexer";
26
26
  import { farmEnvironmentFunctionsPlugin } from "@farm.js/core/environment/vite";
27
+ import { generateClientCachePersistenceCode, resolveFarmClientCacheAdapterEntry, } from "@farm.js/core/vite";
27
28
  import { generateRscEntry } from "./entries/rsc.js";
28
29
  import { generateSsrEntry } from "./entries/ssr.js";
29
- import { generateClientEntry } from "./entries/client.js";
30
+ import { generateErrorFallbackEntry } from "./entries/error-fallback.js";
31
+ import { generateClientEntry, serverFnTransportErrorClientRuntime } from "./entries/client.js";
32
+ import { transformFarmRouteActionClients } from "./route-action-transform.js";
30
33
  import { transformFarmServerFns } from "./server-fn-transform.js";
34
+ import { transformAutomaticOptimizedBoundaries } from "./automatic-optimized-boundary.js";
31
35
  import { resolveRscBuildOutputPath } from "./build-paths.js";
36
+ import { assertRscPackageCompatibility } from "./compatibility.js";
37
+ import { sendRscDevelopmentResponse } from "./dev-response.js";
32
38
  import fs from "fs/promises";
33
39
  import path from "path";
40
+ import { devServableFileExists } from "../dev-static.js";
34
41
  import { pathToFileURL } from "node:url";
35
42
  import { createRequire } from "node:module";
36
43
  // Use API and middleware plugins from @farm.js/core (require so CJS build resolves when ESM .mjs is missing)
37
44
  const require_ = createRequire(import.meta.url);
38
45
  const { farmApiPlugin, farmMiddlewarePlugin } = require_("@farm.js/core");
39
- const { resolveServerActionsConfig } = require_("@farm.js/core/server-action-security");
46
+ const { resolveServerActionsConfig, validateServerActionRequest, createServerActionRequestErrorResponse, } = require_("@farm.js/core/server-action-security");
40
47
  const { normalizeFarmDeploymentId } = require_("@farm.js/core/deployment");
41
48
  const { getFarmLayerAliases, getFarmSourceRoots, resolveFarmLayers } = require_("@farm.js/core/server");
49
+ const { searchParamsToObject, readNodeRequestBody, resolveFarmServerConfig, createFarmRequestBodyErrorResponse, } = require_("@farm.js/core/internal/production-runtime");
50
+ const { isFarmAPIPathname } = require_("@farm.js/core/api/runtime");
51
+ const { resolveFarmAPIConfig, resolveFarmAPIServerBasePath } = require_("@farm.js/core/api");
52
+ const { getResolvedEnv } = require_("@farm.js/core/env");
42
53
  export { buildRscNitro, waitForRscManifest, waitForRscOutputs } from "./nitro-build.js";
43
54
  import { registerRscNitroRuntimePackage } from "./nitro-build.js";
44
55
  export { default as nitro } from "./vite-plugin-nitro.js";
@@ -53,7 +64,7 @@ export function defineConfig(config = {}) {
53
64
  // Core Farm RSC settings
54
65
  experimental: {
55
66
  serverComponents: config.experimental?.serverComponents ?? true,
56
- serverActions: config.experimental?.serverActions ?? true,
67
+ serverActions: config.experimental?.serverActions ?? false,
57
68
  optimizedBoundary: config.experimental?.optimizedBoundary ?? false,
58
69
  },
59
70
  srcDir: config.srcDir ?? "src",
@@ -61,11 +72,13 @@ export function defineConfig(config = {}) {
61
72
  layers: config.layers,
62
73
  outDir: config.outDir ?? "dist",
63
74
  basePath: config.basePath ?? "/",
75
+ api: config.api,
64
76
  serverActions: config.serverActions,
65
77
  deploymentId: config.deploymentId,
66
78
  generateBuildId: config.generateBuildId,
67
79
  // Vite server configuration
68
80
  server: {
81
+ ...config.server,
69
82
  port,
70
83
  strictPort: false,
71
84
  },
@@ -78,7 +91,11 @@ export function defineConfig(config = {}) {
78
91
  },
79
92
  plugins: [
80
93
  farmMiddlewarePlugin({ srcDir: config.srcDir ?? "src", debug }),
81
- farmApiPlugin({ srcDir: config.srcDir ?? "src", debug }),
94
+ farmApiPlugin({
95
+ srcDir: config.srcDir ?? "src",
96
+ debug,
97
+ bodySizeLimit: config.server?.bodySizeLimit,
98
+ }),
82
99
  farmRsc({
83
100
  debug,
84
101
  encryptActions: config.encryptActions,
@@ -370,13 +387,42 @@ export default function farmRsc(options = {}) {
370
387
  let rscEnabled = false;
371
388
  let actionsEnabled = false;
372
389
  let optimizedBoundaryEnabled = false;
390
+ let bodySizeLimit = resolveFarmServerConfig(undefined).bodySizeLimit;
373
391
  // Context passed to entry generators
374
392
  let entryContext;
375
- /** Set in config when RSC enabled; used by build plugin to run Nitro after client writeBundle. */
376
393
  let rscBuildRoot;
377
394
  // Store for debugging
378
395
  const debug = options.debug ?? false;
379
396
  const automaticDeploymentId = `build-${Date.now()}`;
397
+ const supportsBuildAppHook = Number(viteVersion.split(".")[0]) >= 7;
398
+ const runNitroAfterApplicationBuild = async (builder) => {
399
+ const nitroEnabled = builder.config?.plugins?.some((plugin) => plugin.name === "vite-plugin-nitro");
400
+ if (!nitroEnabled || !rscBuildRoot)
401
+ return;
402
+ const { runNitroFromBuildApp } = await import("./vite-plugin-nitro.js");
403
+ if (globalThis.__FARM_NITRO_PATHS) {
404
+ await runNitroFromBuildApp();
405
+ return;
406
+ }
407
+ // Vite creates environment-specific plugin instances by default, so state
408
+ // captured by a Rollup hook is not guaranteed to be visible here. Resolve
409
+ // the final environment outputs directly when that state is unavailable.
410
+ const rscBuild = builder.environments.rsc?.config?.build;
411
+ const ssrBuild = builder.environments.ssr?.config?.build;
412
+ const clientBuild = builder.environments.client?.config?.build;
413
+ if (!rscBuild?.outDir || !ssrBuild?.outDir || !clientBuild?.outDir)
414
+ return;
415
+ const { buildRscNitro } = await import("./nitro-build.js");
416
+ const root = path.resolve(rscBuildRoot);
417
+ await buildRscNitro({
418
+ root,
419
+ rendererPath: resolveRscBuildOutputPath(root, rscBuild.outDir, "index.js"),
420
+ publicDir: resolveRscBuildOutputPath(root, clientBuild.outDir),
421
+ ssrPath: resolveRscBuildOutputPath(root, ssrBuild.outDir, "index.js"),
422
+ assetsDir: clientBuild.assetsDir,
423
+ preset: process.env.NITRO_PRESET || "vercel",
424
+ });
425
+ };
380
426
  const getColors = () => {
381
427
  try {
382
428
  const pico = require_("picocolors");
@@ -469,6 +515,7 @@ export default function farmRsc(options = {}) {
469
515
  // Check if user enabled RSC in their config
470
516
  rscEnabled = c.experimental?.serverComponents === true;
471
517
  actionsEnabled = c.experimental?.serverActions === true;
518
+ bodySizeLimit = resolveFarmServerConfig(c.server).bodySizeLimit;
472
519
  optimizedBoundaryEnabled = c.experimental?.optimizedBoundary === true;
473
520
  logInfo(`RSC enabled: ${rscEnabled}`);
474
521
  logInfo(`Actions enabled: ${actionsEnabled}`);
@@ -481,6 +528,7 @@ export default function farmRsc(options = {}) {
481
528
  return;
482
529
  }
483
530
  const root = c.root ?? process.cwd();
531
+ assertRscPackageCompatibility(root);
484
532
  if (optimizedBoundaryEnabled) {
485
533
  registerRscNitroRuntimePackage(root, STRATA_PACKAGE, resolveOptimizedBoundaryRuntimeRoot());
486
534
  }
@@ -492,6 +540,11 @@ export default function farmRsc(options = {}) {
492
540
  Object.assign(c, layerResolution.config);
493
541
  }
494
542
  // Read user's directory configuration
543
+ const api = await resolveFarmAPIConfig(c.api, {
544
+ root,
545
+ mode: env.command === "build" ? "production" : "development",
546
+ env: getResolvedEnv(),
547
+ });
495
548
  const srcDir = c.srcDir ?? "src";
496
549
  const outDir = c.outDir ?? "dist";
497
550
  const deploymentId = normalizeFarmDeploymentId(options.deploymentId ||
@@ -532,6 +585,7 @@ export default function farmRsc(options = {}) {
532
585
  srcDir,
533
586
  outDir,
534
587
  basePath: c.basePath ?? "/",
588
+ apiBasePath: resolveFarmAPIServerBasePath(api),
535
589
  routesDir: options.routesDir,
536
590
  globalCssPath,
537
591
  routeRoots,
@@ -543,6 +597,8 @@ export default function farmRsc(options = {}) {
543
597
  }),
544
598
  deploymentId,
545
599
  debug,
600
+ development: env.command === "serve",
601
+ clientCachePersistence: generateClientCachePersistenceCode(resolveFarmClientCacheAdapterEntry(root, c.cache)),
546
602
  };
547
603
  logInfo(`srcDir: ${entryContext.srcDir}, outDir: ${entryContext.outDir}`);
548
604
  // Write real entry files so @vitejs/plugin-rsc can use file-based entries.
@@ -553,6 +609,7 @@ export default function farmRsc(options = {}) {
553
609
  await fs.writeFile(entryRscPath, generateRscEntry(entryContext));
554
610
  await fs.writeFile(entrySsrPath, generateSsrEntry(entryContext));
555
611
  await fs.writeFile(entryClientPath, generateClientEntry(entryContext));
612
+ await fs.writeFile(path.join(entriesDir, "error-fallback.tsx"), generateErrorFallbackEntry());
556
613
  logInfo(`Wrote RSC entries to ${entriesDir}`);
557
614
  // User must add rsc({ entries: { rsc, ssr, client } }) to config.plugins so the RSC plugin
558
615
  // runs for every environment and client build can resolve virtual:vite-rsc/client-references.
@@ -579,6 +636,9 @@ export default function farmRsc(options = {}) {
579
636
  return {
580
637
  appType: "custom",
581
638
  builder: { sharedConfigBuild: true },
639
+ define: {
640
+ __FARM_API_BASE_URL__: JSON.stringify(api.baseURL),
641
+ },
582
642
  ssr: {
583
643
  external: [
584
644
  "react",
@@ -599,7 +659,7 @@ export default function farmRsc(options = {}) {
599
659
  }
600
660
  : {}),
601
661
  resolve: {
602
- dedupe: ["react", "react-dom"],
662
+ dedupe: ["react", "react-dom", "react-server-dom-webpack"],
603
663
  alias: [
604
664
  ...Object.entries(layerAliases).map(([find, replacement]) => ({
605
665
  find,
@@ -708,6 +768,42 @@ export default function farmRsc(options = {}) {
708
768
  return null;
709
769
  },
710
770
  },
771
+ {
772
+ name: "@farm.js/plugin/rsc:automatic-optimized-boundary",
773
+ enforce: "post",
774
+ transform(code, id) {
775
+ if (!rscEnabled || !optimizedBoundaryEnabled)
776
+ return null;
777
+ const environmentName = this.environment?.name;
778
+ if (environmentName !== "rsc")
779
+ return null;
780
+ const result = transformAutomaticOptimizedBoundaries(code, id);
781
+ if (!result)
782
+ return null;
783
+ if (debug) {
784
+ console.log(`[Farm.js] Added ${result.boundaryCount} automatic optimized-boundary candidate${result.boundaryCount === 1 ? "" : "s"} in ${id}`);
785
+ }
786
+ return { code: result.code, map: null };
787
+ },
788
+ },
789
+ {
790
+ name: "@farm.js/plugin/rsc:route-action-clients",
791
+ enforce: "pre",
792
+ transform(code, id) {
793
+ if (!rscEnabled || !actionsEnabled)
794
+ return null;
795
+ const environmentName = this.environment?.name;
796
+ if (environmentName !== "client")
797
+ return null;
798
+ const result = transformFarmRouteActionClients(code, id);
799
+ if (!result)
800
+ return null;
801
+ return {
802
+ code: result.code,
803
+ map: null,
804
+ };
805
+ },
806
+ },
711
807
  {
712
808
  name: "@farm.js/plugin/rsc:server-fn-actions",
713
809
  enforce: "pre",
@@ -723,38 +819,38 @@ export default function farmRsc(options = {}) {
723
819
  };
724
820
  },
725
821
  },
726
- // Nitro: run after all environments (like @hiogawa/vite-plugin-nitro). If the runtime supports
727
- // plugin buildApp order "post", this runs automatically; else use build script (see comment below).
728
822
  {
729
- name: "@farm.js/plugin/rsc:nitro-build",
823
+ name: "@farm.js/plugin/rsc:nitro-application-build",
824
+ enforce: "post",
730
825
  apply: "build",
826
+ // @vitejs/plugin-rsc owns the Vite 6 builder callback. Wrap its final
827
+ // callback after config composition so Nitro starts only after the RSC,
828
+ // client, and SSR outputs have all been written.
829
+ config(config) {
830
+ const builderConfig = config.builder;
831
+ const buildApp = builderConfig?.buildApp;
832
+ if (typeof buildApp !== "function")
833
+ return;
834
+ return {
835
+ builder: {
836
+ ...builderConfig,
837
+ async buildApp(builder) {
838
+ await buildApp(builder);
839
+ if (!supportsBuildAppHook) {
840
+ await runNitroAfterApplicationBuild(builder);
841
+ }
842
+ },
843
+ },
844
+ };
845
+ },
846
+ // Vite 7+ composes application build hooks directly. Keep the Vite 6
847
+ // wrapper above for the versions where this hook is not dispatched.
731
848
  buildApp: {
732
849
  order: "post",
733
- handler: async (builder) => {
734
- if (!rscEnabled || !rscBuildRoot || !entryContext)
735
- return;
736
- if (globalThis.__FARM_NITRO_PLUGIN_RAN)
737
- return;
738
- const root = path.resolve(rscBuildRoot);
739
- if (globalThis.__FARM_NITRO_PATHS) {
740
- const { runNitroFromBuildApp } = await import("./vite-plugin-nitro.js");
741
- await runNitroFromBuildApp();
742
- return;
850
+ async handler(builder) {
851
+ if (supportsBuildAppHook) {
852
+ await runNitroAfterApplicationBuild(builder);
743
853
  }
744
- const rscEnv = builder.environments?.rsc;
745
- const ssrEnv = builder.environments?.ssr;
746
- const clientEnv = builder.environments?.client;
747
- if (!rscEnv || !ssrEnv || !clientEnv)
748
- return;
749
- const { buildRscNitro } = await import("./nitro-build.js");
750
- await buildRscNitro({
751
- root,
752
- rendererPath: resolveRscBuildOutputPath(root, rscEnv.config.build.outDir, "index.js"),
753
- publicDir: resolveRscBuildOutputPath(root, clientEnv.config.build.outDir),
754
- ssrPath: resolveRscBuildOutputPath(root, ssrEnv.config.build.outDir, "index.js"),
755
- assetsDir: clientEnv.config.build.assetsDir,
756
- preset: process.env.NITRO_PRESET || "vercel",
757
- });
758
854
  },
759
855
  },
760
856
  },
@@ -827,6 +923,7 @@ import {
827
923
  createFarmDeploymentRequestHeaders,
828
924
  isFarmDeploymentMismatchResponse,
829
925
  } from '@farm.js/core/deployment';
926
+ ${serverFnTransportErrorClientRuntime}
830
927
  const farmDeploymentId = ${JSON.stringify(entryContext.deploymentId)};
831
928
  setServerCallback(async (id, args) => {
832
929
  const refs = createTemporaryReferenceSet();
@@ -853,9 +950,7 @@ setServerCallback(async (id, args) => {
853
950
  if (!res.ok) throw new Error('Server action failed: ' + res.status);
854
951
  const p = await createFromReadableStream(res.body, { temporaryReferences: refs });
855
952
  if (p?.returnValue?.ok) return p.returnValue.data;
856
- const error = new Error(p?.returnValue?.data?.message || 'Server function failed');
857
- error.name = 'ServerActionError';
858
- throw error;
953
+ throw createFarmServerFnTransportError(p?.returnValue?.data);
859
954
  });
860
955
  `
861
956
  : "";
@@ -923,12 +1018,16 @@ if (document.readyState === 'loading') {
923
1018
  const url = req.url || "/";
924
1019
  const pathname = url.split("?")[0];
925
1020
  const method = req.method || "GET";
1021
+ // Dotted paths only skip RSC rendering when they map to a real
1022
+ // file on disk — route segments may legitimately contain dots.
926
1023
  if (pathname.startsWith("/@") ||
927
1024
  pathname.startsWith("/__") ||
928
1025
  pathname.startsWith("/node_modules") ||
929
1026
  pathname.startsWith("/src/") ||
930
1027
  pathname.startsWith("/api/") ||
931
- (pathname.includes(".") && !pathname.endsWith("/"))) {
1028
+ (pathname.includes(".") &&
1029
+ !pathname.endsWith("/") &&
1030
+ devServableFileExists(pathname, [server.config.publicDir, server.config.root]))) {
932
1031
  return next();
933
1032
  }
934
1033
  const startTime = Date.now();
@@ -955,19 +1054,26 @@ if (document.readyState === 'loading') {
955
1054
  globalThis.__FARM_VITE_RSC_LOAD_BOOTSTRAP__ = async () => bootstrapPrefix +
956
1055
  `import("/@react-refresh").then(m=>{m.default.injectIntoGlobalHook(window);window.$RefreshReg$=()=>{};window.$RefreshSig$=()=>type=>type;window.__vite_plugin_react_preamble_installed__=true;return import("/@vite/client");}).then(()=>import(${JSON.stringify(clientEntryUrl)}));`;
957
1056
  const base = `http://${req.headers.host || "localhost:3000"}`;
1057
+ const requestUrl = new URL(url, base);
1058
+ const isAction = actionsEnabled &&
1059
+ method === "POST" &&
1060
+ !isFarmAPIPathname(requestUrl.pathname, entryContext.apiBasePath) &&
1061
+ !isFarmAPIPathname(requestUrl.pathname);
1062
+ if (isAction) {
1063
+ validateServerActionRequest(new Request(requestUrl, {
1064
+ method,
1065
+ headers: req.headers,
1066
+ }), entryContext.serverActions);
1067
+ }
958
1068
  let body;
959
- if (method === "POST") {
960
- const chunks = [];
961
- for await (const chunk of req)
962
- chunks.push(chunk);
963
- body = Buffer.concat(chunks);
964
- // Keep as buffer so request.formData() / request.text() in RSC handler work (multipart must not be UTF-8 decoded)
1069
+ if (method !== "GET" && method !== "HEAD") {
1070
+ body = await readNodeRequestBody(req, isAction ? entryContext.serverActions.bodySizeLimit : bodySizeLimit);
965
1071
  }
966
- const request = new Request(new URL(url, base), {
1072
+ const request = new Request(requestUrl, {
967
1073
  method,
968
1074
  headers: req.headers,
969
- body: method === "POST" && body && body.length > 0
970
- ? body
1075
+ body: body && body.length > 0
1076
+ ? body.buffer.slice(body.byteOffset, body.byteOffset + body.byteLength)
971
1077
  : undefined,
972
1078
  });
973
1079
  // Load the RSC entry in the "rsc" environment so @vitejs/plugin-rsc transforms run with this.environment.name === "rsc".
@@ -1001,32 +1107,24 @@ if (document.readyState === 'loading') {
1001
1107
  if (!rscEntry?.default?.fetch)
1002
1108
  throw new Error("[Farm.js] Could not load RSC entry in rsc environment");
1003
1109
  const response = await rscEntry.default.fetch(request);
1004
- res.statusCode = response.status;
1005
- response.headers.forEach((value, key) => {
1006
- if (key.toLowerCase() !== "transfer-encoding")
1007
- res.setHeader(key, value);
1008
- });
1009
- if (response.body) {
1010
- const reader = response.body.getReader();
1011
- const pump = async () => {
1012
- while (true) {
1013
- const { done, value } = await reader.read();
1014
- if (done)
1015
- break;
1016
- res.write(Buffer.from(value));
1017
- }
1018
- res.end();
1019
- };
1020
- await pump();
1021
- }
1022
- else {
1023
- res.end();
1024
- }
1110
+ await sendRscDevelopmentResponse(res, response);
1025
1111
  const duration = Date.now() - startTime;
1026
1112
  logResponse(method, pathname, response.status, duration);
1027
1113
  return;
1028
1114
  }
1029
1115
  catch (rscError) {
1116
+ const rejection = createFarmRequestBodyErrorResponse(rscError) ??
1117
+ createServerActionRequestErrorResponse(rscError);
1118
+ if (rejection && !res.headersSent && !res.writableEnded && !res.destroyed) {
1119
+ await sendRscDevelopmentResponse(res, rejection);
1120
+ return;
1121
+ }
1122
+ if (res.headersSent || res.writableEnded || res.destroyed) {
1123
+ console.error("[Farm.js] RSC dev response error:", rscError);
1124
+ if (!res.destroyed)
1125
+ res.destroy(rscError instanceof Error ? rscError : new Error(String(rscError)));
1126
+ return;
1127
+ }
1030
1128
  // RSC pipeline failed; fall back to legacy SSR for GET only
1031
1129
  if (method !== "GET") {
1032
1130
  const duration = Date.now() - startTime;
@@ -1110,7 +1208,7 @@ if (document.readyState === 'loading') {
1110
1208
  const Layout = layoutModule?.default;
1111
1209
  // Parse URL params (basic dynamic route support)
1112
1210
  const params = {};
1113
- const searchParams = Object.fromEntries(new URLSearchParams(url.split("?")[1] || ""));
1211
+ const searchParams = searchParamsToObject(new URL(url, `http://${req.headers.host || "localhost:3000"}`).searchParams);
1114
1212
  // Render the page with middleware data
1115
1213
  const pageProps = {
1116
1214
  params,
@@ -1 +1 @@
1
- {"version":3,"file":"nitro-build.d.ts","sourceRoot":"","sources":["../../src/rsc/nitro-build.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAwBH;;;GAGG;AACH,wBAAgB,8BAA8B,CAC5C,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,GAClB,IAAI,CAKN;AAwFD;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GACxD,OAAO,CAAC,IAAI,CAAC,CA0Bf;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GACxD,OAAO,CAAC,IAAI,CAAC,CAgCf;AA+DD,MAAM,MAAM,oBAAoB,GAAG;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,wBAAsB,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CAqMhF"}
1
+ {"version":3,"file":"nitro-build.d.ts","sourceRoot":"","sources":["../../src/rsc/nitro-build.ts"],"names":[],"mappings":"AAAA;;;;GAIG;AAwBH;;;GAGG;AACH,wBAAgB,8BAA8B,CAC5C,IAAI,EAAE,MAAM,EACZ,WAAW,EAAE,MAAM,EACnB,WAAW,EAAE,MAAM,GAClB,IAAI,CAKN;AAwFD;;;GAGG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GACxD,OAAO,CAAC,IAAI,CAAC,CA0Bf;AAED;;GAEG;AACH,wBAAgB,iBAAiB,CAC/B,IAAI,EAAE,MAAM,EACZ,MAAM,EAAE,MAAM,EACd,OAAO,GAAE;IAAE,UAAU,CAAC,EAAE,MAAM,CAAC;IAAC,SAAS,CAAC,EAAE,MAAM,CAAA;CAAO,GACxD,OAAO,CAAC,IAAI,CAAC,CAgCf;AA0ED,MAAM,MAAM,oBAAoB,GAAG;IACjC,YAAY,EAAE,MAAM,CAAC;IACrB,SAAS,EAAE,MAAM,CAAC;IAClB,OAAO,CAAC,EAAE,MAAM,CAAC;IACjB,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,IAAI,EAAE,MAAM,CAAC;IACb,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,MAAM,CAAC,EAAE,MAAM,CAAC;CACjB,CAAC;AAEF,wBAAsB,aAAa,CAAC,OAAO,EAAE,oBAAoB,GAAG,OAAO,CAAC,IAAI,CAAC,CA+MhF"}