@farm.js/plugin 0.1.0-beta.10 → 0.1.0-beta.13

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 (52) hide show
  1. package/dist/api/index.d.ts +42 -0
  2. package/dist/api/index.d.ts.map +1 -0
  3. package/dist/api/index.js +493 -0
  4. package/dist/context/index.d.ts +61 -0
  5. package/dist/context/index.d.ts.map +1 -0
  6. package/dist/context/index.js +75 -0
  7. package/dist/index.d.ts +43 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +49 -0
  10. package/dist/middleware/index.d.ts +97 -0
  11. package/dist/middleware/index.d.ts.map +1 -0
  12. package/dist/middleware/index.js +469 -0
  13. package/dist/observability/index.d.ts +190 -0
  14. package/dist/observability/index.d.ts.map +1 -0
  15. package/dist/observability/index.js +399 -0
  16. package/dist/rsc/automatic-optimized-boundary.d.ts +11 -0
  17. package/dist/rsc/automatic-optimized-boundary.d.ts.map +1 -0
  18. package/dist/rsc/automatic-optimized-boundary.js +162 -0
  19. package/dist/rsc/build-paths.d.ts +3 -0
  20. package/dist/rsc/build-paths.d.ts.map +1 -0
  21. package/dist/rsc/build-paths.js +8 -0
  22. package/dist/rsc/compatibility.d.ts +8 -0
  23. package/dist/rsc/compatibility.d.ts.map +1 -0
  24. package/dist/rsc/compatibility.js +46 -0
  25. package/dist/rsc/entries/client.d.ts +14 -0
  26. package/dist/rsc/entries/client.d.ts.map +1 -0
  27. package/dist/rsc/entries/client.js +283 -0
  28. package/dist/rsc/entries/rsc.d.ts +13 -0
  29. package/dist/rsc/entries/rsc.d.ts.map +1 -0
  30. package/dist/rsc/entries/rsc.js +932 -0
  31. package/dist/rsc/entries/ssr.d.ts +13 -0
  32. package/dist/rsc/entries/ssr.d.ts.map +1 -0
  33. package/dist/rsc/entries/ssr.js +245 -0
  34. package/dist/rsc/index.d.ts +87 -0
  35. package/dist/rsc/index.d.ts.map +1 -0
  36. package/dist/rsc/index.js +1389 -0
  37. package/dist/rsc/nitro-build.d.ts +36 -0
  38. package/dist/rsc/nitro-build.d.ts.map +1 -0
  39. package/dist/rsc/nitro-build.js +396 -0
  40. package/dist/rsc/optimized-boundary.d.ts +29 -0
  41. package/dist/rsc/optimized-boundary.d.ts.map +1 -0
  42. package/dist/rsc/optimized-boundary.js +243 -0
  43. package/dist/rsc/server-fn-transform.d.ts +6 -0
  44. package/dist/rsc/server-fn-transform.d.ts.map +1 -0
  45. package/dist/rsc/server-fn-transform.js +152 -0
  46. package/dist/rsc/types.d.ts +123 -0
  47. package/dist/rsc/types.d.ts.map +1 -0
  48. package/dist/rsc/types.js +1 -0
  49. package/dist/rsc/vite-plugin-nitro.d.ts +33 -0
  50. package/dist/rsc/vite-plugin-nitro.d.ts.map +1 -0
  51. package/dist/rsc/vite-plugin-nitro.js +163 -0
  52. package/package.json +3 -3
