@solidjs/router 1.0.0 → 2.0.0-next.13

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 (56) hide show
  1. package/README.md +606 -697
  2. package/dist/claims.d.ts +21 -0
  3. package/dist/claims.js +115 -0
  4. package/dist/data/action.d.ts +40 -11
  5. package/dist/data/action.js +307 -101
  6. package/dist/data/events.d.ts +8 -0
  7. package/dist/data/events.js +24 -23
  8. package/dist/data/index.d.ts +2 -4
  9. package/dist/data/index.js +2 -4
  10. package/dist/data/query.d.ts +1 -3
  11. package/dist/data/query.js +75 -32
  12. package/dist/data/serverForms.d.ts +1 -0
  13. package/dist/data/serverForms.js +5 -0
  14. package/dist/fs.d.ts +94 -0
  15. package/dist/fs.js +51 -0
  16. package/dist/index.d.ts +24 -3
  17. package/dist/index.js +1949 -1204
  18. package/dist/index.jsx +2 -2
  19. package/dist/lifecycle.d.ts +29 -4
  20. package/dist/lifecycle.js +40 -37
  21. package/dist/paths.d.ts +117 -0
  22. package/dist/paths.js +41 -0
  23. package/dist/routers/components.d.ts +12 -26
  24. package/dist/routers/components.jsx +68 -54
  25. package/dist/routers/factory.d.ts +121 -0
  26. package/dist/routers/factory.jsx +154 -0
  27. package/dist/routers/history.d.ts +26 -0
  28. package/dist/routers/history.js +180 -0
  29. package/dist/routers/index.d.ts +5 -11
  30. package/dist/routers/index.js +2 -6
  31. package/dist/routers/scrollRestoration.d.ts +10 -1
  32. package/dist/routers/scrollRestoration.js +49 -11
  33. package/dist/routing.d.ts +96 -52
  34. package/dist/routing.js +429 -172
  35. package/dist/server.d.ts +47 -0
  36. package/dist/server.js +156 -0
  37. package/dist/types.d.ts +158 -44
  38. package/dist/utils.d.ts +2 -0
  39. package/dist/utils.js +2 -0
  40. package/package.json +12 -7
  41. package/dist/components.d.ts +0 -31
  42. package/dist/components.jsx +0 -40
  43. package/dist/data/createAsync.d.ts +0 -32
  44. package/dist/data/createAsync.js +0 -96
  45. package/dist/data/response.d.ts +0 -4
  46. package/dist/data/response.js +0 -42
  47. package/dist/routers/HashRouter.d.ts +0 -9
  48. package/dist/routers/HashRouter.js +0 -41
  49. package/dist/routers/MemoryRouter.d.ts +0 -24
  50. package/dist/routers/MemoryRouter.js +0 -57
  51. package/dist/routers/Router.d.ts +0 -17
  52. package/dist/routers/Router.js +0 -59
  53. package/dist/routers/StaticRouter.d.ts +0 -6
  54. package/dist/routers/StaticRouter.js +0 -15
  55. package/dist/routers/createRouter.d.ts +0 -10
  56. package/dist/routers/createRouter.js +0 -41
@@ -1,9 +1,125 @@
1
- import { $TRACK, createMemo, createSignal, onCleanup, getOwner } from "solid-js";
2
- import { isServer } from "solid-js/web";
3
- import { useRouter } from "../routing.js";
1
+ import { $TRACK, action as createSolidAction, createMemo, onCleanup, getOwner } from "solid-js";
2
+ import { isResponseEnvelope, isServer, REVALIDATE_HEADER } from "@solidjs/web";
3
+ import { createServerReference, decodeResponsePayload, subscribeFlightData } from "@solidjs/web/server-functions";
4
+ // The explicit /server specifier is safe here: the only call site is
5
+ // server-guarded, so client builds tree-shake the codec away.
6
+ import { decodeFlashCookie } from "@solidjs/web/server-functions/server";
7
+ import { provideFlashDecoder, provideFlightConsumer, useRouter } from "../routing.js";
8
+ import { setRouterFormHandler } from "./events.js";
4
9
  import { mockBase, setFunctionName } from "../utils.js";
5
10
  import { cacheKeyOp, hashKey, revalidate, query } from "./query.js";
