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

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
@@ -358,7 +358,7 @@ The import only fires when something needs the subtree — hovering a link into
358
358
  - **Types**: TypeScript never runs the thunk — inference flows through the import's promise type, so `paths.admin.users(2)` typechecks (match filters and search schemas included) before any of the subtree's code exists client-side. The module's `default` or `routes` export is used. Only tables genuinely built at runtime (typed as plain `RouteDefinition[]`) degrade to untyped.
359
359
  - **Navigation**: the table load folds into the navigation transition — the old screen holds until the subtree (and its matched components) are ready, exactly like a `lazy()` route component.
360
360
  - **Preloading**: hover intent kicks the table load, and when it lands the preload continues into the inner routes' components and `preload` functions — one cascading warm-up from the earliest possible moment.
361
- - **Server**: SSR resolves matched boundaries during the render (use the streaming entry points — `renderToStream`/`renderToStringAsync` — as with any async work), and the single-flight collector resolves them before its data pass.
361
+ - **Server**: SSR resolves matched boundaries during the render (use the streaming entry point `renderToStream` — awaiting it resolves with the settled HTML — as with any async work), and the single-flight collector resolves them before its data pass.
362
362
 
363
363
  Resolution is cached per thunk and append-only: the tree never changes shape after a subtree lands, it just gets more specific. Keep thunks deterministic — `() => import(...)` — rather than switching tables on runtime state.
364
364
 
@@ -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,10 +1,23 @@
1
1
  import { createSignal, getObserver, getOwner, onCleanup, sharedConfig } from "solid-js";
2
- import { getRequestEvent, isResponseEnvelope, isServer, REVALIDATE_HEADER } from "@solidjs/web";
3
- import { GET, decodeResponse, getServerFunctionMetadata, isServerFunction } from "@solidjs/web/server-functions";
4
- import { useRouter, useRoute, getIntent, getInPreloadFn } from "../routing.js";
2
+ // Everything server-function-shaped comes off the CORE entry: detection
3
+ // (isServerFunction/getServerFunctionMetadata, registered-symbol reads) and
4
+ // the late-bound RPC seam (getServerFunctionRPC). The server-functions
5
+ // entry itself — the fetch transport + the seroval codec behind it — is
6
+ // deliberately NOT imported here: query() is in every router app's eager
7
+ // graph, and a static import made every zero-server-function app ship
8
+ // ~9 KB gz of codec it could never invoke. The transport registers itself
9
+ // into the seam when a `'use server'` reference is created (compiled
10
+ // output, module scope), so by the time a server function can reach
11
+ // query() the seam is filled; plain-fetch apps read undefined forever.
12
+ import { getRequestEvent, getServerFunctionMetadata, getServerFunctionRPC, isResponseEnvelope, isServer, isServerFunction, REVALIDATE_HEADER } from "@solidjs/web";
13
+ import { useRouter, getIntent, getInPreloadFn } from "../routing.js";
5
14
  const LocationHeader = "Location";
6
15
  const PRELOAD_TIMEOUT = 5000;
7
16
  const CACHE_TIMEOUT = 180000;
17
+ // When this client booted. Flight-registry entries (sharedConfig.has/load)
18
+ // hold values the server computed while rendering THIS page, so their age is
19
+ // anchored here — not at whenever a late query() call happens to consume one.
20
+ const bootTime = Date.now();
8
21
  let cacheMap = new Map();
9
22
  // cleanup forward/back cache
