@solidjs/router 2.0.0-next.16 → 2.0.0-next.18

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/paths.d.ts CHANGED
@@ -108,6 +108,14 @@ type MultiPathContrib<Ps extends readonly string[], Def, PAcc extends Params> =
108
108
  export type RoutePaths<R extends readonly RouteDefinition[]> = number extends R["length"] ? any : PathEnd<DefaultSearchTypes, {}> & TuplePaths<R, {}>;
109
109
  /** Extracts the params record a paths node binds, as runtime (string-valued) params. */
110
110
  export type PathParamsOf<N> = N extends TypedPath<infer P> ? Flat<P> : Params;
111
+ /**
112
+ * The global Href brand (`@solidjs/web` reads the same registry symbol).
113
+ * On a paths node it holds the logical path — the routable pathname before
114
+ * `renderPath` decorates it for display (eg. hash mode's `#` prefix) — so
115
+ * `navigate()` and `redirect()` can route the node while `toString()` keeps
116
+ * yielding the display href for the DOM.
117
+ */
118
+ export declare const HREF: unique symbol;
111
119
  /**
112
120
  * Creates the runtime path proxy. It is instance-scoped: `renderPath` comes
113
121
  * from the router's history adapter (eg. hash routing prefixes `#`), and
package/dist/paths.js CHANGED
@@ -5,6 +5,14 @@ export const int = ((s) => /^-?\d+$/.test(s));
5
5
  // Runtime
6
6
  // ---------------------------------------------------------------------------
7
7
  const encodeParam = (value) => String(value).split("/").map(encodeURIComponent).join("/");
8
+ /**
9
+ * The global Href brand (`@solidjs/web` reads the same registry symbol).
10
+ * On a paths node it holds the logical path — the routable pathname before
11
+ * `renderPath` decorates it for display (eg. hash mode's `#` prefix) — so
12
+ * `navigate()` and `redirect()` can route the node while `toString()` keeps
13
+ * yielding the display href for the DOM.
14
+ */
15
+ export const HREF = Symbol.for("solid.Href");
8
16
  /**
9
17
  * Creates the runtime path proxy. It is instance-scoped: `renderPath` comes
10
18
  * from the router's history adapter (eg. hash routing prefixes `#`), and
@@ -32,7 +40,11 @@ export function createPathsProxy(renderPath = p => p, base = "") {
32
40
  if (prop === "toString")
33
41
  return () => toHref(pathname);
34
42
  if (typeof prop === "symbol")
35
- return prop === Symbol.toPrimitive ? () => toHref(pathname) : undefined;
43
+ return prop === Symbol.toPrimitive
44
+ ? () => toHref(pathname)
45
+ : prop === HREF
46
+ ? pathname || "/"
47
+ : undefined;
36
48
  return node(`${pathname}/${prop}`);
37
49
  }
38
50
  });
@@ -1,5 +1,5 @@
1
1
  /*@refresh skip*/
2
- import { createMemo, createRoot, getOwner, onCleanup, runWithOwner, untrack } from "solid-js";
2
+ import { createMemo, createRoot, getOwner, onCleanup, runWithOwner, untrack, Show } from "solid-js";
3
3
  import { getRequestEvent, isServer } from "@solidjs/web";
4
4
  import { createRouteContext, getIntent, getRouteMatches, resolveLazySubtree, RouteContextObj, setInPreloadFn, unresolvedLazyMatches } from "../routing.js";
