@farm.js/plugin 0.1.0-beta.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (51) hide show
  1. package/LICENSE +22 -0
  2. package/README.md +11 -0
  3. package/dist/api/index.d.ts +42 -0
  4. package/dist/api/index.d.ts.map +1 -0
  5. package/dist/api/index.js +567 -0
  6. package/dist/context/index.d.ts +61 -0
  7. package/dist/context/index.d.ts.map +1 -0
  8. package/dist/context/index.js +75 -0
  9. package/dist/index.d.ts +43 -0
  10. package/dist/index.d.ts.map +1 -0
  11. package/dist/index.js +49 -0
  12. package/dist/middleware/index.d.ts +97 -0
  13. package/dist/middleware/index.d.ts.map +1 -0
  14. package/dist/middleware/index.js +469 -0
  15. package/dist/observability/index.d.ts +190 -0
  16. package/dist/observability/index.d.ts.map +1 -0
  17. package/dist/observability/index.js +399 -0
  18. package/dist/rsc/build-paths.d.ts +3 -0
  19. package/dist/rsc/build-paths.d.ts.map +1 -0
  20. package/dist/rsc/build-paths.js +8 -0
  21. package/dist/rsc/entries/client.d.ts +14 -0
  22. package/dist/rsc/entries/client.d.ts.map +1 -0
  23. package/dist/rsc/entries/client.js +283 -0
  24. package/dist/rsc/entries/rsc.d.ts +13 -0
  25. package/dist/rsc/entries/rsc.d.ts.map +1 -0
  26. package/dist/rsc/entries/rsc.js +932 -0
  27. package/dist/rsc/entries/ssr.d.ts +13 -0
  28. package/dist/rsc/entries/ssr.d.ts.map +1 -0
  29. package/dist/rsc/entries/ssr.js +245 -0
  30. package/dist/rsc/index.d.ts +78 -0
  31. package/dist/rsc/index.d.ts.map +1 -0
  32. package/dist/rsc/index.js +1368 -0
  33. package/dist/rsc/nitro-build.d.ts +36 -0
  34. package/dist/rsc/nitro-build.d.ts.map +1 -0
  35. package/dist/rsc/nitro-build.js +396 -0
  36. package/dist/rsc/optimized-boundary.d.ts +20 -0
  37. package/dist/rsc/optimized-boundary.d.ts.map +1 -0
  38. package/dist/rsc/optimized-boundary.js +15 -0
  39. package/dist/rsc/server-fn-transform.d.ts +6 -0
  40. package/dist/rsc/server-fn-transform.d.ts.map +1 -0
  41. package/dist/rsc/server-fn-transform.js +152 -0
  42. package/dist/rsc/types.d.ts +123 -0
  43. package/dist/rsc/types.d.ts.map +1 -0
  44. package/dist/rsc/types.js +1 -0
  45. package/dist/rsc/vite-plugin-nitro.d.ts +33 -0
  46. package/dist/rsc/vite-plugin-nitro.d.ts.map +1 -0
  47. package/dist/rsc/vite-plugin-nitro.js +163 -0
  48. package/package.json +94 -0
  49. package/scripts/build.js +7 -0
  50. package/scripts/clean.js +6 -0
  51. package/scripts/run-nitro.mjs +18 -0
