@solidjs/router 2.0.0-next.30 → 2.0.0-next.31

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/README.md CHANGED
@@ -850,7 +850,7 @@ const isRouting = useIsRouting();
850
850
  return <div classList={{ "grey-out": isRouting() }}>...</div>;
851
851
  ```
852
852
 
853
- In Solid's dev and observe builds the router also declares every navigation to the attribution engine (`solid-js/attribution`): holds and re-runs caused by a navigation are named after the route pattern (`navigation to /users/:id`), timed from the user event that started it, and redirect hops fold onto the navigation they belong to. `attribution.navigations()` and `feedback().navigations` list them; nothing of this exists in production builds.
853
+ In Solid's dev and observe builds the router also declares every navigation to the attribution engine (`solid-js/attribution`): holds and re-runs caused by a navigation are named after the route pattern (`navigation to /users/:id`), timed from the user event that started it, and redirect hops fold onto the navigation they belong to. `attribution.history("navigation")` and `feedback().navigations` list them; nothing of this exists in production builds.
854
854
 
855
855
  ### useMatch
856
856
 
@@ -29,9 +29,10 @@ export const actions = /* #__PURE__ */ new Map();
29
29
  export function handleFormAction(evt, router, actionBase) {
30
30
  if (evt.defaultPrevented)
31
31
  return;
32
+ const form = evt.target;
32
33
  let actionRef = evt.submitter && evt.submitter.hasAttribute("formaction")
33
34
  ? evt.submitter.getAttribute("formaction")
34
- : evt.target.getAttribute("action");
35
+ : form.getAttribute("action");
35
36
  if (!actionRef)
36
37
  return;
37
38
  const serverAction = !actionRef.startsWith("https://action/");
@@ -42,7 +43,7 @@ export function handleFormAction(evt, router, actionBase) {
42
43
  if (!actionRef.startsWith(actionBase))
43
44
  return;
44
45
  }
45
- if (evt.target.method.toUpperCase() !== "POST")
46
+ if (form.method.toUpperCase() !== "POST")
46
47
  throw new Error("Only POST forms are supported for Actions");
47
48
  // A registry miss on a server-action url is a direct bind whose module
48
49
  // never loaded client-side (server components): the url is self-describing
@@ -54,10 +55,8 @@ export function handleFormAction(evt, router, actionBase) {
54
55
  const handler = actions.get(actionRef) || (serverAction && createServerFormAction(actionRef));
55
56
  if (handler) {
56
57
  evt.preventDefault();
57
- const data = new FormData(evt.target, evt.submitter);
58
- handler.call({ r: router, f: evt.target }, evt.target.enctype === "multipart/form-data"
59
- ? data
60
- : new URLSearchParams(data));
58
+ const data = new FormData(form, evt.submitter);
59
+ handler.call({ r: router, f: form }, form.enctype === "multipart/form-data" ? data : new URLSearchParams(data));
61
60
  }
62
61
  }
63
62
  /**
@@ -159,18 +158,22 @@ function actionImpl(fn, options = {}) {
159
158
  const runMutation = () => fn(...variables);
160
159
  const run = createSolidAction(async function* (context) {
161
160
  context.optimistic?.();
161
+ let value;
162
+ let error = false;
162
163
  try {
163
- const value = await context.call();
164
- yield;
165
- return { error: false, value };
164
+ value = await context.call();
166
165
  }
167
- catch (error) {
168
- yield;
169
- return { error: true, value: error };
166
+ catch (e) {
167
+ value = e;
168
+ error = true;
170
169
  }
170
+ const read = await readResponse(value, error);
171
+ yield;
172
+ // Apply inside the transition so the default revalidation's refetch and
173
+ // the release of the caller's optimistic writes commit as one frame (#619).
174
+ return applyResponse(read, router.navigatorFactory(), flightApplications !== flightApplicationsBefore);
171
175
  });
172
176
  form && setFormBusy(form, 1);
173
- let settled;
174
177
  let response;
175
178
  // The transport consumer is awaited before a single-flight mutation
176
179
  // resolves, so a counter delta over the call tells whether this action's
@@ -180,7 +183,7 @@ function actionImpl(fn, options = {}) {
180
183
  // which misses every response the server returned without flight data.
181
184
  const flightApplicationsBefore = flightApplications;
182
185
  try {
183
- settled = await settleActionResult(run({
186
+ response = await settleActionResult(run({
184
187
  call: runMutation,
185
188
  optimistic: submitHooks.size
186
189
  ? () => {
@@ -189,7 +192,6 @@ function actionImpl(fn, options = {}) {
189
192
  }
190
193
  : undefined
191
194
  }));
192
- response = await handleResponse(settled.value, settled.error, router.navigatorFactory(), flightApplications !== flightApplicationsBefore);
193
195
  }
194
196
  finally {
195
197
  form && setFormBusy(form, -1);
@@ -198,8 +200,8 @@ function actionImpl(fn, options = {}) {
198
200
  submission = {
199
201
  input: variables,
200
202
  url,
201
- result: response && response.data,
202
- error: response && response.error,
203
+ result: response?.data,
204
+ error: response?.error,
203
205
  clear() {
204
206
  router.submissions[1](entries => entries.filter(entry => entry !== submission));
205
207
  },
@@ -244,8 +246,7 @@ function toAction(invoke, url, boundArgs = [], base = url, submitHooks = new Map
244
246
  fn.with = function (...args) {
245
247
  const uri = new URL(url, mockBase);
246
248
  uri.searchParams.set("args", hashKey(args));
247
- const next = toAction(invoke, (uri.origin === "https://action" ? uri.origin : "") + uri.pathname + uri.search, [...boundArgs, ...args], base, submitHooks, settledHooks);
248
- return next;
249
+ return toAction(invoke, (uri.origin === "https://action" ? uri.origin : "") + uri.pathname + uri.search, [...boundArgs, ...args], base, submitHooks, settledHooks);
249
250
  };
250
251
  fn.onSubmit = function (hook) {
251
252
  const id = Symbol("actionOnSubmitHook");
@@ -362,8 +363,10 @@ function applyResponseMetadata(metadata, navigate, flightData) {
362
363
  cacheKeyOp(keys, entry => (entry[0] = 0));
363
364
  // set cache — and fan the payload out to other keyed stores (live query
364
365
  // channels adopt delivered values instead of reconnecting)
365
- flightData && Object.keys(flightData).forEach(k => query.set(k, flightData[k]));
366
- flightData && deliverFlightData(flightData);
366
+ if (flightData) {
367
+ Object.keys(flightData).forEach(k => query.set(k, flightData[k]));
368
+ deliverFlightData(flightData);
369
+ }
367
370
  // trigger revalidation inside the same transition as the navigation, so
368
371
  // the redirect commits atomically with fresh data: surviving consumers
369
372
  // (shared layouts) the flight payload didn't seed refetch and hold the
@@ -371,7 +374,7 @@ function applyResponseMetadata(metadata, navigate, flightData) {
371
374
  // are fresh again by now, so the sweep re-reads them from cache.
372
375
  revalidate(keys, false);
373
376
  }
374
- async function handleResponse(response, error, navigate, metadataHandled) {
377
+ async function readResponse(response, error) {
375
378
  let data;
376
379
  let flightData;
377
380
  let metadata;
@@ -397,6 +400,12 @@ async function handleResponse(response, error, navigate, metadataHandled) {
397
400
  return { error: response };
398
401
  else
399
402
  data = response;
403
+ return { data, flightData, metadata };
404
+ }
405
+ function applyResponse(read, navigate, metadataHandled) {
406
+ if ("error" in read)
407
+ return read;
408
+ const { data, flightData, metadata } = read;
400
409
  // The transport consumer applies metadata before returning a server
401
410
  // function's unwrapped value. Do not treat that value as a second plain
402
411
  // action response and invalidate the freshly seeded query cache again.
@@ -32,7 +32,7 @@ export function setupNativeEvents({ preload = true, explicitLinks = false, actio
32
32
  if (target || (!href && !a.hasAttribute("state")))
33
33
  return;
34
34
  const rel = (a.getAttribute("rel") || "").split(/\s+/);
35
- if (a.hasAttribute("download") || (rel && rel.includes("external")))
35
+ if (a.hasAttribute("download") || rel.includes("external"))
36
36
  return;
37
37
  const url = svg ? new URL(href, document.baseURI) : new URL(href);
38
38
  // Skip non-http(s) schemes (blob:, mailto:, tel:, data:, ...). blob: URLs
@@ -156,10 +156,7 @@ export function query(fn, name) {
156
156
  }
157
157
  let res = cached[1];
158
158
  if (intent !== "preload") {
159
- res =
160
- "then" in cached[1]
161
- ? cached[1].then(handleResponse(false), handleResponse(true))
162
- : handleResponse(false)(cached[1]);
159
+ res = settle(res);
163
160
  !isServer && intent === "navigate" && cached[4][1](cached[0]); // update version
164
161
  }
165
162
  inPreloadFn && "then" in res && res.catch(() => { });
@@ -225,12 +222,8 @@ export function query(fn, name) {
225
222
  if (e && e.router.dataOnly)
226
223
  return (e.router.data[key] = res);
227
224
  }
228
- if (intent !== "preload") {
229
- res =
230
- "then" in res
231
- ? res.then(handleResponse(false), handleResponse(true))
232
- : handleResponse(false)(res);
233
- }
225
+ if (intent !== "preload")
226
+ res = settle(res);
234
227
  inPreloadFn && "then" in res && res.catch(() => { });
235
228
  // serialize on server
236
229
  if (isServer &&
@@ -241,6 +234,12 @@ export function query(fn, name) {
241
234
  (!e || !e.serverOnly) && sharedConfig.context.serialize(key, res);
242
235
  }
243
236
  return res;
237
+ // a settled read: the response handling applied to the value or the promise
238
+ function settle(v) {
239
+ return "then" in v
240
+ ? v.then(handleResponse(false), handleResponse(true))
241
+ : handleResponse(false)(v);
242
+ }
244
243
  function handleResponse(error) {
245
244
  return async (v) => {
246
245
  let enveloped;
package/dist/fs.d.ts CHANGED
@@ -35,7 +35,7 @@ export type FileRouteConfig<S extends string = string, T = unknown, F = undefine
35
35
  * `matchFilters` are validated against the pattern's params, and their
36
36
  * types (and any `search` schema) flow through the manifest into `paths`.
37
37
  */
38
- export declare function defineFileRoute<S extends string, T = unknown, const F = DefinedRouteFilters<S>, Sch extends StandardSchemaV1<any, any> | undefined = undefined>(path: S, config: {
38
+ export declare function defineFileRoute<S extends string, T = unknown, const F = DefinedRouteFilters<S>, Sch extends StandardSchemaV1<any, any> | undefined = undefined>(path: S, config?: {
39
39
  matchFilters?: (F & ValidFilters<F, S>) | undefined;
40
40
  preload?: ((args: RoutePreloadFuncArgs<RouteParams<S>>) => T) | undefined;
41
41
  /** Standard Schema validator for this route's search params; its input type flows into the typed path proxy. */
package/dist/fs.js CHANGED
@@ -20,7 +20,7 @@ import * as server from "./fsServer.js";
20
20
  * `matchFilters` are validated against the pattern's params, and their
21
21
  * types (and any `search` schema) flow through the manifest into `paths`.
22
22
  */
23
- export function defineFileRoute(path, config) {
23
+ export function defineFileRoute(path, config = {}) {
24
24
  return config;
25
25
  }
26
26
  /**
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { getOwner, runWithOwner, createMemo, createRenderEffect, onCleanup, untrack, createContext, createSignal, useContext, NotReadyError, isPending, DEV, latest, createComponent, createRoot, Show, createEffect, sharedConfig, OBSERVE, onSettled, getObserver, $TRACK, action as action$1 } from 'solid-js';
1
+ import { getOwner, runWithOwner, createMemo, createRenderEffect, onCleanup, untrack, createContext, createSignal, useContext, NotReadyError, DEV, isPending, latest, createComponent, createRoot, Show, createEffect, sharedConfig, OBSERVE, onSettled, getObserver, $TRACK, action as action$1 } from 'solid-js';
2
2
  import { registerElementClaim, delegateEvents, isServer, getRequestEvent, hasFlashCookie, clearFlashCookie, createComponent as createComponent$1, memo, dynamic, isServerFunction, getServerFunctionMetadata, getServerFunctionRPC, isResponseEnvelope, REVALIDATE_HEADER } from '@solidjs/web';
3
3
  import { REDIRECT_HEADER, decodeRedirectHeaderValue, subscribeFlightData, decodeResponsePayload, parseServerFunctionActionUrl, createServerReference } from '@solidjs/web/server-functions';
4
4
 
@@ -72,13 +72,13 @@ function createMatcher(path, partial, matchFilters) {
72
72
  path: len ? "" : "/",
73
73
  params: {}
74
74
  };
75
- const matchFilter = s => matchFilters === undefined ? undefined : matchFilters[s];
75
+ const filters = matchFilters;
76
76
  for (let i = 0; i < len; i++) {
77
77
  const segment = segments[i];
78
78
  const dynamic = segment[0] === ":";
79
79
  const locSegment = dynamic ? locSegments[i] : locSegments[i].toLowerCase();
80
80
  const key = dynamic ? segment.slice(1) : segment.toLowerCase();
81
- if (dynamic && matchSegment(locSegment, matchFilter(key))) {
81
+ if (dynamic && matchSegment(locSegment, filters?.[key])) {
82
82
  match.params[key] = locSegment;
83
83
  } else if (dynamic || !matchSegment(locSegment, key)) {
84
84
  return null;
@@ -87,7 +87,7 @@ function createMatcher(path, partial, matchFilters) {
87
87
  }
88
88
  if (splat) {
89
89
  const remainder = lenDiff ? locSegments.slice(-lenDiff).join("/") : "";
90
- if (matchSegment(remainder, matchFilter(splat))) {
90
+ if (matchSegment(remainder, filters?.[splat])) {
91
91
  match.params[splat] = remainder;
92
92
  } else {
93
93
  return null;
@@ -96,21 +96,9 @@ function createMatcher(path, partial, matchFilters) {
96
96
  return match;
97
97
  };
98
98
  }
99
- function matchSegment(input, filter) {
100
- const isEqual = s => s === input;
101
- if (filter === undefined) {
102
- return true;
103
- } else if (typeof filter === "string") {
104
- return isEqual(filter);
105
- } else if (typeof filter === "function") {
106
- return filter(input);
107
- } else if (Array.isArray(filter)) {
108
- return filter.some(isEqual);
109
- } else if (filter instanceof RegExp) {
110
- return filter.test(input);
111
- }
112
- return false;
113
- }
99
+
100
+ // a string filter is the segment itself; anything else not listed never matches
101
+ const matchSegment = (input, filter) => filter === undefined || (typeof filter === "function" ? filter(input) : Array.isArray(filter) ? filter.includes(input) : filter instanceof RegExp ? filter.test(input) : filter === input);
114
102
  function scoreRoute(route) {
115
103
  const [pattern, splat] = route.pattern.split("/*", 2);
116
104
  const segments = pattern.split("/").filter(Boolean);
@@ -340,7 +328,7 @@ function setupNativeEvents({
340
328
  const target = svg ? a.target.baseVal : a.target;
341
329
  if (target || !href && !a.hasAttribute("state")) return;
342
330
  const rel = (a.getAttribute("rel") || "").split(/\s+/);
343
- if (a.hasAttribute("download") || rel && rel.includes("external")) return;
331
+ if (a.hasAttribute("download") || rel.includes("external")) return;
344
332
  const url = svg ? new URL(href, document.baseURI) : new URL(href);
345
333
  // Skip non-http(s) schemes (blob:, mailto:, tel:, data:, ...). blob: URLs
346
334
  // inherit the page origin, so the origin check below won't reject them. #382
@@ -514,8 +502,8 @@ function createPathsProxy(renderPath = p => p, base = "") {
514
502
  };
515
503
  return new Proxy(build, {
516
504
  get(_, prop) {
517
- if (prop === "toString") return () => toHref(pathname);
518
- if (typeof prop === "symbol") return prop === Symbol.toPrimitive ? () => toHref(pathname) : prop === HREF ? pathname || "/" : undefined;
505
+ if (prop === "toString" || prop === Symbol.toPrimitive) return () => toHref(pathname);
506
+ if (typeof prop === "symbol") return prop === HREF ? pathname || "/" : undefined;
519
507
  return node(`${pathname}/${prop}`);
520
508
  }
521
509
  });
@@ -1023,9 +1011,7 @@ function createRoutes(routeDef, base = "") {
1023
1011
  for (const expandedPath of expandOptionals(originalPath)) {
1024
1012
  const path = joinPaths(base, expandedPath);
1025
1013
  let pattern = isLeaf ? path : path.split("/*", 1)[0];
1026
- pattern = pattern.split("/").map(s => {
1027
- return s.startsWith(":") || s.startsWith("*") ? s : encodeSegment(s);
1028
- }).join("/");
1014
+ pattern = pattern.split("/").map(s => /^[:*]/.test(s) ? s : encodeSegment(s)).join("/");
1029
1015
  acc.push({
1030
1016
  ...shared,
1031
1017
  originalPath,
@@ -1116,13 +1102,12 @@ function mergeParams(matches) {
1116
1102
  return params;
1117
1103
  }
1118
1104
  function createLocation(path, state, queryWrapper) {
1119
- const origin = new URL(mockBase);
1120
- const url = createMemo((prev = origin) => {
1105
+ const url = createMemo((prev = new URL(mockBase)) => {
1121
1106
  const path_ = path();
1122
1107
  try {
1123
1108
  // anchor rooted paths against the origin explicitly - a path with
1124
1109
  // doubled leading slashes would otherwise parse as protocol-relative
1125
- return new URL(path_[0] === "/" ? mockBase + path_ : path_, origin);
1110
+ return new URL(path_[0] === "/" ? mockBase + path_ : path_, mockBase);
1126
1111
  } catch (err) {
1127
1112
  DEV && console.error(`Invalid path ${path_}`);
1128
1113
  return prev;
@@ -1133,7 +1118,6 @@ function createLocation(path, state, queryWrapper) {
1133
1118
  const pathname = createMemo(() => url().pathname);
1134
1119
  const search = createMemo(() => url().search);
1135
1120
  const hash = createMemo(() => url().hash);
1136
- const key = () => "";
1137
1121
  const queryFn = createMemo(() => extractSearchParams(url()));
1138
1122
  return {
1139
1123
  get pathname() {
@@ -1149,7 +1133,7 @@ function createLocation(path, state, queryWrapper) {
1149
1133
  return state();
1150
1134
  },
1151
1135
  get key() {
1152
- return key();
1136
+ return "";
1153
1137
  },
1154
1138
  query: queryWrapper ? queryWrapper(queryFn) : createMemoObject(queryFn)
1155
1139
  };
@@ -1315,7 +1299,7 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1315
1299
  read = runWithOwner(routerOwner, () => createMemo(() => {
1316
1300
  const result = resolveLazySubtree(record);
1317
1301
  return result instanceof Promise ? result.then(() => undefined) : undefined;
1318
- }, {
1302
+ }, DEV && {
1319
1303
  name: "lazyRoutes"
1320
1304
  }));
1321
1305
  lazyReaders.set(record, read);
@@ -1348,7 +1332,7 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1348
1332
  }
1349
1333
  }
1350
1334
  return m;
1351
- }, {
1335
+ }, DEV && {
1352
1336
  name: "matches"
1353
1337
  });
1354
1338
  const routingPending = createMemo(() => isPending(() => {
@@ -1359,7 +1343,7 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1359
1343
  }
1360
1344
  location.search;
1361
1345
  location.hash;
1362
- }), {
1346
+ }), DEV && {
1363
1347
  name: "routingPending"
1364
1348
  });
1365
1349
  const isRouting = () => routingPending() || isPending(source);
@@ -1565,11 +1549,11 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1565
1549
  route,
1566
1550
  params
1567
1551
  } = next[match];
1568
- route.component && route.component.preload && route.component.preload();
1569
1552
  const {
1570
1553
  preload,
1571
1554
  component
1572
1555
  } = route;
1556
+ component?.preload?.();
1573
1557
  const now = current && current[match];
1574
1558
  const unchanged = now && now.route.key === route.key && serverRouteArgsEqual(inputs(params, query, url.search, route), inputs(now.params, location.query, location.search, route));
1575
1559
  inPreloadFn = true;
@@ -1633,7 +1617,7 @@ function createRouteContext(router, parent, outlet, match, matches = () => [matc
1633
1617
  // its last valid value while this route is being torn down, so outgoing
1634
1618
  // components and preloads never observe another route's params.
1635
1619
  const params = wrapParams(() => mergeParams(matches()));
1636
- component && component.preload && component.preload();
1620
+ component?.preload?.();
1637
1621
  inPreloadFn = true;
1638
1622
  const data = preload ? preload({
1639
1623
  params,
@@ -1760,9 +1744,7 @@ function Routes(props) {
1760
1744
  next[i] = prev[i];
1761
1745
  } else {
1762
1746
  equal = false;
1763
- if (disposers[i]) {
1764
- disposers[i]();
1765
- }
1747
+ disposers[i]?.();
1766
1748
  runWithOwner(owner, () => createRoot(dispose => {
1767
1749
  disposers[i] = dispose;
1768
1750
  const routeKey = nextMatch.route.key;
@@ -1851,13 +1833,14 @@ function bindEvent(target, type, handler) {
1851
1833
 
1852
1834
  let depth;
1853
1835
  function saveCurrentDepth() {
1854
- if (!window.history.state || window.history.state._depth == null) {
1855
- window.history.replaceState({
1856
- ...window.history.state,
1857
- _depth: window.history.length - 1
1836
+ const h = window.history;
1837
+ if (!h.state || h.state._depth == null) {
1838
+ h.replaceState({
1839
+ ...h.state,
1840
+ _depth: h.length - 1
1858
1841
  }, "");
1859
1842
  }
1860
- depth = window.history.state._depth;
1843
+ depth = h.state?._depth ?? h.length - 1;
1861
1844
  }
1862
1845
  function keepDepth(state) {
1863
1846
  return {
@@ -1901,11 +1884,10 @@ function scrollToHash(hash, fallbackTop) {
1901
1884
 
1902
1885
  function browserHistory() {
1903
1886
  const getSource = () => {
1904
- const url = window.location.pathname + window.location.search;
1905
- const state = window.history.state && window.history.state._depth && Object.keys(window.history.state).length === 1 ? undefined : window.history.state;
1887
+ const s = window.history.state;
1906
1888
  return {
1907
- value: url + window.location.hash,
1908
- state
1889
+ value: window.location.pathname + window.location.search + window.location.hash,
1890
+ state: s && s._depth && Object.keys(s).length === 1 ? undefined : s
1909
1891
  };
1910
1892
  };
1911
1893
  const beforeLeave = {};
@@ -2133,7 +2115,7 @@ function createScrollRestoration() {
2133
2115
  // reload/back_forward document loads land on an existing entry (a fresh
2134
2116
  // navigation starts a new one and belongs at the top); the effect's
2135
2117
  // initial run performs the restore after first render
2136
- const [nav] = performance.getEntriesByType && performance.getEntriesByType("navigation");
2118
+ const [nav] = performance.getEntriesByType?.("navigation");
2137
2119
  if (nav && nav.type !== "navigate") pending = depth();
2138
2120
  }
2139
2121
  };
@@ -2432,11 +2414,10 @@ function createRouter(config) {
2432
2414
  }));
2433
2415
  },
2434
2416
  keysFor(url) {
2435
- const pathname = typeof url === "string" ? new URL(url, mockBase).pathname : url[HREF];
2436
2417
  const keys = [];
2437
2418
  for (const {
2438
2419
  route
2439
- } of matchPath(pathname)) {
2420
+ } of matchPath(pathnameOf(url))) {
2440
2421
  const key = serverRouteOf(route.component)?.call?.key;
2441
2422
  typeof key === "string" && key && !keys.includes(key) && keys.push(key);
2442
2423
  }
@@ -2751,7 +2732,7 @@ function query(fn, name) {
2751
2732
  }
2752
2733
  let res = cached[1];
2753
2734
  if (intent !== "preload") {
2754
- res = "then" in cached[1] ? cached[1].then(handleResponse(false), handleResponse(true)) : handleResponse(false)(cached[1]);
2735
+ res = settle(res);
2755
2736
  !isServer && intent === "navigate" && cached[4][1](cached[0]); // update version
2756
2737
  }
2757
2738
  inPreloadFn && "then" in res && res.catch(() => {});
@@ -2811,9 +2792,7 @@ function query(fn, name) {
2811
2792
  const e = getRequestEvent();
2812
2793
  if (e && e.router.dataOnly) return e.router.data[key] = res;
2813
2794
  }
2814
- if (intent !== "preload") {
2815
- res = "then" in res ? res.then(handleResponse(false), handleResponse(true)) : handleResponse(false)(res);
2816
- }
2795
+ if (intent !== "preload") res = settle(res);
2817
2796
  inPreloadFn && "then" in res && res.catch(() => {});
2818
2797
  // serialize on server
2819
2798
  if (isServer && sharedConfig.context && sharedConfig.context.async && !sharedConfig.context.noHydrate) {
@@ -2821,6 +2800,11 @@ function query(fn, name) {
2821
2800
  (!e || !e.serverOnly) && sharedConfig.context.serialize(key, res);
2822
2801
  }
2823
2802
  return res;
2803
+
2804
+ // a settled read: the response handling applied to the value or the promise
2805
+ function settle(v) {
2806
+ return "then" in v ? v.then(handleResponse(false), handleResponse(true)) : handleResponse(false)(v);
2807
+ }
2824
2808
  function handleResponse(error) {
2825
2809
  return async v => {
2826
2810
  let enveloped;
@@ -2997,7 +2981,8 @@ const actions = /* #__PURE__ */new Map();
2997
2981
  */
2998
2982
  function handleFormAction(evt, router, actionBase) {
2999
2983
  if (evt.defaultPrevented) return;
3000
- let actionRef = evt.submitter && evt.submitter.hasAttribute("formaction") ? evt.submitter.getAttribute("formaction") : evt.target.getAttribute("action");
2984
+ const form = evt.target;
2985
+ let actionRef = evt.submitter && evt.submitter.hasAttribute("formaction") ? evt.submitter.getAttribute("formaction") : form.getAttribute("action");
3001
2986
  if (!actionRef) return;
3002
2987
  const serverAction = !actionRef.startsWith("https://action/");
3003
2988
  if (serverAction) {
@@ -3006,7 +2991,7 @@ function handleFormAction(evt, router, actionBase) {
3006
2991
  actionRef = router.parsePath(url.pathname + url.search);
3007
2992
  if (!actionRef.startsWith(actionBase)) return;
3008
2993
  }
3009
- if (evt.target.method.toUpperCase() !== "POST") throw new Error("Only POST forms are supported for Actions");
2994
+ if (form.method.toUpperCase() !== "POST") throw new Error("Only POST forms are supported for Actions");
3010
2995
  // A registry miss on a server-action url is a direct bind whose module
3011
2996
  // never loaded client-side (server components): the url is self-describing
3012
2997
  // (the id in the path, bound `?args` in the query), so a generic invocation
@@ -3017,11 +3002,11 @@ function handleFormAction(evt, router, actionBase) {
3017
3002
  const handler = actions.get(actionRef) || serverAction && createServerFormAction(actionRef);
3018
3003
  if (handler) {
3019
3004
  evt.preventDefault();
3020
- const data = new FormData(evt.target, evt.submitter);
3005
+ const data = new FormData(form, evt.submitter);
3021
3006
  handler.call({
3022
3007
  r: router,
3023
- f: evt.target
3024
- }, evt.target.enctype === "multipart/form-data" ? data : new URLSearchParams(data));
3008
+ f: form
3009
+ }, form.enctype === "multipart/form-data" ? data : new URLSearchParams(data));
3025
3010
  }
3026
3011
  }
3027
3012
 
@@ -3128,23 +3113,21 @@ function actionImpl(fn, options = {}) {
3128
3113
  const runMutation = () => fn(...variables);
3129
3114
  const run = action$1(async function* (context) {
3130
3115
  context.optimistic?.();
3116
+ let value;
3117
+ let error = false;
3131
3118
  try {
3132
- const value = await context.call();
3133
- yield;
3134
- return {
3135
- error: false,
3136
- value
3137
- };
3138
- } catch (error) {
3139
- yield;
3140
- return {
3141
- error: true,
3142
- value: error
3143
- };
3119
+ value = await context.call();
3120
+ } catch (e) {
3121
+ value = e;
3122
+ error = true;
3144
3123
  }
3124
+ const read = await readResponse(value, error);
3125
+ yield;
3126
+ // Apply inside the transition so the default revalidation's refetch and
3127
+ // the release of the caller's optimistic writes commit as one frame (#619).
3128
+ return applyResponse(read, router.navigatorFactory(), flightApplications !== flightApplicationsBefore);
3145
3129
  });
3146
3130
  form && setFormBusy(form, 1);
3147
- let settled;
3148
3131
  let response;
3149
3132
  // The transport consumer is awaited before a single-flight mutation
3150
3133
  // resolves, so a counter delta over the call tells whether this action's
@@ -3154,13 +3137,12 @@ function actionImpl(fn, options = {}) {
3154
3137
  // which misses every response the server returned without flight data.
3155
3138
  const flightApplicationsBefore = flightApplications;
3156
3139
  try {
3157
- settled = await settleActionResult(run({
3140
+ response = await settleActionResult(run({
3158
3141
  call: runMutation,
3159
3142
  optimistic: submitHooks.size ? () => {
3160
3143
  for (const hook of submitHooks.values()) hook(...variables);
3161
3144
  } : undefined
3162
3145
  }));
3163
- response = await handleResponse(settled.value, settled.error, router.navigatorFactory(), flightApplications !== flightApplicationsBefore);
3164
3146
  } finally {
3165
3147
  form && setFormBusy(form, -1);
3166
3148
  }
@@ -3168,8 +3150,8 @@ function actionImpl(fn, options = {}) {
3168
3150
  submission = {
3169
3151
  input: variables,
3170
3152
  url,
3171
- result: response && response.data,
3172
- error: response && response.error,
3153
+ result: response?.data,
3154
+ error: response?.error,
3173
3155
  clear() {
3174
3156
  router.submissions[1](entries => entries.filter(entry => entry !== submission));
3175
3157
  },
@@ -3215,8 +3197,7 @@ function toAction(invoke, url, boundArgs = [], base = url, submitHooks = new Map
3215
3197
  fn.with = function (...args) {
3216
3198
  const uri = new URL(url, mockBase);
3217
3199
  uri.searchParams.set("args", hashKey(args));
3218
- const next = toAction(invoke, (uri.origin === "https://action" ? uri.origin : "") + uri.pathname + uri.search, [...boundArgs, ...args], base, submitHooks, settledHooks);
3219
- return next;
3200
+ return toAction(invoke, (uri.origin === "https://action" ? uri.origin : "") + uri.pathname + uri.search, [...boundArgs, ...args], base, submitHooks, settledHooks);
3220
3201
  };
3221
3202
  fn.onSubmit = function (hook) {
3222
3203
  const id = Symbol("actionOnSubmitHook");
@@ -3335,8 +3316,10 @@ function applyResponseMetadata(metadata, navigate, flightData) {
3335
3316
  cacheKeyOp(keys, entry => entry[0] = 0);
3336
3317
  // set cache — and fan the payload out to other keyed stores (live query
3337
3318
  // channels adopt delivered values instead of reconnecting)
3338
- flightData && Object.keys(flightData).forEach(k => query.set(k, flightData[k]));
3339
- flightData && deliverFlightData(flightData);
3319
+ if (flightData) {
3320
+ Object.keys(flightData).forEach(k => query.set(k, flightData[k]));
3321
+ deliverFlightData(flightData);
3322
+ }
3340
3323
  // trigger revalidation inside the same transition as the navigation, so
3341
3324
  // the redirect commits atomically with fresh data: surviving consumers
3342
3325
  // (shared layouts) the flight payload didn't seed refetch and hold the
@@ -3344,7 +3327,7 @@ function applyResponseMetadata(metadata, navigate, flightData) {
3344
3327
  // are fresh again by now, so the sweep re-reads them from cache.
3345
3328
  revalidate(keys, false);
3346
3329
  }
3347
- async function handleResponse(response, error, navigate, metadataHandled) {
3330
+ async function readResponse(response, error) {
3348
3331
  let data;
3349
3332
  let flightData;
3350
3333
  let metadata;
@@ -3367,6 +3350,19 @@ async function handleResponse(response, error, navigate, metadataHandled) {
3367
3350
  } else if (error) return {
3368
3351
  error: response
3369
3352
  };else data = response;
3353
+ return {
3354
+ data,
3355
+ flightData,
3356
+ metadata
3357
+ };
3358
+ }
3359
+ function applyResponse(read, navigate, metadataHandled) {
3360
+ if ("error" in read) return read;
3361
+ const {
3362
+ data,
3363
+ flightData,
3364
+ metadata
3365
+ } = read;
3370
3366
  // The transport consumer applies metadata before returning a server
3371
3367
  // function's unwrapped value. Do not treat that value as a second plain
3372
3368
  // action response and invalidate the freshly seeded query cache again.
package/dist/paths.js CHANGED
@@ -37,14 +37,10 @@ export function createPathsProxy(renderPath = p => p, base = "") {
37
37
  };
38
38
  return new Proxy(build, {
39
39
  get(_, prop) {
40
- if (prop === "toString")
40
+ if (prop === "toString" || prop === Symbol.toPrimitive)
41
41
  return () => toHref(pathname);
42
42
  if (typeof prop === "symbol")
43
- return prop === Symbol.toPrimitive
44
- ? () => toHref(pathname)
45
- : prop === HREF
46
- ? pathname || "/"
47
- : undefined;
43
+ return prop === HREF ? pathname || "/" : undefined;
48
44
  return node(`${pathname}/${prop}`);
49
45
  }
50
46
  });
@@ -74,9 +74,7 @@ export function Routes(props) {
74
74
  }
75
75
  else {
76
76
  equal = false;
77
- if (disposers[i]) {
78
- disposers[i]();
79
- }
77
+ disposers[i]?.();
80
78
  runWithOwner(owner, () => createRoot(dispose => {
81
79
  disposers[i] = dispose;
82
80
  const routeKey = nextMatch.route.key;
@@ -247,9 +247,8 @@ export function createRouter(config) {
247
247
  }));
248
248
  },
249
249
  keysFor(url) {
250
- const pathname = typeof url === "string" ? new URL(url, mockBase).pathname : url[HREF];
251
250
  const keys = [];
252
- for (const { route } of matchPath(pathname)) {
251
+ for (const { route } of matchPath(pathnameOf(url))) {
253
252
  const key = serverRouteOf(route.component)?.call?.key;
254
253
  typeof key === "string" && key && !keys.includes(key) && keys.push(key);
255
254
  }
@@ -9,10 +9,11 @@ export function bindEvent(target, type, handler) {
9
9
  // guard machinery itself lives behind the lazy `beforeLeave` slot.
10
10
  let depth;
11
11
  export function saveCurrentDepth() {
12
- if (!window.history.state || window.history.state._depth == null) {
13
- window.history.replaceState({ ...window.history.state, _depth: window.history.length - 1 }, "");
12
+ const h = window.history;
13
+ if (!h.state || h.state._depth == null) {
14
+ h.replaceState({ ...h.state, _depth: h.length - 1 }, "");
14
15
  }
15
- depth = window.history.state._depth;
16
+ depth = h.state?._depth ?? h.length - 1;
16
17
  }
17
18
  function keepDepth(state) {
18
19
  return {
@@ -50,15 +51,10 @@ function scrollToHash(hash, fallbackTop) {
50
51
  }
51
52
  export function browserHistory() {
52
53
  const getSource = () => {
53
- const url = window.location.pathname + window.location.search;
54
- const state = window.history.state &&
55
- window.history.state._depth &&
56
- Object.keys(window.history.state).length === 1
57
- ? undefined
58
- : window.history.state;
54
+ const s = window.history.state;
59
55
  return {
60
- value: url + window.location.hash,
61
- state
56
+ value: window.location.pathname + window.location.search + window.location.hash,
57
+ state: s && s._depth && Object.keys(s).length === 1 ? undefined : s
62
58
  };
63
59
  };
64
60
  const beforeLeave = {};
@@ -105,8 +105,7 @@ export function createScrollRestoration() {
105
105
  // reload/back_forward document loads land on an existing entry (a fresh
106
106
  // navigation starts a new one and belongs at the top); the effect's
107
107
  // initial run performs the restore after first render
108
- const [nav] = (performance.getEntriesByType &&
109
- performance.getEntriesByType("navigation"));
108
+ const [nav] = performance.getEntriesByType?.("navigation");
110
109
  if (nav && nav.type !== "navigate")
111
110
  pending = depth();
112
111
  }
package/dist/routing.js CHANGED
@@ -391,9 +391,7 @@ export function createRoutes(routeDef, base = "") {
391
391
  let pattern = isLeaf ? path : path.split("/*", 1)[0];
392
392
  pattern = pattern
393
393
  .split("/")
394
- .map((s) => {
395
- return s.startsWith(":") || s.startsWith("*") ? s : encodeSegment(s);
396
- })
394
+ .map(s => (/^[:*]/.test(s) ? s : encodeSegment(s)))
397
395
  .join("/");
398
396
  acc.push({
399
397
  ...shared,
@@ -487,13 +485,12 @@ export function mergeParams(matches) {
487
485
  return params;
488
486
  }
489
487
  function createLocation(path, state, queryWrapper) {
490
- const origin = new URL(mockBase);
491
- const url = createMemo((prev = origin) => {
488
+ const url = createMemo((prev = new URL(mockBase)) => {
492
489
  const path_ = path();
493
490
  try {
494
491
  // anchor rooted paths against the origin explicitly - a path with
495
492
  // doubled leading slashes would otherwise parse as protocol-relative
496
- return new URL(path_[0] === "/" ? mockBase + path_ : path_, origin);
493
+ return new URL(path_[0] === "/" ? mockBase + path_ : path_, mockBase);
497
494
  }
498
495
  catch (err) {
499
496
  DEV && console.error(`Invalid path ${path_}`);
@@ -505,7 +502,6 @@ function createLocation(path, state, queryWrapper) {
505
502
  const pathname = createMemo(() => url().pathname);
506
503
  const search = createMemo(() => url().search);
507
504
  const hash = createMemo(() => url().hash);
508
- const key = () => "";
509
505
  const queryFn = createMemo(() => extractSearchParams(url()));
510
506
  return {
511
507
  get pathname() {
@@ -521,7 +517,7 @@ function createLocation(path, state, queryWrapper) {
521
517
  return state();
522
518
  },
523
519
  get key() {
524
- return key();
520
+ return "";
525
521
  },
526
522
  query: queryWrapper ? queryWrapper(queryFn) : createMemoObject(queryFn)
527
523
  };
@@ -660,7 +656,7 @@ export function createRouterContext(integration, branches, getContext, options =
660
656
  read = runWithOwner(routerOwner, () => createMemo(() => {
661
657
  const result = resolveLazySubtree(record);
662
658
  return result instanceof Promise ? result.then(() => undefined) : undefined;
663
- }, { name: "lazyRoutes" }));
659
+ }, DEV && { name: "lazyRoutes" }));
664
660
  lazyReaders.set(record, read);
665
661
  }
666
662
  return read();
@@ -695,7 +691,7 @@ export function createRouterContext(integration, branches, getContext, options =
695
691
  }
696
692
  }
697
693
  return m;
698
- }, { name: "matches" });
694
+ }, DEV && { name: "matches" });
699
695
  const routingPending = createMemo(() => isPending(() => {
700
696
  try {
701
697
  matches();
@@ -706,7 +702,7 @@ export function createRouterContext(integration, branches, getContext, options =
706
702
  }
707
703
  location.search;
708
704
  location.hash;
709
- }), { name: "routingPending" });
705
+ }), DEV && { name: "routingPending" });
710
706
  const isRouting = () => routingPending() || isPending(source);
711
707
  const transitionIntent = () => {
712
708
  if (!isPending(source))
@@ -912,10 +908,8 @@ export function createRouterContext(integration, branches, getContext, options =
912
908
  preloadIntent = "preload";
913
909
  for (let match in next) {
914
910
  const { route, params } = next[match];
915
- route.component &&
916
- route.component.preload &&
917
- route.component.preload();
918
911
  const { preload, component } = route;
912
+ component?.preload?.();
919
913
  const now = current && current[match];
920
914
  const unchanged = now &&
921
915
  now.route.key === route.key &&
@@ -978,9 +972,7 @@ export function createRouteContext(router, parent, outlet, match, matches = () =
978
972
  // its last valid value while this route is being torn down, so outgoing
979
973
  // components and preloads never observe another route's params.
980
974
  const params = wrapParams(() => mergeParams(matches()));
981
- component &&
982
- component.preload &&
983
- component.preload();
975
+ component?.preload?.();
984
976
  inPreloadFn = true;
985
977
  const data = preload
986
978
  ? preload({ params, location, intent: router.intent?.() || "initial" })
package/dist/utils.js CHANGED
@@ -78,13 +78,13 @@ export function createMatcher(path, partial, matchFilters) {
78
78
  path: len ? "" : "/",
79
79
  params: {}
80
80
  };
81
- const matchFilter = (s) => matchFilters === undefined ? undefined : matchFilters[s];
81
+ const filters = matchFilters;
82
82
  for (let i = 0; i < len; i++) {
83
83
  const segment = segments[i];
84
84
  const dynamic = segment[0] === ":";
85
85
  const locSegment = dynamic ? locSegments[i] : locSegments[i].toLowerCase();
86
86
  const key = dynamic ? segment.slice(1) : segment.toLowerCase();
87
- if (dynamic && matchSegment(locSegment, matchFilter(key))) {
87
+ if (dynamic && matchSegment(locSegment, filters?.[key])) {
88
88
  match.params[key] = locSegment;
89
89
  }
90
90
  else if (dynamic || !matchSegment(locSegment, key)) {
@@ -94,7 +94,7 @@ export function createMatcher(path, partial, matchFilters) {
94
94
  }
95
95
  if (splat) {
96
96
  const remainder = lenDiff ? locSegments.slice(-lenDiff).join("/") : "";
97
- if (matchSegment(remainder, matchFilter(splat))) {
97
+ if (matchSegment(remainder, filters?.[splat])) {
98
98
  match.params[splat] = remainder;
99
99
  }
100
100
  else {
@@ -104,25 +104,15 @@ export function createMatcher(path, partial, matchFilters) {
104
104
  return match;
105
105
  };
106
106
  }
107
- function matchSegment(input, filter) {
108
- const isEqual = (s) => s === input;
109
- if (filter === undefined) {
110
- return true;
111
- }
112
- else if (typeof filter === "string") {
113
- return isEqual(filter);
114
- }
115
- else if (typeof filter === "function") {
116
- return filter(input);
117
- }
118
- else if (Array.isArray(filter)) {
119
- return filter.some(isEqual);
120
- }
121
- else if (filter instanceof RegExp) {
122
- return filter.test(input);
123
- }
124
- return false;
125
- }
107
+ // a string filter is the segment itself; anything else not listed never matches
108
+ const matchSegment = (input, filter) => filter === undefined ||
109
+ (typeof filter === "function"
110
+ ? filter(input)
111
+ : Array.isArray(filter)
112
+ ? filter.includes(input)
113
+ : filter instanceof RegExp
114
+ ? filter.test(input)
115
+ : filter === input);
126
116
  export function scoreRoute(route) {
127
117
  const [pattern, splat] = route.pattern.split("/*", 2);
128
118
  const segments = pattern.split("/").filter(Boolean);
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "Ryan Turnquist"
7
7
  ],
8
8
  "license": "MIT",
9
- "version": "2.0.0-next.30",
9
+ "version": "2.0.0-next.31",
10
10
  "homepage": "https://github.com/solidjs/solid-router#readme",
11
11
  "repository": {
12
12
  "type": "git",
@@ -38,22 +38,22 @@
38
38
  "@rollup/plugin-node-resolve": "15.3.0",
39
39
  "@rollup/plugin-terser": "0.4.4",
40
40
  "@solidjs/vite-plugin": "3.0.0-next.35",
41
- "@solidjs/web": "^2.0.0-rc.9",
41
+ "@solidjs/web": "^2.0.0-rc.11",
42
42
  "@types/jest": "^29.5.14",
43
43
  "@types/node": "^22.10.0",
44
44
  "babel-preset-solid": "^2.0.0-rc.2",
45
45
  "jsdom": "^25.0.1",
46
46
  "prettier": "^3.4.1",
47
47
  "rollup": "^4.27.4",
48
- "solid-js": "^2.0.0-rc.9",
48
+ "solid-js": "^2.0.0-rc.11",
49
49
  "typescript": "^5.7.2",
50
50
  "vite": "^8.2.2",
51
51
  "vitest": "^4.1.11"
52
52
  },
53
53
  "peerDependencies": {
54
- "@solidjs/web": "^2.0.0-rc.9",
54
+ "@solidjs/web": "^2.0.0-rc.10",
55
55
  "filesystem-routing": ">=0.4.0",
56
- "solid-js": "^2.0.0-rc.9"
56
+ "solid-js": "^2.0.0-rc.10"
57
57
  },
58
58
  "peerDependenciesMeta": {
59
59
  "filesystem-routing": {