@solidjs/router 2.0.0-next.15 → 2.0.0-next.17

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/fs.js CHANGED
@@ -37,7 +37,9 @@ export function fileRoutes(entries) {
37
37
  }
38
38
  let component = components.get(ref.src);
39
39
  if (!component) {
40
- component = lazy(ref.import, ref.src);
40
+ // moduleUrl is lazy()'s third argument as of solid 2.0.0-rc.1 (options
41
+ // moved second); route components are default exports, so no { export }.
42
+ component = lazy(ref.import, undefined, ref.src);
41
43
  components.set(ref.src, component);
42
44
  }
43
45
  return component;
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- import { getOwner, runWithOwner, createMemo, createRenderEffect, onCleanup, untrack, createContext, createSignal, useContext, NotReadyError, isPending, flush, createComponent, createRoot, createEffect, sharedConfig, getObserver, $TRACK, action as action$1 } from 'solid-js';
1
+ import { getOwner, runWithOwner, createMemo, createRenderEffect, onCleanup, untrack, createContext, createSignal, useContext, NotReadyError, isPending, DEV, flush, createComponent, createRoot, createEffect, sharedConfig, getObserver, $TRACK, action as action$1 } from 'solid-js';
2
2
  import { registerElementClaim, delegateEvents, isServer, getRequestEvent, hasFlashCookie, clearFlashCookie, createComponent as createComponent$1, memo, isServerFunction, getServerFunctionMetadata, getServerFunctionRPC, isResponseEnvelope, REVALIDATE_HEADER } from '@solidjs/web';
3
3
  import { subscribeFlightData, decodeResponsePayload, createServerReference } from '@solidjs/web/server-functions';
4
4
  import { decodeFlashCookie } from '@solidjs/web/server-functions/server';
@@ -834,17 +834,41 @@ function getLazyBoundary(thunk) {
834
834
 
835
835
  /**
836
836
  * Kicks (or joins) a boundary's resolution. Returns the resolved routes
837
- * synchronously once available, the in-flight promise otherwise. Commit is
838
- * always async — even for thunks returning arrays — so the version bump
839
- * never writes a signal from inside a render computation.
837
+ * synchronously once available, the in-flight promise otherwise.
838
+ *
839
+ * Failure contract (same as solid's lazy(), 2.0.0-rc.1: the platform
840
+ * re-fetches a failed dynamic import): a client rejection is held through its
841
+ * settlement flush. The recomputes the settled promise triggers — the parked
842
+ * transition's and the mainline commit's — both consume it here as a
843
+ * synchronous throw, becoming cached error status that reaches the nearest
844
+ * error boundary like a failed lazy() component. The hold clears a microtask
845
+ * after the first delivery, so any later recompute — boundary reset(), a new
846
+ * navigation — finds a clean record and retries the import. Holding through
847
+ * the flush is what keeps the erroring computation from refiring the import
848
+ * in a tight loop while it fails. Server records are shared across requests
849
+ * and hold nothing — each request retries. Commit is always async — even for
850
+ * thunks returning arrays — so the version bump never writes a signal from
851
+ * inside a render computation.
840
852
  */
841
853
  function resolveLazySubtree(record) {
842
854
  if (record.resolved) return record.resolved;
855
+ if (record.error !== undefined) {
856
+ if (!record.sweep) {
857
+ record.sweep = true;
858
+ queueMicrotask(() => record.error = record.sweep = undefined);
859
+ }
860
+ throw record.error;
861
+ }
843
862
  return record.promise ||= Promise.resolve(record.thunk()).then(m => {
844
863
  const routes = Array.isArray(m) ? m : m.default || m.routes || [];
845
864
  record.resolved = routes;
846
865
  setLazyTreeVersion(v => v + 1);
847
866
  return record.resolved;
867
+ }, e => {
868
+ // ?? Error(): a held undefined would read as "no failure" and refire
869
+ if (!isServer) record.error = e ?? new Error();
870
+ record.promise = undefined;
871
+ throw e;
848
872
  });
849
873
  }
850
874
 
@@ -992,7 +1016,7 @@ function createLocation(path, state, queryWrapper) {
992
1016
  // doubled leading slashes would otherwise parse as protocol-relative
993
1017
  return new URL(path_[0] === "/" ? mockBase + path_ : path_, origin);
994
1018
  } catch (err) {
995
- console.error(`Invalid path ${path_}`);
1019
+ DEV && console.error(`Invalid path ${path_}`);
996
1020
  return prev;
997
1021
  }
998
1022
  }, {
@@ -1143,7 +1167,14 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1143
1167
  // from the carried promise's retry on the server; a boundary nested
1144
1168
  // inside a boundary just parks the recomputed chain again.
1145
1169
  const pending = unresolvedLazyMatches(m);
1146
- if (pending.length) throw new NotReadyError(Promise.all(pending.map(resolveLazySubtree)));
1170
+ if (pending.length) {
1171
+ const all = Promise.all(pending.map(resolveLazySubtree));
1172
+ // pre-handle rejections: the failure reaches the app through this
1173
+ // computation's error status, so the raw chain must not also surface
1174
+ // as an unhandled rejection
1175
+ all.catch(() => {});
1176
+ throw new NotReadyError(all);
1177
+ }
1147
1178
  return m;
1148
1179
  });
