@farm.js/plugin 0.1.0-beta.102 → 0.1.0-beta.103

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.
@@ -1 +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;AAWlD,MAAM,WAAW,cAAc;IAC7B,kEAAkE;IAClE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2BAA2B;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,uDAAuD;IACvD,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CACjC;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,CAmhBpE;AAED,OAAO,EAAE,OAAO,EAAE,CAAC"}
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;AAgBlD,MAAM,WAAW,cAAc;IAC7B,kEAAkE;IAClE,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,2BAA2B;IAC3B,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,uDAAuD;IACvD,aAAa,CAAC,EAAE,MAAM,GAAG,MAAM,CAAC;CACjC;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,CA6hBpE;AAED,OAAO,EAAE,OAAO,EAAE,CAAC"}
package/dist/api/index.js CHANGED
@@ -23,7 +23,7 @@
23
23
  */
24
24
  import { _withAfterNodeMiddleware } from "@farm.js/core/after";
25
25
  import { _runWithAPIRequestRuntime, createFarmRequestBodyErrorResponse, readNodeRequestBody, resolveFarmServerConfig, } from "@farm.js/core/internal/production-runtime";
26
- import { invokeAPIRouteEndpoint } from "@farm.js/core/api/runtime";
26
+ import { getAllowedAPIRouteMethods, invokeAPIRouteEndpoint, matchAPIRouteAtBasePath, resolveAPIRouteEndpoint, } from "@farm.js/core/api/runtime";
27
27
  import { sendWebResponse } from "@farm.js/core/server";
28
28
  /**
29
29
  * Farm.js API Routes Plugin
@@ -88,23 +88,30 @@ export default function farmApi(options = {}) {
88
88
  const url = new URL(request.url);
89
89
  const method = request.method;
90
90
  const pathname = url.pathname;
91
- // Find matching route
92
- const route = apiRoutesCache.get(pathname);
93
- if (!route) {
91
+ // Match through core's matcher, the same one the production entry
92
+ // uses, so dynamic segments resolve here too. An exact Map lookup
93
+ // registers `src/api/users/[id]/route.ts` under the literal
94
+ // "/api/users/[id]" and 404s every real request for it.
95
+ const match = matchAPIRouteAtBasePath(apiRoutesCache, pathname);
96
+ if (!match) {
94
97
  return new Response(JSON.stringify({ error: "Not Found" }), {
95
98
  status: 404,
96
99
  headers: { "Content-Type": "application/json" },
97
100
  });
98
101
  }
99
- const endpoint = route.endpoints[method];
102
+ const route = match.route;
103
+ const endpoint = resolveAPIRouteEndpoint(route, method);
100
104
  if (!endpoint) {
101
105
  return new Response(JSON.stringify({ error: "Method Not Allowed" }), {
102
106
  status: 405,
103
- headers: { "Content-Type": "application/json" },
107
+ headers: {
108
+ Allow: getAllowedAPIRouteMethods(route).join(", "),
109
+ "Content-Type": "application/json",
110
+ },
104
111
  });
105
112
  }
106
113
  try {
107
- return await invokeAPIRouteEndpoint(endpoint, request);
114
+ return await invokeAPIRouteEndpoint(endpoint, request, match.params);
108
115
  }
109
116
  catch (error) {
110
117
  return new Response(JSON.stringify({ error: error.message || "Internal Server Error" }), {
@@ -121,19 +128,22 @@ export default function farmApi(options = {}) {
121
128
  return async (request) => {
122
129
  const url = new URL(request.url);
123
130
  const method = request.method;
124
- // Find matching route
125
- const route = apiRoutesCache.get(url.pathname);
126
- if (!route) {
131
+ // Same core matcher as above so dynamic segments resolve here too.
132
+ const match = matchAPIRouteAtBasePath(apiRoutesCache, url.pathname);
133
+ if (!match) {
127
134
  return new Response(JSON.stringify({ error: "Not Found" }), {
128
135
  status: 404,
129
136
  headers: { "Content-Type": "application/json" },
130
137
  });
131
138
  }
132
- const endpoint = route.endpoints[method];
139
+ const endpoint = resolveAPIRouteEndpoint(match.route, method);
133
140
  if (!endpoint) {
134
141
  return new Response(JSON.stringify({ error: "Method Not Allowed" }), {
135
142
  status: 405,
136
- headers: { "Content-Type": "application/json" },
143
+ headers: {
144
+ Allow: getAllowedAPIRouteMethods(match.route).join(", "),
145
+ "Content-Type": "application/json",
146
+ },
137
147
  });
138
148
  }
139
149
  try {
@@ -15,6 +15,7 @@
15
15
  */