@@ -0,0 +1,932 @@
1
+ /**
2
+ * Generates the RSC environment entry file.
3
+ *
4
+ * This entry file:
5
+ * - Auto-discovers page files using import.meta.glob
6
+ * - Implements file-based routing by matching URL paths to page files
7
+ * - Handles server actions if enabled (decoding arguments, executing, returning results)
8
+ * - Renders the React tree to an RSC stream
9
+ * - Either returns the stream directly (for client navigation) or delegates to SSR (for initial page load)
10
+ */
11
+ export function generateRscEntry(ctx) {
12
+ // Build the glob pattern for discovering routes (Farm convention: src/app when routesDir unset)
13
+ const appSegment = ctx.routesDir === undefined ? "app" : ctx.routesDir.trim();
14
+ const glob = appSegment ? `/${ctx.srcDir}/${appSegment}` : `/${ctx.srcDir}`;
15
+ const routeRoots = ctx.routeRoots?.length
16
+ ? ctx.routeRoots
17
+ : [{ name: "project", base: glob, glob }];
18
+ const routeSourceRoots = routeRoots.map((root) => {
19
+ const routeSuffix = appSegment ? `/${appSegment}` : "";
20
+ const sourceGlob = routeSuffix && root.glob.endsWith(routeSuffix)
21
+ ? root.glob.slice(0, -routeSuffix.length)
22
+ : root.glob;
23
+ return { ...root, sourceGlob };
24
+ });
25
+ const debugLog = `// Debug disabled`;
26
+ let code = `
27
+ import React from 'react';
28
+ import {
29
+ renderToReadableStream,
30
+ `;
31
+ if (ctx.actionsEnabled) {
32
+ code += `
33
+ decodeReply,
34
+ loadServerAction,
35
+ decodeAction,
36
+ decodeFormState,
37
+ createTemporaryReferenceSet,
38
+ `;
39
+ }
40
+ code += `} from '@vitejs/plugin-rsc/rsc';
41
+ import {
42
+ createFarmDeploymentCookie,
43
+ createFarmDeploymentMismatchResponse,
44
+ getFarmDeploymentMismatch,
45
+ } from '@farm.js/core/deployment';
46
+ import {
47
+ _runWithMiddlewareContext,
48
+ _runWithMiddlewareData,
49
+ applyProductionMiddlewareHeaders,
50
+ createProductionMiddlewareRunner,
51
+ } from '@farm.js/core/middleware';
52
+ import { invokeAPIRouteEndpoint, matchAPIRoute } from '@farm.js/core/api/runtime';
53
+ import { _runWithAfterRequest } from '@farm.js/core/after';
54
+ import { _runWithCurrentRequest } from '@farm.js/core/internal/production-runtime';
55
+
56
+ const farmDeploymentId = ${JSON.stringify(ctx.deploymentId)};
57
+ `;
58
+ if (ctx.actionsEnabled) {
59
+ code += `import {
60
+ createServerActionRequestErrorResponse,
61
+ getServerActionInvalidations,
62
+ prepareServerActionRequest,
63
+ runWithServerActionRequest,
64
+ sanitizeServerActionError,
65
+ validateServerActionRequest,
66
+ } from '@farm.js/core/server-action-security';
67
+
68
+ const serverActionSecurity = ${JSON.stringify(ctx.serverActions)};
69
+ `;
70
+ }
71
+ // Auto-discover pages, layouts, and middleware using Vite's glob import
72
+ code += `
73
+ // Debug logging helper
74
+ function debug(...args) {
75
+ ${debugLog}
76
+ }
77
+
78
+ function applyActionResponseHeaders(headers, request) {
79
+ headers.set('x-farm-deployment-id', farmDeploymentId);
80
+ const accept = request.headers.get('accept') || '';
81
+ if (request.method === 'GET' && !accept.includes('text/x-component')) {
82
+ headers.append('set-cookie', createFarmDeploymentCookie(
83
+ farmDeploymentId,
84
+ ${JSON.stringify(ctx.basePath)},
85
+ new URL(request.url).protocol === 'https:',
86
+ ));
87
+ }
88
+ if (${ctx.actionsEnabled ? "true" : "false"} && request.method === 'POST') {
89
+ headers.set('cache-control', 'no-store');
90
+ headers.set('x-content-type-options', 'nosniff');
91
+ }
92
+ }
93
+
94
+ // Auto-discover all route modules. Every glob remains a literal so Vite can analyze it.
95
+ ${routeSourceRoots
96
+ .map((root, index) => `const pages${index} = import.meta.glob(${JSON.stringify(`${root.glob}/**/page.{tsx,jsx,ts,js}`)}, { eager: true });
97
+ const layouts${index} = import.meta.glob(${JSON.stringify(`${root.glob}/**/layout.{tsx,jsx,ts,js}`)}, { eager: true });
98
+ const loadings${index} = import.meta.glob(${JSON.stringify(`${root.glob}/**/loading.{tsx,jsx,ts,js}`)}, { eager: true });
99
+ const errors${index} = import.meta.glob(${JSON.stringify(`${root.glob}/**/error.{tsx,jsx,ts,js}`)}, { eager: true });
100
+ const middlewares${index} = import.meta.glob(${JSON.stringify(`${root.glob}/**/middleware.{tsx,jsx,ts,js}`)}, { eager: true });
101
+ const apiRouteModules${index} = import.meta.glob(${JSON.stringify(`${root.glob}/api/**/route.{tsx,jsx,ts,js}`)}, { eager: true });
102
+ const routeDefinitionModules${index} = import.meta.glob(${JSON.stringify(root.sourceGlob === root.glob
103
+ ? `${root.sourceGlob}/{farm.route,farm.routes,routes}.{tsx,jsx,ts,js}`
104
+ : [
105
+ `${root.sourceGlob}/{farm.route,farm.routes,routes}.{tsx,jsx,ts,js}`,
106
+ `${root.glob}/{farm.route,farm.routes,routes}.{tsx,jsx,ts,js}`,
107
+ ])}, { eager: true });`)
108
+ .join("\n")}
109
+
110
+ function mergeRouteModules(sources) {
111
+ const merged = {};
112
+ for (const source of sources) {
113
+ const baseValue = source.base.replace(/\\\\/g, '/').replace(/^\\.\\//, '');
114
+ const normalizedBase = baseValue.endsWith('/') ? baseValue.slice(0, -1) : baseValue;
115
+ for (const [filePath, module] of Object.entries(source.modules)) {
116
+ const normalizedFile = filePath.replace(/\\\\/g, '/').replace(/^\\.\\//, '');
117
+ const baseIndex = normalizedFile.indexOf(normalizedBase);
118
+ let relative = baseIndex === -1
119
+ ? normalizedFile
120
+ : normalizedFile.slice(baseIndex + normalizedBase.length);
121
+ if (!relative.startsWith('/')) relative = '/' + relative;
122
+ merged[relative] = module;
123
+ }
124
+ }
125
+ return merged;
126
+ }
127
+
128
+ function collectRouteModuleEntries(sources) {
129
+ const entries = [];
130
+ for (const [sourceIndex, source] of sources.entries()) {
131
+ const baseValue = source.base.replace(/\\\\/g, '/').replace(/^\\.\\//, '');
132
+ const normalizedBase = baseValue.endsWith('/') ? baseValue.slice(0, -1) : baseValue;
133
+ for (const [filePath, module] of Object.entries(source.modules)) {
134
+ const normalizedFile = filePath.replace(/\\\\/g, '/').replace(/^\\.\\//, '');
135
+ const baseIndex = normalizedFile.indexOf(normalizedBase);
136
+ let relativePath = baseIndex === -1
137
+ ? normalizedFile
138
+ : normalizedFile.slice(baseIndex + normalizedBase.length);
139
+ if (!relativePath.startsWith('/')) relativePath = '/' + relativePath;
140
+ entries.push({ sourceIndex, sourceName: source.name, filePath: normalizedFile, relativePath, module });
141
+ }
142
+ }
143
+ return entries;
144
+ }
145
+
146
+ const pages = mergeRouteModules([${routeRoots
147
+ .map((root, index) => `{ base: ${JSON.stringify(root.base)}, modules: pages${index} }`)
148
+ .join(", ")}]);
149
+ const layouts = mergeRouteModules([${routeRoots
150
+ .map((root, index) => `{ base: ${JSON.stringify(root.base)}, modules: layouts${index} }`)
151
+ .join(", ")}]);
152
+ const loadings = mergeRouteModules([${routeRoots
153
+ .map((root, index) => `{ base: ${JSON.stringify(root.base)}, modules: loadings${index} }`)
154
+ .join(", ")}]);
155
+ const errors = mergeRouteModules([${routeRoots
156
+ .map((root, index) => `{ base: ${JSON.stringify(root.base)}, modules: errors${index} }`)
157
+ .join(", ")}]);
158
+ const middlewares = mergeRouteModules([${routeRoots
159
+ .map((root, index) => `{ base: ${JSON.stringify(root.base)}, modules: middlewares${index} }`)
160
+ .join(", ")}]);
161
+ const apiRouteModules = collectRouteModuleEntries([${routeSourceRoots
162
+ .map((root, index) => `{ name: ${JSON.stringify(root.name)}, base: ${JSON.stringify(root.base)}, modules: apiRouteModules${index} }`)
163
+ .join(", ")}]);
164
+ const routeDefinitionModules = collectRouteModuleEntries([${routeSourceRoots
165
+ .map((root, index) => `{ name: ${JSON.stringify(root.name)}, base: ${JSON.stringify(root.sourceGlob)}, modules: routeDefinitionModules${index} }`)
166
+ .join(", ")}]);
167
+
168
+ const apiRouteMethods = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'];
169
+ const apiRouteMap = new Map();
170
+
171
+ function registerApiEndpoint(routePath, filePath, method, endpoint) {
172
+ if (!routePath || typeof endpoint !== 'function') return;
173
+ const normalizedMethod = String(method || 'GET').toUpperCase();
174
+ let route = apiRouteMap.get(routePath);
175
+ if (!route) {
176
+ route = { path: routePath, methods: [], handlers: {}, files: {} };
177
+ apiRouteMap.set(routePath, route);
178
+ }
179
+ if (!route.methods.includes(normalizedMethod)) route.methods.push(normalizedMethod);
180
+ route.handlers[normalizedMethod] = endpoint;
181
+ route.files[normalizedMethod] = filePath;
182
+ }
183
+
184
+ function getProgrammaticApiRoutes(routeModule) {
185
+ const candidates = [routeModule?.default, routeModule?.routes, routeModule?.Route];
186
+ for (const candidate of candidates) {
187
+ if (candidate?.__farmRoutes === true && Array.isArray(candidate.routes)) {
188
+ return candidate.routes.filter((route) => route?.kind === 'api');
189
+ }
190
+ if (candidate?.kind === 'api') return [candidate];
191
+ if (Array.isArray(candidate)) {
192
+ return candidate.filter((route) => route?.kind === 'api');
193
+ }
194
+ }
195
+ return Object.values(routeModule || {}).filter((route) => route?.kind === 'api');
196
+ }
197
+
198
+ function registerApiRouteSources(fileModules, definitionModules, sourceCount) {
199
+ // Process both discovery styles for each source before advancing from layers
200
+ // to the project. A later project source therefore wins regardless of whether
201
+ // either endpoint came from app/api/**/route or a routes definition file.
202
+ for (let sourceIndex = 0; sourceIndex < sourceCount; sourceIndex++) {
203
+ for (const entry of fileModules) {
204
+ if (entry.sourceIndex !== sourceIndex) continue;
205
+ const { filePath, relativePath, module: routeModule } = entry;
206
+ const routePath = relativePath.replace(/\\/route\\.[tj]sx?$/i, '') || '/api';
207
+ for (const method of apiRouteMethods) {
208
+ if (typeof routeModule?.[method] === 'function') {
209
+ registerApiEndpoint(routePath, filePath, method, routeModule[method]);
210
+ }
211
+ }
212
+ }
213
+
214
+ for (const entry of definitionModules) {
215
+ if (entry.sourceIndex !== sourceIndex) continue;
216
+ const { filePath, module: routeModule } = entry;
217
+ for (const endpoint of Object.values(routeModule)) {
218
+ if (typeof endpoint === 'function' && endpoint.__path) {
219
+ registerApiEndpoint(endpoint.__path, filePath, endpoint.__method || 'GET', endpoint);
220
+ }
221
+ }
222
+
223
+ for (const route of getProgrammaticApiRoutes(routeModule)) {
224
+ for (const [method, endpoint] of Object.entries(route.methods || {})) {
225
+ registerApiEndpoint(route.path, filePath, method, endpoint);
226
+ }
227
+ }
228
+ }
229
+ }
230
+ }
231
+
232
+ registerApiRouteSources(apiRouteModules, routeDefinitionModules, ${routeSourceRoots.length});
233
+
234
+ async function handleAPIRequest(request) {
235
+ const url = new URL(request.url);
236
+ const match = matchAPIRoute(apiRouteMap, url.pathname);
237
+ if (!match) return null;
238
+
239
+ const method = request.method.toUpperCase();
240
+ const endpoint = match.route.handlers[method];
241
+ if (!endpoint) {
242
+ return new Response(JSON.stringify({ error: 'Method Not Allowed' }), {
243
+ status: 405,
244
+ headers: { 'Content-Type': 'application/json' },
245
+ });
246
+ }
247
+
248
+ try {
249
+ return await invokeAPIRouteEndpoint(endpoint, request, match.params);
250
+ } catch (error) {
251
+ console.error('[API Error] ' + url.pathname + ':', error);
252
+ return new Response(JSON.stringify({
253
+ error: 'Internal Server Error',
254
+ }), {
255
+ status: 500,
256
+ headers: { 'Content-Type': 'application/json' },
257
+ });
258
+ }
259
+ }
260
+ const farmMiddlewareRunner = createProductionMiddlewareRunner({
261
+ modules: Object.entries(middlewares).map(([filePath, module]) => ({
262
+ path: middlewarePathToRoute(filePath),
263
+ filePath,
264
+ module,
265
+ })),
266
+ });
267
+
268
+ debug('Discovered pages:', Object.keys(pages));
269
+ debug('Discovered layouts:', Object.keys(layouts));
270
+ debug('Discovered loadings:', Object.keys(loadings));
271
+ debug('Discovered errors:', Object.keys(errors));
272
+ debug('Discovered middlewares:', Object.keys(middlewares));
273
+ debug('Discovered API routes:', Array.from(apiRouteMap.keys()));
274
+
275
+ /**
276
+ * Convert loading/error file path to route pattern (segment the boundary applies to).
277
+ * Inlined to avoid runtime dependency on plugin package; logic is simple and covered by e2e.
278
+ */
279
+ function boundaryPathToRoute(filePath, globVal, kind) {
280
+ const re = kind === 'loading' ? /\\/loading\\.[tj]sx?$/i : /\\/error\\.[tj]sx?$/i;
281
+ let route = filePath.replace(globVal, '').replace(re, '').replace(/\\\\/g, '/') || '/';
282
+ route = route.replace(/\\[([^\\]]+)\\]/g, ':$1');
283
+ return route;
284
+ }
285
+ function getMatchingLoading(pathname, globVal) {
286
+ const normalized = pathname.replace(/\\/$/, '') || '/';
287
+ const pathParts = normalized.split('/').filter(Boolean);
288
+ let best = null, bestLength = -1;
289
+ for (const filePath of Object.keys(loadings)) {
290
+ const pattern = boundaryPathToRoute(filePath, globVal, 'loading');
291
+ const patternParts = pattern === '/' ? [] : pattern.split('/').filter(Boolean);
292
+ if (patternParts.length > pathParts.length) continue;
293
+ let matches = true;
294
+ for (let i = 0; i < patternParts.length; i++) {
295
+ const p = patternParts[i], seg = pathParts[i];
296
+ if (!seg || (p.startsWith(':') && p !== ':...') || p === ':...') continue;
297
+ if (!p.startsWith(':') && p !== seg) { matches = false; break; }
298
+ }
299
+ if (matches && patternParts.length > bestLength && loadings[filePath]?.default) {
300
+ best = loadings[filePath].default; bestLength = patternParts.length;
301
+ }
302
+ }
303
+ return best;
304
+ }
305
+ function getMatchingError(pathname, globVal) {
306
+ const normalized = pathname.replace(/\\/$/, '') || '/';
307
+ const pathParts = normalized.split('/').filter(Boolean);
308
+ let best = null, bestLength = -1;
309
+ for (const filePath of Object.keys(errors)) {
310
+ const pattern = boundaryPathToRoute(filePath, globVal, 'error');
311
+ const patternParts = pattern === '/' ? [] : pattern.split('/').filter(Boolean);
312
+ if (patternParts.length > pathParts.length) continue;
313
+ let matches = true;
314
+ for (let i = 0; i < patternParts.length; i++) {
315
+ const p = patternParts[i], seg = pathParts[i];
316
+ if (!seg || (p.startsWith(':') && p !== ':...') || p === ':...') continue;
317
+ if (!p.startsWith(':') && p !== seg) { matches = false; break; }
318
+ }
319
+ if (matches && patternParts.length > bestLength && errors[filePath]?.default) {
320
+ best = errors[filePath].default; bestLength = patternParts.length;
321
+ }
322
+ }
323
+ return best;
324
+ }
325
+
326
+ /**
327
+ * Convert middleware file path to route path
328
+ * e.g., '/src/middleware.ts' -> '/'
329
+ * e.g., '/src/counter/middleware.ts' -> '/counter'
330
+ */
331
+ function middlewarePathToRoute(filePath) {
332
+ let route = filePath
333
+ .replace('', '')
334
+ .replace(/\\/middleware\\.[tj]sx?$/, '') || '/';
335
+ return route;
336
+ }
337
+
338
+ /**
339
+ * Execute middleware chain for a request
340
+ */
341
+ async function executeMiddleware(request) {
342
+ return farmMiddlewareRunner(request);
343
+ }
344
+
345
+ /**
346
+ * Convert file path to route pattern
347
+ * e.g., '/src/about/page.tsx' -> '/about'
348
+ * e.g., '/src/blog/[slug]/page.tsx' -> '/blog/:slug'
349
+ */
350
+ function filePathToRoute(filePath) {
351
+ let route = filePath
352
+ .replace('', '')
353
+ .replace(/\\/page\\.[tj]sx?$/, '')
354
+ .replace(/\\/page$/, '') || '/';
355
+
356
+ // Convert [param] to :param for matching
357
+ route = route.replace(/\\[([^\\]]+)\\]/g, ':$1');
358
+
359
+ return route;
360
+ }
361
+
362
+ /**
363
+ * Route-level error boundary (React class component for getDerivedStateFromError)
364
+ * Next.js-style: error.tsx receives { error, reset }.
365
+ */
366
+ const RouteErrorBoundary = React.Component
367
+ ? class RouteErrorBoundary extends React.Component {
368
+ static getDerivedStateFromError(error) {
369
+ return { hasError: true, error };
370
+ }
371
+ constructor(props) {
372
+ super(props);
373
+ this.state = { hasError: false, error: null };
374
+ }
375
+ render() {
376
+ if (this.state.hasError) {
377
+ const Fallback = this.props.Fallback;
378
+ const reset = () => this.setState({ hasError: false, error: null });
379
+ return React.createElement(Fallback, { ...this.props.fallbackProps, error: this.state.error, reset });
380
+ }
381
+ return this.props.children;
382
+ }
383
+ }
384
+ : function ServerPassthroughBoundary({ children }) {
385
+ return children;
386
+ };
387
+
388
+ /**
389
+ * Match a URL pathname to a route pattern
390
+ * Supports dynamic segments like :id and catch-all like *
391
+ */
392
+ function matchPath(pattern, pathname) {
393
+ const patternParts = pattern.split('/').filter(Boolean);
394
+ const pathParts = pathname.split('/').filter(Boolean);
395
+
396
+ // Special case for root
397
+ if (pattern === '/' && pathname === '/') {
398
+ return { params: {} };
399
+ }
400
+
401
+ if (patternParts.length !== pathParts.length) {
402
+ // Check for catch-all
403
+ const lastPattern = patternParts[patternParts.length - 1];
404
+ if (!lastPattern?.startsWith(':...')) {
405
+ return null;
406
+ }
407
+ }
408
+
409
+ const params = {};
410
+
411
+ for (let i = 0; i < patternParts.length; i++) {
412
+ const patternPart = patternParts[i];
413
+ const pathPart = pathParts[i];
414
+
415
+ if (patternPart.startsWith(':...')) {
416
+ // Catch-all segment
417
+ const paramName = patternPart.slice(4);
418
+ params[paramName] = pathParts.slice(i).join('/');
419
+ return { params };
420
+ }
421
+
422
+ if (patternPart.startsWith(':')) {
423
+ // Dynamic segment
424
+ const paramName = patternPart.slice(1);
425
+ params[paramName] = pathPart;
426
+ } else if (patternPart !== pathPart) {
427
+ // Static segment mismatch
428
+ return null;
429
+ }
430
+ }
431
+
432
+ return { params };
433
+ }
434
+
435
+ /**
436
+ * Simple file-based router
437
+ * Converts URL pathname to matching page component
438
+ */
439
+ function matchRoute(pathname) {
440
+ const normalized = pathname.replace(/\\/$/, '') || '/';
441
+
442
+ for (const filePath of Object.keys(pages)) {
443
+ const pattern = filePathToRoute(filePath);
444
+ const match = matchPath(pattern, normalized);
445
+
446
+ if (match) {
447
+ debug('Matched route:', pattern, 'for path:', normalized);
448
+ return {
449
+ Page: pages[filePath].default,
450
+ pattern: filePath,
451
+ params: match.params,
452
+ pageMetadata: pages[filePath].metadata,
453
+ };
454
+ }
455
+ }
456
+
457
+ debug('No route matched for:', normalized);
458
+ return null;
459
+ }
460
+
461
+ function mergeDocumentMetadata(...sources) {
462
+ const metadata = {};
463
+ for (const source of sources) {
464
+ if (typeof source?.title === 'string') metadata.title = source.title;
465
+ if (typeof source?.description === 'string') metadata.description = source.description;
466
+ }
467
+ return metadata;
468
+ }
469
+
470
+ /**
471
+ * Find every applicable layout module from root to the page directory.
472
+ * Rendering still uses the nearest layout, while document metadata inherits
473
+ * through the complete root -> nested layouts -> page chain.
474
+ */
475
+ function getLayoutModules(pageFilePath) {
476
+ const tryKeys = (...keys) => {
477
+ for (const k of keys) {
478
+ if (layouts[k]?.default) return layouts[k];
479
+ }
480
+ return null;
481
+ };
482
+ const dir = pageFilePath.replace(/\\/page\\.[tj]sx?$/i, '');
483
+ const extensions = ['tsx', 'jsx', 'ts', 'js'];
484
+ const parts = dir.split('/').filter(Boolean);
485
+ const matches = [];
486
+
487
+ for (let depth = 0; depth <= parts.length; depth++) {
488
+ const relativeDir = parts.slice(0, depth).join('/');
489
+ const absoluteDir = relativeDir ? '/' + relativeDir : '';
490
+ let matchedLayout = null;
491
+ for (const ext of extensions) {
492
+ matchedLayout = tryKeys(
493
+ absoluteDir + '/layout.' + ext,
494
+ relativeDir ? relativeDir + '/layout.' + ext : 'layout.' + ext,
495
+ );
496
+ if (matchedLayout) break;
497
+ }
498
+ if (matchedLayout && !matches.includes(matchedLayout)) matches.push(matchedLayout);
499
+ }
500
+
501
+ return matches;
502
+ }
503
+
504
+ /**
505
+ * Main request handler - entry point for both dev and production (Nitro).
506
+ * Exported as { fetch: handler } for the RSC/Nitro contract (see vite-plugin-rsc-deploy-example).
507
+ */
508
+ async function handleFarmRequest(request) {
509
+ let url = new URL(request.url);
510
+ try {
511
+ debug('Handling request:', request.method, url.pathname);
512
+
513
+ const deploymentMismatch = getFarmDeploymentMismatch(request, farmDeploymentId);
514
+ if (deploymentMismatch) {
515
+ return createFarmDeploymentMismatchResponse(deploymentMismatch);
516
+ }
517
+
518
+ const initialApiMatch = matchAPIRoute(apiRouteMap, url.pathname);
519
+ const isInitialApiRequest = Boolean(initialApiMatch) ||
520
+ url.pathname === '/api' || url.pathname.startsWith('/api/');
521
+
522
+ ${ctx.actionsEnabled
523
+ ? `if (request.method === 'POST' && !isInitialApiRequest) {
524
+ try {
525
+ validateServerActionRequest(request, serverActionSecurity);
526
+ } catch (error) {
527
+ const rejection = createServerActionRequestErrorResponse(error);
528
+ if (rejection) return rejection;
529
+ return new Response('Bad Request', {
530
+ status: 400,
531
+ headers: {
532
+ 'Cache-Control': 'no-store',
533
+ 'Content-Type': 'text/plain; charset=utf-8',
534
+ 'X-Content-Type-Options': 'nosniff',
535
+ },
536
+ });
537
+ }
538
+ }`
539
+ : ""}
540
+
541
+ // Execute middleware first
542
+ const middlewareResult = await executeMiddleware(request);
543
+
544
+ // If middleware handled the request (e.g., redirect, auth), return the response
545
+ if (middlewareResult.response) {
546
+ return middlewareResult.response;
547
+ }
548
+
549
+ request = middlewareResult.request;
550
+ url = new URL(request.url);
551
+
552
+ // Extract middleware data and headers for page rendering
553
+ const middlewareData = Object.fromEntries(middlewareResult.data);
554
+ const middlewareContext = middlewareResult.context;
555
+ const middlewareHeaders = new Headers(middlewareResult.headers);
556
+ if (middlewareResult.data.size || middlewareContext.size) {
557
+ middlewareHeaders.set('cache-control', 'private, no-store');
558
+ }
559
+
560
+ // A rewrite replaces the Request. Re-enter request context so downstream
561
+ // APIs and server components observe the rewritten URL.
562
+ return await _runWithCurrentRequest(request, () =>
563
+ _runWithMiddlewareData(middlewareResult.data, () =>
564
+ _runWithMiddlewareContext(middlewareContext, async () => {
565
+ const glob = '';
566
+
567
+ const apiResponse = await handleAPIRequest(request.clone());
568
+ if (apiResponse) {
569
+ return applyProductionMiddlewareHeaders(apiResponse, middlewareHeaders);
570
+ }
571
+ if (url.pathname === '/api' || url.pathname.startsWith('/api/')) {
572
+ return applyProductionMiddlewareHeaders(new Response(
573
+ JSON.stringify({ error: 'API route not found', pathname: url.pathname }),
574
+ { status: 404, headers: { 'Content-Type': 'application/json' } },
575
+ ), middlewareHeaders);
576
+ }
577
+ `;
578
+ // If actions enabled, add action handling before rendering
579
+ if (ctx.actionsEnabled) {
580
+ code += `
581
+ // Variables to hold action results
582
+ let returnValue, formState, temporaryReferences;
583
+
584
+ // Handle POST requests (server actions)
585
+ if (request.method === 'POST') {
586
+ const actionId = request.headers.get('x-farm-action-id');
587
+ let preparedActionRequest;
588
+
589
+ try {
590
+ preparedActionRequest = await prepareServerActionRequest(
591
+ request,
592
+ serverActionSecurity,
593
+ actionId ? 'javascript' : 'form',
594
+ actionId,
595
+ );
596
+ } catch (error) {
597
+ if (request.signal.aborted) {
598
+ return new Response(null, { status: 499, headers: { 'Cache-Control': 'no-store' } });
599
+ }
600
+ const rejection = createServerActionRequestErrorResponse(error);
601
+ if (rejection) return rejection;
602
+ console.error('[Farm.js] Failed to read server action request:', error);
603
+ return new Response('Bad Request', {
604
+ status: 400,
605
+ headers: {
606
+ 'Cache-Control': 'no-store',
607
+ 'Content-Type': 'text/plain; charset=utf-8',
608
+ 'X-Content-Type-Options': 'nosniff',
609
+ },
610
+ });
611
+ }
612
+
613
+ if (actionId) {
614
+ // Action called via JavaScript (after hydration)
615
+ debug('Executing server action:', actionId);
616
+ temporaryReferences = createTemporaryReferenceSet();
617
+ let args, action;
618
+
619
+ try {
620
+ args = await decodeReply(preparedActionRequest.body, { temporaryReferences });
621
+ action = await loadServerAction(actionId);
622
+ } catch (error) {
623
+ console.error('[Farm.js] Invalid server action request:', error);
624
+ return new Response('Bad Request', {
625
+ status: 400,
626
+ headers: {
627
+ 'Cache-Control': 'no-store',
628
+ 'Content-Type': 'text/plain; charset=utf-8',
629
+ 'X-Content-Type-Options': 'nosniff',
630
+ },
631
+ });
632
+ }
633
+
634
+ try {
635
+ const result = await runWithServerActionRequest(request, async () => {
636
+ const data = await action.apply(null, args);
637
+ return { data, invalidations: getServerActionInvalidations() };
638
+ });
639
+ returnValue = { ok: true, data: result.data, invalidations: result.invalidations };
640
+ debug('Server action succeeded:', actionId);
641
+ } catch (e) {
642
+ if (request.signal.aborted) {
643
+ return new Response(null, { status: 499, headers: { 'Cache-Control': 'no-store' } });
644
+ }
645
+ console.error('[Farm.js] Server action failed:', e);
646
+ returnValue = { ok: false, data: sanitizeServerActionError(e) };
647
+ debug('Server action failed:', actionId, e);
648
+ }
649
+ } else {
650
+ // Progressive enhancement (form submitted before JS loaded)
651
+ debug('Handling progressive enhancement form submission');
652
+ const formData = preparedActionRequest.body;
653
+ let decoded;
654
+
655
+ try {
656
+ decoded = await decodeAction(formData);
657
+ if (typeof decoded !== 'function') throw new Error('Missing form action');
658
+ } catch (error) {
659
+ console.error('[Farm.js] Invalid form action request:', error);
660
+ return new Response('Bad Request', {
661
+ status: 400,
662
+ headers: {
663
+ 'Cache-Control': 'no-store',
664
+ 'Content-Type': 'text/plain; charset=utf-8',
665
+ 'X-Content-Type-Options': 'nosniff',
666
+ },
667
+ });
668
+ }
669
+
670
+ try {
671
+ const result = await runWithServerActionRequest(request, () => decoded());
672
+ formState = await decodeFormState(result, formData);
673
+ } catch (e) {
674
+ if (request.signal.aborted) {
675
+ return new Response(null, { status: 499, headers: { 'Cache-Control': 'no-store' } });
676
+ }
677
+ console.error('[Farm.js] Form action failed:', e);
678
+ debug('Form action failed:', e);
679
+ return new Response('Server function failed', {
680
+ status: 500,
681
+ headers: {
682
+ 'Cache-Control': 'no-store',
683
+ 'Content-Type': 'text/plain; charset=utf-8',
684
+ 'X-Content-Type-Options': 'nosniff',
685
+ },
686
+ });
687
+ }
688
+ }
689
+ }
690
+ `;
691
+ }
692
+ // Route matching and rendering
693
+ code += `
694
+ // Match the URL to a page component
695
+ const matched = matchRoute(url.pathname);
696
+
697
+ if (!matched) {
698
+ debug('404 - No route found for:', url.pathname);
699
+ return new Response('Not Found', { status: 404 });
700
+ }
701
+
702
+ const { Page, pattern, params, pageMetadata } = matched;
703
+ const LayoutModules = getLayoutModules(pattern);
704
+ const LayoutModule = LayoutModules[LayoutModules.length - 1];
705
+ const Layout = LayoutModule?.default || (function PassThrough({ children }) { return children; });
706
+ const metadata = mergeDocumentMetadata(
707
+ ...LayoutModules.map((layoutModule) => layoutModule.metadata),
708
+ pageMetadata,
709
+ );
710
+ const configuredRoutesDir = ${ctx.routesDir === undefined ? "undefined" : JSON.stringify(ctx.routesDir)};
711
+ const routesDir = configuredRoutesDir === undefined ? 'app' : configuredRoutesDir.trim();
712
+ const routesPath = routesDir ? '/' + routesDir : '';
713
+ const globalsCssPath = '/${ctx.srcDir}' + routesPath + '/globals.css';
714
+
715
+ // Parse search params
716
+ const searchParams = Object.fromEntries(url.searchParams);
717
+
718
+ // Page props passed to components (includes middleware shared data)
719
+ const pageProps = { params, searchParams, middlewareData };
720
+
721
+ debug('Rendering page:', pattern, 'with props:', pageProps);
722
+
723
+ // Helper to create elements without JSX
724
+ const h = React.createElement;
725
+
726
+ // Render page content - handle async components
727
+ let pageContent;
728
+ if (Page.constructor.name === 'AsyncFunction' || Page.toString().includes('async')) {
729
+ pageContent = await Page(pageProps);
730
+ } else {
731
+ pageContent = h(Page, pageProps);
732
+ }
733
+
734
+ // Route-level loading boundary: wrap in Suspense so async content shows fallback
735
+ const LoadingComponent = getMatchingLoading(url.pathname, glob);
736
+ if (LoadingComponent) {
737
+ const loadingFallback = h(LoadingComponent, { params, path: url.pathname });
738
+ pageContent = h(React.Suspense, { fallback: loadingFallback }, pageContent);
739
+ }
740
+
741
+ // Route-level error boundary (Next.js error.tsx): catches render errors in this segment
742
+ const ErrorComponent = getMatchingError(url.pathname, glob);
743
+ if (ErrorComponent) {
744
+ pageContent = h(RouteErrorBoundary, {
745
+ Fallback: ErrorComponent,
746
+ fallbackProps: { params, path: url.pathname, searchParams },
747
+ children: pageContent,
748
+ });
749
+ }
750
+
751
+ // Render layout
752
+ let layoutContent;
753
+ if (Layout.constructor.name === 'AsyncFunction' || Layout.toString().includes('async')) {
754
+ layoutContent = await Layout({ children: pageContent });
755
+ } else {
756
+ layoutContent = h(Layout, null, pageContent);
757
+ }
758
+
759
+ // Single wrapper so #root has exactly one child (avoids duplicate block / "two pages" in DOM).
760
+ const rootInner = h('div', { 'data-farm-root': 'true' }, layoutContent);
761
+ // Build the full page. root = full document (for SSR). rootContent = single wrapper + layout (for client hydration).
762
+ const payload = {
763
+ root: h('html', null,
764
+ h('head', null,
765
+ h('meta', { charSet: 'utf-8' }),
766
+ h('meta', { name: 'viewport', content: 'width=device-width, initial-scale=1' }),
767
+ h('link', { rel: 'icon', href: 'data:,' }),
768
+ metadata?.title ? h('title', null, metadata.title) : null,
769
+ metadata?.description ? h('meta', { name: 'description', content: metadata.description }) : null
770
+ ),
771
+ h('body', null,
772
+ h('div', { id: 'root' }, rootInner)
773
+ )
774
+ ),
775
+ rootContent: rootInner,
776
+ metadata: {
777
+ title: typeof metadata?.title === 'string' ? metadata.title : undefined,
778
+ description: typeof metadata?.description === 'string' ? metadata.description : undefined,
779
+ },
780
+ `;
781
+ // Include action results in payload if actions are enabled
782
+ if (ctx.actionsEnabled) {
783
+ code += ` returnValue,
784
+ formState,
785
+ `;
786
+ }
787
+ code += ` };
788
+
789
+ // Check if this is a client-side navigation request
790
+ // Client sends Accept: text/x-component header for RSC requests
791
+ const acceptHeader = request.headers.get('accept') || '';
792
+
793
+ if (renderToReadableStream) {
794
+ // Full RSC mode with streaming
795
+ debug('Using RSC streaming mode');
796
+
797
+ const rscStream = renderToReadableStream(payload${ctx.actionsEnabled ? ", { temporaryReferences }" : ""});
798
+
799
+ if (acceptHeader.includes('text/x-component')) {
800
+ debug('Returning RSC stream for client navigation');
801
+ // Merge middleware headers with response headers
802
+ const responseHeaders = new Headers(middlewareHeaders);
803
+ responseHeaders.set('content-type', 'text/x-component');
804
+ applyActionResponseHeaders(responseHeaders, request);
805
+ return new Response(rscStream, { headers: responseHeaders });
806
+ }
807
+
808
+ // For initial page load, delegate to SSR to produce HTML
809
+ let ssr;
810
+ if (typeof import.meta.viteRsc?.loadModule === 'function') {
811
+ debug('Delegating to SSR for initial HTML render');
812
+ ssr = await import.meta.viteRsc.loadModule('ssr', 'index');
813
+ } else if (typeof globalThis.__VITE_RSC_LOAD_SSR__ === 'function') {
814
+ // Production serverless (e.g. Vercel): wrapper sets this before loading the handler
815
+ debug('Delegating to SSR (runtime loader)');
816
+ ssr = await globalThis.__VITE_RSC_LOAD_SSR__();
817
+ }
818
+ if (ssr) {
819
+ // Pass payload so SSR can stream the tree (Suspense fallback first, then content)
820
+ const html = await ssr.renderHTML({ payload, rscStream }${ctx.actionsEnabled ? ", { formState }" : ""});
821
+ // Merge middleware headers with response headers
822
+ const responseHeaders = new Headers(middlewareHeaders);
823
+ responseHeaders.set('content-type', 'text/html');
824
+ applyActionResponseHeaders(responseHeaders, request);
825
+ return new Response(html, { headers: responseHeaders });
826
+ }
827
+ }
828
+
829
+ // Fallback only when renderToReadableStream is not available (should not happen in RSC build)
830
+ debug('Using fallback SSR mode (react-dom/server)');
831
+ const pageWithScript = h('html', null,
832
+ h('head', null,
833
+ h('meta', { charSet: 'utf-8' }),
834
+ h('meta', { name: 'viewport', content: 'width=device-width, initial-scale=1' }),
835
+ h('link', { rel: 'icon', href: 'data:,' }),
836
+ metadata?.title ? h('title', null, metadata.title) : null,
837
+ metadata?.description ? h('meta', { name: 'description', content: metadata.description }) : null,
838
+ h('link', { rel: 'stylesheet', href: globalsCssPath, as: 'style', precedence: 'default' }),
839
+ h('script', { type: 'module', src: '/@vite/client' })
840
+ ),
841
+ h('body', null,
842
+ h('div', { id: 'root' }, layoutContent)
843
+ )
844
+ );
845
+ const renderToString = (await import('react-dom/server')).renderToString;
846
+ const html = '<!DOCTYPE html>' + renderToString(pageWithScript);
847
+ // Merge middleware headers with response headers
848
+ const responseHeaders = new Headers(middlewareHeaders);
849
+ responseHeaders.set('content-type', 'text/html');
850
+ applyActionResponseHeaders(responseHeaders, request);
851
+ return new Response(html, { headers: responseHeaders });
852
+ })
853
+ )
854
+ );
855
+ } catch (err) {
856
+ console.error('[RSC] Handler error:', err);
857
+ if (request.method === 'POST') {
858
+ if (request.signal.aborted) {
859
+ return new Response(null, { status: 499, headers: { 'Cache-Control': 'no-store' } });
860
+ }
861
+ return new Response('Server function failed', {
862
+ status: 500,
863
+ headers: {
864
+ 'Cache-Control': 'no-store',
865
+ 'Content-Type': 'text/plain; charset=utf-8',
866
+ 'X-Content-Type-Options': 'nosniff',
867
+ },
868
+ });
869
+ }
870
+ // If route has error.tsx, render it (Next.js-style: SSR error goes to route error boundary)
871
+ const pathname = url.pathname.replace(/\\/$/, '') || '/';
872
+ const ErrorComponent = getMatchingError(pathname, glob);
873
+ if (ErrorComponent) {
874
+ try {
875
+ const matched = matchRoute(pathname);
876
+ const layoutPattern = matched ? matched.pattern : null;
877
+ const LayoutModules = matched ? getLayoutModules(layoutPattern) : [];
878
+ const Layout = LayoutModules[LayoutModules.length - 1]?.default;
879
+ const LayoutComp = Layout || (function PassThrough({ children }) { return children; });
880
+ const errParams = matched ? matched.params : {};
881
+ const errSearchParams = Object.fromEntries(url.searchParams);
882
+ const errorElement = h(ErrorComponent, {
883
+ error: err,
884
+ reset: () => {},
885
+ params: errParams,
886
+ path: pathname,
887
+ searchParams: errSearchParams,
888
+ });
889
+ const layoutContent = LayoutComp.constructor.name === 'AsyncFunction' || LayoutComp.toString().includes('async')
890
+ ? await LayoutComp({ children: errorElement })
891
+ : h(LayoutComp, null, errorElement);
892
+ const rootInner = h('div', { 'data-farm-root': 'true' }, layoutContent);
893
+ const doc = h('html', null,
894
+ h('head', null,
895
+ h('meta', { charSet: 'utf-8' }),
896
+ h('meta', { name: 'viewport', content: 'width=device-width, initial-scale=1' }),
897
+ h('link', { rel: 'icon', href: 'data:,' }),
898
+ h('title', null, 'Error'),
899
+ h('link', { rel: 'stylesheet', href: globalsCssPath, as: 'style', precedence: 'default' })
900
+ ),
901
+ h('body', null, h('div', { id: 'root' }, rootInner))
902
+ );
903
+ const renderToString = (await import('react-dom/server')).renderToString;
904
+ const html = '<!DOCTYPE html>' + renderToString(doc);
905
+ return new Response(html, { status: 500, headers: { 'Content-Type': 'text/html; charset=utf-8' } });
906
+ } catch (e) {
907
+ console.error('[RSC] Error boundary render failed:', e);
908
+ }
909
+ }
910
+ const message = 'Internal Server Error';
911
+ return new Response(JSON.stringify({ error: true, url: request.url, status: 500, message }), {
912
+ status: 500,
913
+ headers: { 'Content-Type': 'application/json' },
914
+ });
915
+ }
916
+ }
917
+
918
+ async function handler(request, context) {
919
+ return _runWithCurrentRequest(request, () =>
920
+ _runWithAfterRequest(request, () => handleFarmRequest(request), context)
921
+ );
922
+ }
923
+
924
+ export default { fetch: handler };
925
+
926
+ // Enable HMR - when server files change, accept the update
927
+ if (import.meta.hot) {
928
+ import.meta.hot.accept();
929
+ }
930
+ `;
931
+ return code;
932
+ }