@solidjs/router 2.0.0-next.31 → 2.0.0-next.33

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.
@@ -1,4 +1,4 @@
1
- import { createSignal, getObserver, getOwner, onCleanup, sharedConfig, untrack } from "solid-js";
1
+ import { createSignal, getObserver, getOwner, isHydratable, isHydrating, onCleanup, untrack } from "solid-js";
2
2
  // Everything server-function-shaped comes off the CORE entry: detection
3
3
  // (isServerFunction/getServerFunctionMetadata, registered-symbol reads) and
4
4
  // the late-bound RPC seam (getServerFunctionRPC). The transport itself —
@@ -9,7 +9,7 @@ import { createSignal, getObserver, getOwner, onCleanup, sharedConfig, untrack }
9
9
  // into the seam when a `'use server'` reference is created (compiled
10
10
  // output, module scope), so by the time a server function can reach
11
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";
12
+ import { getHydrationWriter, getRequestEvent, getServerFunctionMetadata, getServerFunctionRPC, isResponseEnvelope, isServer, isServerFunction, REVALIDATE_HEADER, takeHydrationValue } from "@solidjs/web";
13
13
  // The redirect carrier's name and decoder are the exception: two pure,
14
14
  // dependency-free bindings off a `sideEffects: false` entry, so they
15
15
  // tree-shake to a few hundred bytes without dragging the codec in (the same
@@ -19,10 +19,41 @@ import { useRouter, getIntent, getInPreloadFn } from "../routing.js";
19
19
  const LocationHeader = "Location";
20
20
  const PRELOAD_TIMEOUT = 5000;
21
21
  const CACHE_TIMEOUT = 180000;
22
- // When this client booted. Flight-registry entries (sharedConfig.has/load)
22
+ const responseBodyHeaders = /* #__PURE__ */ new Set([
23
+ "content-type",
24
+ "content-length",
25
+ "content-encoding",
26
+ "transfer-encoding",
27
+ "content-disposition",
28
+ "content-language",
29
+ "content-location",
30
+ "content-range",
31
+ "accept-ranges",
32
+ "etag",
33
+ "last-modified"
34
+ ]);
35
+ const forwardedCookies = /* #__PURE__ */ new WeakMap();
36
+ // When this client booted. Flight-registry entries (takeHydrationValue)
23
37
  // hold values the server computed while rendering THIS page, so their age is
24
38
  // anchored here — not at whenever a late query() call happens to consume one.
25
39
  const bootTime = Date.now();
40
+ // Captured at module load: a hydrating client runs a serialized node's
41
+ // compute once as a dependency trace with the global `Promise` swapped for a
42
+ // never-settling mock, and a query() read inside that trace still consumes
43
+ // and caches its flight entry. Wrapped with the global there, a settled
44
+ // entry would cache a promise that never resolves, hanging every navigation
45
+ // that reuses it.
46
+ const NativePromise = Promise;
47
+ // Every hydratable read ships its entry: the writer keeps the first write
48
+ // per key, so a read skipped in a NoHydration zone never hides the entry
49
+ // from an island that reads the same key later.
50
+ function serializeForHydration(key, res) {
51
+ const writer = getHydrationWriter();
52
+ if (!writer || !writer.async || !isHydratable())
53
+ return;
54
+ const e = getRequestEvent();
55
+ (!e || !e.serverOnly) && writer.write(key, res);
56
+ }
26
57
  let cacheMap = new Map();
27
58
  // cleanup forward/back cache