16
16
  import type { Plugin, ViteDevServer } from "vite";
17
17
  import type { IncomingMessage, ServerResponse } from "http";
18
+ import type { MiddlewareConfig } from "@farm.js/core/middleware";
18
19
  export interface FarmMiddlewareOptions {
19
20
  /** Source directory containing middleware files (default: 'src') */
20
21
  srcDir?: string;
@@ -85,9 +86,7 @@ export interface DiscoveredMiddleware {
85
86
  };
86
87
  setBasePath?: (path: string) => void;
87
88
  };
88
- config?: {
89
- matcher?: string[];
90
- };
89
+ config?: MiddlewareConfig;
91
90
  }
92
91
  /**
93
92
  * Farm.js Middleware Plugin
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/middleware/index.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/middleware/index.ts"],"names":[],"mappings":"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"}
@@ -16,6 +16,7 @@
16
16
  import { createRequire } from "node:module";
17
17
  import { _withAfterNodeMiddleware } from "@farm.js/core/after";
18
18
  import { devServableFileExists } from "../dev-static.js";
19
+ import { matchesMiddlewareConfig } from "@farm.js/core/middleware";
19
20
  import { middlewareMatchesPath, parseCookies, serializeCookie } from "./matching.js";
20
21
  // Create require for ESM compatibility
21
22
  const require_ = createRequire(import.meta.url);
@@ -266,9 +267,22 @@ export default function farmMiddleware(options = {}) {
266
267
  if (discoveryPromise && !discoveryComplete) {
267
268
  await discoveryPromise;
268
269
  }
269
- // Find applicable middleware (cascading from root to specific)
270
+ // Create the context first: a function matcher in `export const
271
+ // config` receives it, so it has to exist before the filter runs.
272
+ const ctx = createContext(req, res, server);
273
+ // Use shared data if provided
274
+ if (sharedData) {
275
+ for (const [key, value] of sharedData) {
276
+ ctx.data.set(key, value);
277
+ }
278
+ }
279
+ // Find applicable middleware (cascading from root to specific). The
280
+ // directory path decides the subtree; `export const config` then
281
+ // narrows it, using core's own matcher so a matcher/exclude behaves
282
+ // here exactly as it does in the production runner.
270
283
  const applicable = Array.from(middlewareCache.values())
271
- .filter((mw) => middlewareMatchesPath(pathname, mw.path))
284
+ .filter((mw) => middlewareMatchesPath(pathname, mw.path) &&
285
+ (!mw.config || matchesMiddlewareConfig(pathname, mw.config, ctx).matched))
272
286
  .sort((a, b) => a.path.split("/").length - b.path.split("/").length);
273
287
  if (applicable.length === 0)
274
288
  return false;
@@ -285,14 +299,6 @@ export default function farmMiddleware(options = {}) {
285
299
  pc.dim(` (${applicable.length} middleware)`),
286
300
  ].join(" ");
287
301
  console.log(logMsg);
288
- // Create full middleware context
289
- const ctx = createContext(req, res, server);
290
- // Use shared data if provided
291
- if (sharedData) {
292
- for (const [key, value] of sharedData) {
293
- ctx.data.set(key, value);
294
- }
295
- }
296
302
  for (const mw of applicable) {
297
303
  const middleware = mw.module;
298
304
  // Handle middleware chain object (has .build method)
@@ -1 +1 @@
1
- {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/rsc/entries/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,eAAO,MAAM,mCAAmC,62BA6B9C,CAAC;AAEH;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,YAAY,GAAG,MAAM,CAwR7D"}
1
+ {"version":3,"file":"client.d.ts","sourceRoot":"","sources":["../../../src/rsc/entries/client.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD,eAAO,MAAM,mCAAmC,62BA6B9C,CAAC;AAEH;;;;;;;;;;GAUG;AACH,wBAAgB,mBAAmB,CAAC,GAAG,EAAE,YAAY,GAAG,MAAM,CAyT7D"}
@@ -78,8 +78,11 @@ import {
78
78
  actionSetup = `
79
79
  ${serverFnTransportErrorClientRuntime}
80
80
 
81
- // Ref for payload setter (used by server action callback and refetch)
81
+ // Refs for payload setter and router navigation. refetch() is defined inside
82
+ // main(), so the action callback reaches it through this ref rather than by
83
+ // closing over it.
82
84
  const setPayloadRef = { current: null };
85
+ const farmNavigateRef = { current: null };
83
86
 
84
87
  // Ensure __viteRscCallServer is a function before any other chunk may call it (avoids "is not a function")
85
88
  if (typeof globalThis.__viteRscCallServer !== 'function') {
@@ -130,6 +133,14 @@ setServerCallback(async (id, args) => {
130
133
  applyFarmCacheInvalidations(p.returnValue?.invalidations);
131
134
  }
132
135
  if (!p.returnValue || !p.returnValue.ok) {
136
+ // A redirect() from the action comes back as data, not as a 3xx, so the
137
+ // router performs the navigation here.
138
+ if (p.returnValue?.redirect?.url) {
139
+ debug('Server action redirected:', p.returnValue.redirect.url);
140
+ history.pushState(null, '', p.returnValue.redirect.url);
141
+ await farmNavigateRef.current?.(location.href);
142
+ return;
143
+ }
133
144
  debug('Server action failed:', id);
134
145
  throw createFarmServerFnTransportError(p.returnValue?.data);
135
146
  }
@@ -140,6 +151,7 @@ setServerCallback(async (id, args) => {
140
151
  else {
141
152
  actionSetup = `
142
153
  const setPayloadRef = { current: null };
154
+ const farmNavigateRef = { current: null };
143
155
  `;
144
156
  }
145
157
  return `${imports}
@@ -230,7 +242,14 @@ async function main() {
230
242
  }
231
243
  if (e.metaKey || e.altKey || e.ctrlKey || e.shiftKey) return;
232
244
  if (e.button !== 0) return;
233
-
245
+
246
+ // Same-document fragment link: let the browser update the hash and
247
+ // scroll to the anchor natively. Refetching the route here would
248
+ // discard the browser's fragment scroll and re-render for nothing.
249
+ if (a.hash && a.pathname === location.pathname && a.search === location.search) {
250
+ return;
251
+ }
252
+
234
253
  e.preventDefault();
235
254
  history.pushState(null, '', a.href);
236
255
  nav();
@@ -256,6 +275,7 @@ async function main() {
256
275
  }
257
276
 
258
277
  // Fetch new RSC payload for a URL
278
+ farmNavigateRef.current = (url) => refetch(url);
259
279
  async function refetch(url) {
260
280
  debug('Fetching RSC for:', url);
261
281
 
@@ -279,6 +299,19 @@ async function main() {
279
299
  return;
280
300
  }
281
301
 
302
+ // A server redirect during navigation is followed transparently by
303
+ // fetch, so the payload is for res.url while the address bar still shows
304
+ // the URL the click pushed. Reconcile it so refresh, share, and back all
305
+ // reflect the page actually rendered (auth gates redirect this way).
306
+ if (res.redirected && res.url) {
307
+ const dest = new URL(res.url);
308
+ if (dest.origin === location.origin) {
309
+ const target = dest.pathname + dest.search + dest.hash;
310
+ if (target !== location.pathname + location.search + location.hash) {
311
+ history.replaceState(null, '', target);
312
+ }
313
+ }
314
+ }
282
315
  const newPayload = await createFromReadableStream(res.body);
283
316
  setPayloadRef.current?.(newPayload);
284
317
  debug('RSC navigation complete');
@@ -1 +1 @@
1
- {"version":3,"file":"rsc.d.ts","sourceRoot":"","sources":["../../../src/rsc/entries/rsc.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,YAAY,GAAG,MAAM,CA86B1D"}
1
+ {"version":3,"file":"rsc.d.ts","sourceRoot":"","sources":["../../../src/rsc/entries/rsc.ts"],"names":[],"mappings":"AAAA,OAAO,KAAK,EAAE,YAAY,EAAE,MAAM,aAAa,CAAC;AAEhD;;;;;;;;;GASG;AACH,wBAAgB,gBAAgB,CAAC,GAAG,EAAE,YAAY,GAAG,MAAM,CAk8B1D"}
@@ -599,12 +599,22 @@ async function handleFarmRequest(request) {
599
599
  if (request.signal.aborted) {
600
600
  return new Response(null, { status: 499, headers: { 'Cache-Control': 'no-store' } });
601
601
  }
602
- const actionError = sanitizeServerActionError(e);
603
- if (actionError.name === 'ServerActionError') {
604
- console.error('[Farm.js] Server action failed:', e);
602
+ // redirect() unwinds by throwing, so it arrives here looking like a
603
+ // failure. It is control flow, not an error: carry it to the client as
604
+ // data so the router can navigate. Everything else still goes through
605
+ // sanitizeServerActionError untouched.
606
+ const actionRedirect = getFarmRedirectError(e);
607
+ if (actionRedirect) {
608
+ returnValue = { ok: false, redirect: { url: actionRedirect.url, status: actionRedirect.status } };
609
+ debug('Server action redirected:', actionId, actionRedirect.url);
610
+ } else {
611
+ const actionError = sanitizeServerActionError(e);
612
+ if (actionError.name === 'ServerActionError') {
613
+ console.error('[Farm.js] Server action failed:', e);
614
+ }
615
+ returnValue = { ok: false, data: actionError };
616
+ debug('Server action failed:', actionId, e);
605
617
  }
606
- returnValue = { ok: false, data: actionError };
607
- debug('Server action failed:', actionId, e);
608
618
  }
609
619
  } else {
610
620
  // Progressive enhancement (form submitted before JS loaded)
@@ -634,6 +644,16 @@ async function handleFarmRequest(request) {
634
644
  if (request.signal.aborted) {
635
645
  return new Response(null, { status: 499, headers: { 'Cache-Control': 'no-store' } });
636
646
  }
647
+ // No client router here, so the redirect has to be a real HTTP
648
+ // redirect the browser follows on its own.
649
+ const formRedirect = getFarmRedirectError(e);
650
+ if (formRedirect) {
651
+ debug('Form action redirected:', formRedirect.url);
652
+ return new Response(null, {
653
+ status: formRedirect.status,
654
+ headers: { location: formRedirect.url, 'Cache-Control': 'no-store' },
655
+ });
656
+ }
637
657
  const actionError = sanitizeServerActionError(e);
638
658
  if (actionError.name === 'ServerActionError') {
639
659
  console.error('[Farm.js] Form action failed:', e);
@@ -1 +1 @@
1
- {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/rsc/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAIL,KAAK,MAAM,EACX,KAAK,UAAU,EAChB,MAAM,MAAM,CAAC;AAEd,OAAO,KAAK,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AACrE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,sCAAsC,CAAC;AACpF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2CAA2C,CAAC;AAClF,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAwD9E,YAAY,EAAE,oBAAoB,EAAE,YAAY,EAAE,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAExF,YAAY,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAC7D,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,wBAAwB,CAAC;AAC1D,YAAY,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAGjE,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;IACpC,MAAM,CAAC,EAAE,SAAS,iBAAiB,EAAE,CAAC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,aAAa,CAAC;IACpB,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE;QACb;;;WAGG;QACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;QAC3B;;;WAGG;QACH,aAAa,CAAC,EAAE,OAAO,CAAC;QACxB;;;;;;;WAOG;QACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;KAC7B,CAAC;IACF,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,uBAAuB,CAAC;IACxC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACjD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE;QACR,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,gHAAgH;IAChH,OAAO,CAAC,EAAE,GAAG,EAAE,CAAC;CACjB;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,GAAE,aAAkB,GAAG,UAAU,CAuDnE;AA8TD;;;;;GAKG;AACH,MAAM,CAAC,OAAO,UAAU,OAAO,CAAC,OAAO,GAAE,oBAAyB,GAAG,MAAM,EAAE,CAgvC5E"}
1
+ {"version":3,"file":"index.d.ts","sourceRoot":"","sources":["../../src/rsc/index.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;GAsBG;AAEH,OAAO,EAIL,KAAK,MAAM,EACX,KAAK,UAAU,EAChB,MAAM,MAAM,CAAC;AAEd,OAAO,KAAK,EAAE,oBAAoB,EAAE,YAAY,EAAE,MAAM,YAAY,CAAC;AACrE,OAAO,KAAK,EAAE,uBAAuB,EAAE,MAAM,sCAAsC,CAAC;AACpF,OAAO,KAAK,EAAE,aAAa,EAAE,MAAM,mBAAmB,CAAC;AACvD,OAAO,KAAK,EAAE,gBAAgB,EAAE,MAAM,2CAA2C,CAAC;AAClF,OAAO,KAAK,EAAE,cAAc,EAAE,iBAAiB,EAAE,MAAM,sBAAsB,CAAC;AAwD9E,YAAY,EAAE,oBAAoB,EAAE,YAAY,EAAE,CAAC;AACnD,OAAO,EAAE,aAAa,EAAE,kBAAkB,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAExF,YAAY,EAAE,oBAAoB,EAAE,MAAM,kBAAkB,CAAC;AAC7D,OAAO,EAAE,OAAO,IAAI,KAAK,EAAE,MAAM,wBAAwB,CAAC;AAC1D,YAAY,EAAE,kBAAkB,EAAE,MAAM,wBAAwB,CAAC;AAGjE,MAAM,WAAW,aAAa;IAC5B,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,OAAO,CAAC,EAAE,SAAS,cAAc,EAAE,CAAC;IACpC,MAAM,CAAC,EAAE,SAAS,iBAAiB,EAAE,CAAC;IACtC,MAAM,CAAC,EAAE,MAAM,CAAC;IAChB,QAAQ,CAAC,EAAE,MAAM,CAAC;IAClB,GAAG,CAAC,EAAE,aAAa,CAAC;IACpB,MAAM,CAAC,EAAE,gBAAgB,CAAC;IAC1B,IAAI,CAAC,EAAE,MAAM,CAAC;IACd,YAAY,CAAC,EAAE;QACb;;;WAGG;QACH,gBAAgB,CAAC,EAAE,OAAO,CAAC;QAC3B;;;WAGG;QACH,aAAa,CAAC,EAAE,OAAO,CAAC;QACxB;;;;;;;WAOG;QACH,iBAAiB,CAAC,EAAE,OAAO,CAAC;KAC7B,CAAC;IACF,KAAK,CAAC,EAAE,OAAO,CAAC;IAChB,cAAc,CAAC,EAAE,OAAO,CAAC;IACzB,aAAa,CAAC,EAAE,uBAAuB,CAAC;IACxC,YAAY,CAAC,EAAE,MAAM,CAAC;IACtB,eAAe,CAAC,EAAE,MAAM,MAAM,GAAG,OAAO,CAAC,MAAM,CAAC,CAAC;IACjD,SAAS,CAAC,EAAE,MAAM,CAAC;IACnB,OAAO,CAAC,EAAE;QACR,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,GAAG,CAAC,EAAE,MAAM,CAAC;QACb,MAAM,CAAC,EAAE,MAAM,CAAC;KACjB,CAAC;IACF,gHAAgH;IAChH,OAAO,CAAC,EAAE,GAAG,EAAE,CAAC;CACjB;AAED;;;GAGG;AACH,wBAAgB,YAAY,CAAC,MAAM,GAAE,aAAkB,GAAG,UAAU,CAuDnE;AA8TD;;;;;GAKG;AACH,MAAM,CAAC,OAAO,UAAU,OAAO,CAAC,OAAO,GAAE,oBAAyB,GAAG,MAAM,EAAE,CAsvC5E"}
package/dist/rsc/index.js CHANGED
@@ -950,6 +950,12 @@ setServerCallback(async (id, args) => {
950
950
  if (!res.ok) throw new Error('Server action failed: ' + res.status);
951
951
  const p = await createFromReadableStream(res.body, { temporaryReferences: refs });
952
952
  if (p?.returnValue?.ok) return p.returnValue.data;
953
+ // This entry hydrates a single page and has no client router, so a redirect
954
+ // from the action becomes a full document navigation.
955
+ if (p?.returnValue?.redirect?.url) {
956
+ location.assign(p.returnValue.redirect.url);
957
+ return;
958
+ }
953
959
  throw createFarmServerFnTransportError(p?.returnValue?.data);
954
960
  });
955
961
  `
@@ -1 +1 @@
1
- {"version":3,"file":"server-fn-transform.d.ts","sourceRoot":"","sources":["../../src/rsc/server-fn-transform.ts"],"names":[],"mappings":"AAOA,MAAM,MAAM,2BAA2B,GAAG;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB,CAAC;AAEF,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,GACT,2BAA2B,GAAG,IAAI,CAiCpC"}
1
+ {"version":3,"file":"server-fn-transform.d.ts","sourceRoot":"","sources":["../../src/rsc/server-fn-transform.ts"],"names":[],"mappings":"AASA,MAAM,MAAM,2BAA2B,GAAG;IACxC,IAAI,EAAE,MAAM,CAAC;IACb,OAAO,EAAE,MAAM,EAAE,CAAC;CACnB,CAAC;AAEF,wBAAgB,sBAAsB,CACpC,IAAI,EAAE,MAAM,EACZ,EAAE,EAAE,MAAM,GACT,2BAA2B,GAAG,IAAI,CAiCpC"}
@@ -1,5 +1,6 @@
1
1
  const SERVER_FUNCTION_FACTORY_IMPORT_RE = /\bimport\s+(?:type\s+)?(?:[A-Za-z_$][\w$]*\s*,\s*)?(\{[^}]*\})\s+from\s*["']@farm.js\/core(?:\/(?:server-fn|server-query))?["']\s*;?/g;
2
2
  const EXPORT_SERVER_FUNCTION_FACTORY_RE = /\bexport\s+(?:const|let|var)\s+([A-Za-z_$][\w$]*)\s*=\s*([A-Za-z_$][\w$]*)\s*(?:<[^<>]*(?:<[^<>]*>[^<>]*)*>)?\s*\(/g;
3
+ const EXPORT_DEFAULT_SERVER_FUNCTION_FACTORY_RE = /\bexport\s+default\s+([A-Za-z_$][\w$]*)\s*(?:<[^<>]*(?:<[^<>]*>[^<>]*)*>)?\s*\(/g;
3
4
  const MODULE_EXT_RE = /\.[cm]?[jt]sx?$/;
4
5
  const DECLARATION_RE = /(^|\n)\s*["']([^"']+)["']\s*;?/g;
5
6
  export function transformFarmServerFns(code, id) {
@@ -79,6 +80,32 @@ function findServerFnExportReplacements(code, factoryNames) {
79
80
  code: `const ${privateName} = ${callExpression};\nexport async function ${exportName}(input) {\n return ${privateName}(input);\n}`,
80
81
  });
81
82
  }
83
+ // `export default createServerFn(...)` needs the same treatment. Without it the
84
+ // module gets no "use server" directive and its handler ships to the client.
85
+ for (const match of code.matchAll(EXPORT_DEFAULT_SERVER_FUNCTION_FACTORY_RE)) {
86
+ const calleeName = match[1];
87
+ const factory = calleeName ? factoryNames.get(calleeName) : undefined;
88
+ if (!calleeName || !factory)
89
+ continue;
90
+ const openParenIndex = (match.index ?? 0) + match[0].length - 1;
91
+ const callEnd = findMatchingParen(code, openParenIndex);
92
+ if (callEnd === -1)
93
+ continue;
94
+ let declarationEnd = callEnd + 1;
95
+ while (/\s/.test(code[declarationEnd] ?? ""))
96
+ declarationEnd++;
97
+ if (code[declarationEnd] === ";")
98
+ declarationEnd++;
99
+ const calleeIndex = code.indexOf(calleeName, match.index);
100
+ const callExpression = code.slice(calleeIndex, callEnd + 1);
101
+ const privateName = `$$farm_server_${factory}_default`;
102
+ replacements.push({
103
+ start: match.index ?? 0,
104
+ end: declarationEnd,
105
+ exportName: "default",
106
+ code: `const ${privateName} = ${callExpression};\nexport default async function (input) {\n return ${privateName}(input);\n}`,
107
+ });
108
+ }
82
109
  return replacements;
83
110
  }
84
111
  function hasModuleDirective(code, directive) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@farm.js/plugin",
3
- "version": "0.1.0-beta.102",
3
+ "version": "0.1.0-beta.103",
4
4
  "description": "Official plugins for Farm.js framework",
5
5
  "license": "MIT",
6
6
  "author": "Farm.js Team",
@@ -61,7 +61,7 @@
61
61
  "nitro": "3.0.1-alpha.0",
62
62
  "picocolors": "^1.0.0",
63
63
  "rsc-html-stream": "^0.0.4",
64
- "@farm.js/core": "0.1.0-beta.102"
64
+ "@farm.js/core": "0.1.0-beta.103"
65
65
  },
66
66
  "devDependencies": {
67
67
  "@types/node": "^20.19.21",
@@ -82,7 +82,7 @@
82
82
  "react-dom": "19.2.8",
83
83
  "react-server-dom-webpack": "19.2.8",
84
84
  "vite": ">=6.0.0",
85
- "@farm.js/core": "0.1.0-beta.102"
85
+ "@farm.js/core": "0.1.0-beta.103"
86
86
  },
87
87
  "peerDependenciesMeta": {
88
88
  "@vitejs/plugin-rsc": {