11
+ const submitHooksSymbol = Symbol("routerActionSubmitHooks");
12
+ const settledHooksSymbol = Symbol("routerActionSettledHooks");
13
+ const invokeSymbol = Symbol("routerActionInvoke");
14
+ // Forms submitted through delegation are marked `aria-busy` while their
15
+ // action is in flight — the form half of the attribute vocabulary links get
16
+ // (`data-active`/`data-pending`). Style with `form[aria-busy] button { ... }`.
17
+ // A counter (not a boolean) keeps the attribute through overlapping
18
+ // submissions from the same form.
19
+ const busyForms = /* #__PURE__ */ new WeakMap();
20
+ function setFormBusy(form, delta) {
21
+ const count = (busyForms.get(form) || 0) + delta;
22
+ busyForms.set(form, count);
23
+ count > 0 ? form.setAttribute("aria-busy", "true") : form.removeAttribute("aria-busy");
24
+ }
6
25
  export const actions = /* #__PURE__ */ new Map();
26
+ /**
27
+ * The document-delegation submit handler for router actions. Lives here —
28
+ * not in events.ts — so the router's event wiring holds no static reference
29
+ * to the action module; `installRouterIntegrations` slots it in when the
30
+ * first action is created on the client.
31
+ */
32
+ export function handleFormAction(evt, router, actionBase) {
33
+ if (evt.defaultPrevented)
34
+ return;
35
+ let actionRef = evt.submitter && evt.submitter.hasAttribute("formaction")
36
+ ? evt.submitter.getAttribute("formaction")
37
+ : evt.target.getAttribute("action");
38
+ if (!actionRef)
39
+ return;
40
+ const serverAction = !actionRef.startsWith("https://action/");
41
+ if (serverAction) {
42
+ // normalize server actions
43
+ const url = new URL(actionRef, mockBase);
44
+ actionRef = router.parsePath(url.pathname + url.search);
45
+ if (!actionRef.startsWith(actionBase))
46
+ return;
47
+ }
48
+ if (evt.target.method.toUpperCase() !== "POST")
49
+ throw new Error("Only POST forms are supported for Actions");
50
+ // A registry miss on a server-action url is a direct bind whose module
51
+ // never loaded client-side (server components): the url is self-describing
52
+ // (`?id`, bound `?args`), so a generic invocation is synthesized from it —
53
+ // delegation alone is sufficient, the no-JS path stays a no-JS fallback.
54
+ // Client-only actions (`https://action/`) are their module's JS by
55
+ // definition, so a miss there falls through to native submission.
56
+ const handler = actions.get(actionRef) || (serverAction && createServerFormAction(actionRef));
57
+ if (handler) {
58
+ evt.preventDefault();
59
+ const data = new FormData(evt.target, evt.submitter);
60
+ handler.call({ r: router, f: evt.target }, evt.target.enctype === "multipart/form-data"
61
+ ? data
62
+ : new URLSearchParams(data));
63
+ }
64
+ }
65
+ /**
66
+ * Synthesizes a router action for a server-rendered action url. The url
67
+ * carries everything an invocation needs — the function id and any bound
68
+ * `.with()` arguments (plain JSON in `?args`, which the server prepends for
69
+ * natural-encoding bodies exactly as it does for no-JS posts) — so the
70
+ * FormData is posted to it verbatim through the server-function transport:
71
+ * submissions, `aria-busy`, redirects, revalidation, and single-flight all
72
+ * flow through the normal action machinery. Registered under the url, so
73
+ * repeat submits reuse it (and a later real registration overrides it).
74
+ */
75
+ function createServerFormAction(url) {
76
+ const id = new URL(url, mockBase).searchParams.get("id");
77
+ if (!id)
78
+ return undefined;
79
+ // typecheck resolves the server half of the dual module; this path only
80
+ // runs in the browser, where the client transport's signature applies
81
+ const stub = createServerReference(id, undefined, url);
82
+ const caller = Object.assign((form) => stub(form), { url });
83
+ return actionImpl(caller);
84
+ }
85
+ /**
86
+ * Entry point for delegation's lazy fallback (data/events.ts): when no form
87
+ * handler was ever installed — no action module in the client graph at all —
88
+ * the router intercepts posts to server-action urls synchronously and loads
89
+ * this module to run them. The FormData was captured at submit time; only
90
+ * the enctype conversion and the generic invocation happen here.
91
+ */
92
+ export function submitServerForm(router, url, form, data) {
93
+ const handler = actions.get(url) || createServerFormAction(url);
94
+ // no `?id` — not the server function convention; nothing can run it,
95
+ // resubmit natively (submit() bypasses the delegated handler)
96
+ if (!handler)
97
+ return form.submit();
98
+ handler.call({ r: router, f: form }, form.enctype === "multipart/form-data" ? data : new URLSearchParams(data));
99
+ }
100
+ // Wires the action layer into the router's slots exactly once, triggered by
101
+ // the first action creation. Not an import side effect — with
102
+ // `sideEffects: false`, module evaluation only happens when action() is
103
+ // actually used, which is precisely when the wiring is wanted: no action in
104
+ // the graph means no form interception, no single-flight subscription (the
105
+ // server is never asked to collect), and no flash cookies to decode. On the
106
+ // server, actions are created at module scope, so the flash decoder is
107
+ // always installed before useSubmission can read the submissions signal.
108
+ let integrationsInstalled = false;
109
+ function installRouterIntegrations() {
110
+ if (integrationsInstalled)
111
+ return;
112
+ integrationsInstalled = true;
113
+ if (isServer) {
114
+ // Server-only: initSubmissions only decodes during SSR, so client builds
115
+ // tree-shake the codec (which now lives behind the runtime's server entry).
116
+ provideFlashDecoder(decodeFlashCookie);
117
+ }
118
+ else {
119
+ setRouterFormHandler(handleFormAction);
120
+ provideFlightConsumer(setupFlightDataConsumer);
121
+ }
122
+ }
7
123
  export function useSubmissions(fn, filter) {
8
124
  const router = useRouter();
9
125
  const subs = createMemo(() => router.submissions[0]().filter(s => s.url === fn.base && (!filter || filter(s.input))));
@@ -11,8 +127,6 @@ export function useSubmissions(fn, filter) {
11
127
  get(_, property) {
12
128
  if (property === $TRACK)
13
129
  return subs();
14
- if (property === "pending")
15
- return subs().some(sub => !sub.result);
16
130
  return subs()[property];
17
131
  },
18
132
  has(_, property) {
@@ -20,130 +134,190 @@ export function useSubmissions(fn, filter) {
20
134
  }
21
135
  });
22
136
  }