1149
1180
 
@@ -1153,7 +1184,18 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1153
1184
  // window, and isPending over the location/matches read reports any
1154
1185
  // in-flight fork — including popstate traversals and the lazy-subtree
1155
1186
  // resolution matches() parks on.
1156
- const isRouting = createMemo(() => isNavigating() || isPending(() => (matches(), location.search, location.hash)));
1187
+ const isRouting = createMemo(() => isNavigating() || isPending(() => {
1188
+ // A real error means the navigation settled (failed) — it surfaces
1189
+ // to the app through render reads, not through this derivation.
1190
+ // Not-ready must keep propagating so isPending sees the parking.
1191
+ try {
1192
+ matches();
1193
+ } catch (e) {
1194
+ if (e instanceof NotReadyError) throw e;
1195
+ }
1196
+ location.search;
1197
+ location.hash;
1198
+ }));
1157
1199
  const buildParams = () => mergeParams(matches());
1158
1200
  const wrapParams = utils.paramsWrapper ? getParams => utils.paramsWrapper(getParams, branches) : getParams => createMemoObject(getParams);
1159
1201
  const params = wrapParams(buildParams);
@@ -1195,7 +1237,7 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1195
1237
  if (!to) ; else if (utils.go) {
1196
1238
  utils.go(to);
1197
1239
  } else {
1198
- console.warn("Router integration does not support relative routing");
1240
+ DEV && console.warn("Router integration does not support relative routing");
1199
1241
  }
1200
1242
  return;
1201
1243
  }
@@ -1301,9 +1343,15 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1301
1343
  const matches = getRouteMatches(branches(), url.pathname);
1302
1344
  // An unresolved lazy subtree in the chain: the placeholder's
1303
1345
  // component.preload (below) kicks the table load; once it lands,
1304
- // preload again so the real inner routes warm too.
1346
+ // preload again so the real inner routes warm too. Preloads are
1347
+ // speculative: a failed load (held sync throw or rejection) is ignored
1348
+ // here — the real navigation surfaces and retries it.
1305
1349
  const boundary = matches.find(m => m.route.lazy && !m.route.lazy.resolved);
1306
- boundary && resolveLazySubtree(boundary.route.lazy).then(() => preloadRoute(url, preloadData));
1350
+ if (boundary) {
1351
+ try {
1352
+ resolveLazySubtree(boundary.route.lazy).then(() => preloadRoute(url, preloadData), () => {});
1353
+ } catch {}
1354
+ }
1307
1355
  const prevIntent = intent;
1308
1356
  intent = "preload";
