@solidjs/router 2.0.0-next.23 → 2.0.0-next.24

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
@@ -750,6 +750,8 @@ const isRouting = useIsRouting();
750
750
  return <div classList={{ "grey-out": isRouting() }}>...</div>;
751
751
  ```
752
752
 
753
+ 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.
754
+
753
755
  ### useMatch
754
756
 
755
757
  Tests a path *pattern you supply* against the current location; returns a memo of match information or `undefined`. It never consults the route tree — the pattern doesn't have to correspond to a defined route. The match's `params` are typed from the pattern, and a typed path node works too (a concrete URL — useful for "am I here" checks):
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, onSettled, getObserver, $TRACK, action as action$1 } from 'solid-js';
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';
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, decodeRedirectHeaderValue, REDIRECT_HEADER, decodeResponsePayload, parseServerFunctionUrl, createServerReference } from '@solidjs/web/server-functions';
4
4
  import { decodeFlashCookie } from '@solidjs/web/server-functions/server';
@@ -1221,6 +1221,8 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1221
1221
  read = runWithOwner(routerOwner, () => createMemo(() => {
1222
1222
  const result = resolveLazySubtree(record);
1223
1223
  return result instanceof Promise ? result.then(() => undefined) : undefined;
1224
+ }, {
1225
+ name: "lazyRoutes"
1224
1226
  }));
1225
1227
  lazyReaders.set(record, read);
1226
1228
  }
@@ -1252,6 +1254,8 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1252
1254
  }
1253
1255
  }
1254
1256
  return m;
1257
+ }, {
1258
+ name: "matches"
1255
1259
  });
1256
1260
  const routingPending = createMemo(() => isPending(() => {
1257
1261
  try {
@@ -1261,7 +1265,9 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1261
1265
  }
1262
1266
  location.search;
1263
1267
  location.hash;
1264
- }));
1268
+ }), {
1269
+ name: "routingPending"
1270
+ });
1265
1271
  const isRouting = () => routingPending() || isPending(source);
1266
1272
  const transitionIntent = () => {
1267
1273
  if (!isPending(source)) return;
@@ -2023,32 +2029,74 @@ function defineRoutes(routes) {
2023
2029
  function defineRoute(route) {
2024
2030
  return route;
2025
2031
  }
2032
+ /**
2033
+ * What one location write is, for solid's observe tier: the parametrized
2034
+ * route it heads to, the params, where it came from, and — when the write is
2035
+ * the router chasing a redirect while the previous target is still pending —
2036
+ * which hop of that navigation it is. The engine times the navigation from
2037
+ * this declaration (or from the user event enclosing it) until its writes
2038
+ * are through, and names holds and re-runs after it.
2039
+ *
2040
+ * `name` and `params` are getters: the engine reads them when the navigation
2041
+ * settles, not when it starts, so a lazy route subtree that loaded during the
2042
+ * hold names the exact route it resolved to rather than its placeholder.
2043
+ * Redirect depth comes from `_navigation` (1 = a navigation, n = its
2044
+ * (n - 1)th redirect hop, -1 = the browser moved: back/forward, hash).
2045
+ */
2046
+ function describeNavigation(match, next, from) {
2047
+ const pathname = new URL(next.value, mockBase).pathname;
2048
+ const matches = () => untrack(() => match(pathname));
2049
+ const ref = {
2050
+ kind: "navigation",
2051
+ to: next.value,
2052
+ from,
2053
+ get name() {
2054
+ const m = matches();
2055
+ return m.length ? m[m.length - 1].route.pattern || "/" : pathname;
2056
+ },
2057
+ get params() {
2058
+ const m = matches();
2059
+ return m.length ? mergeParams(m) : undefined;
2060
+ }
2061
+ };
2062
+ if (next._navigation !== undefined && next._navigation > 1) ref.redirect = next._navigation - 1;
2063
+ return ref;
2064
+ }
2065
+
2026
2066
  /** Wraps a history adapter in the integration signal the router core consumes. Must run under a reactive owner. */
2027
- function createIntegration(history) {
2067
+ function createIntegration(history, match) {
2028
2068
  let committing = false;
2029
2069
  const wrap = value => typeof value === "string" ? {
2030
2070
  value
2031
2071
  } : value;
2032
2072
  const [read, write] = createSignal(wrap(history.get()), {
2033
2073
  equals: (a, b) => a.value === b.value && a.state === b.state && a._navigation === b._navigation,
2034
- ownedWrite: true
2074
+ ownedWrite: true,
2075
+ name: "location"
2035
2076
  });
2036
2077
  const signal = [read, next => {
2037
2078
  if (sharedConfig.registry && !sharedConfig.done) sharedConfig.done = true;
2038
- write(next);
2039
- if (next._navigation && next._navigation > 0) {
2040
- // Register out of band so a destination error boundary replacing the
2041
- // Router subtree cannot suppress the winning history commit.
2042
- runWithOwner(null, () => onSettled(() => {
2043
- if (read() !== next) return;
2044
- committing = true;
2045
- try {
2046
- history.set(next);
2047
- } finally {
2048
- committing = false;
2049
- }
2050
- }));
2051
- }
2079
+ const commit = () => {
2080
+ write(next);
2081
+ if (next._navigation && next._navigation > 0) {
2082
+ // Register out of band so a destination error boundary replacing the
2083
+ // Router subtree cannot suppress the winning history commit.
2084
+ runWithOwner(null, () => onSettled(() => {
2085
+ if (read() !== next) return;
2086
+ committing = true;
2087
+ try {
2088
+ history.set(next);
2089
+ } finally {
2090
+ committing = false;
2091
+ }
2092
+ }));
2093
+ }
2094
+ };
2095
+ // Every client location write passes here — navigate(), a redirect hop,
2096
+ // the browser's own back/forward — so this is the one place the
2097
+ // navigation is declared. `read()` still holds the committed location
2098
+ // while a navigation is pending, which is the `from` a hop wants too.
2099
+ OBSERVE ? OBSERVE.attribution.withOrigin(describeNavigation(match, next, untrack(read).value), commit) : commit();
2052
2100
  }];
2053
2101
  history.init && onCleanup(history.init((value = history.get()) => {
2054
2102
  if (committing) return;
@@ -2104,6 +2152,7 @@ function createRouter(config) {
2104
2152
  return compiled;
2105
2153
  };
2106
2154
  const renderPath = config.history && config.history.utils && config.history.utils.renderPath || undefined;
2155
+ const matchPath = pathname => getRouteMatches(branches(), config.transformUrl ? config.transformUrl(pathname) : pathname);
2107
2156
  function RouterComponent(props) {
2108
2157
  // One router per app: the session (location, history, delegation, link
2109
2158
  // claims, preloading) has a single owner, and a second instance would
@@ -2120,7 +2169,7 @@ function createRouter(config) {
2120
2169
  restoration = createScrollRestoration();
2121
2170
  history = withScrollRestoration(history || browserHistory(), restoration);
2122
2171
  }
2123
- const integration = isServer ? staticIntegration(props.url, config.history && config.history.utils) : createIntegration(history || browserHistory());
2172
+ const integration = isServer ? staticIntegration(props.url, config.history && config.history.utils) : createIntegration(history || browserHistory(), matchPath);
2124
2173
  let context;
2125
2174
  const routerState = createRouterContext(integration, branches, () => context, {
2126
2175
  base: basePath,
@@ -2161,9 +2210,7 @@ function createRouter(config) {
2161
2210
  routes: config.routes,
2162
2211
  config,
2163
2212
  match(url) {
2164
- const u = new URL(url, mockBase);
2165
- const pathname = config.transformUrl ? config.transformUrl(u.pathname) : u.pathname;
2166
- return getRouteMatches(branches(), pathname).map(({
2213
+ return matchPath(new URL(url, mockBase).pathname).map(({
2167
2214
  route,
2168
2215
  path,
2169
2216
  params
@@ -1,13 +1,14 @@
1
1
  /*@refresh skip*/
2
2
  import { createSignal, getOwner, onCleanup, onSettled, runWithOwner, 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
+ // standalone imports: `DEV` is undefined in solid's production build and
4
+ // `OBSERVE` outside its observe/dev builds, so app bundlers fold the
5
+ // `DEV &&` diagnostics and the `OBSERVE &&` attribution out of shipped bundles
6
+ import { DEV, OBSERVE } from "solid-js";
6
7
  import { getRequestEvent, isServer } from "@solidjs/web";
7
8
  import { setupLinkClaims } from "../claims.js";
8
9
  import { setupNativeEvents } from "../data/events.js";
9
10
  import { createPathsProxy } from "../paths.js";
10
- import { createBranches, createRouterContext, getRouteMatches, registerFlightRouter, RouterContextObj, trackLazySubtrees, useOptionalContext } from "../routing.js";
11
+ import { createBranches, createRouterContext, getRouteMatches, mergeParams, registerFlightRouter, RouterContextObj, trackLazySubtrees, useOptionalContext } from "../routing.js";
11
12
  import { mockBase } from "../utils.js";
12
13
  import { Root, Routes } from "./components.jsx";
13
14
  import { browserHistory } from "./history.js";
@@ -25,35 +26,79 @@ export function defineRoutes(routes) {
25
26
  export function defineRoute(route) {
26
27
  return route;
27
28
  }
29
+ /**
30
+ * What one location write is, for solid's observe tier: the parametrized
31
+ * route it heads to, the params, where it came from, and — when the write is
32
+ * the router chasing a redirect while the previous target is still pending —
33
+ * which hop of that navigation it is. The engine times the navigation from
34
+ * this declaration (or from the user event enclosing it) until its writes
35
+ * are through, and names holds and re-runs after it.
36
+ *
37
+ * `name` and `params` are getters: the engine reads them when the navigation
38
+ * settles, not when it starts, so a lazy route subtree that loaded during the
39
+ * hold names the exact route it resolved to rather than its placeholder.
40
+ * Redirect depth comes from `_navigation` (1 = a navigation, n = its
41
+ * (n - 1)th redirect hop, -1 = the browser moved: back/forward, hash).
42
+ */
43
+ function describeNavigation(match, next, from) {
44
+ const pathname = new URL(next.value, mockBase).pathname;
45
+ const matches = () => untrack(() => match(pathname));
46
+ const ref = {
47
+ kind: "navigation",
48
+ to: next.value,
49
+ from,
50
+ get name() {
51
+ const m = matches();
52
+ return m.length ? m[m.length - 1].route.pattern || "/" : pathname;
53
+ },
54
+ get params() {
55
+ const m = matches();
56
+ return m.length ? mergeParams(m) : undefined;
57
+ }
58
+ };
59
+ if (next._navigation !== undefined && next._navigation > 1)
60
+ ref.redirect = next._navigation - 1;
61
+ return ref;
62
+ }
28
63
  /** Wraps a history adapter in the integration signal the router core consumes. Must run under a reactive owner. */
29
- function createIntegration(history) {
64
+ function createIntegration(history, match) {
30
65
  let committing = false;
31
66
  const wrap = (value) => (typeof value === "string" ? { value } : value);
32
67
  const [read, write] = createSignal(wrap(history.get()), {
33
68
  equals: (a, b) => a.value === b.value && a.state === b.state && a._navigation === b._navigation,
34
- ownedWrite: true
69
+ ownedWrite: true,
70
+ name: "location"
35
71
  });
36
72
  const signal = [
37
73
  read,
38
74
  (next) => {
39
75
  if (sharedConfig.registry && !sharedConfig.done)
40
76
  sharedConfig.done = true;
41
- write(next);
42
- if (next._navigation && next._navigation > 0) {
43
- // Register out of band so a destination error boundary replacing the
44
- // Router subtree cannot suppress the winning history commit.
45
- runWithOwner(null, () => onSettled(() => {
46
- if (read() !== next)
47
- return;
48
- committing = true;
49
- try {
50
- history.set(next);
51
- }
52
- finally {
53
- committing = false;
54
- }
55
- }));
56
- }
77
+ const commit = () => {
78
+ write(next);
79
+ if (next._navigation && next._navigation > 0) {
80
+ // Register out of band so a destination error boundary replacing the
81
+ // Router subtree cannot suppress the winning history commit.
82
+ runWithOwner(null, () => onSettled(() => {
83
+ if (read() !== next)
84
+ return;
85
+ committing = true;
86
+ try {
87
+ history.set(next);
88
+ }
89
+ finally {
90
+ committing = false;
91
+ }
92
+ }));
93
+ }
94
+ };
95
+ // Every client location write passes here — navigate(), a redirect hop,
96
+ // the browser's own back/forward — so this is the one place the
97
+ // navigation is declared. `read()` still holds the committed location
98
+ // while a navigation is pending, which is the `from` a hop wants too.
99
+ OBSERVE
100
+ ? OBSERVE.attribution.withOrigin(describeNavigation(match, next, untrack(read).value), commit)
101
+ : commit();
57
102
  }
58
103
  ];
59
104
  history.init &&
@@ -100,6 +145,7 @@ export function createRouter(config) {
100
145
  return compiled;
101
146
  };
102
147
  const renderPath = (config.history && config.history.utils && config.history.utils.renderPath) || undefined;
148
+ const matchPath = (pathname) => getRouteMatches(branches(), config.transformUrl ? config.transformUrl(pathname) : pathname);
103
149
  function RouterComponent(props) {
104
150
  // One router per app: the session (location, history, delegation, link
105
151
  // claims, preloading) has a single owner, and a second instance would
@@ -119,7 +165,7 @@ export function createRouter(config) {
119
165
  }
120
166
  const integration = isServer
121
167
  ? staticIntegration(props.url, config.history && config.history.utils)
122
- : createIntegration(history || browserHistory());
168
+ : createIntegration(history || browserHistory(), matchPath);
123
169
  let context;
124
170
  const routerState = createRouterContext(integration, branches, () => context, {
125
171
  base: basePath,
@@ -149,9 +195,7 @@ export function createRouter(config) {
149
195
  routes: config.routes,
150
196
  config,
151
197
  match(url) {
152
- const u = new URL(url, mockBase);
153
- const pathname = config.transformUrl ? config.transformUrl(u.pathname) : u.pathname;
154
- return getRouteMatches(branches(), pathname).map(({ route, path, params }) => ({
198
+ return matchPath(new URL(url, mockBase).pathname).map(({ route, path, params }) => ({
155
199
  path: route.originalPath,
156
200
  pattern: route.pattern,
157
201
  match: path,
package/dist/routing.js CHANGED
@@ -647,7 +647,7 @@ export function createRouterContext(integration, branches, getContext, options =
647
647
  read = runWithOwner(routerOwner, () => createMemo(() => {
648
648
  const result = resolveLazySubtree(record);
649
649
  return result instanceof Promise ? result.then(() => undefined) : undefined;
650
- }));
650
+ }, { name: "lazyRoutes" }));
651
651
  lazyReaders.set(record, read);
652
652
  }
653
653
  return read();
@@ -682,7 +682,7 @@ export function createRouterContext(integration, branches, getContext, options =
682
682
  }
683
683
  }
684
684
  return m;
685
- });
685
+ }, { name: "matches" });
686
686
  const routingPending = createMemo(() => isPending(() => {
687
687
  try {
688
688
  matches();
@@ -693,7 +693,7 @@ export function createRouterContext(integration, branches, getContext, options =
693
693
  }
694
694
  location.search;
695
695
  location.hash;
696
- }));
696
+ }), { name: "routingPending" });
697
697
  const isRouting = () => routingPending() || isPending(source);
698
698
  const transitionIntent = () => {
699
699
  if (!isPending(source))
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "Ryan Turnquist"
7
7
  ],
8
8
  "license": "MIT",
9
- "version": "2.0.0-next.23",
9
+ "version": "2.0.0-next.24",
10
10
  "homepage": "https://github.com/solidjs/solid-router#readme",
11
11
  "repository": {
12
12
  "type": "git",
@@ -38,21 +38,21 @@
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.7",
41
+ "@solidjs/web": "^2.0.0-rc.8",
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.7",
48
+ "solid-js": "^2.0.0-rc.8",
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.7",
55
- "solid-js": "^2.0.0-rc.7"
54
+ "@solidjs/web": "^2.0.0-rc.8",
55
+ "solid-js": "^2.0.0-rc.8"
56
56
  },
57
57
  "scripts": {
58
58
  "build": "rm -rf dist && tsc && rollup -c",