@farm.js/plugin 0.1.0-beta.10 → 0.1.0-beta.11

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (52) hide show
  1. package/dist/api/index.d.ts +42 -0
  2. package/dist/api/index.d.ts.map +1 -0
  3. package/dist/api/index.js +493 -0
  4. package/dist/context/index.d.ts +61 -0
  5. package/dist/context/index.d.ts.map +1 -0
  6. package/dist/context/index.js +75 -0
  7. package/dist/index.d.ts +43 -0
  8. package/dist/index.d.ts.map +1 -0
  9. package/dist/index.js +49 -0
  10. package/dist/middleware/index.d.ts +97 -0
  11. package/dist/middleware/index.d.ts.map +1 -0
  12. package/dist/middleware/index.js +469 -0
  13. package/dist/observability/index.d.ts +190 -0
  14. package/dist/observability/index.d.ts.map +1 -0
  15. package/dist/observability/index.js +399 -0
  16. package/dist/rsc/automatic-optimized-boundary.d.ts +11 -0
  17. package/dist/rsc/automatic-optimized-boundary.d.ts.map +1 -0
  18. package/dist/rsc/automatic-optimized-boundary.js +162 -0
  19. package/dist/rsc/build-paths.d.ts +3 -0
  20. package/dist/rsc/build-paths.d.ts.map +1 -0
  21. package/dist/rsc/build-paths.js +8 -0
  22. package/dist/rsc/compatibility.d.ts +8 -0
  23. package/dist/rsc/compatibility.d.ts.map +1 -0
  24. package/dist/rsc/compatibility.js +46 -0
  25. package/dist/rsc/entries/client.d.ts +14 -0
  26. package/dist/rsc/entries/client.d.ts.map +1 -0
  27. package/dist/rsc/entries/client.js +283 -0
  28. package/dist/rsc/entries/rsc.d.ts +13 -0
  29. package/dist/rsc/entries/rsc.d.ts.map +1 -0
  30. package/dist/rsc/entries/rsc.js +932 -0
  31. package/dist/rsc/entries/ssr.d.ts +13 -0
  32. package/dist/rsc/entries/ssr.d.ts.map +1 -0
  33. package/dist/rsc/entries/ssr.js +245 -0
  34. package/dist/rsc/index.d.ts +87 -0
  35. package/dist/rsc/index.d.ts.map +1 -0
  36. package/dist/rsc/index.js +1389 -0
  37. package/dist/rsc/nitro-build.d.ts +36 -0
  38. package/dist/rsc/nitro-build.d.ts.map +1 -0
  39. package/dist/rsc/nitro-build.js +396 -0
  40. package/dist/rsc/optimized-boundary.d.ts +29 -0
  41. package/dist/rsc/optimized-boundary.d.ts.map +1 -0
  42. package/dist/rsc/optimized-boundary.js +243 -0
  43. package/dist/rsc/server-fn-transform.d.ts +6 -0
  44. package/dist/rsc/server-fn-transform.d.ts.map +1 -0
  45. package/dist/rsc/server-fn-transform.js +152 -0
  46. package/dist/rsc/types.d.ts +123 -0
  47. package/dist/rsc/types.d.ts.map +1 -0
  48. package/dist/rsc/types.js +1 -0
  49. package/dist/rsc/vite-plugin-nitro.d.ts +33 -0
  50. package/dist/rsc/vite-plugin-nitro.d.ts.map +1 -0
  51. package/dist/rsc/vite-plugin-nitro.js +163 -0
  52. package/package.json +3 -3
