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