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

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, 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,34 @@ 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
+ // Every hydratable read ships its entry: the writer keeps the first write
41
+ // per key, so a read skipped in a NoHydration zone never hides the entry
42
+ // from an island that reads the same key later.
43
+ function serializeForHydration(key, res) {
44
+ const writer = getHydrationWriter();
45
+ if (!writer || !writer.async || !isHydratable())
46
+ return;
47
+ const e = getRequestEvent();
48
+ (!e || !e.serverOnly) && writer.write(key, res);
49
+ }
26
50
  let cacheMap = new Map();
27
51
  // cleanup forward/back cache
28
52
  if (!isServer) {
@@ -160,6 +184,7 @@ export function query(fn, name) {
160
184
  !isServer && intent === "navigate" && cached[4][1](cached[0]); // update version
161
185
  }
162
186
  inPreloadFn && "then" in res && res.catch(() => { });
187
+ isServer && serializeForHydration(key, res);
163
188
  return res;
164
189
  }
165
190
  let res;
@@ -176,13 +201,17 @@ export function query(fn, name) {
176
201
  // active navigation (or its preload) accepts the payload only while a
177
202
  // real preload of the same age would still satisfy it — anything older
178
203
  // 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)) {
204
+ if (!isServer &&
205
+ (!intent || now - bootTime < (intent === "native" ? CACHE_TIMEOUT : PRELOAD_TIMEOUT))) {
206
+ const entry = takeHydrationValue(key); // hydrating
207
+ if (entry) {
182
208
  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];
209
+ res =
210
+ entry.status === "pending"
211
+ ? entry.promise
212
+ : entry.status === "resolved"
213
+ ? Promise.resolve(entry.value)
214
+ : Promise.reject(entry.error);
186
215
  }
187
216
  // A payload too old for this intent stays in the registry: it is
188
217
  // inert while the refetched entry below lives, but a later claim
@@ -198,6 +227,7 @@ export function query(fn, name) {
198
227
  // it would for a preload performed when the server fetched the data.
199
228
  const stamp = adopted ? bootTime : now;
200
229
  if (cached) {
230
+ isServer && forwardedCookies.delete(cached);
201
231
  cached[0] = stamp;
202
232
  cached[1] = res;
203
233
  cached[3] = intent;
@@ -225,22 +255,18 @@ export function query(fn, name) {
225
255
  if (intent !== "preload")
226
256
  res = settle(res);
227
257
  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
- }
258
+ isServer && serializeForHydration(key, res);
236
259
  return res;
237
260
  // a settled read: the response handling applied to the value or the promise
238
261
  function settle(v) {
262
+ let cookies = isServer ? forwardedCookies.get(cached) : undefined;
263
+ if (isServer && !cookies)
264
+ forwardedCookies.set(cached, (cookies = new WeakSet()));
239
265
  return "then" in v
240
- ? v.then(handleResponse(false), handleResponse(true))
241
- : handleResponse(false)(v);
266
+ ? v.then(handleResponse(false, cookies), handleResponse(true, cookies))
267
+ : handleResponse(false, cookies)(v);
242
268
  }
243
- function handleResponse(error) {
269
+ function handleResponse(error, cookies) {
244
270
  return async (v) => {
245
271
  let enveloped;
246
272
  let hasEnveloped = false;
@@ -253,11 +279,22 @@ export function query(fn, name) {
253
279
  if (v instanceof Response) {
254
280
  const e = getRequestEvent();
255
281
  if (e) {
282
+ const headers = e.response.headers;
283
+ const skipCookies = cookies?.has(headers);
256
284
  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);
285
+ // Keep metadata for the query's body off the document response.
286
+ if (responseBodyHeaders.has(key))
287
+ continue;
288
+ if (key === "set-cookie") {
289
+ if (!skipCookies)
290
+ headers.append(key, value);
291
+ }
292
+ else if (headers.get(key) !== value)
293
+ headers.set(key, value);
294
+ }
295
+ // Each cached result contributes its cookies once per document.
296
+ if (!skipCookies && v.headers.has("set-cookie")) {
297
+ cookies?.add(headers);
261
298
  }
262
299
  }
263
300
  let url = v.headers.get(LocationHeader);
@@ -346,6 +383,7 @@ query.set = (key, value) => {
346
383
  const now = Date.now();
347
384
  let cached = cache.get(key);
348
385
  if (cached) {
386
+ isServer && forwardedCookies.delete(cached);
349
387
  cached[0] = now;
350
388
  cached[1] = Promise.resolve(value);
351
389
  cached[2] = value;
@@ -399,7 +437,7 @@ export function hashKey(args) {
399
437
  .reduce((result, key) => {
400
438
  result[key] = val[key];
401
439
  return result;
402
- }, {})
440
+ }, Object.create(null))
403
441
  : val);
404
442
  }
405
443
  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, 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,22 @@ 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
+
2667
+ // Every hydratable read ships its entry: the writer keeps the first write
2668
+ // per key, so a read skipped in a NoHydration zone never hides the entry
2669
+ // from an island that reads the same key later.
2670
+ function serializeForHydration(key, res) {
2671
+ const writer = getHydrationWriter();
2672
+ if (!writer || !writer.async || !isHydratable()) return;
2673
+ const e = getRequestEvent();
2674
+ (!e || !e.serverOnly) && writer.write(key, res);
2675
+ }
2611
2676
  let cacheMap = new Map();
2612
2677
 
2613
2678
  // cleanup forward/back cache
@@ -2736,6 +2801,7 @@ function query(fn, name) {
2736
2801
  !isServer && intent === "navigate" && cached[4][1](cached[0]); // update version
2737
2802
  }
2738
2803
  inPreloadFn && "then" in res && res.catch(() => {});
2804
+ isServer && serializeForHydration(key, res);
2739
2805
  return res;
2740
2806
  }
2741
2807
  let res;
@@ -2752,13 +2818,11 @@ function query(fn, name) {
2752
2818
  // active navigation (or its preload) accepts the payload only while a
2753
2819
  // real preload of the same age would still satisfy it — anything older
2754
2820
  // 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)) {
2821
+ if (!isServer && (!intent || now - bootTime < (intent === "native" ? CACHE_TIMEOUT : PRELOAD_TIMEOUT$1))) {
2822
+ const entry = takeHydrationValue(key); // hydrating
2823
+ if (entry) {
2758
2824
  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];
2825
+ res = entry.status === "pending" ? entry.promise : entry.status === "resolved" ? Promise.resolve(entry.value) : Promise.reject(entry.error);
2762
2826
  }
2763
2827
  // A payload too old for this intent stays in the registry: it is
2764
2828
  // inert while the refetched entry below lives, but a later claim
@@ -2774,6 +2838,7 @@ function query(fn, name) {
2774
2838
  // it would for a preload performed when the server fetched the data.
2775
2839
  const stamp = adopted ? bootTime : now;
2776
2840
  if (cached) {
2841
+ isServer && forwardedCookies.delete(cached);
2777
2842
  cached[0] = stamp;
2778
2843
  cached[1] = res;
2779
2844
  cached[3] = intent;
@@ -2794,18 +2859,16 @@ function query(fn, name) {
2794
2859
  }
2795
2860
  if (intent !== "preload") res = settle(res);
2796
2861
  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
- }
2862
+ isServer && serializeForHydration(key, res);
2802
2863
  return res;
2803
2864
 
2804
2865
  // a settled read: the response handling applied to the value or the promise
2805
2866
  function settle(v) {
2806
- return "then" in v ? v.then(handleResponse(false), handleResponse(true)) : handleResponse(false)(v);
2867
+ let cookies = isServer ? forwardedCookies.get(cached) : undefined;
2868
+ if (isServer && !cookies) forwardedCookies.set(cached, cookies = new WeakSet());
2869
+ return "then" in v ? v.then(handleResponse(false, cookies), handleResponse(true, cookies)) : handleResponse(false, cookies)(v);
2807
2870
  }
2808
- function handleResponse(error) {
2871
+ function handleResponse(error, cookies) {
2809
2872
  return async v => {
2810
2873
  let enveloped;
2811
2874
  let hasEnveloped = false;
@@ -2818,8 +2881,18 @@ function query(fn, name) {
2818
2881
  if (v instanceof Response) {
2819
2882
  const e = getRequestEvent();
2820
2883
  if (e) {
2884
+ const headers = e.response.headers;
2885
+ const skipCookies = cookies?.has(headers);
2821
2886
  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);
2887
+ // Keep metadata for the query's body off the document response.
2888
+ if (responseBodyHeaders.has(key)) continue;
2889
+ if (key === "set-cookie") {
2890
+ if (!skipCookies) headers.append(key, value);
2891
+ } else if (headers.get(key) !== value) headers.set(key, value);
2892
+ }
2893
+ // Each cached result contributes its cookies once per document.
2894
+ if (!skipCookies && v.headers.has("set-cookie")) {
2895
+ cookies?.add(headers);
2823
2896
  }
2824
2897
  }
2825
2898
  let url = v.headers.get(LocationHeader);
@@ -2902,6 +2975,7 @@ query.set = (key, value) => {
2902
2975
  const now = Date.now();
2903
2976
  let cached = cache.get(key);
2904
2977
  if (cached) {
2978
+ isServer && forwardedCookies.delete(cached);
2905
2979
  cached[0] = now;
2906
2980
  cached[1] = Promise.resolve(value);
2907
2981
  cached[2] = value;
@@ -2949,7 +3023,7 @@ function hashKey(args) {
2949
3023
  return JSON.stringify(args, (_, val) => isPlainObject(val) ? Object.keys(val).sort().reduce((result, key) => {
2950
3024
  result[key] = val[key];
2951
3025
  return result;
2952
- }, {}) : val);
3026
+ }, Object.create(null)) : val);
2953
3027
  }
2954
3028
  function isPlainObject(obj) {
2955
3029
  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.32",
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": {