@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,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
|
+
}
|