@farm.js/plugin 0.1.0-beta.1 → 0.1.0-beta.100
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/api/index.d.ts +2 -0
- package/dist/api/index.d.ts.map +1 -1
- package/dist/api/index.js +25 -95
- 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 +1 -1
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +1 -1
- package/dist/middleware/index.d.ts.map +1 -1
- package/dist/middleware/index.js +28 -53
- 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/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/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 +1 -0
- package/dist/rsc/entries/client.d.ts.map +1 -1
- package/dist/rsc/entries/client.js +42 -5
- 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.map +1 -1
- package/dist/rsc/entries/rsc.js +187 -189
- package/dist/rsc/entries/ssr.d.ts.map +1 -1
- package/dist/rsc/entries/ssr.js +44 -74
- package/dist/rsc/index.d.ts +15 -2
- package/dist/rsc/index.d.ts.map +1 -1
- package/dist/rsc/index.js +167 -69
- package/dist/rsc/nitro-build.d.ts.map +1 -1
- package/dist/rsc/nitro-build.js +28 -8
- package/dist/rsc/optimized-boundary.d.ts +10 -1
- package/dist/rsc/optimized-boundary.d.ts.map +1 -1
- package/dist/rsc/optimized-boundary.js +229 -1
- 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.js +1 -1
- 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 +9 -0
- package/dist/rsc/types.d.ts.map +1 -1
- package/dist/rsc/vite-plugin-nitro.d.ts +20 -7
- package/dist/rsc/vite-plugin-nitro.d.ts.map +1 -1
- package/dist/rsc/vite-plugin-nitro.js +51 -66
- package/package.json +15 -8
package/dist/rsc/entries/rsc.js
CHANGED
|
@@ -25,6 +25,8 @@ export function generateRscEntry(ctx) {
|
|
|
25
25
|
const debugLog = `// Debug disabled`;
|
|
26
26
|
let code = `
|
|
27
27
|
import React from 'react';
|
|
28
|
+
import ServerErrorFallback from '/.farm/rsc-entries/error-fallback.tsx';
|
|
29
|
+
import { registerAPIRouteShape } from '@farm.js/core/api/runtime';
|
|
28
30
|
import {
|
|
29
31
|
renderToReadableStream,
|
|
30
32
|
`;
|
|
@@ -49,11 +51,24 @@ import {
|
|
|
49
51
|
applyProductionMiddlewareHeaders,
|
|
50
52
|
createProductionMiddlewareRunner,
|
|
51
53
|
} from '@farm.js/core/middleware';
|
|
52
|
-
import {
|
|
54
|
+
import {
|
|
55
|
+
getAllowedAPIRouteMethods,
|
|
56
|
+
invokeAPIRouteEndpoint,
|
|
57
|
+
isFarmAPIPathname,
|
|
58
|
+
matchAPIRouteAtBasePath,
|
|
59
|
+
} from '@farm.js/core/api/runtime';
|
|
53
60
|
import { _runWithAfterRequest } from '@farm.js/core/after';
|
|
54
|
-
import { _runWithCurrentRequest } from '@farm.js/core/internal/production-runtime';
|
|
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';
|
|
55
69
|
|
|
56
70
|
const farmDeploymentId = ${JSON.stringify(ctx.deploymentId)};
|
|
71
|
+
const farmApiBasePath = ${JSON.stringify(ctx.apiBasePath ?? "/api")};
|
|
57
72
|
`;
|
|
58
73
|
if (ctx.actionsEnabled) {
|
|
59
74
|
code += `import {
|
|
@@ -76,6 +91,9 @@ function debug(...args) {
|
|
|
76
91
|
}
|
|
77
92
|
|
|
78
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');
|
|
79
97
|
headers.set('x-farm-deployment-id', farmDeploymentId);
|
|
80
98
|
const accept = request.headers.get('accept') || '';
|
|
81
99
|
if (request.method === 'GET' && !accept.includes('text/x-component')) {
|
|
@@ -165,20 +183,30 @@ const routeDefinitionModules = collectRouteModuleEntries([${routeSourceRoots
|
|
|
165
183
|
.map((root, index) => `{ name: ${JSON.stringify(root.name)}, base: ${JSON.stringify(root.sourceGlob)}, modules: routeDefinitionModules${index} }`)
|
|
166
184
|
.join(", ")}]);
|
|
167
185
|
|
|
168
|
-
const apiRouteMethods = ['GET', 'HEAD', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'];
|
|
186
|
+
const apiRouteMethods = ['GET', 'HEAD', 'QUERY', 'POST', 'PUT', 'DELETE', 'PATCH', 'OPTIONS'];
|
|
169
187
|
const apiRouteMap = new Map();
|
|
188
|
+
const apiRouteShapes = new Map();
|
|
170
189
|
|
|
171
|
-
function registerApiEndpoint(routePath, filePath, method, endpoint) {
|
|
190
|
+
function registerApiEndpoint(routePath, filePath, method, endpoint, sourceIndex) {
|
|
172
191
|
if (!routePath || typeof endpoint !== 'function') return;
|
|
192
|
+
const replacedPath = registerAPIRouteShape(apiRouteShapes, routePath, filePath, sourceIndex);
|
|
193
|
+
if (replacedPath) apiRouteMap.delete(replacedPath);
|
|
173
194
|
const normalizedMethod = String(method || 'GET').toUpperCase();
|
|
174
195
|
let route = apiRouteMap.get(routePath);
|
|
175
196
|
if (!route) {
|
|
176
|
-
route = { path: routePath, methods: [], handlers: {}, files: {} };
|
|
197
|
+
route = { path: routePath, methods: [], handlers: {}, files: {}, sources: {} };
|
|
177
198
|
apiRouteMap.set(routePath, route);
|
|
178
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
|
+
}
|
|
179
206
|
if (!route.methods.includes(normalizedMethod)) route.methods.push(normalizedMethod);
|
|
180
207
|
route.handlers[normalizedMethod] = endpoint;
|
|
181
208
|
route.files[normalizedMethod] = filePath;
|
|
209
|
+
route.sources[normalizedMethod] = sourceIndex;
|
|
182
210
|
}
|
|
183
211
|
|
|
184
212
|
function getProgrammaticApiRoutes(routeModule) {
|
|
@@ -206,7 +234,7 @@ function registerApiRouteSources(fileModules, definitionModules, sourceCount) {
|
|
|
206
234
|
const routePath = relativePath.replace(/\\/route\\.[tj]sx?$/i, '') || '/api';
|
|
207
235
|
for (const method of apiRouteMethods) {
|
|
208
236
|
if (typeof routeModule?.[method] === 'function') {
|
|
209
|
-
registerApiEndpoint(routePath, filePath, method, routeModule[method]);
|
|
237
|
+
registerApiEndpoint(routePath, filePath, method, routeModule[method], sourceIndex);
|
|
210
238
|
}
|
|
211
239
|
}
|
|
212
240
|
}
|
|
@@ -216,13 +244,13 @@ function registerApiRouteSources(fileModules, definitionModules, sourceCount) {
|
|
|
216
244
|
const { filePath, module: routeModule } = entry;
|
|
217
245
|
for (const endpoint of Object.values(routeModule)) {
|
|
218
246
|
if (typeof endpoint === 'function' && endpoint.__path) {
|
|
219
|
-
registerApiEndpoint(endpoint.__path, filePath, endpoint.__method || 'GET', endpoint);
|
|
247
|
+
registerApiEndpoint(endpoint.__path, filePath, endpoint.__method || 'GET', endpoint, sourceIndex);
|
|
220
248
|
}
|
|
221
249
|
}
|
|
222
250
|
|
|
223
251
|
for (const route of getProgrammaticApiRoutes(routeModule)) {
|
|
224
252
|
for (const [method, endpoint] of Object.entries(route.methods || {})) {
|
|
225
|
-
registerApiEndpoint(route.path, filePath, method, endpoint);
|
|
253
|
+
registerApiEndpoint(route.path, filePath, method, endpoint, sourceIndex);
|
|
226
254
|
}
|
|
227
255
|
}
|
|
228
256
|
}
|
|
@@ -233,15 +261,19 @@ registerApiRouteSources(apiRouteModules, routeDefinitionModules, ${routeSourceRo
|
|
|
233
261
|
|
|
234
262
|
async function handleAPIRequest(request) {
|
|
235
263
|
const url = new URL(request.url);
|
|
236
|
-
const match =
|
|
264
|
+
const match = matchAPIRouteAtBasePath(apiRouteMap, url.pathname, farmApiBasePath);
|
|
237
265
|
if (!match) return null;
|
|
238
266
|
|
|
239
267
|
const method = request.method.toUpperCase();
|
|
240
|
-
const endpoint = match.route.handlers[method]
|
|
268
|
+
const endpoint = match.route.handlers[method] ??
|
|
269
|
+
(method === 'HEAD' ? match.route.handlers.GET : undefined);
|
|
241
270
|
if (!endpoint) {
|
|
242
271
|
return new Response(JSON.stringify({ error: 'Method Not Allowed' }), {
|
|
243
272
|
status: 405,
|
|
244
|
-
headers: {
|
|
273
|
+
headers: {
|
|
274
|
+
'Allow': getAllowedAPIRouteMethods(match.route).join(', '),
|
|
275
|
+
'Content-Type': 'application/json',
|
|
276
|
+
},
|
|
245
277
|
});
|
|
246
278
|
}
|
|
247
279
|
|
|
@@ -272,57 +304,6 @@ debug('Discovered errors:', Object.keys(errors));
|
|
|
272
304
|
debug('Discovered middlewares:', Object.keys(middlewares));
|
|
273
305
|
debug('Discovered API routes:', Array.from(apiRouteMap.keys()));
|
|
274
306
|
|
|
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
307
|
/**
|
|
327
308
|
* Convert middleware file path to route path
|
|
328
309
|
* e.g., '/src/middleware.ts' -> '/'
|
|
@@ -345,7 +326,7 @@ async function executeMiddleware(request) {
|
|
|
345
326
|
/**
|
|
346
327
|
* Convert file path to route pattern
|
|
347
328
|
* e.g., '/src/about/page.tsx' -> '/about'
|
|
348
|
-
* e.g., '/
|
|
329
|
+
* e.g., '/blog/[slug]/page.tsx' -> '/blog/[slug]'
|
|
349
330
|
*/
|
|
350
331
|
function filePathToRoute(filePath) {
|
|
351
332
|
let route = filePath
|
|
@@ -353,9 +334,6 @@ function filePathToRoute(filePath) {
|
|
|
353
334
|
.replace(/\\/page\\.[tj]sx?$/, '')
|
|
354
335
|
.replace(/\\/page$/, '') || '/';
|
|
355
336
|
|
|
356
|
-
// Convert [param] to :param for matching
|
|
357
|
-
route = route.replace(/\\[([^\\]]+)\\]/g, ':$1');
|
|
358
|
-
|
|
359
337
|
return route;
|
|
360
338
|
}
|
|
361
339
|
|
|
@@ -385,52 +363,14 @@ const RouteErrorBoundary = React.Component
|
|
|
385
363
|
return children;
|
|
386
364
|
};
|
|
387
365
|
|
|
388
|
-
|
|
389
|
-
|
|
390
|
-
|
|
391
|
-
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
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
|
-
}
|
|
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));
|
|
434
374
|
|
|
435
375
|
/**
|
|
436
376
|
* Simple file-based router
|
|
@@ -439,17 +379,16 @@ function matchPath(pattern, pathname) {
|
|
|
439
379
|
function matchRoute(pathname) {
|
|
440
380
|
const normalized = pathname.replace(/\\/$/, '') || '/';
|
|
441
381
|
|
|
442
|
-
for (const filePath of
|
|
443
|
-
const
|
|
444
|
-
const match = matchPath(pattern, normalized);
|
|
382
|
+
for (const { filePath, module, segments } of pageRoutes) {
|
|
383
|
+
const match = matchFarmPageRoute(normalized, segments);
|
|
445
384
|
|
|
446
|
-
if (match) {
|
|
447
|
-
debug('Matched route:',
|
|
385
|
+
if (match.matches) {
|
|
386
|
+
debug('Matched route:', filePath, 'for path:', normalized);
|
|
448
387
|
return {
|
|
449
|
-
Page:
|
|
388
|
+
Page: module.default,
|
|
450
389
|
pattern: filePath,
|
|
451
390
|
params: match.params,
|
|
452
|
-
pageMetadata:
|
|
391
|
+
pageMetadata: module.metadata,
|
|
453
392
|
};
|
|
454
393
|
}
|
|
455
394
|
}
|
|
@@ -468,14 +407,14 @@ function mergeDocumentMetadata(...sources) {
|
|
|
468
407
|
}
|
|
469
408
|
|
|
470
409
|
/**
|
|
471
|
-
* Find
|
|
472
|
-
*
|
|
473
|
-
*
|
|
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.
|
|
474
413
|
*/
|
|
475
|
-
function
|
|
414
|
+
function getRouteModules(pageFilePath, modules, kind) {
|
|
476
415
|
const tryKeys = (...keys) => {
|
|
477
416
|
for (const k of keys) {
|
|
478
|
-
if (
|
|
417
|
+
if (modules[k]?.default) return modules[k];
|
|
479
418
|
}
|
|
480
419
|
return null;
|
|
481
420
|
};
|
|
@@ -487,26 +426,41 @@ function getLayoutModules(pageFilePath) {
|
|
|
487
426
|
for (let depth = 0; depth <= parts.length; depth++) {
|
|
488
427
|
const relativeDir = parts.slice(0, depth).join('/');
|
|
489
428
|
const absoluteDir = relativeDir ? '/' + relativeDir : '';
|
|
490
|
-
let
|
|
429
|
+
let matchedModule = null;
|
|
491
430
|
for (const ext of extensions) {
|
|
492
|
-
|
|
493
|
-
absoluteDir + '/
|
|
494
|
-
relativeDir ? relativeDir + '/
|
|
431
|
+
matchedModule = tryKeys(
|
|
432
|
+
absoluteDir + '/' + kind + '.' + ext,
|
|
433
|
+
relativeDir ? relativeDir + '/' + kind + '.' + ext : kind + '.' + ext,
|
|
495
434
|
);
|
|
496
|
-
if (
|
|
435
|
+
if (matchedModule) break;
|
|
497
436
|
}
|
|
498
|
-
if (
|
|
437
|
+
if (matchedModule) matches.push(matchedModule);
|
|
499
438
|
}
|
|
500
439
|
|
|
501
440
|
return matches;
|
|
502
441
|
}
|
|
503
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
|
+
|
|
504
454
|
/**
|
|
505
455
|
* Main request handler - entry point for both dev and production (Nitro).
|
|
506
456
|
* Exported as { fetch: handler } for the RSC/Nitro contract (see vite-plugin-rsc-deploy-example).
|
|
507
457
|
*/
|
|
508
458
|
async function handleFarmRequest(request) {
|
|
509
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();
|
|
510
464
|
try {
|
|
511
465
|
debug('Handling request:', request.method, url.pathname);
|
|
512
466
|
|
|
@@ -515,9 +469,9 @@ async function handleFarmRequest(request) {
|
|
|
515
469
|
return createFarmDeploymentMismatchResponse(deploymentMismatch);
|
|
516
470
|
}
|
|
517
471
|
|
|
518
|
-
const initialApiMatch =
|
|
472
|
+
const initialApiMatch = matchAPIRouteAtBasePath(apiRouteMap, url.pathname, farmApiBasePath);
|
|
519
473
|
const isInitialApiRequest = Boolean(initialApiMatch) ||
|
|
520
|
-
url.pathname
|
|
474
|
+
isFarmAPIPathname(url.pathname, farmApiBasePath) || isFarmAPIPathname(url.pathname);
|
|
521
475
|
|
|
522
476
|
${ctx.actionsEnabled
|
|
523
477
|
? `if (request.method === 'POST' && !isInitialApiRequest) {
|
|
@@ -553,6 +507,9 @@ async function handleFarmRequest(request) {
|
|
|
553
507
|
const middlewareData = Object.fromEntries(middlewareResult.data);
|
|
554
508
|
const middlewareContext = middlewareResult.context;
|
|
555
509
|
const middlewareHeaders = new Headers(middlewareResult.headers);
|
|
510
|
+
errorData = middlewareResult.data;
|
|
511
|
+
errorContext = middlewareContext;
|
|
512
|
+
errorHeaders = middlewareHeaders;
|
|
556
513
|
if (middlewareResult.data.size || middlewareContext.size) {
|
|
557
514
|
middlewareHeaders.set('cache-control', 'private, no-store');
|
|
558
515
|
}
|
|
@@ -568,7 +525,7 @@ async function handleFarmRequest(request) {
|
|
|
568
525
|
if (apiResponse) {
|
|
569
526
|
return applyProductionMiddlewareHeaders(apiResponse, middlewareHeaders);
|
|
570
527
|
}
|
|
571
|
-
if (url.pathname
|
|
528
|
+
if (isFarmAPIPathname(url.pathname, farmApiBasePath) || isFarmAPIPathname(url.pathname)) {
|
|
572
529
|
return applyProductionMiddlewareHeaders(new Response(
|
|
573
530
|
JSON.stringify({ error: 'API route not found', pathname: url.pathname }),
|
|
574
531
|
{ status: 404, headers: { 'Content-Type': 'application/json' } },
|
|
@@ -642,8 +599,11 @@ async function handleFarmRequest(request) {
|
|
|
642
599
|
if (request.signal.aborted) {
|
|
643
600
|
return new Response(null, { status: 499, headers: { 'Cache-Control': 'no-store' } });
|
|
644
601
|
}
|
|
645
|
-
|
|
646
|
-
|
|
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 };
|
|
647
607
|
debug('Server action failed:', actionId, e);
|
|
648
608
|
}
|
|
649
609
|
} else {
|
|
@@ -674,10 +634,13 @@ async function handleFarmRequest(request) {
|
|
|
674
634
|
if (request.signal.aborted) {
|
|
675
635
|
return new Response(null, { status: 499, headers: { 'Cache-Control': 'no-store' } });
|
|
676
636
|
}
|
|
677
|
-
|
|
678
|
-
|
|
679
|
-
|
|
680
|
-
|
|
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,
|
|
681
644
|
headers: {
|
|
682
645
|
'Cache-Control': 'no-store',
|
|
683
646
|
'Content-Type': 'text/plain; charset=utf-8',
|
|
@@ -693,6 +656,7 @@ async function handleFarmRequest(request) {
|
|
|
693
656
|
code += `
|
|
694
657
|
// Match the URL to a page component
|
|
695
658
|
const matched = matchRoute(url.pathname);
|
|
659
|
+
matchedPage = matched;
|
|
696
660
|
|
|
697
661
|
if (!matched) {
|
|
698
662
|
debug('404 - No route found for:', url.pathname);
|
|
@@ -701,8 +665,6 @@ async function handleFarmRequest(request) {
|
|
|
701
665
|
|
|
702
666
|
const { Page, pattern, params, pageMetadata } = matched;
|
|
703
667
|
const LayoutModules = getLayoutModules(pattern);
|
|
704
|
-
const LayoutModule = LayoutModules[LayoutModules.length - 1];
|
|
705
|
-
const Layout = LayoutModule?.default || (function PassThrough({ children }) { return children; });
|
|
706
668
|
const metadata = mergeDocumentMetadata(
|
|
707
669
|
...LayoutModules.map((layoutModule) => layoutModule.metadata),
|
|
708
670
|
pageMetadata,
|
|
@@ -713,7 +675,9 @@ async function handleFarmRequest(request) {
|
|
|
713
675
|
const globalsCssPath = '/${ctx.srcDir}' + routesPath + '/globals.css';
|
|
714
676
|
|
|
715
677
|
// Parse search params
|
|
716
|
-
|
|
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);
|
|
717
681
|
|
|
718
682
|
// Page props passed to components (includes middleware shared data)
|
|
719
683
|
const pageProps = { params, searchParams, middlewareData };
|
|
@@ -723,23 +687,25 @@ async function handleFarmRequest(request) {
|
|
|
723
687
|
// Helper to create elements without JSX
|
|
724
688
|
const h = React.createElement;
|
|
725
689
|
|
|
726
|
-
// Render page content -
|
|
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.
|
|
727
693
|
let pageContent;
|
|
728
|
-
if (Page.constructor.name === 'AsyncFunction'
|
|
694
|
+
if (Page.constructor.name === 'AsyncFunction') {
|
|
729
695
|
pageContent = await Page(pageProps);
|
|
730
696
|
} else {
|
|
731
697
|
pageContent = h(Page, pageProps);
|
|
732
698
|
}
|
|
733
699
|
|
|
734
700
|
// Route-level loading boundary: wrap in Suspense so async content shows fallback
|
|
735
|
-
const LoadingComponent =
|
|
701
|
+
const LoadingComponent = getMatchingBoundary(pattern, loadings, 'loading');
|
|
736
702
|
if (LoadingComponent) {
|
|
737
703
|
const loadingFallback = h(LoadingComponent, { params, path: url.pathname });
|
|
738
704
|
pageContent = h(React.Suspense, { fallback: loadingFallback }, pageContent);
|
|
739
705
|
}
|
|
740
706
|
|
|
741
707
|
// Route-level error boundary (Next.js error.tsx): catches render errors in this segment
|
|
742
|
-
const ErrorComponent =
|
|
708
|
+
const ErrorComponent = getMatchingBoundary(pattern, errors, 'error');
|
|
743
709
|
if (ErrorComponent) {
|
|
744
710
|
pageContent = h(RouteErrorBoundary, {
|
|
745
711
|
Fallback: ErrorComponent,
|
|
@@ -748,12 +714,16 @@ async function handleFarmRequest(request) {
|
|
|
748
714
|
});
|
|
749
715
|
}
|
|
750
716
|
|
|
751
|
-
// Render
|
|
752
|
-
let layoutContent;
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
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
|
+
}
|
|
757
727
|
}
|
|
758
728
|
|
|
759
729
|
// Single wrapper so #root has exactly one child (avoids duplicate block / "two pages" in DOM).
|
|
@@ -853,6 +823,9 @@ async function handleFarmRequest(request) {
|
|
|
853
823
|
)
|
|
854
824
|
);
|
|
855
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' } });
|
|
856
829
|
console.error('[RSC] Handler error:', err);
|
|
857
830
|
if (request.method === 'POST') {
|
|
858
831
|
if (request.signal.aborted) {
|
|
@@ -867,58 +840,83 @@ async function handleFarmRequest(request) {
|
|
|
867
840
|
},
|
|
868
841
|
});
|
|
869
842
|
}
|
|
870
|
-
//
|
|
843
|
+
// Render outside the failed page/layout tree, but retain request context.
|
|
871
844
|
const pathname = url.pathname.replace(/\\/$/, '') || '/';
|
|
872
|
-
const ErrorComponent =
|
|
845
|
+
const ErrorComponent = getMatchingBoundary(matchedPage?.pattern, errors, 'error');
|
|
873
846
|
if (ErrorComponent) {
|
|
874
847
|
try {
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
|
|
885
|
-
|
|
886
|
-
|
|
887
|
-
|
|
888
|
-
|
|
889
|
-
|
|
890
|
-
|
|
891
|
-
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
|
|
897
|
-
|
|
898
|
-
|
|
899
|
-
|
|
900
|
-
|
|
901
|
-
|
|
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
|
+
)
|
|
902
899
|
);
|
|
903
|
-
|
|
904
|
-
|
|
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);
|
|
900
|
+
} catch (fallbackError) {
|
|
901
|
+
console.error('[RSC] Error boundary render failed:', fallbackError);
|
|
908
902
|
}
|
|
909
903
|
}
|
|
910
904
|
const message = 'Internal Server Error';
|
|
911
905
|
return new Response(JSON.stringify({ error: true, url: request.url, status: 500, message }), {
|
|
912
906
|
status: 500,
|
|
913
|
-
headers: { 'Content-Type': 'application/json' },
|
|
907
|
+
headers: { 'Content-Type': 'application/json', 'Cache-Control': 'private, no-store' },
|
|
914
908
|
});
|
|
915
909
|
}
|
|
916
910
|
}
|
|
917
911
|
|
|
918
912
|
async function handler(request, context) {
|
|
919
|
-
return
|
|
913
|
+
return _runWithAPIRequestRuntime({
|
|
914
|
+
basePath: farmApiBasePath,
|
|
915
|
+
dispatch: async (localRequest) =>
|
|
916
|
+
(await handleAPIRequest(localRequest)) ?? Response.json({ error: 'Not Found' }, { status: 404 }),
|
|
917
|
+
}, () => _runWithCurrentRequest(request, () =>
|
|
920
918
|
_runWithAfterRequest(request, () => handleFarmRequest(request), context)
|
|
921
|
-
);
|
|
919
|
+
));
|
|
922
920
|
}
|
|
923
921
|
|
|
924
922
|
export default { fetch: handler };
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"ssr.d.ts","sourceRoot":"","sources":["../../../src/rsc/entries/ssr.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,YAAY,GAAG,MAAM,
|
|
1
|
+
{"version":3,"file":"ssr.d.ts","sourceRoot":"","sources":["../../../src/rsc/entries/ssr.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,YAAY,GAAG,MAAM,CA+M1D"}
|