@@ -0,0 +1,42 @@
1
+ /**
2
+ * Farm.js API Routes Plugin
3
+ *
4
+ * Standalone API route support that works with or without RSC.
5
+ *
6
+ * Supports two patterns:
7
+ * 1. File-based routing: /src/api/hello/route.ts -> /api/hello
8
+ * The path is auto-inferred from file location
9
+ *
10
+ * 2. Root routes.ts file: /src/routes.ts with explicit paths
11
+ * createEndpoint('/api/custom', { method: 'GET' }, handler)
12
+ *
13
+ * Uses better-call's createRouter for proper Zod validation and parsing.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * import { farmApi } from '@farm.js/plugin/api'
18
+ *
19
+ * export default defineConfig({
20
+ * plugins: [farmApi({ srcDir: 'src' })],
21
+ * })
22
+ * ```
23
+ */
24
+ import type { Plugin } from "vite";
25
+ export interface FarmApiOptions {
26
+ /** Source directory containing the api folder (default: 'src') */
27
+ srcDir?: string;
28
+ /** Enable debug logging */
29
+ debug?: boolean;
30
+ }
31
+ export interface ApiRoute {
32
+ path: string;
33
+ filePath: string;
34
+ methods: string[];
35
+ endpoints: Record<string, any>;
36
+ }
37
+ /**
38
+ * Farm.js API Routes Plugin
39
+ */
40
+ export default function farmApi(options?: FarmApiOptions): Plugin;
41
+ export { farmApi };
42
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/api/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,KAAK,EAAE,MAAM,EAAiB,MAAM,MAAM,CAAC;AAIlD,MAAM,WAAW,cAAc;IAC7B,kEAAkE;IAClE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2BAA2B;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAC;CACjB;AAED,MAAM,WAAW,QAAQ;IACvB,IAAI,EAAE,MAAM,CAAC;IACb,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,EAAE,CAAC;IAClB,SAAS,EAAE,MAAM,CAAC,MAAM,EAAE,GAAG,CAAC,CAAC;CAChC;AAED;;GAEG;AACH,MAAM,CAAC,OAAO,UAAU,OAAO,CAAC,OAAO,GAAE,cAAmB,GAAG,MAAM,CA2gBpE;AAED,OAAO,EAAE,OAAO,EAAE,CAAC"}
@@ -0,0 +1,493 @@
1
+ /**
2
+ * Farm.js API Routes Plugin
3
+ *
4
+ * Standalone API route support that works with or without RSC.
5
+ *
6
+ * Supports two patterns:
7
+ * 1. File-based routing: /src/api/hello/route.ts -> /api/hello
8
+ * The path is auto-inferred from file location
9
+ *
10
+ * 2. Root routes.ts file: /src/routes.ts with explicit paths
11
+ * createEndpoint('/api/custom', { method: 'GET' }, handler)
12
+ *
13
+ * Uses better-call's createRouter for proper Zod validation and parsing.
14
+ *
15
+ * @example
16
+ * ```ts
17
+ * import { farmApi } from '@farm.js/plugin/api'
18
+ *
19
+ * export default defineConfig({
20
+ * plugins: [farmApi({ srcDir: 'src' })],
21
+ * })
22
+ * ```
23
+ */
24
+ import { _withAfterNodeMiddleware } from "@farm.js/core/after";
25
+ import { invokeAPIRouteEndpoint } from "@farm.js/core/api/runtime";
26
+ /**
27
+ * Farm.js API Routes Plugin
28
+ */
29
+ export default function farmApi(options = {}) {
30
+ const srcDir = options.srcDir ?? "src";
31
+ const debug = options.debug ?? false;
32
+ // API routes cache
33
+ let apiRoutesCache = new Map();
34
+ let apiRouterHandler = null;
35
+ let discoveryComplete = false;
36
+ let discoveryPromise = null;
37
+ let currentServer = null;
38
+ // Debug-only logging for discovery messages with [FARM] prefix
39
+ const log = (message) => {
40
+ if (!debug)
41
+ return;
42
+ try {
43
+ const pc = require("picocolors");
44
+ console.log(pc.dim("[") + pc.bold(pc.blue("FARM")) + pc.dim("]") + " " + pc.gray(message));
45
+ }
46
+ catch {
47
+ console.log(`[FARM] ${message}`);
48
+ }
49
+ };
50
+ // Always log API request/response with [FARM] [API] [METHOD] format
51
+ const logResponse = (method, urlPath, status, duration) => {
52
+ try {
53
+ const pc = require("picocolors");
54
+ let statusColor = pc.green;
55
+ if (status >= 500)
56
+ statusColor = pc.red;
57
+ else if (status >= 400)
58
+ statusColor = pc.yellow;
59
+ else if (status >= 300)
60
+ statusColor = pc.cyan;
61
+ const log = [
62
+ pc.dim("[") + pc.bold(pc.blue("FARM")) + pc.dim("]"),
63
+ pc.dim("[") + pc.bold(pc.cyan("API")) + pc.dim("]"),
64
+ pc.dim("[") + pc.bold(pc.white(method.padEnd(3))) + pc.dim("]"),
65
+ pc.gray(urlPath),
66
+ pc.dim("-"),
67
+ statusColor(status.toString()),
68
+ pc.dim(`(${duration}ms)`),
69
+ ].join(" ");
70
+ console.log(log);
71
+ }
72
+ catch {
73
+ console.log(`[FARM] [API] [${method}] ${urlPath} - ${status} (${duration}ms)`);
74
+ }
75
+ };
76
+ // Create API router handler
77
+ // We use direct invocation for better control over path matching
78
+ const createBetterCallRouter = async () => {
79
+ const totalEndpoints = Array.from(apiRoutesCache.values()).reduce((sum, route) => sum + route.methods.length, 0);
80
+ if (totalEndpoints > 0) {
81
+ // Create a handler that matches routes and invokes endpoints
82
+ apiRouterHandler = async (request) => {
83
+ const url = new URL(request.url);
84
+ const method = request.method;
85
+ const pathname = url.pathname;
86
+ // Find matching route
87
+ const route = apiRoutesCache.get(pathname);
88
+ if (!route) {
89
+ return new Response(JSON.stringify({ error: "Not Found" }), {
90
+ status: 404,
91
+ headers: { "Content-Type": "application/json" },
92
+ });
93
+ }
94
+ const endpoint = route.endpoints[method];
95
+ if (!endpoint) {
96
+ return new Response(JSON.stringify({ error: "Method Not Allowed" }), {
97
+ status: 405,
98
+ headers: { "Content-Type": "application/json" },
99
+ });
100
+ }
101
+ try {
102
+ return await invokeAPIRouteEndpoint(endpoint, request);
103
+ }
104
+ catch (error) {
105
+ return new Response(JSON.stringify({ error: error.message || "Internal Server Error" }), {
106
+ status: 500,
107
+ headers: { "Content-Type": "application/json" },
108
+ });
109
+ }
110
+ };
111
+ log(`better-call router created with ${totalEndpoints} endpoints`);
112
+ }
113
+ };
114
+ // Fallback handler when better-call is not available
115
+ const createFallbackHandler = () => {
116
+ return async (request) => {
117
+ const url = new URL(request.url);
118
+ const method = request.method;
119
+ // Find matching route
120
+ const route = apiRoutesCache.get(url.pathname);
121
+ if (!route) {
122
+ return new Response(JSON.stringify({ error: "Not Found" }), {
123
+ status: 404,
124
+ headers: { "Content-Type": "application/json" },
125
+ });
126
+ }
127
+ const endpoint = route.endpoints[method];
128
+ if (!endpoint) {
129
+ return new Response(JSON.stringify({ error: "Method Not Allowed" }), {
130
+ status: 405,
131
+ headers: { "Content-Type": "application/json" },
132
+ });
133
+ }
134
+ try {
135
+ // For fallback, call the endpoint directly (without Zod parsing)
136
+ const result = await endpoint(request);
137
+ if (result instanceof Response) {
138
+ return result;
139
+ }
140
+ return new Response(JSON.stringify(result), {
141
+ status: 200,
142
+ headers: { "Content-Type": "application/json" },
143
+ });
144
+ }
145
+ catch (error) {
146
+ return new Response(JSON.stringify({ error: error.message || "Internal Server Error" }), {
147
+ status: 500,
148
+ headers: { "Content-Type": "application/json" },
149
+ });
150
+ }
151
+ };
152
+ };
153
+ return {
154
+ name: "@farm.js/plugin/api",
155
+ enforce: "pre",
156
+ configureServer(server) {
157
+ currentServer = server;
158
+ // Discover API routes
159
+ const discoverAPIRoutes = async (apiDir) => {
160
+ const fs = await import("fs");
161
+ const path = await import("path");
162
+ if (!fs.existsSync(apiDir)) {
163
+ log("No api directory found");
164
+ return;
165
+ }
166
+ const findRouteFiles = (dir) => {
167
+ const files = [];
168
+ if (!fs.existsSync(dir))
169
+ return files;
170
+ const entries = fs.readdirSync(dir, { withFileTypes: true });
171
+ for (const entry of entries) {
172
+ const fullPath = path.join(dir, entry.name);
173
+ if (entry.isDirectory()) {
174
+ files.push(...findRouteFiles(fullPath));
175
+ }
176
+ else if (entry.name === "route.ts" ||
177
+ entry.name === "route.tsx" ||
178
+ entry.name === "route.js") {
179
+ files.push(fullPath);
180
+ }
181
+ }
182
+ return files;
183
+ };
184
+ const routeFiles = findRouteFiles(apiDir);
185
+ // Clear cache for rediscovery
186
+ apiRoutesCache.clear();
187
+ for (const filePath of routeFiles) {
188
+ try {
189
+ const relativePath = path.relative(apiDir, path.dirname(filePath));
190
+ const routePath = "/api/" + (relativePath === "." ? "" : relativePath.replace(/\\/g, "/"));
191
+ const routeModule = await server.ssrLoadModule(filePath);
192
+ const methods = ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"];
193
+ const endpoints = {};
194
+ const availableMethods = [];
195
+ for (const method of methods) {
196
+ if (routeModule[method]) {
197
+ availableMethods.push(method);
198
+ endpoints[method] = routeModule[method];
199
+ }
200
+ }
201
+ if (availableMethods.length > 0) {
202
+ apiRoutesCache.set(routePath, {
203
+ path: routePath,
204
+ filePath,
205
+ methods: availableMethods,
206
+ endpoints,
207
+ });
208
+ log(`API route discovered: ${availableMethods.join(", ")} ${routePath}`);
209
+ }
210
+ }
211
+ catch (e) {
212
+ log(`API route load failed at ${filePath}: ${e.message}`);
213
+ }
214
+ }
215
+ // Router is created after all routes are discovered (including root routes.ts)
216
+ };
217
+ // Discover routes from root routes.ts file
218
+ const discoverRootRoutes = async () => {
219
+ const fs = await import("fs");
220
+ const path = await import("path");
221
+ const routesFiles = [
222
+ path.join(server.config.root, srcDir, "routes.ts"),
223
+ path.join(server.config.root, srcDir, "routes.tsx"),
224
+ path.join(server.config.root, srcDir, "routes.js"),
225
+ ];
226
+ for (const routesFile of routesFiles) {
227
+ if (fs.existsSync(routesFile)) {
228
+ try {
229
+ const routesModule = await server.ssrLoadModule(routesFile);
230
+ // Look for exported endpoints with explicit paths
231
+ for (const [exportName, exportValue] of Object.entries(routesModule)) {
232
+ // Check both object and function exports (better-call returns functions)
233
+ const endpoint = exportValue;
234
+ if (endpoint && endpoint.__path) {
235
+ const routePath = endpoint.__path;
236
+ const method = endpoint.__method || "GET";
237
+ // Add to cache
238
+ const existing = apiRoutesCache.get(routePath);
239
+ if (existing) {
240
+ existing.methods.push(method);
241
+ existing.endpoints[method] = endpoint;
242
+ }
243
+ else {
244
+ apiRoutesCache.set(routePath, {
245
+ path: routePath,
246
+ filePath: routesFile,
247
+ methods: [method],
248
+ endpoints: { [method]: endpoint },
249
+ });
250
+ }
251
+ // Root route discovery log omitted
252
+ }
253
+ }
254
+ }
255
+ catch (e) {
256
+ log(`Root routes.ts load failed: ${e.message}`);
257
+ }
258
+ break; // Only load the first routes file found
259
+ }
260
+ }
261
+ };
262
+ // Initialize discovery
263
+ const initializeDiscovery = async () => {
264
+ const path = await import("path");
265
+ const apiDir = path.join(server.config.root, srcDir, "api");
266
+ await discoverAPIRoutes(apiDir);
267
+ await discoverRootRoutes();
268
+ await createBetterCallRouter();
269
+ discoveryComplete = true;
270
+ };
271
+ discoveryPromise = initializeDiscovery().catch((e) => {
272
+ console.error("[FARM] API discovery error:", e);
273
+ });
274
+ // Expose API router for other plugins
275
+ server.__farmApi__ = {
276
+ getHandler: () => apiRouterHandler,
277
+ getRoutes: () => apiRoutesCache,
278
+ isReady: () => discoveryComplete,
279
+ waitForDiscovery: () => discoveryPromise,
280
+ };
281
+ // Add middleware to handle API requests
282
+ return () => {
283
+ server.middlewares.use(_withAfterNodeMiddleware(async (req, res, next) => {
284
+ const url = req.url || "/";
285
+ const pathname = url.split("?")[0];
286
+ const method = req.method || "GET";
287
+ // Only handle /api/ routes
288
+ if (!pathname.startsWith("/api/")) {
289
+ return next();
290
+ }
291
+ // Wait for discovery
292
+ if (discoveryPromise && !discoveryComplete) {
293
+ await discoveryPromise;
294
+ }
295
+ if (!apiRouterHandler) {
296
+ return next();
297
+ }
298
+ const startTime = Date.now();
299
+ try {
300
+ // Execute middleware if available
301
+ const farmMiddleware = server.__farmMiddleware__;
302
+ if (farmMiddleware) {
303
+ await farmMiddleware.waitForDiscovery?.();
304
+ const middlewareData = new Map();
305
+ const handled = await farmMiddleware.execute(req, res, pathname, middlewareData);
306
+ if (handled) {
307
+ const duration = Date.now() - startTime;
308
+ logResponse(method, pathname, res.statusCode || 200, duration);
309
+ return;
310
+ }
311
+ }
312
+ // Convert Node request to Web Request
313
+ const fullUrl = `http://${req.headers.host || "localhost:3000"}${url}`;
314
+ const headers = new Headers();
315
+ for (const [key, value] of Object.entries(req.headers)) {
316
+ if (value) {
317
+ headers.set(key, Array.isArray(value) ? value.join(", ") : value);
318
+ }
319
+ }
320
+ // Get body for non-GET requests
321
+ let body;
322
+ if (method !== "GET" && method !== "HEAD") {
323
+ body = await new Promise((resolve) => {
324
+ let data = "";
325
+ req.on("data", (chunk) => {
326
+ data += chunk;
327
+ });
328
+ req.on("end", () => {
329
+ resolve(data);
330
+ });
331
+ });
332
+ }
333
+ const request = new Request(fullUrl, {
334
+ method,
335
+ headers,
336
+ body: body || undefined,
337
+ });
338
+ const response = await apiRouterHandler(request);
339
+ const duration = Date.now() - startTime;
340
+ logResponse(method, pathname, response.status, duration);
341
+ // Send response
342
+ res.statusCode = response.status;
343
+ response.headers.forEach((value, key) => {
344
+ res.setHeader(key, value);
345
+ });
346
+ const responseBody = await response.text();
347
+ res.end(responseBody);
348
+ }
349
+ catch (error) {
350
+ const duration = Date.now() - startTime;
351
+ logResponse(method, pathname, 500, duration);
352
+ console.error("[FARM] API error:", error);
353
+ res.statusCode = 500;
354
+ res.setHeader("Content-Type", "application/json");
355
+ res.end(JSON.stringify({ error: "Internal server error" }));
356
+ }
357
+ }));
358
+ };
359
+ },
360
+ async handleHotUpdate({ file, server, modules }) {
361
+ const fileName = file.split("/").pop() || "";
362
+ // Handle root routes.ts updates
363
+ if (fileName === "routes.ts" || fileName === "routes.tsx" || fileName === "routes.js") {
364
+ log(`Root routes file updated: ${fileName}`);
365
+ // Invalidate all affected modules
366
+ for (const mod of modules) {
367
+ server.moduleGraph.invalidateModule(mod);
368
+ }
369
+ // Clear routes that came from this file and rediscover
370
+ for (const [routePath, route] of apiRoutesCache) {
371
+ if (route.filePath === file) {
372
+ apiRoutesCache.delete(routePath);
373
+ }
374
+ }
375
+ try {
376
+ const routesModule = await server.ssrLoadModule(file);
377
+ // Look for exported endpoints with explicit paths
378
+ // Endpoints can be functions (from better-call) or objects
379
+ for (const [exportName, exportValue] of Object.entries(routesModule)) {
380
+ const endpoint = exportValue;
381
+ if (endpoint && endpoint.__path) {
382
+ const routePath = endpoint.__path;
383
+ const method = endpoint.__method || "GET";
384
+ // Add to cache
385
+ const existing = apiRoutesCache.get(routePath);
386
+ if (existing) {
387
+ existing.methods.push(method);
388
+ existing.endpoints[method] = endpoint;
389
+ }
390
+ else {
391
+ apiRoutesCache.set(routePath, {
392
+ path: routePath,
393
+ filePath: file,
394
+ methods: [method],
395
+ endpoints: { [method]: endpoint },
396
+ });
397
+ }
398
+ log(`Root route reloaded: ${method} ${routePath}`);
399
+ }
400
+ }
401
+ // Recreate better-call router
402
+ await createBetterCallRouter();
403
+ log(`better-call router recreated`);
404
+ }
405
+ catch (e) {
406
+ log(`Root routes.ts HMR failed: ${e.message}`);
407
+ }
408
+ return [];
409
+ }
410
+ if (file.includes("/api/") && fileName.startsWith("route.")) {
411
+ const shortPath = file.split("/api/")[1] || file;
412
+ log(`API route updated: ${shortPath}`);
413
+ // Invalidate all affected modules
414
+ for (const mod of modules) {
415
+ server.moduleGraph.invalidateModule(mod);
416
+ }
417
+ // Find the route path for this file
418
+ let routePathToUpdate = null;
419
+ for (const [routePath, route] of apiRoutesCache) {
420
+ if (route.filePath === file) {
421
+ routePathToUpdate = routePath;
422
+ break;
423
+ }
424
+ }
425
+ if (routePathToUpdate) {
426
+ try {
427
+ // Reload the module
428
+ const routeModule = await server.ssrLoadModule(file);
429
+ const methods = ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"];
430
+ const endpoints = {};
431
+ const availableMethods = [];
432
+ for (const method of methods) {
433
+ if (routeModule[method]) {
434
+ availableMethods.push(method);
435
+ endpoints[method] = routeModule[method];
436
+ }
437
+ }
438
+ if (availableMethods.length > 0) {
439
+ apiRoutesCache.set(routePathToUpdate, {
440
+ path: routePathToUpdate,
441
+ filePath: file,
442
+ methods: availableMethods,
443
+ endpoints,
444
+ });
445
+ log(`API route reloaded: ${routePathToUpdate}`);
446
+ // Recreate better-call router with updated endpoints
447
+ await createBetterCallRouter();
448
+ log(`better-call router recreated`);
449
+ }
450
+ }
451
+ catch (e) {
452
+ log(`API route HMR failed: ${e.message}`);
453
+ }
454
+ }
455
+ else {
456
+ // New route file - discover it
457
+ try {
458
+ const path = await import("path");
459
+ const apiDir = path.join(server.config.root, srcDir, "api");
460
+ const relativePath = path.relative(apiDir, path.dirname(file));
461
+ const routePath = "/api/" + (relativePath === "." ? "" : relativePath.replace(/\\/g, "/"));
462
+ const routeModule = await server.ssrLoadModule(file);
463
+ const methods = ["GET", "POST", "PUT", "DELETE", "PATCH", "OPTIONS"];
464
+ const endpoints = {};
465
+ const availableMethods = [];
466
+ for (const method of methods) {
467
+ if (routeModule[method]) {
468
+ availableMethods.push(method);
469
+ endpoints[method] = routeModule[method];
470
+ }
471
+ }
472
+ if (availableMethods.length > 0) {
473
+ apiRoutesCache.set(routePath, {
474
+ path: routePath,
475
+ filePath: file,
476
+ methods: availableMethods,
477
+ endpoints,
478
+ });
479
+ log(`New API route discovered: ${routePath}`);
480
+ // Recreate better-call router with new endpoint
481
+ await createBetterCallRouter();
482
+ }
483
+ }
484
+ catch (e) {
485
+ log(`New API route load failed: ${e.message}`);
486
+ }
487
+ }
488
+ return [];
489
+ }
490
+ },
491
+ };
492
+ }
493
+ export { farmApi };
@@ -0,0 +1,61 @@
1
+ interface RequestLike {
2
+ url?: string;
3
+ headers: Record<string, string | string[] | undefined>;
4
+ }
5
+ interface PluginContextLike {
6
+ req: {
7
+ set: (key: string, value: any, options?: {
8
+ exposeToPage?: boolean;
9
+ }) => void;
10
+ get: <T = any>(key: string) => T | undefined;
11
+ };
12
+ }
13
+ interface ContextPluginLike {
14
+ name: string;
15
+ enforce?: "pre" | "post";
16
+ beforeRequest?: (req: RequestLike, res: unknown, context: PluginContextLike) => void | Promise<void>;
17
+ afterResponse?: (req: RequestLike, res: {
18
+ statusCode?: number;
19
+ } | unknown, context: PluginContextLike) => void | Promise<void>;
20
+ }
21
+ export interface ContextPluginOptions {
22
+ requestIdHeader?: string;
23
+ localeHeader?: string;
24
+ exposePathname?: boolean;
25
+ beforeRequest?: (payload: {
26
+ requestId: string;
27
+ locale: string;
28
+ pathname: string;
29
+ }) => void;
30
+ afterResponse?: (payload: {
31
+ requestId: string;
32
+ locale: string;
33
+ pathname: string;
34
+ statusCode: number;
35
+ durationMs: number;
36
+ }) => void;
37
+ }
38
+ /**
39
+ * Demo plugin that exposes request context to server page props as `props.context`.
40
+ *
41
+ * @example
42
+ * ```ts
43
+ * import { contextPlugin } from "@farm.js/plugin/context";
44
+ *
45
+ * export default defineConfig({
46
+ * plugins: [contextPlugin()],
47
+ * });
48
+ * ```
49
+ *
50
+ * @example
51
+ * ```tsx
52
+ * export default function Page(props: any) {
53
+ * const requestId = props.context?.data.get("requestId");
54
+ * const locale = props.context?.data.get("locale");
55
+ * return <div>{requestId} - {locale}</div>;
56
+ * }
57
+ * ```
58
+ */
59
+ export declare function contextPlugin(options?: ContextPluginOptions): ContextPluginLike;
60
+ export {};
61
+ //# sourceMappingURL=index.d.ts.map
@@ -0,0 +1 @@
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/context/index.ts"],"names":[],"mappings":"AAEA,UAAU,WAAW;IACnB,GAAG,CAAC,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,CAAC,MAAM,EAAE,MAAM,GAAG,MAAM,EAAE,GAAG,SAAS,CAAC,CAAC;CACxD;AAED,UAAU,iBAAiB;IACzB,GAAG,EAAE;QACH,GAAG,EAAE,CAAC,GAAG,EAAE,MAAM,EAAE,KAAK,EAAE,GAAG,EAAE,OAAO,CAAC,EAAE;YAAE,YAAY,CAAC,EAAE,OAAO,CAAA;SAAE,KAAK,IAAI,CAAC;QAC7E,GAAG,EAAE,CAAC,CAAC,GAAG,GAAG,EAAE,GAAG,EAAE,MAAM,KAAK,CAAC,GAAG,SAAS,CAAC;KAC9C,CAAC;CACH;AAED,UAAU,iBAAiB;IACzB,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,CAAC,EAAE,KAAK,GAAG,MAAM,CAAC;IACzB,aAAa,CAAC,EAAE,CACd,GAAG,EAAE,WAAW,EAChB,GAAG,EAAE,OAAO,EACZ,OAAO,EAAE,iBAAiB,KACvB,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;IAC1B,aAAa,CAAC,EAAE,CACd,GAAG,EAAE,WAAW,EAChB,GAAG,EAAE;QAAE,UAAU,CAAC,EAAE,MAAM,CAAA;KAAE,GAAG,OAAO,EACtC,OAAO,EAAE,iBAAiB,KACvB,IAAI,GAAG,OAAO,CAAC,IAAI,CAAC,CAAC;CAC3B;AAED,MAAM,WAAW,oBAAoB;IACnC,eAAe,CAAC,EAAE,MAAM,CAAC;IACzB,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE;QAAE,SAAS,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAA;KAAE,KAAK,IAAI,CAAC;IAC3F,aAAa,CAAC,EAAE,CAAC,OAAO,EAAE;QACxB,SAAS,EAAE,MAAM,CAAC;QAClB,MAAM,EAAE,MAAM,CAAC;QACf,QAAQ,EAAE,MAAM,CAAC;QACjB,UAAU,EAAE,MAAM,CAAC;QACnB,UAAU,EAAE,MAAM,CAAC;KACpB,KAAK,IAAI,CAAC;CACZ;AAeD;;;;;;;;;;;;;;;;;;;;GAoBG;AACH,wBAAgB,aAAa,CAAC,OAAO,GAAE,oBAAyB,GAAG,iBAAiB,CA0CnF"}
@@ -0,0 +1,75 @@
1
+ import { randomUUID } from "crypto";
2
+ function normalizeHeaderValue(value) {
3
+ if (!value)
4
+ return undefined;
5
+ if (Array.isArray(value))
6
+ return value[0];
7
+ return value;
8
+ }
9
+ function getHeader(req, name) {
10
+ const direct = normalizeHeaderValue(req.headers[name.toLowerCase()]);
11
+ if (direct)
12
+ return direct;
13
+ const fromRaw = normalizeHeaderValue(req.headers[name]);
14
+ return fromRaw;
15
+ }
16
+ /**
17
+ * Demo plugin that exposes request context to server page props as `props.context`.
18
+ *
19
+ * @example
20
+ * ```ts
21
+ * import { contextPlugin } from "@farm.js/plugin/context";
22
+ *
23
+ * export default defineConfig({
24
+ * plugins: [contextPlugin()],
25
+ * });
26
+ * ```
27
+ *
28
+ * @example
29
+ * ```tsx
30
+ * export default function Page(props: any) {
31
+ * const requestId = props.context?.data.get("requestId");
32
+ * const locale = props.context?.data.get("locale");
33
+ * return <div>{requestId} - {locale}</div>;
34
+ * }
35
+ * ```
36
+ */
37
+ export function contextPlugin(options = {}) {
38
+ const requestIdHeader = (options.requestIdHeader || "x-request-id").toLowerCase();
39
+ const localeHeader = (options.localeHeader || "x-locale").toLowerCase();
40
+ const exposePathname = options.exposePathname ?? true;
41
+ const beforeRequestCb = options.beforeRequest;
42
+ const afterResponseCb = options.afterResponse;
43
+ return {
44
+ name: "@farm.js/plugin-context",
45
+ enforce: "pre",
46
+ beforeRequest(req, _res, context) {
47
+ const pathname = req.url || "/";
48
+ const requestId = getHeader(req, requestIdHeader) || randomUUID();
49
+ const locale = getHeader(req, localeHeader) || "en";
50
+ context.req.set("requestId", requestId, { exposeToPage: true });
51
+ context.req.set("locale", locale, { exposeToPage: true });
52
+ if (exposePathname) {
53
+ context.req.set("pathname", pathname, { exposeToPage: true });
54
+ }
55
+ // Private value stays available only to plugin hooks.
56
+ context.req.set("internal:requestStart", Date.now());
57
+ if (beforeRequestCb) {
58
+ beforeRequestCb({ requestId, locale, pathname });
59
+ }
60
+ },
61
+ afterResponse(req, res, context) {
62
+ if (!afterResponseCb)
63
+ return;
64
+ const requestId = context.req.get("requestId") || "unknown";
65
+ const locale = context.req.get("locale") || "en";
66
+ const started = context.req.get("internal:requestStart");
67
+ const pathname = req.url || "/";
68
+ const durationMs = typeof started === "number" ? Date.now() - started : 0;
69
+ const statusCode = typeof res === "object" && res !== null && "statusCode" in res
70
+ ? Number(res.statusCode || 200)
71
+ : 200;
72
+ afterResponseCb({ requestId, locale, pathname, statusCode, durationMs });
73
+ },
74
+ };
75
+ }