@farm.js/plugin 0.1.0-beta.0

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