@@ -0,0 +1,1389 @@
1
+ /**
2
+ * Farm.js RSC Plugin
3
+ *
4
+ * Provides React Server Components support for Farm.js.
5
+ * When enabled, this plugin:
6
+ * - Configures three build environments (rsc, ssr, client)
7
+ * - Generates virtual entry files for routing, rendering, and hydration
8
+ * - Supports server actions when experimental.serverActions is true
9
+ * - Integrates with @vitejs/plugin-rsc for core RSC transforms
10
+ *
11
+ * @example
12
+ * ```ts
13
+ * import { defineConfig } from '@farm.js/plugin/rsc'
14
+ *
15
+ * export default defineConfig({
16
+ * srcDir: 'src',
17
+ * experimental: {
18
+ * serverComponents: true,
19
+ * serverActions: true,
20
+ * },
21
+ * })
22
+ * ```
23
+ */
24
+ import { parseAst } from "vite";
25
+ import { init as initModuleLexer, parse as parseModuleImports } from "es-module-lexer";
26
+ import { farmEnvironmentFunctionsPlugin } from "@farm.js/core/environment/vite";
27
+ import { generateRscEntry } from "./entries/rsc.js";
28
+ import { generateSsrEntry } from "./entries/ssr.js";
29
+ import { generateClientEntry } from "./entries/client.js";
30
+ import { transformFarmServerFns } from "./server-fn-transform.js";
31
+ import { transformAutomaticOptimizedBoundaries } from "./automatic-optimized-boundary.js";
32
+ import { resolveRscBuildOutputPath } from "./build-paths.js";
33
+ import { assertRscPackageCompatibility } from "./compatibility.js";
34
+ import fs from "fs/promises";
35
+ import path from "path";
36
+ import { pathToFileURL } from "node:url";
37
+ import { createRequire } from "node:module";
38
+ // Use API and middleware plugins from @farm.js/core (require so CJS build resolves when ESM .mjs is missing)
39
+ const require_ = createRequire(import.meta.url);
40
+ const { farmApiPlugin, farmMiddlewarePlugin } = require_("@farm.js/core");
41
+ const { resolveServerActionsConfig } = require_("@farm.js/core/server-action-security");
42
+ const { normalizeFarmDeploymentId } = require_("@farm.js/core/deployment");
43
+ const { getFarmLayerAliases, getFarmSourceRoots, resolveFarmLayers } = require_("@farm.js/core/server");
44
+ export { buildRscNitro, waitForRscManifest, waitForRscOutputs } from "./nitro-build.js";
45
+ import { registerRscNitroRuntimePackage } from "./nitro-build.js";
46
+ export { default as nitro } from "./vite-plugin-nitro.js";
47
+ /**
48
+ * Define a Farm.js RSC configuration
49
+ * This is the recommended way to configure your RSC app
50
+ */
51
+ export function defineConfig(config = {}) {
52
+ const port = config.port ?? 3000;
53
+ const debug = config.debug ?? false;
54
+ return {
55
+ // Core Farm RSC settings
56
+ experimental: {
57
+ serverComponents: config.experimental?.serverComponents ?? true,
58
+ serverActions: config.experimental?.serverActions ?? false,
59
+ optimizedBoundary: config.experimental?.optimizedBoundary ?? false,
60
+ },
61
+ srcDir: config.srcDir ?? "src",
62
+ extends: config.extends,
63
+ layers: config.layers,
64
+ outDir: config.outDir ?? "dist",
65
+ basePath: config.basePath ?? "/",
66
+ serverActions: config.serverActions,
67
+ deploymentId: config.deploymentId,
68
+ generateBuildId: config.generateBuildId,
69
+ // Vite server configuration
70
+ server: {
71
+ port,
72
+ strictPort: false,
73
+ },
74
+ // Custom logger to hide Vite's default startup banner
75
+ customLogger: createFarmLogger(port, debug),
76
+ // Configure esbuild for JSX transformation
77
+ esbuild: {
78
+ jsx: "automatic",
79
+ jsxImportSource: "react",
80
+ },
81
+ plugins: [
82
+ farmMiddlewarePlugin({ srcDir: config.srcDir ?? "src", debug }),
83
+ farmApiPlugin({ srcDir: config.srcDir ?? "src", debug }),
84
+ farmRsc({
85
+ debug,
86
+ encryptActions: config.encryptActions,
87
+ serverActions: config.serverActions,
88
+ deploymentId: config.deploymentId,
89
+ routesDir: config.routesDir,
90
+ entries: config.entries,
91
+ }),
92
+ ...(Array.isArray(config.plugins) ? config.plugins : []),
93
+ ],
94
+ };
95
+ }
96
+ /**
97
+ * Create a custom logger that shows Farm.js styled startup messages
98
+ */
99
+ function createFarmLogger(port, debug) {
100
+ const noop = (s) => s;
101
+ let pc;
102
+ try {
103
+ pc = require("picocolors");
104
+ if (typeof pc?.red !== "function")
105
+ pc = null;
106
+ }
107
+ catch {
108
+ pc = null;
109
+ }
110
+ if (!pc) {
111
+ pc = {
112
+ dim: noop,
113
+ bold: noop,
114
+ blue: noop,
115
+ cyan: noop,
116
+ green: noop,
117
+ yellow: noop,
118
+ red: noop,
119
+ gray: noop,
120
+ };
121
+ }
122
+ return {
123
+ hasWarned: false,
124
+ info(msg) {
125
+ // Suppress Vite's startup banner (ready in, Local:, Network:, etc.)
126
+ if (msg.includes("VITE v") ||
127
+ msg.includes("ready in") ||
128
+ msg.includes("Local:") ||
129
+ msg.includes("Network:") ||
130
+ msg.includes("press h")) {
131
+ return;
132
+ }
133
+ // Pass through other messages only in debug mode
134
+ if (debug) {
135
+ console.log(msg);
136
+ }
137
+ },
138
+ warn(msg) {
139
+ this.hasWarned = true;
140
+ const prefix = pc.dim("[") + pc.bold(pc.blue("FARM")) + pc.dim("]");
141
+ console.warn(`${prefix} ${pc.yellow("⚠")} ${msg}`);
142
+ },
143
+ warnOnce(msg) {
144
+ this.warn(msg);
145
+ },
146
+ error(msg) {
147
+ const prefix = pc.dim("[") + pc.bold(pc.blue("FARM")) + pc.dim("]");
148
+ console.error(`${prefix} ${pc.bold(pc.red("✖"))} ${msg}`);
149
+ },
150
+ clearScreen() { },
151
+ hasErrorLogged() {
152
+ return false;
153
+ },
154
+ };
155
+ }
156
+ const VIRTUAL_PREFIX = "\0";
157
+ const VIRTUAL_RSC_ENTRY = "virtual:@farm.js/rsc/entry-rsc";
158
+ const VIRTUAL_SSR_ENTRY = "virtual:@farm.js/rsc/entry-ssr";
159
+ const VIRTUAL_CLIENT_ENTRY = "virtual:@farm.js/rsc/entry-client";
160
+ const VIRTUAL_HYDRATE_ENTRY = "virtual:@farm.js/rsc/hydrate";
161
+ const OPTIMIZED_BOUNDARY_ADAPTER = "@farm.js/plugin/rsc/optimized-boundary";
162
+ const STRATA_PACKAGE = "@farming-labs/strata";
163
+ const FARM_CORE_PACKAGE_ROOT = path.resolve(path.dirname(require_.resolve("@farm.js/core")), "..");
164
+ function isOptimizedBoundaryModule(id) {
165
+ return (id === OPTIMIZED_BOUNDARY_ADAPTER ||
166
+ id === STRATA_PACKAGE ||
167
+ id.startsWith(`${STRATA_PACKAGE}/`));
168
+ }
169
+ function resolveOptimizedBoundaryRuntimeRoot() {
170
+ try {
171
+ return path.dirname(require_.resolve(`${STRATA_PACKAGE}/package.json`));
172
+ }
173
+ catch {
174
+ throw new Error(`[Farm.js] experimental.optimizedBoundary requires the bundled ${STRATA_PACKAGE} runtime. ` +
175
+ "Reinstall @farm.js/plugin for a supported Node platform.");
176
+ }
177
+ }
178
+ // Exact-root imports in RSC application code are rewritten to focused public
179
+ // runtime subpaths. The root barrel itself also exports config/build/plugin
180
+ // surfaces and is intentionally not bundled into standalone request handlers.
181
+ // Keeping this list to published runtime entries prevents Vite, Nitro builders,
182
+ // Rolldown, and native build integrations from entering the production graph.
183
+ const CORE_RUNTIME_SUBPATHS = [
184
+ "integrations",
185
+ "api",
186
+ "query/parsers",
187
+ "query/client",
188
+ "query/server",
189
+ "query",
190
+ "middleware",
191
+ "router",
192
+ "routes",
193
+ "docs",
194
+ "markdown",
195
+ "app-markdown",
196
+ "observability",
197
+ "workflows",
198
+ "cron",
199
+ "env",
200
+ "environment",
201
+ "i18n/server",
202
+ "i18n/client",
203
+ "i18n",
204
+ "server-fn",
205
+ "server-fn/client",
206
+ "server-query",
207
+ "server-query/client",
208
+ "server-action-security",
209
+ "deployment",
210
+ "client",
211
+ "plugin/client",
212
+ "cache",
213
+ "deferred",
214
+ "after",
215
+ "navigation",
216
+ "headers",
217
+ "request",
218
+ "agent-runtime",
219
+ "image",
220
+ "image/server",
221
+ ];
222
+ // These public runtime entries depend on packages that Nitro does not
223
+ // currently trace from the rewritten RSC graph. Rejecting them is preferable
224
+ // to producing an isolated server that builds successfully and then fails on
225
+ // its first request.
226
+ const CORE_UNSUPPORTED_STANDALONE_SUBPATHS = ["storage"];
227
+ const CORE_RUNTIME_EXPORT_OVERRIDES = {
228
+ api: "integrations",
229
+ getCurrentRequest: "request",
230
+ getFarmRedirectError: "navigation",
231
+ isFarmNotFoundError: "navigation",
232
+ isFarmRedirectError: "navigation",
233
+ notFound: "navigation",
234
+ permanentRedirect: "navigation",
235
+ redirect: "navigation",
236
+ usePathname: "navigation",
237
+ useRouter: "client",
238
+ useSearchParams: "navigation",
239
+ };
240
+ let coreRuntimeExportSourcesPromise;
241
+ function collectEsmExportNames(source) {
242
+ const names = new Set();
243
+ const ast = parseAst(source);
244
+ for (const statement of ast.body || []) {
245
+ if (statement.type !== "ExportNamedDeclaration")
246
+ continue;
247
+ for (const specifier of statement.specifiers || []) {
248
+ const exported = specifier.exported;
249
+ const name = exported?.name ?? exported?.value;
250
+ if (typeof name === "string")
251
+ names.add(name);
252
+ }
253
+ }
254
+ return names;
255
+ }
256
+ async function getCoreRuntimeExportSources() {
257
+ if (coreRuntimeExportSourcesPromise)
258
+ return coreRuntimeExportSourcesPromise;
259
+ coreRuntimeExportSourcesPromise = (async () => {
260
+ const manifest = JSON.parse(await fs.readFile(path.join(FARM_CORE_PACKAGE_ROOT, "package.json"), "utf8"));
261
+ const rootEntry = manifest.exports["."]?.import;
262
+ if (!rootEntry)
263
+ throw new Error("@farm.js/core has no ESM root export");
264
+ const rootExports = collectEsmExportNames(await fs.readFile(path.resolve(FARM_CORE_PACKAGE_ROOT, rootEntry), "utf8"));
265
+ const sources = new Map();
266
+ const unsupportedSources = new Map();
267
+ const exportsBySubpath = new Map();
268
+ for (const subpath of CORE_RUNTIME_SUBPATHS) {
269
+ const entry = manifest.exports[`./${subpath}`]?.import;
270
+ if (!entry)
271
+ continue;
272
+ const exportedNames = collectEsmExportNames(await fs.readFile(path.resolve(FARM_CORE_PACKAGE_ROOT, entry), "utf8"));
273
+ exportsBySubpath.set(subpath, exportedNames);
274
+ for (const name of exportedNames) {
275
+ if (rootExports.has(name))
276
+ sources.set(name, subpath);
277
+ }
278
+ }
279
+ for (const subpath of CORE_UNSUPPORTED_STANDALONE_SUBPATHS) {
280
+ const entry = manifest.exports[`./${subpath}`]?.import;
281
+ if (!entry)
282
+ continue;
283
+ const exportedNames = collectEsmExportNames(await fs.readFile(path.resolve(FARM_CORE_PACKAGE_ROOT, entry), "utf8"));
284
+ for (const name of exportedNames) {
285
+ if (rootExports.has(name))
286
+ unsupportedSources.set(name, subpath);
287
+ }
288
+ }
289
+ for (const [name, subpath] of Object.entries(CORE_RUNTIME_EXPORT_OVERRIDES)) {
290
+ if (rootExports.has(name) && exportsBySubpath.get(subpath)?.has(name)) {
291
+ sources.set(name, subpath);
292
+ }
293
+ }
294
+ return { supported: sources, unsupported: unsupportedSources };
295
+ })();
296
+ return coreRuntimeExportSourcesPromise;
297
+ }
298
+ function unsupportedStandaloneSubpathError(subpath, id) {
299
+ return new Error(`[Farm.js] @farm.js/core/${subpath} is not supported in the standalone RSC runtime (${id}). ` +
300
+ "Its external runtime dependencies are not copied into the isolated server output yet. " +
301
+ "Importing it would create a production build that boots but fails when the module is used.");
302
+ }
303
+ const CORE_ROOT_NAMED_IMPORT_RE = /^import\s*\{([\s\S]*?)\}\s*from\s*(["'])@farm.js\/core\2\s*$/;
304
+ async function rewriteCoreRuntimeImports(code, id) {
305
+ if (!code.includes("@farm.js/core"))
306
+ return null;
307
+ await initModuleLexer;
308
+ const [moduleImports] = parseModuleImports(code, id);
309
+ const rootImports = moduleImports.filter((moduleImport) => moduleImport.n === "@farm.js/core" && moduleImport.d === -1);
310
+ if (rootImports.length === 0)
311
+ return null;
312
+ const { supported: exportSources, unsupported: unsupportedSources } = await getCoreRuntimeExportSources();
313
+ const replacements = [];
314
+ for (const rootImport of rootImports) {
315
+ const statement = code.slice(rootImport.ss, rootImport.se);
316
+ if (/^import\s+type\b/.test(statement))
317
+ continue;
318
+ const namedImport = CORE_ROOT_NAMED_IMPORT_RE.exec(statement);
319
+ if (!namedImport) {
320
+ throw new Error(`[Farm.js] Unsupported @farm.js/core import syntax in ${id}. ` +
321
+ "Standalone RSC request modules must use named imports from the root or a supported focused public subpath.");
322
+ }
323
+ const body = namedImport[1];
324
+ const grouped = new Map();
325
+ const specifiers = body
326
+ .replace(/\/\*[\s\S]*?\*\//g, "")
327
+ .replace(/\/\/.*$/gm, "")
328
+ .split(",")
329
+ .map((specifier) => specifier.trim())
330
+ .filter(Boolean);
331
+ for (const specifier of specifiers) {
332
+ if (specifier.startsWith("type "))
333
+ continue;
334
+ const match = /^([A-Za-z_$][\w$]*)(?:\s+as\s+([A-Za-z_$][\w$]*))?$/.exec(specifier);
335
+ if (!match) {
336
+ throw new Error(`[Farm.js] Unsupported @farm.js/core import syntax in ${id}: ${specifier}`);
337
+ }
338
+ const [, imported, local = imported] = match;
339
+ const unsupportedSubpath = unsupportedSources.get(imported);
340
+ if (unsupportedSubpath) {
341
+ throw unsupportedStandaloneSubpathError(unsupportedSubpath, id);
342
+ }
343
+ const subpath = exportSources.get(imported);
344
+ if (!subpath) {
345
+ throw new Error(`[Farm.js] The @farm.js/core root export "${imported}" is not available in the standalone RSC runtime. ` +
346
+ "Config, plugin, Vite, build, code-generation, and framework-bootstrap APIs must stay outside application request modules.");
347
+ }
348
+ const imports = grouped.get(subpath) || [];
349
+ imports.push(imported === local ? imported : `${imported} as ${local}`);
350
+ grouped.set(subpath, imports);
351
+ }
352
+ const replacement = Array.from(grouped, ([subpath, imports]) => `import { ${imports.join(", ")} } from "@farm.js/core/${subpath}";`).join("\n");
353
+ const end = code[rootImport.se] === ";" ? rootImport.se + 1 : rootImport.se;
354
+ replacements.push({ start: rootImport.ss, end, code: replacement });
355
+ }
356
+ if (replacements.length === 0)
357
+ return null;
358
+ let rewritten = code;
359
+ for (const replacement of replacements.sort((left, right) => right.start - left.start)) {
360
+ rewritten =
361
+ rewritten.slice(0, replacement.start) + replacement.code + rewritten.slice(replacement.end);
362
+ }
363
+ return rewritten;
364
+ }
365
+ /**
366
+ * Farm.js RSC Plugin
367
+ *
368
+ * @param options - Plugin configuration options
369
+ * @returns Array of Vite plugins
370
+ */
371
+ export default function farmRsc(options = {}) {
372
+ let rscEnabled = false;
373
+ let actionsEnabled = false;
374
+ let optimizedBoundaryEnabled = false;
375
+ // Context passed to entry generators
376
+ let entryContext;
377
+ /** Set in config when RSC enabled; used by build plugin to run Nitro after client writeBundle. */
378
+ let rscBuildRoot;
379
+ // Store for debugging
380
+ const debug = options.debug ?? false;
381
+ const automaticDeploymentId = `build-${Date.now()}`;
382
+ const getColors = () => {
383
+ try {
384
+ const pico = require_("picocolors");
385
+ // Use createColors(true) to force colors, overriding NO_COLOR env
386
+ if (typeof pico?.createColors === "function") {
387
+ return pico.createColors(true);
388
+ }
389
+ if (typeof pico?.green === "function")
390
+ return pico;
391
+ }
392
+ catch { }
393
+ const id = (s) => s;
394
+ return {
395
+ bold: id,
396
+ green: id,
397
+ dim: id,
398
+ cyan: id,
399
+ red: id,
400
+ yellow: id,
401
+ blue: id,
402
+ white: id,
403
+ gray: id,
404
+ };
405
+ };
406
+ const logResponse = (method, urlPath, status, duration, tag = "PAGE") => {
407
+ const pc = getColors();
408
+ let statusColor = pc.green;
409
+ if (status >= 500)
410
+ statusColor = pc.red;
411
+ else if (status >= 400)
412
+ statusColor = pc.yellow;
413
+ else if (status >= 300)
414
+ statusColor = pc.cyan;
415
+ const log = [
416
+ pc.dim("[") + pc.bold(pc.blue("FARM")) + pc.dim("]"),
417
+ pc.dim("[") + pc.bold(pc.cyan(tag)) + pc.dim("]"),
418
+ pc.dim("[") + pc.bold(pc.white(method.padEnd(3))) + pc.dim("]"),
419
+ pc.gray(urlPath),
420
+ pc.dim("-"),
421
+ statusColor(status.toString()),
422
+ pc.dim(`(${duration}ms)`),
423
+ ].join(" ");
424
+ console.log(log);
425
+ };
426
+ const logInfo = (_message) => { };
427
+ const originalWarn = console.warn;
428
+ console.warn = (...args) => {
429
+ const msg = args[0]?.toString?.() ?? "";
430
+ if (msg.includes("[FARM] ⚠ warning") ||
431
+ msg.includes("registryPath") ||
432
+ msg.includes("farm-registry")) {
433
+ return;
434
+ }
435
+ originalWarn.apply(console, args);
436
+ };
437
+ return [
438
+ farmEnvironmentFunctionsPlugin(),
439
+ {
440
+ name: "@farm.js/plugin/rsc:core-runtime",
441
+ // Run after Vite/esbuild has lowered TS/JSX. es-module-lexer deliberately
442
+ // parses JavaScript module syntax and rejects raw JSX expression text.
443
+ enforce: "post",
444
+ apply: "build",
445
+ async transform(code, id, options) {
446
+ const environmentName = this.environment?.name;
447
+ const isServerEnvironment = options?.ssr || environmentName === "rsc" || environmentName === "ssr";
448
+ if (!isServerEnvironment)
449
+ return null;
450
+ const rewritten = await rewriteCoreRuntimeImports(code, id);
451
+ return rewritten ? { code: rewritten, map: null } : null;
452
+ },
453
+ resolveId(id, _importer, options) {
454
+ const environmentName = this.environment?.name;
455
+ const isServerEnvironment = options?.ssr || environmentName === "rsc" || environmentName === "ssr";
456
+ if (isServerEnvironment && id === "@farm.js/core/storage") {
457
+ throw unsupportedStandaloneSubpathError("storage", _importer || "unknown importer");
458
+ }
459
+ if (isServerEnvironment && id === "@farm.js/core") {
460
+ throw new Error("[Farm.js] Standalone RSC runtime modules must use named @farm.js/core imports. " +
461
+ "Namespace/default imports and build-only root exports cannot be bundled safely; use a focused public subpath.");
462
+ }
463
+ return null;
464
+ },
465
+ },
466
+ {
467
+ name: "@farm.js/plugin/rsc:config",
468
+ enforce: "pre",
469
+ async config(config, env) {
470
+ const c = config;
471
+ // Check if user enabled RSC in their config
472
+ rscEnabled = c.experimental?.serverComponents === true;
473
+ actionsEnabled = c.experimental?.serverActions === true;
474
+ optimizedBoundaryEnabled = c.experimental?.optimizedBoundary === true;
475
+ logInfo(`RSC enabled: ${rscEnabled}`);
476
+ logInfo(`Actions enabled: ${actionsEnabled}`);
477
+ logInfo(`Optimized boundary enabled: ${optimizedBoundaryEnabled}`);
478
+ if (optimizedBoundaryEnabled && !rscEnabled) {
479
+ throw new Error("[Farm.js] experimental.optimizedBoundary requires experimental.serverComponents.");
480
+ }
481
+ // If RSC not enabled, don't add environment config
482
+ if (!rscEnabled) {
483
+ return;
484
+ }
485
+ const root = c.root ?? process.cwd();
486
+ assertRscPackageCompatibility(root);
487
+ if (optimizedBoundaryEnabled) {
488
+ registerRscNitroRuntimePackage(root, STRATA_PACKAGE, resolveOptimizedBoundaryRuntimeRoot());
489
+ }
490
+ if (c.extends?.length) {
491
+ const layerResolution = await resolveFarmLayers(c, {
492
+ root,
493
+ mode: process.env.NODE_ENV === "production" ? "production" : "development",
494
+ });
495
+ Object.assign(c, layerResolution.config);
496
+ }
497
+ // Read user's directory configuration
498
+ const srcDir = c.srcDir ?? "src";
499
+ const outDir = c.outDir ?? "dist";
500
+ const deploymentId = normalizeFarmDeploymentId(options.deploymentId ||
501
+ c.deploymentId ||
502
+ process.env.FARM_DEPLOYMENT_ID ||
503
+ process.env.VERCEL_GIT_COMMIT_SHA ||
504
+ process.env.CF_PAGES_COMMIT_SHA ||
505
+ (process.env.NODE_ENV === "production"
506
+ ? ((await c.generateBuildId?.()) ?? automaticDeploymentId)
507
+ : "development"));
508
+ rscBuildRoot = root;
509
+ const entriesDir = path.join(root, ".farm", "rsc-entries");
510
+ await fs.mkdir(entriesDir, { recursive: true });
511
+ const configuredRoutesDir = options.routesDir === undefined ? "app" : options.routesDir.trim();
512
+ const globalCssFile = path.resolve(root, srcDir, configuredRoutesDir, "globals.css");
513
+ let globalCssPath;
514
+ try {
515
+ if ((await fs.stat(globalCssFile)).isFile()) {
516
+ const rootRelativeCssPath = path.relative(root, globalCssFile).replace(/\\/g, "/");
517
+ globalCssPath = rootRelativeCssPath.startsWith("../")
518
+ ? `/@fs/${globalCssFile.replace(/\\/g, "/")}`
519
+ : `/${rootRelativeCssPath}`;
520
+ }
521
+ }
522
+ catch {
523
+ // Global CSS is optional.
524
+ }
525
+ const routeRoots = getFarmSourceRoots(c).map((source) => {
526
+ const routeSuffix = configuredRoutesDir ? `/${configuredRoutesDir}` : "";
527
+ const projectSourceDir = source.srcDir.replace(/\\/g, "/").replace(/^\.?\//, "");
528
+ const base = source.layer
529
+ ? `#layers/${source.name}${routeSuffix}`
530
+ : `/${projectSourceDir}${routeSuffix}`;
531
+ return { name: source.name, base, glob: base };
532
+ });
533
+ // Build context for entry generators
534
+ entryContext = {
535
+ srcDir,
536
+ outDir,
537
+ basePath: c.basePath ?? "/",
538
+ routesDir: options.routesDir,
539
+ globalCssPath,
540
+ routeRoots,
541
+ actionsEnabled,
542
+ serverActions: resolveServerActionsConfig({
543
+ ...c.serverActions,
544
+ ...options.serverActions,
545
+ allowedOrigins: options.serverActions?.allowedOrigins ?? c.serverActions?.allowedOrigins,
546
+ }),
547
+ deploymentId,
548
+ debug,
549
+ };
550
+ logInfo(`srcDir: ${entryContext.srcDir}, outDir: ${entryContext.outDir}`);
551
+ // Write real entry files so @vitejs/plugin-rsc can use file-based entries.
552
+ // This ensures the RSC plugin runs for every environment (rsc, ssr, client) and client build/deploy works.
553
+ const entryRscPath = path.join(entriesDir, "entry.rsc.tsx");
554
+ const entrySsrPath = path.join(entriesDir, "entry.ssr.tsx");
555
+ const entryClientPath = path.join(entriesDir, "entry.browser.tsx");
556
+ await fs.writeFile(entryRscPath, generateRscEntry(entryContext));
557
+ await fs.writeFile(entrySsrPath, generateSsrEntry(entryContext));
558
+ await fs.writeFile(entryClientPath, generateClientEntry(entryContext));
559
+ logInfo(`Wrote RSC entries to ${entriesDir}`);
560
+ // User must add rsc({ entries: { rsc, ssr, client } }) to config.plugins so the RSC plugin
561
+ // runs for every environment and client build can resolve virtual:vite-rsc/client-references.
562
+ // Entry paths (relative to root) so Vite runs rsc/ssr/client builds (not index.html).
563
+ const entryRsc = "./.farm/rsc-entries/entry.rsc.tsx";
564
+ const entrySsr = "./.farm/rsc-entries/entry.ssr.tsx";
565
+ const entryClient = "./.farm/rsc-entries/entry.browser.tsx";
566
+ // Resolve @farm.js/core so Vite (and rsc/ssr envs) can load it when app code imports it (fixes "Failed to resolve entry" in dev).
567
+ let farmCorePath = null;
568
+ try {
569
+ farmCorePath = path.dirname(require_.resolve("@farm.js/core/package.json"));
570
+ }
571
+ catch {
572
+ try {
573
+ farmCorePath = path.resolve(path.dirname(require_.resolve("@farm.js/core/server")), "..");
574
+ }
575
+ catch {
576
+ // @farm.js/core not installed or not built; core aliases are not added
577
+ }
578
+ }
579
+ const layerAliases = getFarmLayerAliases(c.layers);
580
+ // Return shared config and environments. RSC plugin (in plugins) needs to run in same
581
+ // pipeline for client build to resolve virtual:vite-rsc/client-references.
582
+ return {
583
+ appType: "custom",
584
+ builder: { sharedConfigBuild: true },
585
+ ssr: {
586
+ external: [
587
+ "react",
588
+ "react-dom",
589
+ "react-dom/server",
590
+ "react/jsx-runtime",
591
+ "react/jsx-dev-runtime",
592
+ ...(optimizedBoundaryEnabled
593
+ ? [STRATA_PACKAGE, `${STRATA_PACKAGE}/react-server`]
594
+ : []),
595
+ ],
596
+ },
597
+ ...(optimizedBoundaryEnabled
598
+ ? {
599
+ optimizeDeps: {
600
+ exclude: [STRATA_PACKAGE, `${STRATA_PACKAGE}/react-server`],
601
+ },
602
+ }
603
+ : {}),
604
+ resolve: {
605
+ dedupe: ["react", "react-dom", "react-server-dom-webpack"],
606
+ alias: [
607
+ ...Object.entries(layerAliases).map(([find, replacement]) => ({
608
+ find,
609
+ replacement,
610
+ })),
611
+ ...(farmCorePath
612
+ ? [
613
+ {
614
+ find: /^@farm.js\/core\/middleware$/,
615
+ replacement: path.join(farmCorePath, "dist/middleware.mjs"),
616
+ },
617
+ {
618
+ find: /^@farm.js\/core\/api$/,
619
+ replacement: path.join(farmCorePath, "dist/api.mjs"),
620
+ },
621
+ {
622
+ find: /^@farm.js\/core\/server-fn$/,
623
+ replacement: path.join(farmCorePath, "dist/server-fn.mjs"),
624
+ },
625
+ {
626
+ find: /^@farm.js\/core\/server-action-security$/,
627
+ replacement: path.join(farmCorePath, "dist/server-action-security.mjs"),
628
+ },
629
+ {
630
+ find: /^@farm.js\/core\/environment$/,
631
+ replacement: path.join(farmCorePath, "dist/environment.mjs"),
632
+ },
633
+ {
634
+ find: /^@farm.js\/core\/headers$/,
635
+ replacement: path.join(farmCorePath, "dist/headers.mjs"),
636
+ },
637
+ ...(env.command === "serve"
638
+ ? [
639
+ {
640
+ find: /^@farm.js\/core$/,
641
+ replacement: path.join(farmCorePath, "dist/index.mjs"),
642
+ },
643
+ ]
644
+ : []),
645
+ ]
646
+ : []),
647
+ ],
648
+ },
649
+ esbuild: {
650
+ jsx: "automatic",
651
+ jsxImportSource: "react",
652
+ },
653
+ ...(c.layers?.length
654
+ ? {
655
+ server: {
656
+ fs: {
657
+ allow: [root, ...c.layers.map((layer) => layer.root)],
658
+ },
659
+ },
660
+ }
661
+ : {}),
662
+ environments: {
663
+ rsc: {
664
+ build: {
665
+ outDir: `${outDir}/rsc`,
666
+ copyPublicDir: false,
667
+ rollupOptions: { input: { index: entryRsc } },
668
+ },
669
+ resolve: { conditions: ["react-server", "node", "import"] },
670
+ },
671
+ ssr: {
672
+ build: {
673
+ outDir: `${outDir}/ssr`,
674
+ copyPublicDir: false,
675
+ rollupOptions: { input: { index: entrySsr } },
676
+ },
677
+ resolve: { conditions: ["node", "import"] },
678
+ },
679
+ client: {
680
+ build: {
681
+ outDir: `${outDir}/client`,
682
+ rollupOptions: { input: { index: entryClient } },
683
+ },
684
+ resolve: { conditions: ["browser", "import"] },
685
+ },
686
+ },
687
+ };
688
+ },
689
+ },
690
+ {
691
+ name: "@farm.js/plugin/rsc:optimized-boundary",
692
+ enforce: "pre",
693
+ resolveId(id, importer, options) {
694
+ if (!isOptimizedBoundaryModule(id))
695
+ return null;
696
+ if (!optimizedBoundaryEnabled) {
697
+ throw new Error(`[Farm.js] ${id} was imported by ${importer || "an unknown module"}, but ` +
698
+ "experimental.optimizedBoundary is disabled.");
699
+ }
700
+ const environmentName = this.environment?.name;
701
+ const isServerEnvironment = options?.ssr || environmentName === "rsc" || environmentName === "ssr";
702
+ if (!isServerEnvironment) {
703
+ throw new Error(`[Farm.js] ${id} is server-only and cannot be imported into the client environment.`);
704
+ }
705
+ // Strata's published entry points are native CommonJS loaders. Keep
706
+ // them outside Vite's RSC dependency scan and let Node resolve the
707
+ // platform package at runtime.
708
+ if (id === STRATA_PACKAGE || id.startsWith(`${STRATA_PACKAGE}/`)) {
709
+ return { id, external: true };
710
+ }
711
+ return null;
712
+ },
713
+ },
714
+ {
715
+ name: "@farm.js/plugin/rsc:automatic-optimized-boundary",
716
+ enforce: "post",
717
+ transform(code, id) {
718
+ if (!rscEnabled || !optimizedBoundaryEnabled)
719
+ return null;
720
+ const environmentName = this.environment?.name;
721
+ if (environmentName !== "rsc")
722
+ return null;
723
+ const result = transformAutomaticOptimizedBoundaries(code, id);
724
+ if (!result)
725
+ return null;
726
+ if (debug) {
727
+ console.log(`[Farm.js] Added ${result.boundaryCount} automatic optimized-boundary candidate${result.boundaryCount === 1 ? "" : "s"} in ${id}`);
728
+ }
729
+ return { code: result.code, map: null };
730
+ },
731
+ },
732
+ {
733
+ name: "@farm.js/plugin/rsc:server-fn-actions",
734
+ enforce: "pre",
735
+ transform(code, id) {
736
+ if (!rscEnabled || !actionsEnabled)
737
+ return null;
738
+ const result = transformFarmServerFns(code, id);
739
+ if (!result)
740
+ return null;
741
+ return {
742
+ code: result.code,
743
+ map: null,
744
+ };
745
+ },
746
+ },
747
+ // Nitro: run after all environments (like @hiogawa/vite-plugin-nitro). If the runtime supports
748
+ // plugin buildApp order "post", this runs automatically; else use build script (see comment below).
749
+ {
750
+ name: "@farm.js/plugin/rsc:nitro-build",
751
+ apply: "build",
752
+ buildApp: {
753
+ order: "post",
754
+ handler: async (builder) => {
755
+ if (!rscEnabled || !rscBuildRoot || !entryContext)
756
+ return;
757
+ if (globalThis.__FARM_NITRO_PLUGIN_RAN)
758
+ return;
759
+ const root = path.resolve(rscBuildRoot);
760
+ if (globalThis.__FARM_NITRO_PATHS) {
761
+ const { runNitroFromBuildApp } = await import("./vite-plugin-nitro.js");
762
+ await runNitroFromBuildApp();
763
+ return;
764
+ }
765
+ const rscEnv = builder.environments?.rsc;
766
+ const ssrEnv = builder.environments?.ssr;
767
+ const clientEnv = builder.environments?.client;
768
+ if (!rscEnv || !ssrEnv || !clientEnv)
769
+ return;
770
+ const { buildRscNitro } = await import("./nitro-build.js");
771
+ await buildRscNitro({
772
+ root,
773
+ rendererPath: resolveRscBuildOutputPath(root, rscEnv.config.build.outDir, "index.js"),
774
+ publicDir: resolveRscBuildOutputPath(root, clientEnv.config.build.outDir),
775
+ ssrPath: resolveRscBuildOutputPath(root, ssrEnv.config.build.outDir, "index.js"),
776
+ assetsDir: clientEnv.config.build.assetsDir,
777
+ preset: process.env.NITRO_PRESET || "vercel",
778
+ });
779
+ },
780
+ },
781
+ },
782
+ // ────────────────────────────────────────────────────────
783
+ // VIRTUAL ENTRIES PLUGIN
784
+ // Generates entry files dynamically based on user's project structure
785
+ // ────────────────────────────────────────────────────────
786
+ {
787
+ name: "@farm.js/plugin/rsc:virtual-entries",
788
+ enforce: "pre",
789
+ resolveId(source) {
790
+ // Mark our virtual modules with \0 prefix (Vite convention)
791
+ if (source === VIRTUAL_RSC_ENTRY) {
792
+ return VIRTUAL_PREFIX + VIRTUAL_RSC_ENTRY;
793
+ }
794
+ if (source === VIRTUAL_SSR_ENTRY) {
795
+ return VIRTUAL_PREFIX + VIRTUAL_SSR_ENTRY;
796
+ }
797
+ if (source === VIRTUAL_CLIENT_ENTRY) {
798
+ return VIRTUAL_PREFIX + VIRTUAL_CLIENT_ENTRY;
799
+ }
800
+ // Resolve file-based entry paths to virtual entries so we always serve generated content
801
+ // (avoids stale .farm/rsc-entries/* on disk causing duplicate content / wrong rootContent fallback)
802
+ if (source.includes("rsc-entries") && source.includes("entry.browser.tsx")) {
803
+ return VIRTUAL_PREFIX + VIRTUAL_CLIENT_ENTRY;
804
+ }
805
+ if (source.includes("rsc-entries") && source.includes("entry.rsc.tsx")) {
806
+ return VIRTUAL_PREFIX + VIRTUAL_RSC_ENTRY;
807
+ }
808
+ if (source.includes("rsc-entries") && source.includes("entry.ssr.tsx")) {
809
+ return VIRTUAL_PREFIX + VIRTUAL_SSR_ENTRY;
810
+ }
811
+ // Handle dynamic hydration entries like /@rsc-hydrate/counter
812
+ if (source.startsWith("/@rsc-hydrate/")) {
813
+ return VIRTUAL_PREFIX + source;
814
+ }
815
+ return null;
816
+ },
817
+ load(id) {
818
+ // Only generate entries if RSC is enabled
819
+ if (!rscEnabled) {
820
+ return null;
821
+ }
822
+ // Generate the appropriate entry based on the virtual module ID
823
+ if (id === VIRTUAL_PREFIX + VIRTUAL_RSC_ENTRY) {
824
+ logInfo("Generating RSC entry");
825
+ return generateRscEntry(entryContext);
826
+ }
827
+ if (id === VIRTUAL_PREFIX + VIRTUAL_SSR_ENTRY) {
828
+ logInfo("Generating SSR entry");
829
+ return generateSsrEntry(entryContext);
830
+ }
831
+ if (id === VIRTUAL_PREFIX + VIRTUAL_CLIENT_ENTRY) {
832
+ logInfo("Generating client entry");
833
+ return generateClientEntry(entryContext);
834
+ }
835
+ // Handle dynamic hydration entries
836
+ if (id.startsWith(VIRTUAL_PREFIX + "/@rsc-hydrate/")) {
837
+ const pagePath = id.replace(VIRTUAL_PREFIX + "/@rsc-hydrate", "");
838
+ const srcDir = entryContext.srcDir;
839
+ const appSegment = entryContext.routesDir === undefined ? "app" : entryContext.routesDir.trim();
840
+ const basePath = appSegment ? `/${srcDir}/${appSegment}` : `/${srcDir}`;
841
+ const pageImportPath = pagePath === "/" ? `${basePath}/page.tsx` : `${basePath}${pagePath}/page.tsx`;
842
+ const layoutImportPath = `${basePath}/layout.tsx`;
843
+ const actionBlock = entryContext.actionsEnabled
844
+ ? `
845
+ import { setServerCallback, encodeReply, createTemporaryReferenceSet, createFromReadableStream } from '@vitejs/plugin-rsc/browser';
846
+ import {
847
+ createFarmDeploymentMismatchError,
848
+ createFarmDeploymentRequestHeaders,
849
+ isFarmDeploymentMismatchResponse,
850
+ } from '@farm.js/core/deployment';
851
+ const farmDeploymentId = ${JSON.stringify(entryContext.deploymentId)};
852
+ setServerCallback(async (id, args) => {
853
+ const refs = createTemporaryReferenceSet();
854
+ const body = await encodeReply(args, { temporaryReferences: refs });
855
+ const headers = createFarmDeploymentRequestHeaders(farmDeploymentId, {
856
+ 'x-farm-action-id': id,
857
+ 'Accept': 'text/x-component',
858
+ });
859
+ if (typeof body === 'string') headers.set('Content-Type', 'text/plain; charset=utf-8');
860
+ else if (!(body instanceof FormData)) headers.set('Content-Type', 'application/octet-stream');
861
+ const res = await fetch(location.href, {
862
+ method: 'POST',
863
+ headers,
864
+ body,
865
+ cache: 'no-store',
866
+ credentials: 'same-origin',
867
+ redirect: 'error',
868
+ });
869
+ if (isFarmDeploymentMismatchResponse(res, farmDeploymentId)) {
870
+ const error = createFarmDeploymentMismatchError(res, farmDeploymentId);
871
+ globalThis.dispatchEvent?.(new CustomEvent('farm:deployment-mismatch', { detail: error }));
872
+ throw error;
873
+ }
874
+ if (!res.ok) throw new Error('Server action failed: ' + res.status);
875
+ const p = await createFromReadableStream(res.body, { temporaryReferences: refs });
876
+ if (p?.returnValue?.ok) return p.returnValue.data;
877
+ const error = new Error(p?.returnValue?.data?.message || 'Server function failed');
878
+ error.name = 'ServerActionError';
879
+ throw error;
880
+ });
881
+ `
882
+ : "";
883
+ // Generate a hydration entry for this page
884
+ return `${actionBlock}
885
+ import React from 'react';
886
+ import { hydrateRoot } from 'react-dom/client';
887
+
888
+ // Import page and layout using absolute paths
889
+ import Page from '${pageImportPath}';
890
+ import Layout from '${layoutImportPath}';
891
+
892
+ // Hydrate when DOM is ready
893
+ function hydrate() {
894
+ const root = document.getElementById('root');
895
+ if (root) {
896
+ const pageContent = React.createElement(Page, window.__PAGE_PROPS__ || {});
897
+ const app = React.createElement(Layout, null, pageContent);
898
+ hydrateRoot(root, app);
899
+ }
900
+ }
901
+
902
+ if (document.readyState === 'loading') {
903
+ document.addEventListener('DOMContentLoaded', hydrate);
904
+ } else {
905
+ hydrate();
906
+ }
907
+ `;
908
+ }
909
+ return null;
910
+ },
911
+ },
912
+ // ────────────────────────────────────────────────────────
913
+ // DEV SERVER PLUGIN
914
+ // Handles page rendering during development
915
+ // Middleware and API routes are handled by standalone plugins
916
+ // ────────────────────────────────────────────────────────
917
+ {
918
+ name: "@farm.js/plugin/rsc:dev-server",
919
+ configureServer(server) {
920
+ if (!rscEnabled) {
921
+ return;
922
+ }
923
+ logInfo("Dev server middleware ready");
924
+ const serverStartTime = Date.now();
925
+ let bannerPrinted = false;
926
+ const pageCache = new Map();
927
+ server.httpServer?.once("listening", () => {
928
+ if (bannerPrinted)
929
+ return;
930
+ bannerPrinted = true;
931
+ const elapsed = Date.now() - serverStartTime;
932
+ const address = server.httpServer?.address();
933
+ const port = typeof address === "object" && address ? address.port : 3000;
934
+ const colors = getColors();
935
+ console.log("");
936
+ console.log(` ${colors.bold(colors.green("Farm.js"))} ${colors.dim("v1.0.0")} ${colors.dim(`ready in ${elapsed}ms`)}`);
937
+ console.log("");
938
+ console.log(` ${colors.dim("➜")} ${colors.bold("Local:")} ${colors.cyan(`http://localhost:${port}/`)}`);
939
+ console.log(` ${colors.dim("➜")} ${colors.bold("Network:")} ${colors.dim("use --host to expose")}`);
940
+ console.log("");
941
+ });
942
+ return () => {
943
+ server.middlewares.use(async (req, res, next) => {
944
+ const url = req.url || "/";
945
+ const pathname = url.split("?")[0];
946
+ const method = req.method || "GET";
947
+ if (pathname.startsWith("/@") ||
948
+ pathname.startsWith("/__") ||
949
+ pathname.startsWith("/node_modules") ||
950
+ pathname.startsWith("/src/") ||
951
+ pathname.startsWith("/api/") ||
952
+ (pathname.includes(".") && !pathname.endsWith("/"))) {
953
+ return next();
954
+ }
955
+ const startTime = Date.now();
956
+ try {
957
+ const clientEntryUrl = "/.farm/rsc-entries/entry.browser.tsx";
958
+ const ssrEnv = server.environments?.ssr;
959
+ globalThis.__VITE_RSC_LOAD_SSR__ = async () => {
960
+ if (ssrEnv) {
961
+ const ssrSource = ssrEnv.config?.build?.rollupOptions?.input?.index ??
962
+ path.join(server.config.root, ".farm/rsc-entries/entry.ssr.tsx");
963
+ const resolved = await ssrEnv.pluginContainer.resolveId(ssrSource, undefined, {
964
+ ssr: true,
965
+ });
966
+ if (resolved?.id)
967
+ return ssrEnv.runner.import(resolved.id);
968
+ }
969
+ return server.ssrLoadModule("./.farm/rsc-entries/entry.ssr.tsx");
970
+ };
971
+ // When server actions are enabled, prepend an inline assignment so __viteRscCallServer
972
+ // is always a function before any chunk runs (avoids "globalThis.__viteRscCallServer is not a function").
973
+ const bootstrapPrefix = actionsEnabled
974
+ ? `(function(){if(typeof globalThis.__viteRscCallServer!=='function'){globalThis.__viteRscCallServer=function(){return Promise.reject(new Error('Farm.js: server actions not ready'));}}})();\n`
975
+ : "";
976
+ globalThis.__FARM_VITE_RSC_LOAD_BOOTSTRAP__ = async () => bootstrapPrefix +
977
+ `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)}));`;
978
+ const base = `http://${req.headers.host || "localhost:3000"}`;
979
+ let body;
980
+ if (method === "POST") {
981
+ const chunks = [];
982
+ for await (const chunk of req)
983
+ chunks.push(chunk);
984
+ body = Buffer.concat(chunks);
985
+ // Keep as buffer so request.formData() / request.text() in RSC handler work (multipart must not be UTF-8 decoded)
986
+ }
987
+ const request = new Request(new URL(url, base), {
988
+ method,
989
+ headers: req.headers,
990
+ body: method === "POST" && body && body.length > 0
991
+ ? body
992
+ : undefined,
993
+ });
994
+ // Load the RSC entry in the "rsc" environment so @vitejs/plugin-rsc transforms run with this.environment.name === "rsc".
995
+ // Fallback: when Farm dev server has no rsc environment, load via main server so streaming/loading still works.
996
+ const rscEnv = server.environments?.rsc;
997
+ let rscEntry = null;
998
+ if (rscEnv) {
999
+ const rscSource = rscEnv.config?.build?.rollupOptions?.input?.index ??
1000
+ "./.farm/rsc-entries/entry.rsc.tsx";
1001
+ const root = server.config.root;
1002
+ const importer = path.join(root, "vite.config.ts");
1003
+ const absoluteRscEntry = path.resolve(root, ".farm", "rsc-entries", "entry.rsc.tsx");
1004
+ const resolved = (await rscEnv.pluginContainer.resolveId(rscSource, importer, {
1005
+ ssr: true,
1006
+ })) ??
1007
+ (await rscEnv.pluginContainer.resolveId(rscSource, undefined, { ssr: true })) ??
1008
+ (await rscEnv.pluginContainer.resolveId(absoluteRscEntry, undefined, {
1009
+ ssr: true,
1010
+ }));
1011
+ const id = resolved?.id ?? pathToFileURL(absoluteRscEntry).href;
1012
+ rscEntry = await rscEnv.runner.import(id);
1013
+ }
1014
+ if (!rscEntry?.default?.fetch) {
1015
+ try {
1016
+ rscEntry = await server.ssrLoadModule("./.farm/rsc-entries/entry.rsc.tsx");
1017
+ }
1018
+ catch (_) {
1019
+ // Ignore; will fall through to legacy handler
1020
+ }
1021
+ }
1022
+ if (!rscEntry?.default?.fetch)
1023
+ throw new Error("[Farm.js] Could not load RSC entry in rsc environment");
1024
+ const response = await rscEntry.default.fetch(request);
1025
+ res.statusCode = response.status;
1026
+ response.headers.forEach((value, key) => {
1027
+ if (key.toLowerCase() !== "transfer-encoding")
1028
+ res.setHeader(key, value);
1029
+ });
1030
+ if (response.body) {
1031
+ const reader = response.body.getReader();
1032
+ const pump = async () => {
1033
+ while (true) {
1034
+ const { done, value } = await reader.read();
1035
+ if (done)
1036
+ break;
1037
+ res.write(Buffer.from(value));
1038
+ }
1039
+ res.end();
1040
+ };
1041
+ await pump();
1042
+ }
1043
+ else {
1044
+ res.end();
1045
+ }
1046
+ const duration = Date.now() - startTime;
1047
+ logResponse(method, pathname, response.status, duration);
1048
+ return;
1049
+ }
1050
+ catch (rscError) {
1051
+ // RSC pipeline failed; fall back to legacy SSR for GET only
1052
+ if (method !== "GET") {
1053
+ const duration = Date.now() - startTime;
1054
+ logResponse(method, pathname, 500, duration);
1055
+ console.error("[Farm.js] RSC dev handler error:", rscError);
1056
+ res.statusCode = 500;
1057
+ res.setHeader("Content-Type", "text/html");
1058
+ res.end(`<!DOCTYPE html><html><head><title>Error</title></head><body><pre>${rscError.message}</pre></body></html>`);
1059
+ return;
1060
+ }
1061
+ }
1062
+ try {
1063
+ if (method !== "GET")
1064
+ return next();
1065
+ // Get middleware data from standalone middleware plugin
1066
+ const middlewareData = req.__FARM_MIDDLEWARE_DATA__ || {};
1067
+ // Build the glob pattern for discovering routes (Farm convention: src/app when routesDir unset)
1068
+ const srcDir = entryContext.srcDir;
1069
+ const appSegment = entryContext.routesDir === undefined ? "app" : entryContext.routesDir.trim();
1070
+ const glob = appSegment ? `/${srcDir}/${appSegment}` : `/${srcDir}`;
1071
+ // Find matching page file
1072
+ const normalized = pathname.replace(/\/$/, "") || "/";
1073
+ // Common page file patterns
1074
+ const possiblePaths = [
1075
+ `${glob}${normalized === "/" ? "" : normalized}/page.tsx`,
1076
+ `${glob}${normalized === "/" ? "" : normalized}/page.jsx`,
1077
+ `${glob}/page.tsx`,
1078
+ `${glob}/page.jsx`,
1079
+ ];
1080
+ // Also check for dynamic routes by walking up the path
1081
+ const parts = normalized.split("/").filter(Boolean);
1082
+ for (let i = parts.length; i >= 0; i--) {
1083
+ const base = parts.slice(0, i).join("/");
1084
+ possiblePaths.push(base ? `${glob}/${base}/page.tsx` : `${glob}/page.tsx`);
1085
+ possiblePaths.push(base ? `${glob}/${base}/page.jsx` : `${glob}/page.jsx`);
1086
+ }
1087
+ let pageModule = null;
1088
+ let matchedPath = "";
1089
+ let layoutModule = null;
1090
+ // Try to find and load the page
1091
+ for (const pagePath of possiblePaths) {
1092
+ try {
1093
+ // Convert glob path to actual file path
1094
+ const actualPath = pagePath.startsWith("/") ? `.${pagePath}` : pagePath;
1095
+ if (pageCache.has(pagePath)) {
1096
+ pageModule = pageCache.get(pagePath);
1097
+ matchedPath = pagePath;
1098
+ break;
1099
+ }
1100
+ pageModule = await server.ssrLoadModule(actualPath);
1101
+ if (pageModule?.default) {
1102
+ pageCache.set(pagePath, pageModule);
1103
+ matchedPath = pagePath;
1104
+ // Page loaded successfully
1105
+ break;
1106
+ }
1107
+ }
1108
+ catch (e) {
1109
+ // Page not found at this path, continue
1110
+ }
1111
+ }
1112
+ if (!pageModule?.default) {
1113
+ return next();
1114
+ }
1115
+ try {
1116
+ const layoutPath = `./${srcDir}${appSegment ? `/${appSegment}` : ""}/layout.tsx`;
1117
+ layoutModule = await server.ssrLoadModule(layoutPath);
1118
+ }
1119
+ catch { }
1120
+ const React = await import("react");
1121
+ const ReactDOMServer = await import("react-dom/server");
1122
+ const Page = pageModule.default;
1123
+ const metadata = {
1124
+ title: typeof pageModule.metadata?.title === "string"
1125
+ ? pageModule.metadata.title
1126
+ : layoutModule?.metadata?.title,
1127
+ description: typeof pageModule.metadata?.description === "string"
1128
+ ? pageModule.metadata.description
1129
+ : layoutModule?.metadata?.description,
1130
+ };
1131
+ const Layout = layoutModule?.default;
1132
+ // Parse URL params (basic dynamic route support)
1133
+ const params = {};
1134
+ const searchParams = Object.fromEntries(new URLSearchParams(url.split("?")[1] || ""));
1135
+ // Render the page with middleware data
1136
+ const pageProps = {
1137
+ params,
1138
+ searchParams,
1139
+ // Middleware shared data is available to pages
1140
+ middlewareData,
1141
+ };
1142
+ // Helper to check if a function is async or returns a Promise
1143
+ // We need to actually call the function to detect this reliably
1144
+ // since transpiled async functions may not be detectable by constructor
1145
+ const isAsyncFunction = (fn) => {
1146
+ if (!fn)
1147
+ return false;
1148
+ // Check if it's an AsyncFunction
1149
+ if (fn.constructor?.name === "AsyncFunction")
1150
+ return true;
1151
+ // Check if function.toString() contains async
1152
+ try {
1153
+ const str = fn.toString();
1154
+ // Check for "async function" or "async (" patterns
1155
+ if (/^async\s/.test(str) || /^async\s*\(/.test(str))
1156
+ return true;
1157
+ }
1158
+ catch { }
1159
+ return false;
1160
+ };
1161
+ // Try to render the page - if it returns a Promise, it's async
1162
+ let pageContent;
1163
+ let isAsyncPage = isAsyncFunction(Page);
1164
+ try {
1165
+ const result = Page(pageProps);
1166
+ // Check if result is a Promise (thenable)
1167
+ if (result && typeof result.then === "function") {
1168
+ isAsyncPage = true;
1169
+ pageContent = await result;
1170
+ }
1171
+ else {
1172
+ // If it's not a Promise, it's a React element or we need to use createElement
1173
+ if (React.default.isValidElement(result)) {
1174
+ pageContent = result;
1175
+ }
1176
+ else {
1177
+ pageContent = React.default.createElement(Page, pageProps);
1178
+ }
1179
+ }
1180
+ }
1181
+ catch (e) {
1182
+ // If calling directly failed, use createElement
1183
+ pageContent = React.default.createElement(Page, pageProps);
1184
+ }
1185
+ // Check if Layout is async
1186
+ let isAsyncLayout = isAsyncFunction(Layout);
1187
+ // Wrap in layout if available
1188
+ let content = pageContent;
1189
+ if (Layout) {
1190
+ try {
1191
+ const layoutResult = Layout({ children: pageContent });
1192
+ if (layoutResult && typeof layoutResult.then === "function") {
1193
+ isAsyncLayout = true;
1194
+ content = await layoutResult;
1195
+ }
1196
+ else if (React.default.isValidElement(layoutResult)) {
1197
+ content = layoutResult;
1198
+ }
1199
+ else {
1200
+ content = React.default.createElement(Layout, null, pageContent);
1201
+ }
1202
+ }
1203
+ catch (e) {
1204
+ content = React.default.createElement(Layout, null, pageContent);
1205
+ }
1206
+ }
1207
+ // Create full HTML page
1208
+ const h = React.default.createElement;
1209
+ const isSyncPage = !isAsyncPage && !isAsyncLayout;
1210
+ // For sync pages we load preamble + vite client + hydrate in one script below; do not load vite-client in head so order is guaranteed.
1211
+ const routesDir = entryContext.routesDir === undefined ? "app" : entryContext.routesDir.trim();
1212
+ const routesPath = routesDir ? `/${routesDir}` : "";
1213
+ const headElements = [
1214
+ h("meta", { key: "charset", charSet: "utf-8" }),
1215
+ h("meta", {
1216
+ key: "viewport",
1217
+ name: "viewport",
1218
+ content: "width=device-width, initial-scale=1",
1219
+ }),
1220
+ metadata.title
1221
+ ? h("title", { key: "title" }, metadata.title)
1222
+ : h("title", { key: "title" }, "Farm.js"),
1223
+ metadata.description
1224
+ ? h("meta", {
1225
+ key: "description",
1226
+ name: "description",
1227
+ content: metadata.description,
1228
+ })
1229
+ : null,
1230
+ h("link", {
1231
+ key: "globals-css",
1232
+ rel: "stylesheet",
1233
+ href: `/${srcDir}${routesPath}/globals.css`,
1234
+ }),
1235
+ ...(isSyncPage
1236
+ ? []
1237
+ : [
1238
+ h("script", {
1239
+ key: "vite-client",
1240
+ type: "module",
1241
+ src: "/@vite/client",
1242
+ }),
1243
+ ]),
1244
+ ].filter(Boolean);
1245
+ // Create body elements
1246
+ // For async (server) pages, we don't hydrate the page itself - only client components within
1247
+ // For sync pages, we can hydrate the whole thing
1248
+ const bodyElements = [h("div", { key: "root", id: "root" }, content)];
1249
+ // Only add hydration script for sync pages (non-async)
1250
+ // When server actions are enabled, set __viteRscCallServer first so form submission works.
1251
+ if (actionsEnabled) {
1252
+ bodyElements.push(h("script", {
1253
+ key: "rsc-call-server",
1254
+ dangerouslySetInnerHTML: {
1255
+ __html: `(function(){if(typeof globalThis.__viteRscCallServer!=='function'){globalThis.__viteRscCallServer=function(){return Promise.reject(new Error('Farm.js: server actions not ready'));}}})();`,
1256
+ },
1257
+ }));
1258
+ }
1259
+ // Set React refresh preamble synchronously first so "use client" components don't throw when they load.
1260
+ if (!isAsyncPage && !isAsyncLayout) {
1261
+ bodyElements.push(h("script", {
1262
+ key: "preamble-sync",
1263
+ dangerouslySetInnerHTML: {
1264
+ __html: `window.__vite_plugin_react_preamble_installed__=true;window.$RefreshReg$=function(){};window.$RefreshSig$=function(){return function(t){return t;}};`,
1265
+ },
1266
+ }), h("script", {
1267
+ key: "page-props",
1268
+ dangerouslySetInnerHTML: {
1269
+ __html: `window.__PAGE_PROPS__ = ${JSON.stringify(pageProps)};`,
1270
+ },
1271
+ }), h("script", {
1272
+ key: "hydrate",
1273
+ type: "module",
1274
+ dangerouslySetInnerHTML: {
1275
+ __html: `import("/@react-refresh").then(m=>{m.default.injectIntoGlobalHook(window);return import("/@vite/client");}).then(()=>import(${JSON.stringify(`/@rsc-hydrate${normalized}`)}));`,
1276
+ },
1277
+ }));
1278
+ }
1279
+ else {
1280
+ // For async pages, we need to hydrate client component islands
1281
+ // Load a minimal script that finds and hydrates "use client" components
1282
+ bodyElements.push(h("script", {
1283
+ key: "client-islands",
1284
+ type: "module",
1285
+ dangerouslySetInnerHTML: {
1286
+ __html: `
1287
+ // Hydrate client component islands
1288
+ import '/@vite/client';
1289
+
1290
+ // Find all client components and hydrate them
1291
+ // The actual hydration is handled by @vitejs/plugin-rsc transforms
1292
+ `,
1293
+ },
1294
+ }));
1295
+ }
1296
+ const fullPage = h("html", { lang: "en" }, h("head", null, ...headElements), h("body", null, ...bodyElements));
1297
+ const html = "<!DOCTYPE html>" + ReactDOMServer.renderToString(fullPage);
1298
+ res.statusCode = 200;
1299
+ res.setHeader("Content-Type", "text/html");
1300
+ res.end(html);
1301
+ const duration = Date.now() - startTime;
1302
+ logResponse("GET", pathname, 200, duration);
1303
+ }
1304
+ catch (error) {
1305
+ const duration = Date.now() - startTime;
1306
+ logResponse("GET", pathname, 500, duration);
1307
+ console.error(error);
1308
+ // Return error page
1309
+ res.statusCode = 500;
1310
+ res.setHeader("Content-Type", "text/html");
1311
+ res.end(`
1312
+ <!DOCTYPE html>
1313
+ <html>
1314
+ <head><title>Error</title></head>
1315
+ <body style="font-family: system-ui; padding: 2rem; background: #1a1a2e; color: #eee;">
1316
+ <h1 style="color: #ff6b6b;">Error</h1>
1317
+ <pre style="background: #16213e; padding: 1rem; border-radius: 8px; overflow: auto;">${error.stack || error.message}</pre>
1318
+ </body>
1319
+ </html>
1320
+ `);
1321
+ }
1322
+ });
1323
+ };
1324
+ },
1325
+ },
1326
+ // ────────────────────────────────────────────────────────
1327
+ // RSC CORE TRANSFORMS PLUGIN (placeholder for any configResolved logic)
1328
+ // @vitejs/plugin-rsc is now injected in the config hook above so its
1329
+ // virtual modules (e.g. virtual:vite-rsc/client-references) run in all environments.
1330
+ // ────────────────────────────────────────────────────────
1331
+ {
1332
+ name: "@farm.js/plugin/rsc:core-loader",
1333
+ enforce: "pre",
1334
+ },
1335
+ // ────────────────────────────────────────────────────────
1336
+ // HMR PLUGIN
1337
+ // Sends HMR updates when server components, middleware, or API routes change
1338
+ // ────────────────────────────────────────────────────────
1339
+ {
1340
+ name: "@farm.js/plugin/rsc:hmr",
1341
+ handleHotUpdate({ file, server, modules }) {
1342
+ if (!rscEnabled) {
1343
+ return;
1344
+ }
1345
+ const srcDir = entryContext?.srcDir || "src";
1346
+ const fileName = file.split("/").pop() || "";
1347
+ // Handle middleware changes
1348
+ if (fileName.startsWith("middleware.")) {
1349
+ logInfo(`Middleware updated: ${fileName}`);
1350
+ // Full reload for middleware changes
1351
+ server.ws.send({ type: "full-reload", path: "*" });
1352
+ return [];
1353
+ }
1354
+ // Handle API route changes
1355
+ if (file.includes("/api/") && fileName.startsWith("route.")) {
1356
+ const shortPath = file.split("/api/")[1] || file;
1357
+ logInfo(`API route updated: ${shortPath}`);
1358
+ // Invalidate modules and reload
1359
+ for (const mod of modules) {
1360
+ server.moduleGraph.invalidateModule(mod);
1361
+ }
1362
+ return [];
1363
+ }
1364
+ // Handle page/layout changes
1365
+ if (file.includes(srcDir) && (file.endsWith(".tsx") || file.endsWith(".jsx"))) {
1366
+ if (fileName.startsWith("page.") || fileName.startsWith("layout.")) {
1367
+ const shortPath = file.split(`/${srcDir}/`)[1] || file;
1368
+ logInfo(`Updated: ${shortPath}`);
1369
+ // Invalidate modules
1370
+ for (const mod of modules) {
1371
+ server.moduleGraph.invalidateModule(mod);
1372
+ }
1373
+ // Full reload for page/layout changes
1374
+ server.ws.send({ type: "full-reload", path: "*" });
1375
+ return [];
1376
+ }
1377
+ // Component changes - send HMR update
1378
+ logInfo(`HMR update: ${fileName}`);
1379
+ server.ws.send({
1380
+ type: "custom",
1381
+ event: "rsc:update",
1382
+ data: { file },
1383
+ });
1384
+ }
1385
+ return modules;
1386
+ },
1387
+ },
1388
+ ];
1389
+ }