28
59
  if (!isServer) {
@@ -141,11 +172,18 @@ export function query(fn, name) {
141
172
  tracking = true;
142
173
  onCleanup(() => cached[4].count--);
143
174
  }
175
+ // A read with no navigation in flight while hydrating is claiming server
176
+ // DOM, so it takes the cached entry at any age — the rule adoption below
177
+ // applies to flight entries. It can come from a boundary resuming long
178
+ // after boot, once the entry was adopted and its first reader disposed;
179
+ // refetching there would run inside Solid's hydration trace, where fetch
180
+ // and Promise never settle, and cache that.
144
181
  if (cached &&
145
182
  cached[0] &&
146
183
  (isServer ||
147
184
  intent === "native" ||
148
185
  cached[4].count ||
186
+ (!intent && isHydrating()) ||
149
187
  Date.now() - cached[0] < PRELOAD_TIMEOUT)) {
150
188
  if (tracking) {
151
189
  cached[4].count++;
@@ -160,6 +198,7 @@ export function query(fn, name) {
160
198
  !isServer && intent === "navigate" && cached[4][1](cached[0]); // update version
161
199
  }
162
200
  inPreloadFn && "then" in res && res.catch(() => { });
201
+ isServer && serializeForHydration(key, res);
163
202
  return res;
164
203
  }
165
204
  let res;
@@ -176,13 +215,17 @@ export function query(fn, name) {
176
215
  // active navigation (or its preload) accepts the payload only while a
177
216
  // real preload of the same age would still satisfy it — anything older
178
217
  // refetches instead of serving a minutes-old value on a fresh navigation.
179
- if (!isServer && sharedConfig.has && sharedConfig.has(key)) {
180
- const payloadAge = now - bootTime;
181
- if (!intent || payloadAge < (intent === "native" ? CACHE_TIMEOUT : PRELOAD_TIMEOUT)) {
218
+ if (!isServer &&
219
+ (!intent || now - bootTime < (intent === "native" ? CACHE_TIMEOUT : PRELOAD_TIMEOUT))) {
220
+ const entry = takeHydrationValue(key); // hydrating
221
+ if (entry) {
182
222
  adopted = true;
183
- res = sharedConfig.load(key); // hydrating
184
- // @ts-ignore at least until we add a delete method to sharedConfig
185
- delete globalThis._$HY.r[key];
223
+ res =
224
+ entry.status === "pending"
225
+ ? entry.promise
226
+ : entry.status === "resolved"
227
+ ? NativePromise.resolve(entry.value)
228
+ : NativePromise.reject(entry.error);
186
229
  }
187
230
  // A payload too old for this intent stays in the registry: it is
188
231
  // inert while the refetched entry below lives, but a later claim
@@ -191,13 +234,13 @@ export function query(fn, name) {
191
234
  if (!adopted)
192
235
  res = fn(...args);
193
236
  // Adopted entries are stamped at boot — when their data was actually
194
- // fetched — not at consumption. The hydration render burst still dedups
195
- // through the normal window/count reuse above (it happens within
196
- // milliseconds of boot), while a navigation that comes along later sees
197
- // the payload's true age and refetches past PRELOAD_TIMEOUT, exactly as
198
- // it would for a preload performed when the server fetched the data.
237
+ // fetched — not at consumption. Later hydration reads still reuse them
238
+ // through the claim rule above, while a navigation that comes along later
239
+ // sees the payload's true age and refetches past PRELOAD_TIMEOUT, exactly
240
+ // as it would for a preload performed when the server fetched the data.
199
241
  const stamp = adopted ? bootTime : now;
200
242
  if (cached) {
243
+ isServer && forwardedCookies.delete(cached);
201
244
  cached[0] = stamp;
202
245
  cached[1] = res;
203
246
  cached[3] = intent;
@@ -225,22 +268,18 @@ export function query(fn, name) {
225
268
  if (intent !== "preload")
226
269
  res = settle(res);
227
270
  inPreloadFn && "then" in res && res.catch(() => { });
228
- // serialize on server
229
- if (isServer &&
230
- sharedConfig.context &&
231
- sharedConfig.context.async &&
232
- !sharedConfig.context.noHydrate) {
233
- const e = getRequestEvent();
234
- (!e || !e.serverOnly) && sharedConfig.context.serialize(key, res);
235
- }
271
+ isServer && serializeForHydration(key, res);
236
272
  return res;
237
273
  // a settled read: the response handling applied to the value or the promise
238
274
  function settle(v) {
275
+ let cookies = isServer ? forwardedCookies.get(cached) : undefined;
276
+ if (isServer && !cookies)
277
+ forwardedCookies.set(cached, (cookies = new WeakSet()));
239
278
  return "then" in v
240
- ? v.then(handleResponse(false), handleResponse(true))
241
- : handleResponse(false)(v);
279
+ ? v.then(handleResponse(false, cookies), handleResponse(true, cookies))
280
+ : handleResponse(false, cookies)(v);
242
281
  }
243
- function handleResponse(error) {
282
+ function handleResponse(error, cookies) {
244
283
  return async (v) => {
245
284
  let enveloped;
246
285
  let hasEnveloped = false;
@@ -253,11 +292,22 @@ export function query(fn, name) {
253
292
  if (v instanceof Response) {
254
293
  const e = getRequestEvent();
255
294
  if (e) {
295
+ const headers = e.response.headers;
296
+ const skipCookies = cookies?.has(headers);
256
297
  for (const [key, value] of v.headers) {
257
- if (key == "set-cookie")
258
- e.response.headers.append("set-cookie", value);
259
- else
260
- e.response.headers.set(key, value);
298
+ // Keep metadata for the query's body off the document response.
299
+ if (responseBodyHeaders.has(key))
300
+ continue;
301
+ if (key === "set-cookie") {
302
+ if (!skipCookies)
303
+ headers.append(key, value);
304
+ }
305
+ else if (headers.get(key) !== value)
306
+ headers.set(key, value);
307
+ }
308
+ // Each cached result contributes its cookies once per document.
309
+ if (!skipCookies && v.headers.has("set-cookie")) {
310
+ cookies?.add(headers);
261
311
  }
262
312
  }
263
313
  let url = v.headers.get(LocationHeader);
@@ -346,6 +396,7 @@ query.set = (key, value) => {
346
396
  const now = Date.now();
347
397
  let cached = cache.get(key);
348
398
  if (cached) {
399
+ isServer && forwardedCookies.delete(cached);
349
400
  cached[0] = now;
350
401
  cached[1] = Promise.resolve(value);
351
402
  cached[2] = value;
@@ -399,7 +450,7 @@ export function hashKey(args) {
399
450
  .reduce((result, key) => {
400
451
  result[key] = val[key];
401
452
  return result;
402
- }, {})
453
+ }, Object.create(null))
403
454
  : val);
404
455
  }
405
456
  function isPlainObject(obj) {
package/dist/index.js CHANGED
@@ -1,5 +1,5 @@
1
- import { getOwner, runWithOwner, createMemo, createRenderEffect, onCleanup, untrack, createContext, createSignal, useContext, NotReadyError, DEV, isPending, latest, createComponent, createRoot, Show, createEffect, sharedConfig, OBSERVE, onSettled, getObserver, $TRACK, action as action$1 } from 'solid-js';
2
- import { registerElementClaim, delegateEvents, isServer, getRequestEvent, hasFlashCookie, clearFlashCookie, createComponent as createComponent$1, memo, dynamic, isServerFunction, getServerFunctionMetadata, getServerFunctionRPC, isResponseEnvelope, REVALIDATE_HEADER } from '@solidjs/web';
1
+ import { getOwner, runWithOwner, createMemo, createRenderEffect, onCleanup, untrack, createContext, createSignal, useContext, NotReadyError, DEV, isPending, latest, createComponent, createRoot, Show, createEffect, OBSERVE, onSettled, getObserver, isHydrating, isHydratable, $TRACK, action as action$1 } from 'solid-js';
2
+ import { registerElementClaim, delegateEvents, isServer, getRequestEvent, hasFlashCookie, clearFlashCookie, createComponent as createComponent$1, memo, dynamic, isServerFunction, getServerFunctionMetadata, getServerFunctionRPC, takeHydrationValue, isResponseEnvelope, REVALIDATE_HEADER, getHydrationWriter } from '@solidjs/web';
3
3
  import { REDIRECT_HEADER, decodeRedirectHeaderValue, subscribeFlightData, decodeResponsePayload, parseServerFunctionActionUrl, createServerReference } from '@solidjs/web/server-functions';
4
4
 
5
5
  const hasSchemeRegex = /^(?:[a-z0-9]+:)?\/\//i;
@@ -45,13 +45,16 @@ function validateSearch(schema, raw) {
45
45
  return outcome;
46
46
  }
47
47
  function extractSearchParams(url) {
48
- const params = {};
48
+ const params = Object.create(null);
49
49
  url.searchParams.forEach((value, key) => {
50
50
  if (key in params) {
51
51
  if (Array.isArray(params[key])) params[key].push(value);else params[key] = [params[key], value];
52
52
  } else params[key] = value;
53
53
  });
54
- return params;
54
+ // Keep __proto__ as an own property on the plain object returned to callers.
55
+ return {
56
+ ...params
57
+ };
55
58
  }
56
59
  function createMatcher(path, partial, matchFilters) {
57
60
  const [pattern, splat] = path.split("/*", 2);
@@ -1118,7 +1121,8 @@ function createLocation(path, state, queryWrapper) {
1118
1121
  const pathname = createMemo(() => url().pathname);
1119
1122
  const search = createMemo(() => url().search);
1120
1123
  const hash = createMemo(() => url().hash);
1121
- const queryFn = createMemo(() => extractSearchParams(url()));
1124
+ // Pathname and hash changes do not affect the query.
1125
+ const queryFn = createMemo(() => extractSearchParams(new URL(search(), mockBase)));
1122
1126
  return {
1123
1127
  get pathname() {
1124
1128
  return pathname();
@@ -2211,7 +2215,6 @@ function describeNavigation(match, next, written, from) {
2211
2215
  const t = to();
2212
2216
  return t === undefined ? undefined : new URL(t, mockBase).pathname;
2213
2217
  };
2214
- const matches = pathname => untrack(() => match(pathname));
2215
2218
  const ref = {
2216
2219
  kind: "navigation",
2217
2220
  get to() {
@@ -2219,21 +2222,62 @@ function describeNavigation(match, next, written, from) {
2219
2222
  },
2220
2223
  from,
2221
2224
  get name() {
2222
- const p = pathname();
2223
- if (p === undefined) return undefined;
2224
- const m = matches(p);
2225
- return m.length ? m[m.length - 1].route.pattern || "/" : p;
2225
+ return routeName(match, pathname());
2226
2226
  },
2227
2227
  get params() {
2228
- const p = pathname();
2229
- const m = p === undefined ? [] : matches(p);
2230
- return m.length ? mergeParams(m) : undefined;
2228
+ return routeParams(match, pathname());
2231
2229
  }
2232
2230
  };
2233
2231
  if (next._navigation !== undefined && next._navigation > 1) ref.redirect = next._navigation - 1;
2234
2232
  return ref;
2235
2233
  }
2236
2234
 
2235
+ /** The parametrized route `pathname` matches — the last match's pattern, `/` for the root; the pathname itself when nothing matched. */
2236
+ function routeName(match, pathname) {
2237
+ if (pathname === undefined) return undefined;
2238
+ const m = untrack(() => match(pathname));
2239
+ return m.length ? m[m.length - 1].route.pattern || "/" : pathname;
2240
+ }
2241
+
2242
+ /** The params the matched chain bound, merged; `undefined` when nothing matched. */
2243
+ function routeParams(match, pathname) {
2244
+ if (pathname === undefined) return undefined;
2245
+ const m = untrack(() => match(pathname));
2246
+ return m.length ? mergeParams(m) : undefined;
2247
+ }
2248
+
2249
+ /**
2250
+ * The route the document arrived on, for solid's observe tier: the same
2251
+ * declaration as a navigation's, around the work that establishes the
2252
+ * router's initial match — building its context — since a fresh document has
2253
+ * no location write to wrap. On the client the engine opens it at the time
2254
+ * origin and settles it as the context is built (the first `"navigation"`
2255
+ * record, `initial: true`); on the server the same call names the request's
2256
+ * `"render"` record (`RenderEvent.route`). Every field is a getter read at
2257
+ * settle, as for a navigation: `to` is the location as the router then holds
2258
+ * it — the arrival normalized onto the base path when it came in empty, a
2259
+ * write the frame itself took (see the setter) — and `name`/`params` match
2260
+ * it then, so a lazy subtree that resolved while the server rendered names
2261
+ * the exact route.
2262
+ */
2263
+ function describeInitial(match, location) {
2264
+ const to = () => untrack(location).value;
2265
+ const pathname = () => new URL(to(), mockBase).pathname;
2266
+ return {
2267
+ kind: "navigation",
2268
+ initial: true,
2269
+ get to() {
2270
+ return to();
2271
+ },
2272
+ get name() {
2273
+ return routeName(match, pathname());
2274
+ },
2275
+ get params() {
2276
+ return routeParams(match, pathname());
2277
+ }
2278
+ };
2279
+ }
2280
+
2237
2281
  /** Wraps a history adapter in the integration signal the router core consumes. Must run under a reactive owner. */
2238
2282
  function createIntegration(history, match) {
2239
2283
  let committing = false;
@@ -2248,7 +2292,6 @@ function createIntegration(history, match) {
2248
2292
  name: "location"
2249
2293
  });
2250
2294
  const signal = [read, next => {
2251
- if (sharedConfig.registry && !sharedConfig.done) sharedConfig.done = true;
2252
2295
  // What the write resolved to, or undefined when there was nothing to
2253
2296
  // write (see `resolveLocationWrite`).
2254
2297
  let written;
@@ -2281,7 +2324,14 @@ function createIntegration(history, match) {
2281
2324
  // while a navigation is pending, which is the `from` a hop wants too.
2282
2325
  // A write the no-op rule drops is still declared: the engine settles a
2283
2326
  // navigation whose write "did not survive the equality gate" on the spot.
2284
- OBSERVE ? OBSERVE.attribution.withOrigin(describeNavigation(match, next, () => written, untrack(read).value), commit) : commit();
2327
+ // A write from NO location is not a navigation: it is the arrival being
2328
+ // normalized (`createRouterContext` moving an empty location — a hash
2329
+ // history with no hash, a memory history seeded with "" — onto the base
2330
+ // path), and the arrival is already declared by the initial frame open
2331
+ // around the context build (`describeInitial`), which this write then
2332
+ // stamps. The location is never empty again after that first write.
2333
+ const from = untrack(read).value;
2334
+ OBSERVE && from ? OBSERVE.attribution.withOrigin(describeNavigation(match, next, () => written, from), commit) : commit();
2285
2335
  }];
2286
2336
  history.init && onCleanup(history.init((value = history.get()) => {
2287
2337
  if (committing) return;
@@ -2362,11 +2412,14 @@ function createRouter(config) {
2362
2412
  }
2363
2413
  const integration = isServer ? staticIntegration(props.url, config.history && config.history.utils) : createIntegration(history || browserHistory(), matchPath);
2364
2414
  let context;
2365
- const routerState = createRouterContext(integration, branches, () => context, {
2415
+ const buildContext = () => createRouterContext(integration, branches, () => context, {
2366
2416
  base: basePath,
2367
2417
  singleFlight: config.singleFlight,
2368
2418
  transformUrl: config.transformUrl
2369
2419
  });
2420
+ // The route the document arrived on, declared around the initial match
2421
+ // (see `describeInitial`); folded out of shipped bundles with `OBSERVE`.
2422
+ const routerState = OBSERVE ? OBSERVE.attribution.withOrigin(describeInitial(matchPath, integration.signal[0]), buildContext) : buildContext();
2370
2423
  if (!isServer) {
2371
2424
  setupNativeEvents({
2372
2425
  preload: config.preloadLinks,
@@ -2604,10 +2657,29 @@ source) {
2604
2657
  const LocationHeader = "Location";
2605
2658
  const PRELOAD_TIMEOUT$1 = 5000;
2606
2659
  const CACHE_TIMEOUT = 180000;
2607
- // When this client booted. Flight-registry entries (sharedConfig.has/load)
2660
+ const responseBodyHeaders = /* #__PURE__ */new Set(["content-type", "content-length", "content-encoding", "transfer-encoding", "content-disposition", "content-language", "content-location", "content-range", "accept-ranges", "etag", "last-modified"]);
2661
+ const forwardedCookies = /* #__PURE__ */new WeakMap();
2662
+ // When this client booted. Flight-registry entries (takeHydrationValue)
2608
2663
  // hold values the server computed while rendering THIS page, so their age is
2609
2664
  // anchored here — not at whenever a late query() call happens to consume one.
2610
2665
  const bootTime = Date.now();
2666
+ // Captured at module load: a hydrating client runs a serialized node's
2667
+ // compute once as a dependency trace with the global `Promise` swapped for a
2668
+ // never-settling mock, and a query() read inside that trace still consumes
2669
+ // and caches its flight entry. Wrapped with the global there, a settled
2670
+ // entry would cache a promise that never resolves, hanging every navigation
2671
+ // that reuses it.
2672
+ const NativePromise = Promise;
2673
+
2674
+ // Every hydratable read ships its entry: the writer keeps the first write
2675
+ // per key, so a read skipped in a NoHydration zone never hides the entry
2676
+ // from an island that reads the same key later.
2677
+ function serializeForHydration(key, res) {
2678
+ const writer = getHydrationWriter();
2679
+ if (!writer || !writer.async || !isHydratable()) return;
2680
+ const e = getRequestEvent();
2681
+ (!e || !e.serverOnly) && writer.write(key, res);
2682
+ }
2611
2683
  let cacheMap = new Map();
2612
2684
 
2613
2685
  // cleanup forward/back cache
@@ -2722,7 +2794,14 @@ function query(fn, name) {
2722
2794
  tracking = true;
2723
2795
  onCleanup(() => cached[4].count--);
2724
2796
  }
2725
- if (cached && cached[0] && (isServer || intent === "native" || cached[4].count || Date.now() - cached[0] < PRELOAD_TIMEOUT$1)) {
2797
+
2798
+ // A read with no navigation in flight while hydrating is claiming server
2799
+ // DOM, so it takes the cached entry at any age — the rule adoption below
2800
+ // applies to flight entries. It can come from a boundary resuming long
2801
+ // after boot, once the entry was adopted and its first reader disposed;
2802
+ // refetching there would run inside Solid's hydration trace, where fetch
2803
+ // and Promise never settle, and cache that.
2804
+ if (cached && cached[0] && (isServer || intent === "native" || cached[4].count || !intent && isHydrating() || Date.now() - cached[0] < PRELOAD_TIMEOUT$1)) {
2726
2805
  if (tracking) {
2727
2806
  cached[4].count++;
2728
2807
  cached[4][0](); // track
@@ -2736,6 +2815,7 @@ function query(fn, name) {
2736
2815
  !isServer && intent === "navigate" && cached[4][1](cached[0]); // update version
2737
2816
  }
2738
2817
  inPreloadFn && "then" in res && res.catch(() => {});
2818
+ isServer && serializeForHydration(key, res);
2739
2819
  return res;
2740
2820
  }
2741
2821
  let res;
@@ -2752,13 +2832,11 @@ function query(fn, name) {
2752
2832
  // active navigation (or its preload) accepts the payload only while a
2753
2833
  // real preload of the same age would still satisfy it — anything older
2754
2834
  // refetches instead of serving a minutes-old value on a fresh navigation.
2755
- if (!isServer && sharedConfig.has && sharedConfig.has(key)) {
2756
- const payloadAge = now - bootTime;
2757
- if (!intent || payloadAge < (intent === "native" ? CACHE_TIMEOUT : PRELOAD_TIMEOUT$1)) {
2835
+ if (!isServer && (!intent || now - bootTime < (intent === "native" ? CACHE_TIMEOUT : PRELOAD_TIMEOUT$1))) {
2836
+ const entry = takeHydrationValue(key); // hydrating
2837
+ if (entry) {
2758
2838
  adopted = true;
2759
- res = sharedConfig.load(key); // hydrating
2760
- // @ts-ignore at least until we add a delete method to sharedConfig
2761
- delete globalThis._$HY.r[key];
2839
+ res = entry.status === "pending" ? entry.promise : entry.status === "resolved" ? NativePromise.resolve(entry.value) : NativePromise.reject(entry.error);
2762
2840
  }
2763
2841
  // A payload too old for this intent stays in the registry: it is
2764
2842
  // inert while the refetched entry below lives, but a later claim
@@ -2767,13 +2845,13 @@ function query(fn, name) {
2767
2845
  if (!adopted) res = fn(...args);
2768
2846
 
2769
2847
  // Adopted entries are stamped at boot — when their data was actually
2770
- // fetched — not at consumption. The hydration render burst still dedups
2771
- // through the normal window/count reuse above (it happens within
2772
- // milliseconds of boot), while a navigation that comes along later sees
2773
- // the payload's true age and refetches past PRELOAD_TIMEOUT, exactly as
2774
- // it would for a preload performed when the server fetched the data.
2848
+ // fetched — not at consumption. Later hydration reads still reuse them
2849
+ // through the claim rule above, while a navigation that comes along later
2850
+ // sees the payload's true age and refetches past PRELOAD_TIMEOUT, exactly
2851
+ // as it would for a preload performed when the server fetched the data.
2775
2852
  const stamp = adopted ? bootTime : now;
2776
2853
  if (cached) {
2854
+ isServer && forwardedCookies.delete(cached);
2777
2855
  cached[0] = stamp;
2778
2856
  cached[1] = res;
2779
2857
  cached[3] = intent;
@@ -2794,18 +2872,16 @@ function query(fn, name) {
2794
2872
  }
2795
2873
  if (intent !== "preload") res = settle(res);
2796
2874
  inPreloadFn && "then" in res && res.catch(() => {});
2797
- // serialize on server
2798
- if (isServer && sharedConfig.context && sharedConfig.context.async && !sharedConfig.context.noHydrate) {
2799
- const e = getRequestEvent();
2800
- (!e || !e.serverOnly) && sharedConfig.context.serialize(key, res);
2801
- }
2875
+ isServer && serializeForHydration(key, res);
2802
2876
  return res;
2803
2877
 
2804
2878
  // a settled read: the response handling applied to the value or the promise
2805
2879
  function settle(v) {
2806
- return "then" in v ? v.then(handleResponse(false), handleResponse(true)) : handleResponse(false)(v);
2880
+ let cookies = isServer ? forwardedCookies.get(cached) : undefined;
2881
+ if (isServer && !cookies) forwardedCookies.set(cached, cookies = new WeakSet());
2882
+ return "then" in v ? v.then(handleResponse(false, cookies), handleResponse(true, cookies)) : handleResponse(false, cookies)(v);
2807
2883
  }
2808
- function handleResponse(error) {
2884
+ function handleResponse(error, cookies) {
2809
2885
  return async v => {
2810
2886
  let enveloped;
2811
2887
  let hasEnveloped = false;
@@ -2818,8 +2894,18 @@ function query(fn, name) {
2818
2894
  if (v instanceof Response) {
2819
2895
  const e = getRequestEvent();
2820
2896
  if (e) {
2897
+ const headers = e.response.headers;
2898
+ const skipCookies = cookies?.has(headers);
2821
2899
  for (const [key, value] of v.headers) {
2822
- if (key == "set-cookie") e.response.headers.append("set-cookie", value);else e.response.headers.set(key, value);
2900
+ // Keep metadata for the query's body off the document response.
2901
+ if (responseBodyHeaders.has(key)) continue;
2902
+ if (key === "set-cookie") {
2903
+ if (!skipCookies) headers.append(key, value);
2904
+ } else if (headers.get(key) !== value) headers.set(key, value);
2905
+ }
2906
+ // Each cached result contributes its cookies once per document.
2907
+ if (!skipCookies && v.headers.has("set-cookie")) {
2908
+ cookies?.add(headers);
2823
2909
  }
2824
2910
  }
2825
2911
  let url = v.headers.get(LocationHeader);
@@ -2902,6 +2988,7 @@ query.set = (key, value) => {
2902
2988
  const now = Date.now();
2903
2989
  let cached = cache.get(key);
2904
2990
  if (cached) {
2991
+ isServer && forwardedCookies.delete(cached);
2905
2992
  cached[0] = now;
2906
2993
  cached[1] = Promise.resolve(value);
2907
2994
  cached[2] = value;
@@ -2949,7 +3036,7 @@ function hashKey(args) {
2949
3036
  return JSON.stringify(args, (_, val) => isPlainObject(val) ? Object.keys(val).sort().reduce((result, key) => {
2950
3037
  result[key] = val[key];
2951
3038
  return result;
2952
- }, {}) : val);
3039
+ }, Object.create(null)) : val);
2953
3040
  }
2954
3041
  function isPlainObject(obj) {
2955
3042
  let proto;
@@ -1,5 +1,5 @@
1
1
  /*@refresh skip*/
2
- import { createSignal, getOwner, onCleanup, onSettled, runWithOwner, sharedConfig, untrack } from "solid-js";
2
+ import { createSignal, getOwner, onCleanup, onSettled, runWithOwner, untrack } from "solid-js";
3
3
  // standalone imports: `DEV` is undefined in solid's production build and
4
4
  // `OBSERVE` outside its observe/dev builds, so app bundlers fold the
5
5
  // `DEV &&` diagnostics and the `OBSERVE &&` attribution out of shipped bundles
@@ -53,7 +53,6 @@ function describeNavigation(match, next, written, from) {
53
53
  const t = to();
54
54
  return t === undefined ? undefined : new URL(t, mockBase).pathname;
55
55
  };
56
- const matches = (pathname) => untrack(() => match(pathname));
57
56
  const ref = {
58
57
  kind: "navigation",
59
58
  get to() {
@@ -61,22 +60,61 @@ function describeNavigation(match, next, written, from) {
61
60
  },
62
61
  from,
63
62
  get name() {
64
- const p = pathname();
65
- if (p === undefined)
66
- return undefined;
67
- const m = matches(p);
68
- return m.length ? m[m.length - 1].route.pattern || "/" : p;
63
+ return routeName(match, pathname());
69
64
  },
70
65
  get params() {
71
- const p = pathname();
72
- const m = p === undefined ? [] : matches(p);
73
- return m.length ? mergeParams(m) : undefined;
66
+ return routeParams(match, pathname());
74
67
  }
75
68
  };
76
69
  if (next._navigation !== undefined && next._navigation > 1)
77
70
  ref.redirect = next._navigation - 1;
78
71
  return ref;
79
72
  }
73
+ /** The parametrized route `pathname` matches — the last match's pattern, `/` for the root; the pathname itself when nothing matched. */
74
+ function routeName(match, pathname) {
75
+ if (pathname === undefined)
76
+ return undefined;
77
+ const m = untrack(() => match(pathname));
78
+ return m.length ? m[m.length - 1].route.pattern || "/" : pathname;
79
+ }
80
+ /** The params the matched chain bound, merged; `undefined` when nothing matched. */
81
+ function routeParams(match, pathname) {
82
+ if (pathname === undefined)
83
+ return undefined;
84
+ const m = untrack(() => match(pathname));
85
+ return m.length ? mergeParams(m) : undefined;
86
+ }
87
+ /**
88
+ * The route the document arrived on, for solid's observe tier: the same
89
+ * declaration as a navigation's, around the work that establishes the
90
+ * router's initial match — building its context — since a fresh document has
91
+ * no location write to wrap. On the client the engine opens it at the time
92
+ * origin and settles it as the context is built (the first `"navigation"`
93
+ * record, `initial: true`); on the server the same call names the request's
94
+ * `"render"` record (`RenderEvent.route`). Every field is a getter read at
95
+ * settle, as for a navigation: `to` is the location as the router then holds
96
+ * it — the arrival normalized onto the base path when it came in empty, a
97
+ * write the frame itself took (see the setter) — and `name`/`params` match
98
+ * it then, so a lazy subtree that resolved while the server rendered names
99
+ * the exact route.
100
+ */
101
+ function describeInitial(match, location) {
102
+ const to = () => untrack(location).value;
103
+ const pathname = () => new URL(to(), mockBase).pathname;
104
+ return {
105
+ kind: "navigation",
106
+ initial: true,
107
+ get to() {
108
+ return to();
109
+ },
110
+ get name() {
111
+ return routeName(match, pathname());
112
+ },
113
+ get params() {
114
+ return routeParams(match, pathname());
115
+ }
116
+ };
117
+ }
80
118
  /** Wraps a history adapter in the integration signal the router core consumes. Must run under a reactive owner. */
81
119
  function createIntegration(history, match) {
82
120
  let committing = false;
@@ -91,8 +129,6 @@ function createIntegration(history, match) {
91
129
  const signal = [
92
130
  read,
93
131
  (next) => {
94
- if (sharedConfig.registry && !sharedConfig.done)
95
- sharedConfig.done = true;
96
132
  // What the write resolved to, or undefined when there was nothing to
97
133
  // write (see `resolveLocationWrite`).
98
134
  let written;
@@ -128,8 +164,15 @@ function createIntegration(history, match) {
128
164
  // while a navigation is pending, which is the `from` a hop wants too.
129
165
  // A write the no-op rule drops is still declared: the engine settles a
130
166
  // navigation whose write "did not survive the equality gate" on the spot.
131
- OBSERVE
132
- ? OBSERVE.attribution.withOrigin(describeNavigation(match, next, () => written, untrack(read).value), commit)
167
+ // A write from NO location is not a navigation: it is the arrival being
168
+ // normalized (`createRouterContext` moving an empty location — a hash
169
+ // history with no hash, a memory history seeded with "" — onto the base
170
+ // path), and the arrival is already declared by the initial frame open
171
+ // around the context build (`describeInitial`), which this write then
172
+ // stamps. The location is never empty again after that first write.
173
+ const from = untrack(read).value;
174
+ OBSERVE && from
175
+ ? OBSERVE.attribution.withOrigin(describeNavigation(match, next, () => written, from), commit)
133
176
  : commit();
134
177
  }
135
178
  ];
@@ -210,11 +253,16 @@ export function createRouter(config) {
210
253
  ? staticIntegration(props.url, config.history && config.history.utils)
211
254
  : createIntegration(history || browserHistory(), matchPath);
212
255
  let context;
213
- const routerState = createRouterContext(integration, branches, () => context, {
256
+ const buildContext = () => createRouterContext(integration, branches, () => context, {
214
257
  base: basePath,
215
258
  singleFlight: config.singleFlight,
216
259
  transformUrl: config.transformUrl
217
260
  });
261
+ // The route the document arrived on, declared around the initial match
262
+ // (see `describeInitial`); folded out of shipped bundles with `OBSERVE`.
263
+ const routerState = OBSERVE
264
+ ? OBSERVE.attribution.withOrigin(describeInitial(matchPath, integration.signal[0]), buildContext)
265
+ : buildContext();
218
266
  if (!isServer) {
219
267
  setupNativeEvents({
220
268
  preload: config.preloadLinks,
package/dist/routing.js CHANGED
@@ -502,7 +502,8 @@ function createLocation(path, state, queryWrapper) {
502
502
  const pathname = createMemo(() => url().pathname);
503
503
  const search = createMemo(() => url().search);
504
504
  const hash = createMemo(() => url().hash);
505
- const queryFn = createMemo(() => extractSearchParams(url()));
505
+ // Pathname and hash changes do not affect the query.
506
+ const queryFn = createMemo(() => extractSearchParams(new URL(search(), mockBase)));
506
507
  return {
507
508
  get pathname() {
508
509
  return pathname();
package/dist/utils.js CHANGED
@@ -43,7 +43,7 @@ export function validateSearch(schema, raw) {
43
43
  return outcome;
44
44
  }
45
45
  export function extractSearchParams(url) {
46
- const params = {};
46
+ const params = Object.create(null);
47
47
  url.searchParams.forEach((value, key) => {
48
48
  if (key in params) {
49
49
  if (Array.isArray(params[key]))
@@ -54,7 +54,8 @@ export function extractSearchParams(url) {
54
54
  else
55
55
  params[key] = value;
56
56
  });
57
- return params;
57
+ // Keep __proto__ as an own property on the plain object returned to callers.
58
+ return { ...params };
58
59
  }
59
60
  export function createMatcher(path, partial, matchFilters) {
60
61
  const [pattern, splat] = path.split("/*", 2);
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "Ryan Turnquist"
7
7
  ],
8
8
  "license": "MIT",
9
- "version": "2.0.0-next.31",
9
+ "version": "2.0.0-next.33",
10
10
  "homepage": "https://github.com/solidjs/solid-router#readme",
11
11
  "repository": {
12
12
  "type": "git",
@@ -38,22 +38,22 @@
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.11",
41
+ "@solidjs/web": "^2.0.0-rc.13",
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.11",
48
+ "solid-js": "^2.0.0-rc.13",
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.10",
54
+ "@solidjs/web": "^2.0.0-rc.13",
55
55
  "filesystem-routing": ">=0.4.0",
56
- "solid-js": "^2.0.0-rc.10"
56
+ "solid-js": "^2.0.0-rc.13"
57
57
  },
58
58
  "peerDependenciesMeta": {
59
59
  "filesystem-routing": {