@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.
- package/LICENSE +22 -0
- package/README.md +11 -0
- package/dist/api/index.d.ts +42 -0
- package/dist/api/index.d.ts.map +1 -0
- package/dist/api/index.js +567 -0
- package/dist/context/index.d.ts +61 -0
- package/dist/context/index.d.ts.map +1 -0
- package/dist/context/index.js +75 -0
- package/dist/index.d.ts +43 -0
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +49 -0
- package/dist/middleware/index.d.ts +97 -0
- package/dist/middleware/index.d.ts.map +1 -0
- package/dist/middleware/index.js +469 -0
- package/dist/observability/index.d.ts +190 -0
- package/dist/observability/index.d.ts.map +1 -0
- package/dist/observability/index.js +399 -0
- package/dist/rsc/build-paths.d.ts +3 -0
- package/dist/rsc/build-paths.d.ts.map +1 -0
- package/dist/rsc/build-paths.js +8 -0
- package/dist/rsc/entries/client.d.ts +14 -0
- package/dist/rsc/entries/client.d.ts.map +1 -0
- package/dist/rsc/entries/client.js +283 -0
- package/dist/rsc/entries/rsc.d.ts +13 -0
- package/dist/rsc/entries/rsc.d.ts.map +1 -0
- package/dist/rsc/entries/rsc.js +932 -0
- package/dist/rsc/entries/ssr.d.ts +13 -0
- package/dist/rsc/entries/ssr.d.ts.map +1 -0
- package/dist/rsc/entries/ssr.js +245 -0
- package/dist/rsc/index.d.ts +78 -0
- package/dist/rsc/index.d.ts.map +1 -0
- package/dist/rsc/index.js +1368 -0
- package/dist/rsc/nitro-build.d.ts +36 -0
- package/dist/rsc/nitro-build.d.ts.map +1 -0
- package/dist/rsc/nitro-build.js +396 -0
- package/dist/rsc/optimized-boundary.d.ts +20 -0
- package/dist/rsc/optimized-boundary.d.ts.map +1 -0
- package/dist/rsc/optimized-boundary.js +15 -0
- package/dist/rsc/server-fn-transform.d.ts +6 -0
- package/dist/rsc/server-fn-transform.d.ts.map +1 -0
- package/dist/rsc/server-fn-transform.js +152 -0
- package/dist/rsc/types.d.ts +123 -0
- package/dist/rsc/types.d.ts.map +1 -0
- package/dist/rsc/types.js +1 -0
- package/dist/rsc/vite-plugin-nitro.d.ts +33 -0
- package/dist/rsc/vite-plugin-nitro.d.ts.map +1 -0
- package/dist/rsc/vite-plugin-nitro.js +163 -0
- package/package.json +94 -0
- package/scripts/build.js +7 -0
- package/scripts/clean.js +6 -0
- 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
|
+
}
|