10
23
  if (!isServer) {
@@ -49,9 +62,16 @@ export function query(fn, name) {
49
62
  // declaration for dispatch, the client half swaps in the GET transport.
50
63
  // An explicit `GET(fn)` already carries the declaration on the metadata
51
64
  // channel (`getServerFunctionMetadata(fn)?.method === "GET"`) and passes
52
- // through; non-server functions are untouched.
65
+ // through; non-server functions are untouched. `GET` comes through the
66
+ // late-bound RPC seam: a reference in the graph guarantees the transport
67
+ // registered it (references are created at module scope, ahead of the
68
+ // module handing one to query()), so the read only misses for hand-built
69
+ // brands that never saw the runtime — which have no transport to declare
70
+ // on either.
53
71
  if (isServerFunction(fn) && !getServerFunctionMetadata(fn)?.method) {
54
- fn = GET(fn);
72
+ const rpc = getServerFunctionRPC();
73
+ if (rpc)
74
+ fn = rpc.GET(fn);
55
75
  }
56
76
  const cachedFn = ((...args) => {
57
77
  const cache = getCache();
@@ -59,7 +79,6 @@ export function query(fn, name) {
59
79
  const inPreloadFn = getInPreloadFn();
60
80
  const owner = getOwner();
61
81
  const router = owner ? useRouter() : undefined;
62
- const route = owner ? useRoute() : undefined;
63
82
  const navigate = router && router.navigatorFactory();
64
83
  const now = Date.now();
65
84
  const key = name + hashKey(args);
@@ -84,27 +103,17 @@ export function query(fn, name) {
84
103
  tracking = true;
85
104
  onCleanup(() => cached[4].count--);
86
105
  }
87
- // A read under a route section the in-flight navigation is leaving keeps
88
- // its invalidated value instead of refetching: this render is disposed at
89
- // commit and never paints, and refiring would put a second request on
90
- // what single flight promises is one. The entry stays a cache miss, so
91
- // the fetch happens at its next real use — a later navigation's preload
92
- // or read — unless the flight payload seeds it first. Retained sections
93
- // still refetch here, inside the navigation's transition, so the
94
- // redirect commits with fresh data.
95
- const leaving = cached && !cached[0] && route && router.leaving?.(route);
96
106
  if (cached &&
97
- (cached[0]
98
- ? isServer ||
99
- intent === "native" ||
100
- cached[4].count ||
101
- Date.now() - cached[0] < PRELOAD_TIMEOUT
102
- : leaving)) {
107
+ cached[0] &&
108
+ (isServer ||
109
+ intent === "native" ||
110
+ cached[4].count ||
111
+ Date.now() - cached[0] < PRELOAD_TIMEOUT)) {
103
112
  if (tracking) {
104
113
  cached[4].count++;
105
114
  cached[4][0](); // track
106
115
  }
107
- if (!leaving && cached[3] === "preload" && intent !== "preload") {
116
+ if (cached[3] === "preload" && intent !== "preload") {
108
117
  cached[0] = now;
109
118
  }
110
119
  let res = cached[1];
@@ -113,27 +122,54 @@ export function query(fn, name) {
113
122
  "then" in cached[1]
114
123
  ? cached[1].then(handleResponse(false), handleResponse(true))
115
124
  : handleResponse(false)(cached[1]);
116
- !leaving && !isServer && intent === "navigate" && cached[4][1](cached[0]); // update version
125
+ !isServer && intent === "navigate" && cached[4][1](cached[0]); // update version
117
126
  }
118
127
  inPreloadFn && "then" in res && res.catch(() => { });
119
128
  return res;
120
129
  }
121
130
  let res;
131
+ let adopted = false;
132
+ // Flight entries stay consumable past hydration `done` on purpose — a
133
+ // boundary inside a deferred claim scope (a lazy route module) can
134
+ // legitimately make its first query() call after global hydration reads
135
+ // as done, and must adopt the promise the server DOM was rendered from
136
+ // (#2964). Adoption slots into the cache's own freshness model as a
137
+ // fetch that happened at boot: outside a navigation (no intent — the
138
+ // hydration/claim case) DOM consistency wins at any age; a back/forward
139
+ // restore honors the payload within the cache's retention window, the
140
+ // same bar the sweep applies to entries the router fetched itself; an
141
+ // active navigation (or its preload) accepts the payload only while a
142
+ // real preload of the same age would still satisfy it — anything older
143
+ // refetches instead of serving a minutes-old value on a fresh navigation.
122
144
  if (!isServer && sharedConfig.has && sharedConfig.has(key)) {
123
- res = sharedConfig.load(key); // hydrating
124
- // @ts-ignore at least until we add a delete method to sharedConfig
125
- delete globalThis._$HY.r[key];
145
+ const payloadAge = now - bootTime;
146
+ if (!intent || payloadAge < (intent === "native" ? CACHE_TIMEOUT : PRELOAD_TIMEOUT)) {
147
+ adopted = true;
148
+ res = sharedConfig.load(key); // hydrating
149
+ // @ts-ignore at least until we add a delete method to sharedConfig
150
+ delete globalThis._$HY.r[key];
151
+ }
152
+ // A payload too old for this intent stays in the registry: it is
153
+ // inert while the refetched entry below lives, but a later claim
154
+ // read (no intent) may still need it for server-DOM consistency.
126
155
  }
127
- else
156
+ if (!adopted)
128
157
  res = fn(...args);
158
+ // Adopted entries are stamped at boot — when their data was actually
159
+ // fetched — not at consumption. The hydration render burst still dedups
160
+ // through the normal window/count reuse above (it happens within
161
+ // milliseconds of boot), while a navigation that comes along later sees
162
+ // the payload's true age and refetches past PRELOAD_TIMEOUT, exactly as
163
+ // it would for a preload performed when the server fetched the data.
164
+ const stamp = adopted ? bootTime : now;
129
165
  if (cached) {
130
- cached[0] = now;
166
+ cached[0] = stamp;
131
167
  cached[1] = res;
132
168
  cached[3] = intent;
133
169
  !isServer && intent === "navigate" && cached[4][1](cached[0]); // update version
134
170
  }
135
171
  else {
136
- cache.set(key, (cached = [now, res, , intent, createSignal(now, { ownedWrite: true })]));
172
+ cache.set(key, (cached = [stamp, res, , intent, createSignal(stamp, { ownedWrite: true })]));
137
173
  cached[4].count = 0;
138
174
  }
139
175
  if (tracking) {
@@ -210,10 +246,17 @@ export function query(fn, name) {
210
246
  v = enveloped;
211
247
  else if (v.body) {
212
248
  // responses the transport hands over whole (revalidation) carry a
213
- // codec-encoded body; anything else (a raw user Response) stays whole
214
- const decoded = await decodeResponse(v);
215
- if (decoded !== undefined)
216
- v = decoded;
249
+ // codec-encoded body; anything else (a raw user Response) stays
250
+ // whole. The decoder rides the late-bound RPC seam: only a graph
251
+ // with server functions has codec-encoded responses to decode,
252
+ // and only such a graph has the codec registered — a plain-fetch
253
+ // query's raw Response passes through identically either way.
254
+ const rpc = getServerFunctionRPC();
255
+ if (rpc) {
256
+ const decoded = await rpc.decodeResponse(v);
257
+ if (decoded !== undefined)
258
+ v = decoded;
259
+ }
217
260
  }
218
261
  }
219
262
  if (error)
package/dist/fs.d.ts CHANGED
@@ -47,13 +47,15 @@ export interface FileRouteLazyRef<M = Record<string, unknown>> {
47
47
  }
48
48
  /** An eager module ref: its picked exports are imported statically. */
49
49
  export interface FileRouteEagerRef<M = Record<string, unknown>> {
50
+ src?: string;
50
51
  require(): M;
51
52
  }
52
53
  /** The shape of a nested route-manifest entry the adapter consumes. */
53
54
  export interface FileRouteEntry {
54
55
  path: string;
55
56
  page?: boolean;
56
- $component?: FileRouteLazyRef<any> | undefined;
57
+ /** Code-split by default; an eager ref when delivered with `codeSplitting: false`. */
58
+ $component?: FileRouteLazyRef<any> | FileRouteEagerRef<any> | undefined;
57
59
  $$route?: FileRouteEagerRef<any> | undefined;
58
60
  children?: readonly FileRouteEntry[] | undefined;
59
61
  }
@@ -85,10 +87,12 @@ export type FileRoutesFrom<T extends readonly FileRouteEntry[]> = {
85
87
  };
86
88
  /**
87
89
  * Converts the nested page entries of a file-system route manifest into
88
- * route definitions: `$component` refs become code-split `lazy` components
90
+ * route definitions: code-split `$component` refs become `lazy` components
89
91
  * (their `src` doubles as the `moduleUrl` core resolves assets and islands
90
- * against), and each entry's `route` config export is spread into its
91
- * definition. Pass the result to `createRouter({ routes })`.
92
+ * against), eager refs — a manifest delivered with `codeSplitting: false` —
93
+ * pass their component through as-is (no `lazy` wrapper; the module is
94
+ * already statically imported), and each entry's `route` config export is
95
+ * spread into its definition. Pass the result to `createRouter({ routes })`.
92
96
  */
93
97
  export declare function fileRoutes<const T extends readonly FileRouteEntry[]>(entries: T): FileRoutesFrom<T>;
94
98
  export {};
package/dist/fs.js CHANGED
@@ -22,14 +22,19 @@ export function defineFileRoute(path, config) {
22
22
  }
23
23
  /**
24
24
  * Converts the nested page entries of a file-system route manifest into
25
- * route definitions: `$component` refs become code-split `lazy` components
25
+ * route definitions: code-split `$component` refs become `lazy` components
26
26
  * (their `src` doubles as the `moduleUrl` core resolves assets and islands
27
- * against), and each entry's `route` config export is spread into its
28
- * definition. Pass the result to `createRouter({ routes })`.
27
+ * against), eager refs — a manifest delivered with `codeSplitting: false` —
28
+ * pass their component through as-is (no `lazy` wrapper; the module is
29
+ * already statically imported), and each entry's `route` config export is
30
+ * spread into its definition. Pass the result to `createRouter({ routes })`.
29
31
  */
30
32
  export function fileRoutes(entries) {
31
33
  const components = new Map();
32
34
  const componentOf = (ref) => {
35
+ if ("require" in ref) {
36
+ return ref.require().default;
37
+ }
33
38
  let component = components.get(ref.src);
34
39
  if (!component) {
35
40
  component = lazy(ref.import, ref.src);
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
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';
2
- import { registerElementClaim, delegateEvents, isServer, getRequestEvent, createComponent as createComponent$1, memo, isResponseEnvelope, REVALIDATE_HEADER } from '@solidjs/web';
3
- import { hasFlashCookie, clearFlashCookie, isServerFunction, getServerFunctionMetadata, GET, decodeResponse, subscribeFlightData, decodeResponsePayload, createServerReference } from '@solidjs/web/server-functions';
2
+ import { registerElementClaim, delegateEvents, isServer, getRequestEvent, hasFlashCookie, clearFlashCookie, createComponent as createComponent$1, memo, isServerFunction, getServerFunctionMetadata, getServerFunctionRPC, isResponseEnvelope, REVALIDATE_HEADER } from '@solidjs/web';
3
+ import { subscribeFlightData, decodeResponsePayload, createServerReference } from '@solidjs/web/server-functions';
4
4
  import { decodeFlashCookie } from '@solidjs/web/server-functions/server';
5
5
 
6
6
  const hasSchemeRegex = /^(?:[a-z0-9]+:)?\/\//i;
@@ -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
  }
@@ -505,6 +508,13 @@ function createPathsProxy(renderPath = p => p, base = "") {
505
508
  return node(normalizePath(base));
506
509
  }
507
510
 
511
+ // Flash-cookie detection/clearing are cookie utilities on the CORE entry
512
+ // (they live beside the cookie codec) — routing is in every app's eager
513
+ // graph and must never import the server-functions package, whose client
514
+ // half is the fetch transport + the seroval codec. The type below is
515
+ // erased at build; only the server-only action path imports that module's
516
+ // values (behind isServer, tree-shaken from client bundles).
517
+
508
518
  const MAX_REDIRECTS = 100;
509
519
 
510
520
  /** Consider this API opaque and internal. It is likely to change in the future. */
@@ -1165,12 +1175,6 @@ function createRouterContext(integration, branches, getContext, options = {}) {
1165
1175
  get pendingTarget() {
1166
1176
  return lastTransitionTarget;
1167
1177
  },
1168
- leaving(route) {
1169
- const t = lastTransitionTarget;
1170
- if (!t || !route.retained) return false;
1171
- const pathname = t.value.split(/[?#]/, 1)[0];
1172
- return !route.retained(options.transformUrl ? options.transformUrl(pathname) : pathname);
1173
- },
1174
1178
  renderPath,
1175
1179
  parsePath,
1176
1180
  navigatorFactory,
@@ -1375,13 +1379,6 @@ function createRouteContext(router, parent, outlet, match, matches = () => [matc
1375
1379
  pattern,
1376
1380
  params,
1377
1381
  path,
1378
- retained(target) {
1379
- const m = match();
1380
- const p = m.route.matcher(target);
1381
- // params derive from the matched path, so equal paths mean this
1382
- // section instance (layouts match prefixes via partial matchers)
1383
- return !!p && p.path === m.path;
1384
- },
1385
1382
  outlet: () => component ? createComponent(component, {
1386
1383
  params,
1387
1384
  location,
@@ -2144,6 +2141,10 @@ const useBeforeLeave = listener => {
2144
2141
  const LocationHeader = "Location";
2145
2142
  const PRELOAD_TIMEOUT = 5000;
2146
2143
  const CACHE_TIMEOUT = 180000;
2144
+ // When this client booted. Flight-registry entries (sharedConfig.has/load)
2145
+ // hold values the server computed while rendering THIS page, so their age is
2146
+ // anchored here — not at whenever a late query() call happens to consume one.
2147
+ const bootTime = Date.now();
2147
2148
  let cacheMap = new Map();
2148
2149
 
2149
2150
  // cleanup forward/back cache
@@ -2187,9 +2188,15 @@ function query(fn, name) {
2187
2188
  // declaration for dispatch, the client half swaps in the GET transport.
2188
2189
  // An explicit `GET(fn)` already carries the declaration on the metadata
2189
2190
  // channel (`getServerFunctionMetadata(fn)?.method === "GET"`) and passes
2190
- // through; non-server functions are untouched.
2191
+ // through; non-server functions are untouched. `GET` comes through the
2192
+ // late-bound RPC seam: a reference in the graph guarantees the transport
2193
+ // registered it (references are created at module scope, ahead of the
2194
+ // module handing one to query()), so the read only misses for hand-built
2195
+ // brands that never saw the runtime — which have no transport to declare
2196
+ // on either.
2191
2197
  if (isServerFunction(fn) && !getServerFunctionMetadata(fn)?.method) {
2192
- fn = GET(fn);
2198
+ const rpc = getServerFunctionRPC();
2199
+ if (rpc) fn = rpc.GET(fn);
2193
2200
  }
2194
2201
  const cachedFn = (...args) => {
2195
2202
  const cache = getCache();
@@ -2197,7 +2204,6 @@ function query(fn, name) {
2197
2204
  const inPreloadFn = getInPreloadFn();
2198
2205
  const owner = getOwner();
2199
2206
  const router = owner ? useRouter() : undefined;
2200
- const route = owner ? useRoute() : undefined;
2201
2207
  const navigate = router && router.navigatorFactory();
2202
2208
  const now = Date.now();
2203
2209
  const key = name + hashKey(args);
@@ -2221,45 +2227,64 @@ function query(fn, name) {
2221
2227
  tracking = true;
2222
2228
  onCleanup(() => cached[4].count--);
2223
2229
  }
2224
-
2225
- // A read under a route section the in-flight navigation is leaving keeps
2226
- // its invalidated value instead of refetching: this render is disposed at
2227
- // commit and never paints, and refiring would put a second request on
2228
- // what single flight promises is one. The entry stays a cache miss, so
2229
- // the fetch happens at its next real use — a later navigation's preload
2230
- // or read — unless the flight payload seeds it first. Retained sections
2231
- // still refetch here, inside the navigation's transition, so the
2232
- // redirect commits with fresh data.
2233
- const leaving = cached && !cached[0] && route && router.leaving?.(route);
2234
- if (cached && (cached[0] ? isServer || intent === "native" || cached[4].count || Date.now() - cached[0] < PRELOAD_TIMEOUT : leaving)) {
2230
+ if (cached && cached[0] && (isServer || intent === "native" || cached[4].count || Date.now() - cached[0] < PRELOAD_TIMEOUT)) {
2235
2231
  if (tracking) {
2236
2232
  cached[4].count++;
2237
2233
  cached[4][0](); // track
2238
2234
  }
2239
- if (!leaving && cached[3] === "preload" && intent !== "preload") {
2235
+ if (cached[3] === "preload" && intent !== "preload") {
2240
2236
  cached[0] = now;
2241
2237
  }
2242
2238
  let res = cached[1];
2243
2239
  if (intent !== "preload") {
2244
2240
  res = "then" in cached[1] ? cached[1].then(handleResponse(false), handleResponse(true)) : handleResponse(false)(cached[1]);
2245
- !leaving && !isServer && intent === "navigate" && cached[4][1](cached[0]); // update version
2241
+ !isServer && intent === "navigate" && cached[4][1](cached[0]); // update version
2246
2242
  }
2247
2243
  inPreloadFn && "then" in res && res.catch(() => {});
2248
2244
  return res;
2249
2245
  }
2250
2246
  let res;
2247
+ let adopted = false;
2248
+ // Flight entries stay consumable past hydration `done` on purpose — a
2249
+ // boundary inside a deferred claim scope (a lazy route module) can
2250
+ // legitimately make its first query() call after global hydration reads
2251
+ // as done, and must adopt the promise the server DOM was rendered from
2252
+ // (#2964). Adoption slots into the cache's own freshness model as a
2253
+ // fetch that happened at boot: outside a navigation (no intent — the
2254
+ // hydration/claim case) DOM consistency wins at any age; a back/forward
2255
+ // restore honors the payload within the cache's retention window, the
2256
+ // same bar the sweep applies to entries the router fetched itself; an
2257
+ // active navigation (or its preload) accepts the payload only while a
2258
+ // real preload of the same age would still satisfy it — anything older
2259
+ // refetches instead of serving a minutes-old value on a fresh navigation.
2251
2260
  if (!isServer && sharedConfig.has && sharedConfig.has(key)) {
2252
- res = sharedConfig.load(key); // hydrating
2253
- // @ts-ignore at least until we add a delete method to sharedConfig
2254
- delete globalThis._$HY.r[key];
2255
- } else res = fn(...args);
2261
+ const payloadAge = now - bootTime;
2262
+ if (!intent || payloadAge < (intent === "native" ? CACHE_TIMEOUT : PRELOAD_TIMEOUT)) {
2263
+ adopted = true;
2264
+ res = sharedConfig.load(key); // hydrating
2265
+ // @ts-ignore at least until we add a delete method to sharedConfig
2266
+ delete globalThis._$HY.r[key];
2267
+ }
2268
+ // A payload too old for this intent stays in the registry: it is
2269
+ // inert while the refetched entry below lives, but a later claim
2270
+ // read (no intent) may still need it for server-DOM consistency.
2271
+ }
2272
+ if (!adopted) res = fn(...args);
2273
+
2274
+ // Adopted entries are stamped at boot — when their data was actually
2275
+ // fetched — not at consumption. The hydration render burst still dedups
2276
+ // through the normal window/count reuse above (it happens within
2277
+ // milliseconds of boot), while a navigation that comes along later sees
2278
+ // the payload's true age and refetches past PRELOAD_TIMEOUT, exactly as
2279
+ // it would for a preload performed when the server fetched the data.
2280
+ const stamp = adopted ? bootTime : now;
2256
2281
  if (cached) {
2257
- cached[0] = now;
2282
+ cached[0] = stamp;
2258
2283
  cached[1] = res;
2259
2284
  cached[3] = intent;
2260
2285
  !isServer && intent === "navigate" && cached[4][1](cached[0]); // update version
2261
2286
  } else {
2262
- cache.set(key, cached = [now, res,, intent, createSignal(now, {
2287
+ cache.set(key, cached = [stamp, res,, intent, createSignal(stamp, {
2263
2288
  ownedWrite: true
2264
2289
  })]);
2265
2290
  cached[4].count = 0;
@@ -2326,9 +2351,16 @@ function query(fn, name) {
2326
2351
  }
2327
2352
  if (hasEnveloped) v = enveloped;else if (v.body) {
2328
2353
  // responses the transport hands over whole (revalidation) carry a
2329
- // codec-encoded body; anything else (a raw user Response) stays whole
2330
- const decoded = await decodeResponse(v);
2331
- if (decoded !== undefined) v = decoded;
2354
+ // codec-encoded body; anything else (a raw user Response) stays
2355
+ // whole. The decoder rides the late-bound RPC seam: only a graph
2356
+ // with server functions has codec-encoded responses to decode,
2357
+ // and only such a graph has the codec registered — a plain-fetch
2358
+ // query's raw Response passes through identically either way.
2359
+ const rpc = getServerFunctionRPC();
2360
+ if (rpc) {
2361
+ const decoded = await rpc.decodeResponse(v);
2362
+ if (decoded !== undefined) v = decoded;
2363
+ }
2332
2364
  }
2333
2365
  }
2334
2366
  if (error) throw v;
package/dist/routing.js CHANGED
@@ -1,8 +1,7 @@
1
1
  import { flush, runWithOwner } from "solid-js";
2
2
  import { createComponent, createContext, createMemo, createSignal, isPending, NotReadyError, untrack, useContext } from "solid-js";
3
- import { isServer, getRequestEvent } from "@solidjs/web";
3
+ import { clearFlashCookie, getRequestEvent, hasFlashCookie, isServer } from "@solidjs/web";
4
4
  import { mockBase, comparablePath, createMemoObject, extractSearchParams, invariant, resolvePath, createMatcher, joinPaths, scoreRoute, mergeSearchString, expandOptionals } from "./utils.js";
5
- import { clearFlashCookie, hasFlashCookie } from "@solidjs/web/server-functions";
6
5
  const MAX_REDIRECTS = 100;
7
6
  /** Consider this API opaque and internal. It is likely to change in the future. */
8
7
  export const RouterContextObj = createContext();
@@ -621,13 +620,6 @@ export function createRouterContext(integration, branches, getContext, options =
621
620
  get pendingTarget() {
622
621
  return lastTransitionTarget;
623
622
  },
624
- leaving(route) {
625
- const t = lastTransitionTarget;
626
- if (!t || !route.retained)
627
- return false;
628
- const pathname = t.value.split(/[?#]/, 1)[0];
629
- return !route.retained(options.transformUrl ? options.transformUrl(pathname) : pathname);
630
- },
631
623
  renderPath,
632
624
  parsePath,
633
625
  navigatorFactory,
@@ -815,13 +807,6 @@ export function createRouteContext(router, parent, outlet, match, matches = () =
815
807
  pattern,
816
808
  params,
817
809
  path,
818
- retained(target) {
819
- const m = match();
820
- const p = m.route.matcher(target);
821
- // params derive from the matched path, so equal paths mean this
822
- // section instance (layouts match prefixes via partial matchers)
823
- return !!p && p.path === m.path;
824
- },
825
810
  outlet: () => component
826
811
  ? createComponent(component, {
827
812
  params,
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[];
@@ -244,8 +245,6 @@ export interface RouteContext {
244
245
  path: () => string;
245
246
  outlet: () => JSX.Element;
246
247
  resolvePath(to: string): string | undefined;
247
- /** Whether this route section still matches `target` (same matched path), i.e. survives a navigation there. Absent on the base route, which always survives. */
248
- retained?(target: string): boolean;
249
248
  }
250
249
  export interface RouterUtils {
251
250
  renderPath(path: string): string;
@@ -264,8 +263,6 @@ export interface RouterContext {
264
263
  isRouting: () => boolean;
265
264
  /** The target of the in-flight navigation transition, if any. Not reactive. */
266
265
  readonly pendingTarget?: LocationChange;
267
- /** Whether `route` is a section the in-flight navigation is leaving. False when no navigation is pending. */
268
- leaving?(route: RouteContext): boolean;
269
266
  matches: () => RouteMatch[];
270
267
  renderPath(path: string): string;
271
268
  parsePath(str: string): string;
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "Ryan Turnquist"
7
7
  ],
8
8
  "license": "MIT",
9
- "version": "2.0.0-next.13",
9
+ "version": "2.0.0-next.15",
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.33",
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.33",
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.33",
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.33 <2.0.0-experimental.0",
55
+ "solid-js": ">=2.0.0-beta.33 <2.0.0-experimental.0"
56
56
  },
57
57
  "scripts": {
58
58
  "build": "rm -rf dist && tsc && rollup -c",