1309
1357
  for (let match in matches) {
@@ -1982,7 +2030,7 @@ function createRouter(config) {
1982
2030
  // fight it — stale content on click navigations, conflicting link
1983
2031
  // attributes. Compose route trees instead; lazy subtrees are the planned
1984
2032
  // answer for definitions unknown at build time.
1985
- if (useOptionalContext(RouterContextObj)) {
2033
+ if (DEV && useOptionalContext(RouterContextObj)) {
1986
2034
  console.warn("Mounting a router inside another router is not supported. " + "Compose route trees in one createRouter config instead.");
1987
2035
  }
1988
2036
  const root = untrack(() => props.children);
@@ -1,5 +1,8 @@
1
1
  /*@refresh skip*/
2
2
  import { createSignal, getOwner, onCleanup, sharedConfig, untrack } from "solid-js";
3
+ // standalone import: `DEV` is undefined in solid's production build, so app
4
+ // bundlers fold `DEV &&` diagnostics out of shipped bundles
5
+ import { DEV } from "solid-js";
3
6
  import { getRequestEvent, isServer } from "@solidjs/web";
4
7
  import { setupLinkClaims } from "../claims.js";
5
8
  import { setupNativeEvents } from "../data/events.js";
@@ -89,7 +92,7 @@ export function createRouter(config) {
89
92
  // fight it — stale content on click navigations, conflicting link
90
93
  // attributes. Compose route trees instead; lazy subtrees are the planned
91
94
  // answer for definitions unknown at build time.
92
- if (useOptionalContext(RouterContextObj)) {
95
+ if (DEV && useOptionalContext(RouterContextObj)) {
93
96
  console.warn("Mounting a router inside another router is not supported. " +
94
97
  "Compose route trees in one createRouter config instead.");
95
98
  }
package/dist/routing.d.ts CHANGED
@@ -189,9 +189,21 @@ export declare function trackLazySubtrees(): number;
189
189
  export declare function peekLazySubtrees(): number;
190
190
  /**
191
191
  * Kicks (or joins) a boundary's resolution. Returns the resolved routes
192
- * synchronously once available, the in-flight promise otherwise. Commit is
193
- * always async — even for thunks returning arrays — so the version bump
194
- * never writes a signal from inside a render computation.
192
+ * synchronously once available, the in-flight promise otherwise.
193
+ *
194
+ * Failure contract (same as solid's lazy(), 2.0.0-rc.1: the platform
195
+ * re-fetches a failed dynamic import): a client rejection is held through its
196
+ * settlement flush. The recomputes the settled promise triggers — the parked
197
+ * transition's and the mainline commit's — both consume it here as a
198
+ * synchronous throw, becoming cached error status that reaches the nearest
199
+ * error boundary like a failed lazy() component. The hold clears a microtask
200
+ * after the first delivery, so any later recompute — boundary reset(), a new
201
+ * navigation — finds a clean record and retries the import. Holding through
202
+ * the flush is what keeps the erroring computation from refiring the import
203
+ * in a tight loop while it fails. Server records are shared across requests
204
+ * and hold nothing — each request retries. Commit is always async — even for
205
+ * thunks returning arrays — so the version bump never writes a signal from
206
+ * inside a render computation.
195
207
  */
196
208
  export declare function resolveLazySubtree(record: LazyBoundary): readonly RouteDefinition[] | Promise<readonly RouteDefinition[]>;
197
209
  /**
package/dist/routing.js CHANGED
@@ -1,4 +1,7 @@
1
1
  import { flush, runWithOwner } from "solid-js";
2
+ // standalone import: `DEV` is undefined in solid's production build, so app
3
+ // bundlers fold `DEV &&` diagnostics out of shipped bundles
4
+ import { DEV } from "solid-js";
2
5
  import { createComponent, createContext, createMemo, createSignal, isPending, NotReadyError, untrack, useContext } from "solid-js";
3
6
  import { clearFlashCookie, getRequestEvent, hasFlashCookie, isServer } from "@solidjs/web";
4
7
  import { mockBase, comparablePath, createMemoObject, extractSearchParams, invariant, resolvePath, createMatcher, joinPaths, scoreRoute, mergeSearchString, expandOptionals } from "./utils.js";
@@ -277,13 +280,32 @@ function getLazyBoundary(thunk) {
277
280
  }
278
281
  /**
279
282
  * Kicks (or joins) a boundary's resolution. Returns the resolved routes
280
- * synchronously once available, the in-flight promise otherwise. Commit is
281
- * always async — even for thunks returning arrays — so the version bump
282
- * never writes a signal from inside a render computation.
283
+ * synchronously once available, the in-flight promise otherwise.
284
+ *
285
+ * Failure contract (same as solid's lazy(), 2.0.0-rc.1: the platform
286
+ * re-fetches a failed dynamic import): a client rejection is held through its
287
+ * settlement flush. The recomputes the settled promise triggers — the parked
288
+ * transition's and the mainline commit's — both consume it here as a
289
+ * synchronous throw, becoming cached error status that reaches the nearest
290
+ * error boundary like a failed lazy() component. The hold clears a microtask
291
+ * after the first delivery, so any later recompute — boundary reset(), a new
292
+ * navigation — finds a clean record and retries the import. Holding through
293
+ * the flush is what keeps the erroring computation from refiring the import
294
+ * in a tight loop while it fails. Server records are shared across requests
295
+ * and hold nothing — each request retries. Commit is always async — even for
296
+ * thunks returning arrays — so the version bump never writes a signal from
297
+ * inside a render computation.
283
298
  */
284
299
  export function resolveLazySubtree(record) {
285
300
  if (record.resolved)
286
301
  return record.resolved;
302
+ if (record.error !== undefined) {
303
+ if (!record.sweep) {
304
+ record.sweep = true;
305
+ queueMicrotask(() => (record.error = record.sweep = undefined));
306
+ }
307
+ throw record.error;
308
+ }
287
309
  return (record.promise ||= Promise.resolve(record.thunk()).then(m => {
288
310
  const routes = Array.isArray(m)
289
311
  ? m
@@ -294,6 +316,12 @@ export function resolveLazySubtree(record) {
294
316
  record.resolved = routes;
295
317
  setLazyTreeVersion(v => v + 1);
296
318
  return record.resolved;
319
+ }, e => {
320
+ // ?? Error(): a held undefined would read as "no failure" and refire
321
+ if (!isServer)
322
+ record.error = e ?? new Error();
323
+ record.promise = undefined;
324
+ throw e;
297
325
  }));
298
326
  }
299
327
  /**
@@ -443,7 +471,7 @@ function createLocation(path, state, queryWrapper) {
443
471
  return new URL(path_[0] === "/" ? mockBase + path_ : path_, origin);
444
472
  }
445
473
  catch (err) {
446
- console.error(`Invalid path ${path_}`);
474
+ DEV && console.error(`Invalid path ${path_}`);
447
475
  return prev;
448
476
  }
449
477
  }, {
@@ -586,8 +614,14 @@ export function createRouterContext(integration, branches, getContext, options =
586
614
  // from the carried promise's retry on the server; a boundary nested
587
615
  // inside a boundary just parks the recomputed chain again.
588
616
  const pending = unresolvedLazyMatches(m);
589
- if (pending.length)
590
- throw new NotReadyError(Promise.all(pending.map(resolveLazySubtree)));
617
+ if (pending.length) {
618
+ const all = Promise.all(pending.map(resolveLazySubtree));
619
+ // pre-handle rejections: the failure reaches the app through this
620
+ // computation's error status, so the raw chain must not also surface
621
+ // as an unhandled rejection
622
+ all.catch(() => { });
623
+ throw new NotReadyError(all);
624
+ }
591
625
  return m;
592
626
  });
593
627
  // Every write is a transition in Solid 2, so a native history pop forks the
@@ -596,7 +630,21 @@ export function createRouterContext(integration, branches, getContext, options =
596
630
  // window, and isPending over the location/matches read reports any
597
631
  // in-flight fork — including popstate traversals and the lazy-subtree
598
632
  // resolution matches() parks on.
599
- const isRouting = createMemo(() => isNavigating() || isPending(() => (matches(), location.search, location.hash)));
633
+ const isRouting = createMemo(() => isNavigating() ||
634
+ isPending(() => {
635
+ // A real error means the navigation settled (failed) — it surfaces
636
+ // to the app through render reads, not through this derivation.
637
+ // Not-ready must keep propagating so isPending sees the parking.
638
+ try {
639
+ matches();
640
+ }
641
+ catch (e) {
642
+ if (e instanceof NotReadyError)
643
+ throw e;
644
+ }
645
+ location.search;
646
+ location.hash;
647
+ }));
600
648
  const buildParams = () => mergeParams(matches());
601
649
  const wrapParams = utils.paramsWrapper
602
650
  ? (getParams) => utils.paramsWrapper(getParams, branches)
@@ -642,7 +690,7 @@ export function createRouterContext(integration, branches, getContext, options =
642
690
  utils.go(to);
643
691
  }
644
692
  else {
645
- console.warn("Router integration does not support relative routing");
693
+ DEV && console.warn("Router integration does not support relative routing");
646
694
  }
647
695
  return;
648
696
  }
@@ -734,10 +782,16 @@ export function createRouterContext(integration, branches, getContext, options =
734
782
  const matches = getRouteMatches(branches(), url.pathname);
735
783
  // An unresolved lazy subtree in the chain: the placeholder's
736
784
  // component.preload (below) kicks the table load; once it lands,
737
- // preload again so the real inner routes warm too.
785
+ // preload again so the real inner routes warm too. Preloads are
786
+ // speculative: a failed load (held sync throw or rejection) is ignored
787
+ // here — the real navigation surfaces and retries it.
738
788
  const boundary = matches.find(m => m.route.lazy && !m.route.lazy.resolved);
739
- boundary &&
740
- resolveLazySubtree(boundary.route.lazy).then(() => preloadRoute(url, preloadData));
789
+ if (boundary) {
790
+ try {
791
+ resolveLazySubtree(boundary.route.lazy).then(() => preloadRoute(url, preloadData), () => { });
792
+ }
793
+ catch { }
794
+ }
741
795
  const prevIntent = intent;
742
796
  intent = "preload";
743
797
  for (let match in matches) {
package/dist/types.d.ts CHANGED
@@ -231,6 +231,12 @@ export interface LazyBoundary {
231
231
  thunk: LazyRouteChildren;
232
232
  promise?: Promise<readonly RouteDefinition[]>;
233
233
  resolved?: readonly RouteDefinition[];
234
+ /** A client load failure, held through its settlement flush so every
235
+ * recompute it triggers delivers the same error instead of refiring the
236
+ * import; cleared a microtask after first delivery so retries can run. */
237
+ error?: unknown;
238
+ /** The pending clear of `error` has been scheduled. */
239
+ sweep?: boolean;
234
240
  }
235
241
  export interface Branch {
236
242
  routes: RouteDescription[];
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "Ryan Turnquist"
7
7
  ],
8
8
  "license": "MIT",
9
- "version": "2.0.0-next.15",
9
+ "version": "2.0.0-next.17",
10
10
  "homepage": "https://github.com/solidjs/solid-router#readme",
11
11
  "repository": {
12
12
  "type": "git",
@@ -31,28 +31,28 @@
31
31
  ],
32
32
  "sideEffects": false,
33
33
  "devDependencies": {
34
- "@solidjs/web": "2.0.0-beta.33",
35
34
  "@babel/core": "^7.26.0",
36
35
  "@babel/preset-typescript": "^7.26.0",
37
36
  "@changesets/cli": "^2.27.10",
38
37
  "@rollup/plugin-babel": "6.0.4",
39
38
  "@rollup/plugin-node-resolve": "15.3.0",
40
39
  "@rollup/plugin-terser": "0.4.4",
40
+ "@solidjs/vite-plugin": "3.0.0-next.28",
41
+ "@solidjs/web": "^2.0.0-rc.1",
41
42
  "@types/jest": "^29.5.14",
42
43
  "@types/node": "^22.10.0",
43
- "babel-preset-solid": "2.0.0-beta.33",
44
+ "babel-preset-solid": "^2.0.0-rc.1",
44
45
  "jsdom": "^25.0.1",
45
46
  "prettier": "^3.4.1",
46
47
  "rollup": "^4.27.4",
47
- "solid-js": "2.0.0-beta.33",
48
+ "solid-js": "^2.0.0-rc.1",
48
49
  "typescript": "^5.7.2",
49
50
  "vite": "^6.0.0",
50
- "vite-plugin-solid": "3.0.0-next.10",
51
51
  "vitest": "^2.1.6"
52
52
  },
53
53
  "peerDependencies": {
54
- "@solidjs/web": ">=2.0.0-beta.33 <2.0.0-experimental.0",
55
- "solid-js": ">=2.0.0-beta.33 <2.0.0-experimental.0"
54
+ "@solidjs/web": "^2.0.0-rc.1",
55
+ "solid-js": "^2.0.0-rc.1"
56
56
  },
57
57
  "scripts": {
58
58
  "build": "rm -rf dist && tsc && rollup -c",