5
5
  export function Root(props) {
@@ -104,13 +104,17 @@ export function Routes(props) {
104
104
  return <>{outlet()}</>;
105
105
  }
106
106
  const createOutlet = (child) => {
107
- return () => {
108
- const c = child();
109
- if (c) {
110
- return <RouteContextObj value={c}>{c.outlet()}</RouteContextObj>;
111
- }
112
- return undefined;
113
- };
107
+ // Keyed on the context's identity (#588). `when` compiles to a lazy prop
108
+ // getter, so child() is tracked inside Show's memo — the outlet function
109
+ // reads nothing reactive and Show owns the subtree. Levels whose context is
110
+ // reference-reused (parent layouts) keep their subtree across navigations;
111
+ // a changed context must re-create the provider, not just re-render under
112
+ // it: a context value is registered once at provider creation
113
+ // (setContext(provider, props.value)), so non-keyed reuse would leave new
114
+ // route content reading the previous level's context.
115
+ return () => (<Show when={child()} keyed>
116
+ {c => <RouteContextObj value={c}>{c.outlet()}</RouteContextObj>}
117
+ </Show>);
114
118
  };
115
119
  // for data only mode with single flight mutations
116
120
  function dataOnly(event, routerState, branches) {
@@ -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,7 +1,11 @@
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";
8
+ import { HREF } from "./paths.js";
5
9
  const MAX_REDIRECTS = 100;
6
10
  /** Consider this API opaque and internal. It is likely to change in the future. */
7
11
  export const RouterContextObj = createContext();
@@ -259,15 +263,20 @@ const encodeSegment = (s) => encodeURIComponent(s).replace(/%(2B|40|3A|24|26|2C|
259
263
  const lazyBoundaries = new WeakMap();
260
264
  // Module scope: boundary resolution is global, deterministic state (same
261
265
  // thunk -> same routes), shared by every factory instance and the server's
262
- // flight collector.
266
+ // flight collector. The counter is the source of truth; the client mirrors
267
+ // it into a signal for subscription. Server render is pure (signal writes
268
+ // are inert as of solid 2.0.0-rc.3): there the plain counter serves reads,
269
+ // and re-runs come from the parked computation's promise retry, not from
270
+ // reactivity.
271
+ let lazyTreeCounter = 0;
263
272
  const [lazyTreeVersion, setLazyTreeVersion] = createSignal(0);
264
273
  /** Reactive read of the lazy-subtree version — recompile compiled branches when it changes. */
265
274
  export function trackLazySubtrees() {
266
- return lazyTreeVersion();
275
+ return isServer ? lazyTreeCounter : lazyTreeVersion();
267
276
  }
268
277
  /** Non-reactive read, for cache-busting outside the reactive graph (server collectors). */
269
278
  export function peekLazySubtrees() {
270
- return untrack(lazyTreeVersion);
279
+ return lazyTreeCounter;
271
280
  }
272
281
  function getLazyBoundary(thunk) {
273
282
  let record = lazyBoundaries.get(thunk);
@@ -277,13 +286,32 @@ function getLazyBoundary(thunk) {
277
286
  }
278
287
  /**
279
288
  * 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.
289
+ * synchronously once available, the in-flight promise otherwise.
290
+ *
291
+ * Failure contract (same as solid's lazy(), 2.0.0-rc.1: the platform
292
+ * re-fetches a failed dynamic import): a client rejection is held through its
293
+ * settlement flush. The recomputes the settled promise triggers — the parked
294
+ * transition's and the mainline commit's — both consume it here as a
295
+ * synchronous throw, becoming cached error status that reaches the nearest
296
+ * error boundary like a failed lazy() component. The hold clears a microtask
297
+ * after the first delivery, so any later recompute — boundary reset(), a new
298
+ * navigation — finds a clean record and retries the import. Holding through
299
+ * the flush is what keeps the erroring computation from refiring the import
300
+ * in a tight loop while it fails. Server records are shared across requests
301
+ * and hold nothing — each request retries. Commit is always async — even for
302
+ * thunks returning arrays — so the version bump never writes a signal from
303
+ * inside a render computation.
283
304
  */
284
305
  export function resolveLazySubtree(record) {
285
306
  if (record.resolved)
286
307
  return record.resolved;
308
+ if (record.error !== undefined) {
309
+ if (!record.sweep) {
310
+ record.sweep = true;
311
+ queueMicrotask(() => (record.error = record.sweep = undefined));
312
+ }
313
+ throw record.error;
314
+ }
287
315
  return (record.promise ||= Promise.resolve(record.thunk()).then(m => {
288
316
  const routes = Array.isArray(m)
289
317
  ? m
@@ -292,8 +320,15 @@ export function resolveLazySubtree(record) {
292
320
  m.routes ||
293
321
  [];
294
322
  record.resolved = routes;
295
- setLazyTreeVersion(v => v + 1);
323
+ lazyTreeCounter++;
324
+ isServer || setLazyTreeVersion(lazyTreeCounter);
296
325
  return record.resolved;
326
+ }, e => {
327
+ // ?? Error(): a held undefined would read as "no failure" and refire
328
+ if (!isServer)
329
+ record.error = e ?? new Error();
330
+ record.promise = undefined;
331
+ throw e;
297
332
  }));
298
333
  }
299
334
  /**
@@ -443,7 +478,7 @@ function createLocation(path, state, queryWrapper) {
443
478
  return new URL(path_[0] === "/" ? mockBase + path_ : path_, origin);
444
479
  }
445
480
  catch (err) {
446
- console.error(`Invalid path ${path_}`);
481
+ DEV && console.error(`Invalid path ${path_}`);
447
482
  return prev;
448
483
  }
449
484
  }, {
@@ -586,8 +621,14 @@ export function createRouterContext(integration, branches, getContext, options =
586
621
  // from the carried promise's retry on the server; a boundary nested
587
622
  // inside a boundary just parks the recomputed chain again.
588
623
  const pending = unresolvedLazyMatches(m);
589
- if (pending.length)
590
- throw new NotReadyError(Promise.all(pending.map(resolveLazySubtree)));
624
+ if (pending.length) {
625
+ const all = Promise.all(pending.map(resolveLazySubtree));
626
+ // pre-handle rejections: the failure reaches the app through this
627
+ // computation's error status, so the raw chain must not also surface
628
+ // as an unhandled rejection
629
+ all.catch(() => { });
630
+ throw new NotReadyError(all);
631
+ }
591
632
  return m;
592
633
  });
593
634
  // Every write is a transition in Solid 2, so a native history pop forks the
@@ -596,7 +637,21 @@ export function createRouterContext(integration, branches, getContext, options =
596
637
  // window, and isPending over the location/matches read reports any
597
638
  // in-flight fork — including popstate traversals and the lazy-subtree
598
639
  // resolution matches() parks on.
599
- const isRouting = createMemo(() => isNavigating() || isPending(() => (matches(), location.search, location.hash)));
640
+ const isRouting = createMemo(() => isNavigating() ||
641
+ isPending(() => {
642
+ // A real error means the navigation settled (failed) — it surfaces
643
+ // to the app through render reads, not through this derivation.
644
+ // Not-ready must keep propagating so isPending sees the parking.
645
+ try {
646
+ matches();
647
+ }
648
+ catch (e) {
649
+ if (e instanceof NotReadyError)
650
+ throw e;
651
+ }
652
+ location.search;
653
+ location.hash;
654
+ }));
600
655
  const buildParams = () => mergeParams(matches());
601
656
  const wrapParams = utils.paramsWrapper
602
657
  ? (getParams) => utils.paramsWrapper(getParams, branches)
@@ -642,13 +697,24 @@ export function createRouterContext(integration, branches, getContext, options =
642
697
  utils.go(to);
643
698
  }
644
699
  else {
645
- console.warn("Router integration does not support relative routing");
700
+ DEV && console.warn("Router integration does not support relative routing");
646
701
  }
647
702
  return;
648
703
  }
649
- // typed path proxy nodes coerce to their href
704
+ // A paths node carries its logical path under the Href brand — read
705
+ // that rather than coercing: toString() renders the *display* href
706
+ // (eg. hash mode's `#` prefix), which is for the DOM, not for routing.
707
+ // Foreign Href-branded values without the slot still coerce.
650
708
  if (typeof to !== "string")
651
- to = to.toString();
709
+ to = to[HREF] || to.toString();
710
+ // Display hrefs can still arrive as plain strings: terminating paths
711
+ // calls type as `string`, and redirect Location headers round-trip
712
+ // through here. Under hash mode those start with `#` — a spelling no
713
+ // logical path uses — so map them back through the integration's
714
+ // parser, exactly like the anchor click handler does. Elsewhere
715
+ // parsePath is identity and `#...` keeps its URL meaning below.
716
+ if (to[0] === "#")
717
+ to = parsePath(to);
652
718
  const { replace, resolve, scroll, state: nextState } = {
653
719
  replace: false,
654
720
  resolve: true,
@@ -734,10 +800,16 @@ export function createRouterContext(integration, branches, getContext, options =
734
800
  const matches = getRouteMatches(branches(), url.pathname);
735
801
  // An unresolved lazy subtree in the chain: the placeholder's
736
802
  // component.preload (below) kicks the table load; once it lands,
737
- // preload again so the real inner routes warm too.
803
+ // preload again so the real inner routes warm too. Preloads are
804
+ // speculative: a failed load (held sync throw or rejection) is ignored
805
+ // here — the real navigation surfaces and retries it.
738
806
  const boundary = matches.find(m => m.route.lazy && !m.route.lazy.resolved);
739
- boundary &&
740
- resolveLazySubtree(boundary.route.lazy).then(() => preloadRoute(url, preloadData));
807
+ if (boundary) {
808
+ try {
809
+ resolveLazySubtree(boundary.route.lazy).then(() => preloadRoute(url, preloadData), () => { });
810
+ }
811
+ catch { }
812
+ }
741
813
  const prevIntent = intent;
742
814
  intent = "preload";
743
815
  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.16",
9
+ "version": "2.0.0-next.18",
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/vite-plugin": "3.0.0-next.28",
35
- "@solidjs/web": "^2.0.0-rc.0",
36
34
  "@babel/core": "^7.26.0",
37
35
  "@babel/preset-typescript": "^7.26.0",
38
36
  "@changesets/cli": "^2.27.10",
39
37
  "@rollup/plugin-babel": "6.0.4",
40
38
  "@rollup/plugin-node-resolve": "15.3.0",
41
39
  "@rollup/plugin-terser": "0.4.4",
40
+ "@solidjs/vite-plugin": "3.0.0-next.28",
41
+ "@solidjs/web": "^2.0.0-rc.3",
42
42
  "@types/jest": "^29.5.14",
43
43
  "@types/node": "^22.10.0",
44
- "babel-preset-solid": "^2.0.0-rc.0",
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.0",
48
+ "solid-js": "^2.0.0-rc.3",
49
49
  "typescript": "^5.7.2",
50
50
  "vite": "^6.0.0",
51
51
  "vitest": "^2.1.6"
52
52
  },
53
53
  "peerDependencies": {
54
- "@solidjs/web": "^2.0.0-rc.0",
55
- "solid-js": "^2.0.0-rc.0"
54
+ "@solidjs/web": "^2.0.0-rc.3",
55
+ "solid-js": "^2.0.0-rc.3"
56
56
  },
57
57
  "scripts": {
58
58
  "build": "rm -rf dist && tsc && rollup -c",