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

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