@solidjs/router 2.0.0-next.12 → 2.0.0-next.14

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.
@@ -330,7 +330,11 @@ function applyResponseMetadata(metadata, navigate, flightData) {
330
330
  cacheKeyOp(keys, entry => (entry[0] = 0));
331
331
  // set cache
332
332
  flightData && Object.keys(flightData).forEach(k => query.set(k, flightData[k]));
333
- // trigger revalidation
333
+ // trigger revalidation inside the same transition as the navigation, so
334
+ // the redirect commits atomically with fresh data: surviving consumers
335
+ // (shared layouts) the flight payload didn't seed refetch and hold the
336
+ // commit rather than painting stale and updating after. Seeded entries
337
+ // are fresh again by now, so the sweep re-reads them from cache.
334
338
  revalidate(keys, false);
335
339
  }
336
340
  async function handleResponse(response, error, navigate, metadataHandled) {
@@ -35,6 +35,10 @@ export function setupNativeEvents({ preload = true, explicitLinks = false, actio
35
35
  if (a.hasAttribute("download") || (rel && rel.includes("external")))
36
36
  return;
37
37
  const url = svg ? new URL(href, document.baseURI) : new URL(href);
38
+ // Skip non-http(s) schemes (blob:, mailto:, tel:, data:, ...). blob: URLs
39
+ // inherit the page origin, so the origin check below won't reject them. #382
40
+ if (url.protocol !== "https:" && url.protocol !== "http:")
41
+ return;
38
42
  if (url.origin !== window.location.origin ||
39
43
  (basePath && url.pathname && !url.pathname.toLowerCase().startsWith(basePath.toLowerCase())))
40
44
  return;
@@ -1,7 +1,7 @@
1
1
  import { createSignal, getObserver, getOwner, onCleanup, sharedConfig } from "solid-js";
2
- import { getRequestEvent, isResponseEnvelope, isServer } from "@solidjs/web";
2
+ import { getRequestEvent, isResponseEnvelope, isServer, REVALIDATE_HEADER } from "@solidjs/web";
3
3
  import { GET, decodeResponse, getServerFunctionMetadata, isServerFunction } from "@solidjs/web/server-functions";
4
- import { useNavigate, getIntent, getInPreloadFn } from "../routing.js";
4
+ import { useRouter, getIntent, getInPreloadFn } from "../routing.js";
5
5
  const LocationHeader = "Location";
6
6
  const PRELOAD_TIMEOUT = 5000;
7
7
  const CACHE_TIMEOUT = 180000;
@@ -58,7 +58,8 @@ export function query(fn, name) {
58
58
  const intent = getIntent();
59
59
  const inPreloadFn = getInPreloadFn();
60
60
  const owner = getOwner();
61
- const navigate = owner ? useNavigate() : undefined;
61
+ const router = owner ? useRouter() : undefined;
62
+ const navigate = router && router.navigatorFactory();
62
63
  const now = Date.now();
63
64
  const key = name + hashKey(args);
64
65
  let cached = cache.get(key);
@@ -171,14 +172,28 @@ export function query(fn, name) {
171
172
  }
172
173
  const url = v.headers.get(LocationHeader);
173
174
  if (url !== null) {
175
+ // invalidate the redirect's revalidation keys before navigating so
176
+ // the destination's preloads see the miss and fetch fresh (#580 thread)
177
+ const keys = !isServer && v.headers.get(REVALIDATE_HEADER)?.split(",");
178
+ keys && cacheKeyOp(keys, entry => (entry[0] = 0));
174
179
  // client + server relative redirect
175
- if (navigate && url.startsWith("/"))
180
+ const soft = navigate && url.startsWith("/");
181
+ if (soft)
176
182
  navigate(url, { replace: true });
177
183
  else if (!isServer)
178
184
  window.location.href = url;
179
185
  else if (e)
180
186
  e.response.status = 302;
181
- return;
187
+ // sweep the live signals inside the same transition as the
188
+ // navigation: surviving consumers (shared layouts) refetch and
189
+ // hold the commit, so the destination never paints stale data
190
+ keys && revalidate(keys, false);
191
+ // Hold the read pending on the client: the navigation unmounts this
192
+ // consumer, and resolving `undefined` instead hands a missing value
193
+ // to whatever renders before the transition commits (a downstream
194
+ // `.map()` crashes). The server keeps resolving — its render can't
195
+ // await a navigation and the 302 discards the body anyway.
196
+ return isServer ? undefined : new Promise(() => { });
182
197
  }
183
198
  if (hasEnveloped)
184
199
  v = enveloped;
package/dist/index.js CHANGED
@@ -336,6 +336,9 @@ function setupNativeEvents({
336
336
  const rel = (a.getAttribute("rel") || "").split(/\s+/);
337
337
  if (a.hasAttribute("download") || rel && rel.includes("external")) return;
338
338
  const url = svg ? new URL(href, document.baseURI) : new URL(href);
339
+ // Skip non-http(s) schemes (blob:, mailto:, tel:, data:, ...). blob: URLs
340
+ // inherit the page origin, so the origin check below won't reject them. #382
341
+ if (url.protocol !== "https:" && url.protocol !== "http:") return;
339
342
  if (url.origin !== window.location.origin || basePath && url.pathname && !url.pathname.toLowerCase().startsWith(basePath.toLowerCase())) return;
340
343
  return [a, url];
341
344
  }
@@ -1191,7 +1194,6 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1191
1194
  }
1192
1195
  // typed path proxy nodes coerce to their href
1193
1196
  if (typeof to !== "string") to = to.toString();
1194
- const queryOnly = !to || to[0] === "?";
1195
1197
  const {
1196
1198
  replace,
1197
1199
  resolve,
@@ -1199,11 +1201,21 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1199
1201
  state: nextState
1200
1202
  } = {
1201
1203
  replace: false,
1202
- resolve: !queryOnly,
1204
+ resolve: true,
1203
1205
  scroll: true,
1204
1206
  ...options
1205
1207
  };
1206
- const resolvedTo = resolve ? route.resolvePath(to) : resolvePath(queryOnly && location.pathname || "", to);
1208
+
1209
+ // A string means what the same string means as an href. Leading "/"
1210
+ // stays base-prefixed; anything else resolves URL-style against the
1211
+ // current location — `new URL` collapses `..` and handles `?`/`#`-only
1212
+ // references natively (#502). A cross-origin result (scheme or
1213
+ // protocol-relative input) falls through as unroutable.
1214
+ let resolvedTo;
1215
+ if (!resolve) resolvedTo = resolvePath((!to || to[0] === "?") && location.pathname || "", to);else if (to[0] === "/") resolvedTo = route.resolvePath(to);else {
1216
+ const url = new URL(to, mockBase + location.pathname + location.search + location.hash);
1217
+ resolvedTo = url.origin === mockBase ? url.pathname + url.search + url.hash : undefined;
1218
+ }
1207
1219
  if (resolvedTo === undefined) {
1208
1220
  throw new Error(`Path '${to}' is not a routable path`);
1209
1221
  } else if (referrers.length >= MAX_REDIRECTS) {
@@ -2174,7 +2186,8 @@ function query(fn, name) {
2174
2186
  const intent = getIntent();
2175
2187
  const inPreloadFn = getInPreloadFn();
2176
2188
  const owner = getOwner();
2177
- const navigate = owner ? useNavigate() : undefined;
2189
+ const router = owner ? useRouter() : undefined;
2190
+ const navigate = router && router.navigatorFactory();
2178
2191
  const now = Date.now();
2179
2192
  const key = name + hashKey(args);
2180
2193
  let cached = cache.get(key);
@@ -2267,11 +2280,28 @@ function query(fn, name) {
2267
2280
  }
2268
2281
  const url = v.headers.get(LocationHeader);
2269
2282
  if (url !== null) {
2283
+ // invalidate the redirect's revalidation keys before navigating so
2284
+ // the destination's preloads see the miss and fetch fresh (#580 thread)
2285
+ const keys = !isServer && v.headers.get(REVALIDATE_HEADER)?.split(",");
2286
+ keys && cacheKeyOp(keys, entry => entry[0] = 0);
2287
+
2270
2288
  // client + server relative redirect
2271
- if (navigate && url.startsWith("/")) navigate(url, {
2289
+ const soft = navigate && url.startsWith("/");
2290
+ if (soft) navigate(url, {
2272
2291
  replace: true
2273
2292
  });else if (!isServer) window.location.href = url;else if (e) e.response.status = 302;
2274
- return;
2293
+
2294
+ // sweep the live signals inside the same transition as the
2295
+ // navigation: surviving consumers (shared layouts) refetch and
2296
+ // hold the commit, so the destination never paints stale data
2297
+ keys && revalidate(keys, false);
2298
+
2299
+ // Hold the read pending on the client: the navigation unmounts this
2300
+ // consumer, and resolving `undefined` instead hands a missing value
2301
+ // to whatever renders before the transition commits (a downstream
2302
+ // `.map()` crashes). The server keeps resolving — its render can't
2303
+ // await a navigation and the 302 discards the body anyway.
2304
+ return isServer ? undefined : new Promise(() => {});
2275
2305
  }
2276
2306
  if (hasEnveloped) v = enveloped;else if (v.body) {
2277
2307
  // responses the transport hands over whole (revalidation) carry a
@@ -2660,7 +2690,11 @@ function applyResponseMetadata(metadata, navigate, flightData) {
2660
2690
  cacheKeyOp(keys, entry => entry[0] = 0);
2661
2691
  // set cache
2662
2692
  flightData && Object.keys(flightData).forEach(k => query.set(k, flightData[k]));
2663
- // trigger revalidation
2693
+ // trigger revalidation inside the same transition as the navigation, so
2694
+ // the redirect commits atomically with fresh data: surviving consumers
2695
+ // (shared layouts) the flight payload didn't seed refetch and hold the
2696
+ // commit rather than painting stale and updating after. Seeded entries
2697
+ // are fresh again by now, so the sweep re-reads them from cache.
2664
2698
  revalidate(keys, false);
2665
2699
  }
2666
2700
  async function handleResponse(response, error, navigate, metadataHandled) {
package/dist/routing.js CHANGED
@@ -650,16 +650,27 @@ export function createRouterContext(integration, branches, getContext, options =
650
650
  // typed path proxy nodes coerce to their href
651
651
  if (typeof to !== "string")
652
652
  to = to.toString();
653
- const queryOnly = !to || to[0] === "?";
654
653
  const { replace, resolve, scroll, state: nextState } = {
655
654
  replace: false,
656
- resolve: !queryOnly,
655
+ resolve: true,
657
656
  scroll: true,
658
657
  ...options
659
658
  };
660
- const resolvedTo = resolve
661
- ? route.resolvePath(to)
662
- : resolvePath((queryOnly && location.pathname) || "", to);
659
+ // A string means what the same string means as an href. Leading "/"
660
+ // stays base-prefixed; anything else resolves URL-style against the
661
+ // current location — `new URL` collapses `..` and handles `?`/`#`-only
662
+ // references natively (#502). A cross-origin result (scheme or
663
+ // protocol-relative input) falls through as unroutable.
664
+ let resolvedTo;
665
+ if (!resolve)
666
+ resolvedTo = resolvePath(((!to || to[0] === "?") && location.pathname) || "", to);
667
+ else if (to[0] === "/")
668
+ resolvedTo = route.resolvePath(to);
669
+ else {
670
+ const url = new URL(to, mockBase + location.pathname + location.search + location.hash);
671
+ resolvedTo =
672
+ url.origin === mockBase ? url.pathname + url.search + url.hash : undefined;
673
+ }
663
674
  if (resolvedTo === undefined) {
664
675
  throw new Error(`Path '${to}' is not a routable path`);
665
676
  }
package/dist/types.d.ts CHANGED
@@ -6,6 +6,7 @@ declare module "@solidjs/web" {
6
6
  status?: number;
7
7
  statusText?: string;
8
8
  headers: Headers;
9
+ committed?: boolean;
9
10
  };
10
11
  router?: {
11
12
  matches?: OutputMatch[];
@@ -74,6 +75,12 @@ export interface Location<S = unknown> extends Path {
74
75
  key: string;
75
76
  }
76
77
  export interface NavigateOptions<S = unknown> {
78
+ /**
79
+ * Resolve the target like an href: leading-`/` paths are prefixed with the
80
+ * router base, anything else resolves URL-style against the current
81
+ * location (`..` collapses, `?`/`#`-only strings keep the path). `false`
82
+ * takes the string as the final path. Defaults to `true`.
83
+ */
77
84
  resolve: boolean;
78
85
  replace: boolean;
79
86
  scroll: 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.12",
9
+ "version": "2.0.0-next.14",
10
10
  "homepage": "https://github.com/solidjs/solid-router#readme",
11
11
  "repository": {
12
12
  "type": "git",
@@ -31,7 +31,7 @@
31
31
  ],
32
32
  "sideEffects": false,
33
33
  "devDependencies": {
34
- "@solidjs/web": "2.0.0-beta.28",
34
+ "@solidjs/web": "2.0.0-beta.30",
35
35
  "@babel/core": "^7.26.0",
36
36
  "@babel/preset-typescript": "^7.26.0",
37
37
  "@changesets/cli": "^2.27.10",
@@ -40,19 +40,19 @@
40
40
  "@rollup/plugin-terser": "0.4.4",
41
41
  "@types/jest": "^29.5.14",
42
42
  "@types/node": "^22.10.0",
43
- "babel-preset-solid": "2.0.0-beta.28",
43
+ "babel-preset-solid": "2.0.0-beta.30",
44
44
  "jsdom": "^25.0.1",
45
45
  "prettier": "^3.4.1",
46
46
  "rollup": "^4.27.4",
47
- "solid-js": "2.0.0-beta.28",
47
+ "solid-js": "2.0.0-beta.30",
48
48
  "typescript": "^5.7.2",
49
49
  "vite": "^6.0.0",
50
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.28 <2.0.0-experimental.0",
55
- "solid-js": ">=2.0.0-beta.28 <2.0.0-experimental.0"
54
+ "@solidjs/web": ">=2.0.0-beta.30 <2.0.0-experimental.0",
55
+ "solid-js": ">=2.0.0-beta.30 <2.0.0-experimental.0"
56
56
  },
57
57
  "scripts": {
58
58
  "build": "rm -rf dist && tsc && rollup -c",