23
- export function useSubmission(fn, filter) {
24
- const submissions = useSubmissions(fn, filter);
25
- return new Proxy({}, {
26
- get(_, property) {
27
- if (submissions.length === 0 && (property === "clear" || property === "retry"))
28
- return () => { };
29
- return submissions[submissions.length - 1]?.[property];
30
- }
31
- });
32
- }
33
137
  export function useAction(action) {
34
138
  const r = useRouter();
35
139
  return (...args) => action.apply({ r }, args);
36
140
  }
37
- export function action(fn, options = {}) {
38
- function mutate(...variables) {
141
+ function actionImpl(fn, options = {}) {
142
+ async function invoke(variables, current) {
39
143
  const router = this.r;
40
144
  const form = this.f;
41
- const p = (router.singleFlight && fn.withOptions
42
- ? fn.withOptions({ headers: { "X-Single-Flight": "true" } })
43
- : fn)(...variables);
44
- const [result, setResult] = createSignal();
45
- let submission;
46
- function handler(error) {
47
- return async (res) => {
48
- const result = await handleResponse(res, error, router.navigatorFactory());
49
- let retry = null;
50
- o.onComplete?.({
51
- ...submission,
52
- result: result?.data,
53
- error: result?.error,
54
- pending: false,
55
- retry() {
56
- return (retry = submission.retry());
145
+ const submitHooks = current[submitHooksSymbol];
146
+ const settledHooks = current[settledHooksSymbol];
147
+ // Single-flight opt-in is no longer per call: the router's registered
148
+ // flight-data consumer (see setupFlightDataConsumer) makes the transport
149
+ // send the request header itself, so the mutation is just called.
150
+ const runMutation = () => fn(...variables);
151
+ const run = createSolidAction(async function* (context) {
152
+ context.optimistic?.();
153
+ try {
154
+ const value = await context.call();
155
+ yield;
156
+ return { error: false, value };
157
+ }
158
+ catch (error) {
159
+ yield;
160
+ return { error: true, value: error };
161
+ }
162
+ });
163
+ form && setFormBusy(form, 1);
164
+ let settled;
165
+ let response;
166
+ // The transport consumer is awaited before a single-flight mutation
167
+ // resolves, so a counter delta over the call tells whether this action's
168
+ // metadata was already applied. Overlapping mutations can cross-attribute
169
+ // a run (skipping one default revalidation another pass just covered) —
170
+ // a far smaller window than predicting from the function's identity,
171
+ // which misses every response the server returned without flight data.
172
+ const flightApplicationsBefore = flightApplications;
173
+ try {
174
+ settled = await settleActionResult(run({
175
+ call: runMutation,
176
+ optimistic: submitHooks.size
177
+ ? () => {
178
+ for (const hook of submitHooks.values())
179
+ hook(...variables);
57
180
  }
58
- });
59
- if (retry)
60
- return retry;
61
- if (!result)
62
- return submission.clear();
63
- setResult(result);
64
- if (result.error && !form)
65
- throw result.error;
66
- return result.data;
67
- };
181
+ : undefined
182
+ }));
183
+ response = await handleResponse(settled.value, settled.error, router.navigatorFactory(), flightApplications !== flightApplicationsBefore);
184
+ }
185
+ finally {
186
+ form && setFormBusy(form, -1);
68
187
  }
69
- router.submissions[1](s => [
70
- ...s,
71
- (submission = {
72
- input: variables,
73
- url,
74
- get result() {
75
- return result()?.data;
76
- },
77
- get error() {
78
- return result()?.error;
79
- },
80
- get pending() {
81
- return !result();
82
- },
83
- clear() {
84
- router.submissions[1](v => v.filter(i => i !== submission));
85
- },
86
- retry() {
87
- setResult(undefined);
88
- const p = fn(...variables);
89
- return p.then(handler(), handler(true));
90
- }
91
- })
92
- ]);
93
- return p.then(handler(), handler(true));
188
+ let submission;
189
+ submission = {
190
+ input: variables,
191
+ url,
192
+ result: response && response.data,
193
+ error: response && response.error,
194
+ clear() {
195
+ router.submissions[1](entries => entries.filter(entry => entry !== submission));
196
+ },
197
+ retry() {
198
+ submission.clear();
199
+ return current[invokeSymbol].call({ r: router, f: form }, variables, current);
200
+ }
201
+ };
202
+ // Book-keeping is intentional: only outcomes worth showing or retrying
203
+ // (a result or an error) enter the submissions list, so the typical void
204
+ // mutation leaves nothing behind. Settled hooks still see every
205
+ // completion — void, metadata-only, and redirects included — one
206
+ // `onSettled` per invocation (#580).
207
+ response && router.submissions[1](entries => [...entries, submission]);
208
+ for (const hook of settledHooks.values())
209
+ hook(submission);
210
+ if (response) {
211
+ if (response.error && !form)
212
+ throw response.error;
213
+ return response.data;
214
+ }
215
+ return undefined;
94
216
  }
95
217
  const o = typeof options === "string" ? { name: options } : options;
96
218
  const name = o.name || (!isServer ? String(hashString(fn.toString())) : undefined);
97
219
  const url = fn.url || (name && `https://action/${name}`) || "";
98
- mutate.base = url;
220
+ const wrapped = toAction(invoke, url);
99
221
  if (name)
100
- setFunctionName(mutate, name);
101
- return toAction(mutate, url);
222
+ setFunctionName(wrapped, name);
223
+ return wrapped;
102
224
  }
103
- function toAction(fn, url) {
225
+ export const action = actionImpl;
226
+ function toAction(invoke, url, boundArgs = [], base = url, submitHooks = new Map(), settledHooks = new Map()) {
227
+ const fn = function (...args) {
228
+ return invoke.call(this, [...boundArgs, ...args], fn);
229
+ };
104
230
  fn.toString = () => {
105
231
  if (!url)
106
232
  throw new Error("Client Actions need explicit names if server rendered");
107
233
  return url;
108
234
  };
109
235
  fn.with = function (...args) {
110
- const newFn = function (...passedArgs) {
111
- return fn.call(this, ...args, ...passedArgs);
112
- };
113
- newFn.base = fn.base;
114
236
  const uri = new URL(url, mockBase);
115
237
  uri.searchParams.set("args", hashKey(args));
116
- return toAction(newFn, (uri.origin === "https://action" ? uri.origin : "") + uri.pathname + uri.search);
238
+ const next = toAction(invoke, (uri.origin === "https://action" ? uri.origin : "") + uri.pathname + uri.search, [...boundArgs, ...args], base, submitHooks, settledHooks);
239
+ return next;
240
+ };
241
+ fn.onSubmit = function (hook) {
242
+ const id = Symbol("actionOnSubmitHook");
243
+ submitHooks.set(id, hook);
244
+ getOwner() && onCleanup(() => submitHooks.delete(id));
245
+ return this;
246
+ };
247
+ fn.onSettled = function (hook) {
248
+ const id = Symbol("actionOnSettledHook");
249
+ settledHooks.set(id, hook);
250
+ getOwner() && onCleanup(() => settledHooks.delete(id));
251
+ return this;
117
252
  };
118
253
  fn.url = url;
254
+ fn.base = base;
255
+ fn[submitHooksSymbol] = submitHooks;
256
+ fn[settledHooksSymbol] = settledHooks;
257
+ fn[invokeSymbol] = invoke;
258
+ installRouterIntegrations();
119
259
  if (!isServer) {
120
260
  actions.set(url, fn);
121
261
  // Only remove the registration if it still belongs to this instance —
122
262
  // a re-created action (e.g. a new `.with()` binding after revalidation)
123
263
  // may have registered itself under the same URL since.
124
- getOwner() && onCleanup(() => actions.get(url) === fn && actions.delete(url));
264
+ getOwner() &&
265
+ onCleanup(() => actions.get(url) === fn && actions.delete(url));
125
266
  }
126
267
  return fn;
127
268
  }
128
269
  const hashString = (s) => s.split("").reduce((a, b) => ((a << 5) - a + b.charCodeAt(0)) | 0, 0);
129
- async function handleResponse(response, error, navigate) {
130
- let data;
131
- let custom;
132
- let keys;
133
- let flightKeys;
134
- if (response instanceof Response) {
135
- if (response.headers.has("X-Revalidate"))
136
- keys = response.headers.get("X-Revalidate").split(",");
137
- if (response.customBody) {
138
- data = custom = await response.customBody();
139
- if (response.headers.has("X-Single-Flight")) {
140
- data = data._$value;
141
- delete custom._$value;
142
- flightKeys = Object.keys(custom);
143
- }
270
+ async function settleActionResult(result) {
271
+ const value = result;
272
+ if (value && typeof value.then === "function") {
273
+ return result.then(value => value);
274
+ }
275
+ if (value && typeof value.next === "function") {
276
+ const iterator = value;
277
+ let next = await iterator.next();
278
+ while (!next.done) {
279
+ next = await iterator.next();
144
280
  }
145
- if (response.headers.has("Location")) {
146
- const locationUrl = response.headers.get("Location") || "/";
281
+ return next.value;
282
+ }
283
+ return result;
284
+ }
285
+ // Invocation count of the flight-data consumer. An action compares it across
286
+ // its mutation call to learn whether the transport already applied this
287
+ // response's metadata (and so the default revalidation pass must not run
288
+ // again and wipe the freshly seeded cache).
289
+ let flightApplications = 0;
290
+ /**
291
+ * Registers the router as the single-flight consumer of the server function
292
+ * transport. Subscribing is the opt-in: while registered, the transport
293
+ * sends the `X-Single-Flight` request header on mutations and delivers the
294
+ * folded payload here — fresh route data is seeded into the `query` cache
295
+ * and the envelope metadata (redirect `Location`, `X-Revalidate` keys) is
296
+ * applied, all before the action sees its plain return value. Called by the
297
+ * Router component on the client unless `singleFlight={false}`, which now
298
+ * simply means "never subscribe" — no consumer, no request header, no
299
+ * collection work on the server. Returns the unsubscribe function.
300
+ */
301
+ export function setupFlightDataConsumer(router) {
302
+ return subscribeFlightData((data, { response }) => {
303
+ flightApplications++;
304
+ return applyResponseMetadata(response, router.navigatorFactory(), data);
305
+ });
306
+ }
307
+ /**
308
+ * Applies a server function response's integration metadata: `X-Revalidate`
309
+ * keys invalidate, `Location` navigates (hard for absolute urls), flight
310
+ * data seeds the query cache, and matching entries revalidate. Shared by
311
+ * the flight-data consumer and the action response path (which still sees
312
+ * metadata-bearing responses when no flight data was collected).
313
+ */
314
+ function applyResponseMetadata(metadata, navigate, flightData) {
315
+ let keys;
316
+ if (metadata) {
317
+ if (metadata.headers.has(REVALIDATE_HEADER))
318
+ keys = metadata.headers.get(REVALIDATE_HEADER).split(",");
319
+ if (metadata.headers.has("Location")) {
320
+ const locationUrl = metadata.headers.get("Location") || "/";
147
321
  if (locationUrl.startsWith("http")) {
148
322
  window.location.href = locationUrl;
149
323
  }
@@ -152,15 +326,47 @@ async function handleResponse(response, error, navigate) {
152
326
  }
153
327
  }
154
328
  }
329
+ // invalidate
330
+ cacheKeyOp(keys, entry => (entry[0] = 0));
331
+ // set cache
332
+ flightData && Object.keys(flightData).forEach(k => query.set(k, flightData[k]));
333
+ // trigger revalidation inside the same transition as the navigation, so
334
+ // the redirect commits atomically with fresh data: surviving consumers
335
+ // (shared layouts) the flight payload didn't seed refetch and hold the
336
+ // commit rather than painting stale and updating after. Seeded entries
337
+ // are fresh again by now, so the sweep re-reads them from cache.
338
+ revalidate(keys, false);
339
+ }
340
+ async function handleResponse(response, error, navigate, metadataHandled) {
341
+ let data;
342
+ let flightData;
343
+ let metadata;
344
+ if (isResponseEnvelope(response)) {
345
+ // client-only respond(): the value rides in memory beside the metadata
346
+ data = response.value;
347
+ metadata = response.response;
348
+ }
349
+ else if (response instanceof Response) {
350
+ metadata = response;
351
+ // responses the transport hands over whole (redirects, revalidation)
352
+ // carry a codec-encoded body the router decodes itself. With the
353
+ // flight-data consumer registered single-flight payloads never reach
354
+ // this path, but a manually opted-in call (no consumer) still can —
355
+ // the runtime splits its own envelope shape.
356
+ if (response.body) {
357
+ const payload = await decodeResponsePayload(response);
358
+ data = payload.value;
359
+ flightData = payload.flightData;
360
+ }
361
+ }
155
362
  else if (error)
156
363
  return { error: response };
157
364
  else
158
365
  data = response;
159
- // invalidate
160
- cacheKeyOp(keys, entry => (entry[0] = 0));
161
- // set cache
162
- flightKeys && flightKeys.forEach(k => query.set(k, custom[k]));
163
- // trigger revalidation
164
- await revalidate(keys, false);
366
+ // The transport consumer applies metadata before returning a server
367
+ // function's unwrapped value. Do not treat that value as a second plain
368
+ // action response and invalidate the freshly seeded query cache again.
369
+ if (!metadataHandled || metadata || flightData)
370
+ applyResponseMetadata(metadata, navigate, flightData);
165
371
  return data != null ? { data } : undefined;
166
372
  }
@@ -1,4 +1,12 @@
1
1
  import type { RouterContext } from "../types.js";
2
+ /**
3
+ * The submit delegation consults this slot instead of importing the action
4
+ * module: the action side installs its handler on first action creation
5
+ * (see data/action.ts), so an app that never creates an action never pulls
6
+ * the data layer into its bundle through the router's event wiring.
7
+ */
8
+ export type RouterFormHandler = (evt: SubmitEvent, router: RouterContext, actionBase: string) => void;
9
+ export declare function setRouterFormHandler(handler: RouterFormHandler | undefined): void;
2
10
  type NativeEventConfig = {
3
11
  preload?: boolean;
4
12
  explicitLinks?: boolean;
@@ -1,7 +1,9 @@
1
- import { delegateEvents } from "solid-js/web";
1
+ import { delegateEvents } from "@solidjs/web";
2
2
  import { onCleanup } from "solid-js";
3
- import { actions } from "./action.js";
4
- import { mockBase } from "../utils.js";
3
+ let formHandler;
4
+ export function setRouterFormHandler(handler) {
5
+ formHandler = handler;
6
+ }
5
7
  export function setupNativeEvents({ preload = true, explicitLinks = false, actionBase = "/_server", transformUrl } = {}) {
6
8
  return (router) => {
7
9
  const basePath = router.base.path();
@@ -76,30 +78,29 @@ export function setupNativeEvents({ preload = true, explicitLinks = false, actio
76
78
  }, 20);
77
79
  }
78
80
  function handleFormSubmit(evt) {
81
+ if (formHandler)
82
+ return formHandler(evt, router, actionBase);
83
+ // No form handler means no action module in the client graph at all
84
+ // (e.g. server components binding forms straight to server functions).
85
+ // A POST to a url under actionBase is self-describing, so delegation
86
+ // is still sufficient: intercept synchronously — the no-JS treatment
87
+ // is reserved for clients with no JS — capture the FormData, and load
88
+ // the handler lazily. Apps that never submit one never load it.
79
89
  if (evt.defaultPrevented)
80
90
  return;
81
- let actionRef = evt.submitter && evt.submitter.hasAttribute("formaction")
91
+ const form = evt.target;
92
+ const ref = evt.submitter && evt.submitter.hasAttribute("formaction")
82
93
  ? evt.submitter.getAttribute("formaction")
83
- : evt.target.getAttribute("action");
84
- if (!actionRef)
94
+ : form.getAttribute("action");
95
+ if (!ref || ref.startsWith("https://action/"))
85
96
  return;
86
- if (!actionRef.startsWith("https://action/")) {
87
- // normalize server actions
88
- const url = new URL(actionRef, mockBase);
89
- actionRef = router.parsePath(url.pathname + url.search);
90
- if (!actionRef.startsWith(actionBase))
91
- return;
92
- }
93
- if (evt.target.method.toUpperCase() !== "POST")
94
- throw new Error("Only POST forms are supported for Actions");
95
- const handler = actions.get(actionRef);
96
- if (handler) {
97
- evt.preventDefault();
98
- const data = new FormData(evt.target, evt.submitter);
99
- handler.call({ r: router, f: evt.target }, evt.target.enctype === "multipart/form-data"
100
- ? data
101
- : new URLSearchParams(data));
102
- }
97
+ const url = new URL(ref, document.baseURI);
98
+ const path = router.parsePath(url.pathname + url.search);
99
+ if (!path.startsWith(actionBase) || form.method.toUpperCase() !== "POST")
100
+ return;
101
+ evt.preventDefault();
102
+ const data = new FormData(form, evt.submitter);
103
+ import("./serverForms.js").then(m => m.submitServerForm(router, path, form, data));
103
104
  }
104
105
  // ensure delegated event run first
105
106
  delegateEvents(["click", "submit"]);
@@ -1,4 +1,2 @@
1
- export { createAsync, createAsyncStore, type AccessorWithLatest } from "./createAsync.js";
2
- export { action, useSubmission, useSubmissions, useAction, type Action } from "./action.js";
3
- export { query, revalidate, cache, type CachedFunction } from "./query.js";
4
- export { redirect, reload, json } from "./response.js";
1
+ export { action, useSubmissions, useAction, type Action } from "./action.js";
2
+ export { query, revalidate, type CachedFunction } from "./query.js";
@@ -1,4 +1,2 @@
1
- export { createAsync, createAsyncStore } from "./createAsync.js";
2
- export { action, useSubmission, useSubmissions, useAction } from "./action.js";
3
- export { query, revalidate, cache } from "./query.js";
4
- export { redirect, reload, json } from "./response.js";
1
+ export { action, useSubmissions, useAction } from "./action.js";
2
+ export { query, revalidate } from "./query.js";
@@ -2,7 +2,7 @@ import type { CacheEntry, NarrowResponse } from "../types.js";
2
2
  /**
3
3
  * Revalidates the given cache entry/entries.
4
4
  */
5
- export declare function revalidate(key?: string | string[] | void, force?: boolean): Promise<void>;
5
+ export declare function revalidate(key?: string | string[] | void, force?: boolean): void;
6
6
  export declare function cacheKeyOp(key: string | string[] | void, fn: (cacheEntry: CacheEntry) => void): void;
7
7
  export type CachedFunction<T extends (...args: any) => any> = T extends (...args: infer A) => infer R ? ([] extends {
8
8
  [K in keyof A]-?: A[K];
@@ -18,6 +18,4 @@ export declare namespace query {
18
18
  export var clear: () => void;
19
19
  export { _a as delete };
20
20
  }
21
- /** @deprecated use query instead */
22
- export declare const cache: typeof query;
23
21
  export declare function hashKey<T extends Array<any>>(args: T): string;