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

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
 
@@ -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";
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";
4
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();
@@ -108,21 +128,48 @@ export function query(fn, name) {
108
128
  return res;
109
129
  }
110
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.
111
144
  if (!isServer && sharedConfig.has && sharedConfig.has(key)) {
112
- res = sharedConfig.load(key); // hydrating
113
- // @ts-ignore at least until we add a delete method to sharedConfig
114
- 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.
115
155
  }
116
- else
156
+ if (!adopted)
117
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;
118
165
  if (cached) {
119
- cached[0] = now;
166
+ cached[0] = stamp;
120
167
  cached[1] = res;
121
168
  cached[3] = intent;
122
169
  !isServer && intent === "navigate" && cached[4][1](cached[0]); // update version
123
170
  }
124
171
  else {
125
- cache.set(key, (cached = [now, res, , intent, createSignal(now, { ownedWrite: true })]));
172
+ cache.set(key, (cached = [stamp, res, , intent, createSignal(stamp, { ownedWrite: true })]));
126
173
  cached[4].count = 0;
127
174
  }
128
175
  if (tracking) {
@@ -199,10 +246,17 @@ export function query(fn, name) {
199
246
  v = enveloped;
200
247
  else if (v.body) {
201
248
  // responses the transport hands over whole (revalidation) carry a
202
- // codec-encoded body; anything else (a raw user Response) stays whole
203
- const decoded = await decodeResponse(v);
204
- if (decoded !== undefined)
205
- 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
+ }
206
260
  }
207
261
  }
208
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;
@@ -508,6 +508,13 @@ function createPathsProxy(renderPath = p => p, base = "") {
508
508
  return node(normalizePath(base));
509
509
  }
510
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
+
511
518
  const MAX_REDIRECTS = 100;
512
519
 
513
520
  /** Consider this API opaque and internal. It is likely to change in the future. */
@@ -2134,6 +2141,10 @@ const useBeforeLeave = listener => {
2134
2141
  const LocationHeader = "Location";
2135
2142
  const PRELOAD_TIMEOUT = 5000;
2136
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();
2137
2148
  let cacheMap = new Map();
2138
2149
 
2139
2150
  // cleanup forward/back cache
@@ -2177,9 +2188,15 @@ function query(fn, name) {
2177
2188
  // declaration for dispatch, the client half swaps in the GET transport.
2178
2189
  // An explicit `GET(fn)` already carries the declaration on the metadata
2179
2190
  // channel (`getServerFunctionMetadata(fn)?.method === "GET"`) and passes
2180
- // 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.
2181
2197
  if (isServerFunction(fn) && !getServerFunctionMetadata(fn)?.method) {
2182
- fn = GET(fn);
2198
+ const rpc = getServerFunctionRPC();
2199
+ if (rpc) fn = rpc.GET(fn);
2183
2200
  }
2184
2201
  const cachedFn = (...args) => {
2185
2202
  const cache = getCache();
@@ -2227,18 +2244,47 @@ function query(fn, name) {
2227
2244
  return res;
2228
2245
  }
2229
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.
2230
2260
  if (!isServer && sharedConfig.has && sharedConfig.has(key)) {
2231
- res = sharedConfig.load(key); // hydrating
2232
- // @ts-ignore at least until we add a delete method to sharedConfig
2233
- delete globalThis._$HY.r[key];
2234
- } 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;
2235
2281
  if (cached) {
2236
- cached[0] = now;
2282
+ cached[0] = stamp;
2237
2283
  cached[1] = res;
2238
2284
  cached[3] = intent;
2239
2285
  !isServer && intent === "navigate" && cached[4][1](cached[0]); // update version
2240
2286
  } else {
2241
- cache.set(key, cached = [now, res,, intent, createSignal(now, {
2287
+ cache.set(key, cached = [stamp, res,, intent, createSignal(stamp, {
2242
2288
  ownedWrite: true
2243
2289
  })]);
2244
2290
  cached[4].count = 0;
@@ -2305,9 +2351,16 @@ function query(fn, name) {
2305
2351
  }
2306
2352
  if (hasEnveloped) v = enveloped;else if (v.body) {
2307
2353
  // responses the transport hands over whole (revalidation) carry a
2308
- // codec-encoded body; anything else (a raw user Response) stays whole
2309
- const decoded = await decodeResponse(v);
2310
- 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
+ }
2311
2364
  }
2312
2365
  }
2313
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();
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "Ryan Turnquist"
7
7
  ],
8
8
  "license": "MIT",
9
- "version": "2.0.0-next.14",
9
+ "version": "2.0.0-next.16",
10
10
  "homepage": "https://github.com/solidjs/solid-router#readme",
11
11
  "repository": {
12
12
  "type": "git",
@@ -31,7 +31,8 @@
31
31
  ],
32
32
  "sideEffects": false,
33
33
  "devDependencies": {
34
- "@solidjs/web": "2.0.0-beta.30",
34
+ "@solidjs/vite-plugin": "3.0.0-next.28",
35
+ "@solidjs/web": "^2.0.0-rc.0",
35
36
  "@babel/core": "^7.26.0",
36
37
  "@babel/preset-typescript": "^7.26.0",
37
38
  "@changesets/cli": "^2.27.10",
@@ -40,19 +41,18 @@
40
41
  "@rollup/plugin-terser": "0.4.4",
41
42
  "@types/jest": "^29.5.14",
42
43
  "@types/node": "^22.10.0",
43
- "babel-preset-solid": "2.0.0-beta.30",
44
+ "babel-preset-solid": "^2.0.0-rc.0",
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.30",
48
+ "solid-js": "^2.0.0-rc.0",
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.30 <2.0.0-experimental.0",
55
- "solid-js": ">=2.0.0-beta.30 <2.0.0-experimental.0"
54
+ "@solidjs/web": "^2.0.0-rc.0",
55
+ "solid-js": "^2.0.0-rc.0"
56
56
  },
57
57
  "scripts": {
58
58
  "build": "rm -rf dist && tsc && rollup -c",