@solidjs/router 0.17.0-next.6 → 1.0.0-next.7

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.
Files changed (48) hide show
  1. package/README.md +526 -716
  2. package/dist/claims.d.ts +21 -0
  3. package/dist/claims.js +108 -0
  4. package/dist/data/action.d.ts +15 -0
  5. package/dist/data/action.js +127 -12
  6. package/dist/data/events.d.ts +8 -0
  7. package/dist/data/events.js +23 -22
  8. package/dist/data/flash.d.ts +1 -5
  9. package/dist/data/flash.js +10 -18
  10. package/dist/data/flashCookie.d.ts +7 -0
  11. package/dist/data/flashCookie.js +20 -0
  12. package/dist/data/serverForms.d.ts +1 -0
  13. package/dist/data/serverForms.js +5 -0
  14. package/dist/index.d.ts +5 -3
  15. package/dist/index.js +1728 -1158
  16. package/dist/index.jsx +2 -2
  17. package/dist/lifecycle.d.ts +29 -4
  18. package/dist/lifecycle.js +40 -37
  19. package/dist/paths.d.ts +117 -0
  20. package/dist/paths.js +41 -0
  21. package/dist/routers/components.d.ts +10 -21
  22. package/dist/routers/components.jsx +29 -51
  23. package/dist/routers/factory.d.ts +45 -0
  24. package/dist/routers/factory.jsx +143 -0
  25. package/dist/routers/history.d.ts +24 -0
  26. package/dist/routers/history.js +180 -0
  27. package/dist/routers/index.d.ts +4 -11
  28. package/dist/routers/index.js +2 -6
  29. package/dist/routing.d.ts +79 -52
  30. package/dist/routing.js +296 -127
  31. package/dist/server.d.ts +25 -15
  32. package/dist/server.js +43 -7
  33. package/dist/types.d.ts +74 -5
  34. package/dist/utils.d.ts +2 -0
  35. package/dist/utils.js +2 -0
  36. package/package.json +6 -6
  37. package/dist/components.d.ts +0 -31
  38. package/dist/components.jsx +0 -46
  39. package/dist/routers/HashRouter.d.ts +0 -9
  40. package/dist/routers/HashRouter.js +0 -41
  41. package/dist/routers/MemoryRouter.d.ts +0 -24
  42. package/dist/routers/MemoryRouter.js +0 -57
  43. package/dist/routers/Router.d.ts +0 -9
  44. package/dist/routers/Router.js +0 -45
  45. package/dist/routers/StaticRouter.d.ts +0 -6
  46. package/dist/routers/StaticRouter.js +0 -15
  47. package/dist/routers/createRouter.d.ts +0 -10
  48. package/dist/routers/createRouter.js +0 -40
package/dist/routing.js CHANGED
@@ -1,14 +1,13 @@
1
1
  import { flush, runWithOwner } from "solid-js";
2
- import { children, createComponent, createContext, createMemo, createSignal, onCleanup, untrack, useContext } from "solid-js";
2
+ import { createComponent, createContext, createMemo, createSignal, NotReadyError, untrack, useContext } from "solid-js";
3
3
  import { isServer, getRequestEvent } from "@solidjs/web";
4
- import { createBeforeLeave } from "./lifecycle.js";
5
- import { mockBase, createMemoObject, extractSearchParams, invariant, resolvePath, createMatcher, joinPaths, scoreRoute, mergeSearchString, expandOptionals } from "./utils.js";
6
- import { clearFlashCookie, decodeFlashCookie, hasFlashCookie } from "./data/flash.js";
4
+ import { mockBase, comparablePath, createMemoObject, extractSearchParams, invariant, resolvePath, createMatcher, joinPaths, scoreRoute, mergeSearchString, expandOptionals } from "./utils.js";
5
+ import { clearFlashCookie, hasFlashCookie } from "./data/flashCookie.js";
7
6
  const MAX_REDIRECTS = 100;
8
7
  /** Consider this API opaque and internal. It is likely to change in the future. */
9
8
  export const RouterContextObj = createContext();
10
9
  export const RouteContextObj = createContext();
11
- function useOptionalContext(context) {
10
+ export function useOptionalContext(context) {
12
11
  try {
13
12
  return useContext(context);
14
13
  }
@@ -77,21 +76,6 @@ export const useLocation = () => useRouter().location;
77
76
  * ```
78
77
  */
79
78
  export const useIsRouting = () => useRouter().isRouting;
80
- /**
81
- * usePreloadRoute returns a function that can be used to preload a route manual.
82
- * This is what happens automatically with link hovering and similar focus based behavior, but it is available here as an API.
83
- *
84
- * @example
85
- * ```js
86
- * const preload = usePreloadRoute();
87
- *
88
- * preload(`/users/settings`, { preloadData: true });
89
- * ```
90
- */
91
- export const usePreloadRoute = () => {
92
- const pre = useRouter().preloadRoute;
93
- return (url, options = {}) => pre(url instanceof URL ? url : new URL(url, mockBase), options.preloadData);
94
- };
95
79
  /**
96
80
  * `useMatch` takes an accessor that returns the path and creates a `Memo` that returns match information if the current path matches the provided path.
97
81
  * Useful for determining if a given path matches the current route.
@@ -115,61 +99,44 @@ export const useMatch = (path, matchFilters) => {
115
99
  });
116
100
  };
117
101
  /**
118
- * `useCurrentMatches` returns all the matches for the current matched route.
119
- * Useful for getting all the route information.
102
+ * `useRouteMatches` returns an accessor of the router's resolved matches for
103
+ * the current location — the chain of route definitions producing the current
104
+ * render, outermost first. Useful for reading `info` metadata off the matched
105
+ * chain. Unlike `useMatch`, which tests a path pattern you supply against the
106
+ * location, this reflects the route tree itself.
120
107
  *
121
108
  * @example
122
109
  * ```js
123
- * const matches = useCurrentMatches();
110
+ * const matches = useRouteMatches();
124
111
  *
125
- * const breadcrumbs = createMemo(() => matches().map(m => m.route.info.breadcrumb))
112
+ * const breadcrumbs = createMemo(() => matches().map(m => m.route.info.breadcrumb));
126
113
  * ```
127
114
  */
128
- export const useCurrentMatches = () => {
115
+ export const useRouteMatches = () => {
129
116
  const router = useRouter();
130
117
  // return a copy so user mutations (eg. `.reverse()`) can't corrupt router state
131
118
  return () => router.matches().slice();
132
119
  };
133
120
  /**
134
- * Retrieves a reactive, store-like object containing the current route path parameters as defined in the Route.
121
+ * `usePreloadRoute` returns a function for warming a route by hand — the same
122
+ * work link hover/focus intent triggers automatically: the matched routes'
123
+ * lazy components load, and with `preloadData` their `preload` functions run.
135
124
  *
136
125
  * @example
137
126
  * ```js
138
- * const params = useParams();
127
+ * const preload = usePreloadRoute();
139
128
  *
140
- * // fetch user based on the id path parameter
141
- * const getUser = query(() => fetchUser(params.id), "user");
129
+ * preload(paths.users(2).settings, { preloadData: true });
142
130
  * ```
143
131
  */
144
- export const useParams = () => useRoute().params;
145
- /**
146
- * Retrieves a tuple containing a reactive object to read the current location's query parameters and a method to update them.
147
- * The object is a proxy so you must access properties to subscribe to reactive updates.
148
- * **Note** that values will be strings and property names will retain their casing.
149
- *
150
- * The setter method accepts an object whose entries will be merged into the current query string.
151
- * Values `''`, `undefined` and `null` will remove the key from the resulting query string.
152
- * Updates will behave just like a navigation and the setter accepts the same optional second parameter as `navigate` and auto-scrolling is disabled by default.
153
- *
154
- * @examples
155
- * ```js
156
- * const [searchParams, setSearchParams] = useSearchParams();
157
- *
158
- * return (
159
- * <div>
160
- * <span>Page: {searchParams.page}</span>
161
- * <button
162
- * onClick={() =>
163
- * setSearchParams({ page: (parseInt(searchParams.page) || 0) + 1 })
164
- * }
165
- * >
166
- * Next Page
167
- * </button>
168
- * </div>
169
- * );
170
- * ```
171
- */
172
- export const useSearchParams = () => {
132
+ export const usePreloadRoute = () => {
133
+ const pre = useRouter().preloadRoute;
134
+ return (url, options = {}) => pre(url instanceof URL ? url : new URL(String(url), mockBase), options.preloadData);
135
+ };
136
+ export function useParams(_path) {
137
+ return useRoute().params;
138
+ }
139
+ export function useSearchParams(path) {
173
140
  const router = useRouter();
174
141
  const location = router.location;
175
142
  const navigate = useNavigate();
@@ -189,48 +156,165 @@ export const useSearchParams = () => {
189
156
  ...options
190
157
  });
191
158
  };
192
- return [location.query, setSearchParams];
193
- };
159
+ // Passing a paths node opts into schema parsing. The node itself is a
160
+ // type-level reference; the schemas that run come from the currently
161
+ // matched routes (root→leaf), whose outputs merge over the raw query.
162
+ // A schema that reports issues is skipped, leaving raw values — search
163
+ // strings are user input, so defaults belong in the schema itself.
164
+ const query = path
165
+ ? createMemoObject(createMemo(() => {
166
+ const raw = { ...location.query };
167
+ let result;
168
+ for (const match of router.matches()) {
169
+ const schema = match.route.key.search;
170
+ if (!schema)
171
+ continue;
172
+ const outcome = schema["~standard"].validate(raw);
173
+ if (outcome instanceof Promise)
174
+ throw new Error("Async Standard Schema validation is not supported for search params");
175
+ if (!outcome.issues)
176
+ result = Object.assign(result || { ...raw }, outcome.value);
177
+ }
178
+ return result || raw;
179
+ }))
180
+ : location.query;
181
+ return [query, setSearchParams];
182
+ }
194
183
  /**
195
- * useBeforeLeave takes a function that will be called prior to leaving a route.
196
- * The function will be called with:
197
- *
198
- * - from (*Location*): current location (before change).
199
- * - to (*string | number*): path passed to `navigate`.
200
- * - options (*NavigateOptions*): options passed to navigate.
201
- * - preventDefault (*function*): call to block the route change.
202
- * - defaultPrevented (*readonly boolean*): `true` if any previously called leave handlers called `preventDefault`.
203
- * - retry (*function*, force?: boolean ): call to retry the same navigation, perhaps after confirming with the user. Pass `true` to skip running the leave handlers again (i.e. force navigate without confirming).
184
+ * Reactive link state for custom link components — the programmatic
185
+ * counterpart of the attribute vocabulary plain anchors receive
186
+ * (`aria-current`, `data-active`, `data-pending`).
204
187
  *
205
188
  * @example
206
- * ```js
207
- * useBeforeLeave((e: BeforeLeaveEventArgs) => {
208
- * if (form.isDirty && !e.defaultPrevented) {
209
- * // preventDefault to block immediately and prompt user async
210
- * e.preventDefault();
211
- * setTimeout(() => {
212
- * if (window.confirm("Discard unsaved changes - are you sure?")) {
213
- * // user wants to proceed anyway so retry with force=true
214
- * e.retry(true);
215
- * }
216
- * }, 100);
217
- * }
218
- * });
189
+ * ```tsx
190
+ * function TabLink(props: { href: string; children: JSX.Element }) {
191
+ * const link = useLinkState(() => props.href);
192
+ * return (
193
+ * <a href={props.href} class="tab" data-selected={link.active() || undefined}>
194
+ * {props.children}
195
+ * </a>
196
+ * );
197
+ * }
219
198
  * ```
220
199
  */
221
- export const useBeforeLeave = (listener) => {
222
- const s = useRouter().beforeLeave.subscribe({
223
- listener,
224
- location: useLocation(),
225
- navigate: useNavigate()
200
+ export const useLinkState = (href, options = {}) => {
201
+ const router = useRouter();
202
+ const location = router.location;
203
+ const to = useResolvedPath(() => String(href()));
204
+ // trailing slashes are ignored so `/route` and `/route/` share state
205
+ const path = createMemo(() => {
206
+ const to_ = to();
207
+ return to_ === undefined ? undefined : comparablePath(to_);
226
208
  });
227
- onCleanup(s);
209
+ const matches = (loc) => {
210
+ const path_ = path();
211
+ if (path_ === undefined)
212
+ return [false, false];
213
+ const exact = loc === path_;
214
+ return [exact || (!options.end && loc.startsWith(path_ + "/")), exact];
215
+ };
216
+ const state = createMemo(() => matches(decodeURI(comparablePath(location.pathname))));
217
+ return {
218
+ active: () => state()[0],
219
+ current: () => state()[1],
220
+ // match the in-flight target explicitly (rather than active-while-routing)
221
+ // so the answer is the same from pure reads and from effects, which
222
+ // observe the committed location during a transition
223
+ pending: createMemo(() => {
224
+ state(); // location dependency: mid-flight target swaps recompute
225
+ return (router.isRouting() &&
226
+ !!router.pendingTarget &&
227
+ matches(decodeURI(comparablePath(router.pendingTarget.value)))[0]);
228
+ })
229
+ };
228
230
  };
229
231
  // Encodes a static path segment like `encodeURIComponent`, but leaves RFC 3986
230
232
  // pchar characters (sub-delims / ":" / "@") literal, matching how browsers
231
233
  // report them in `location.pathname`. Non-ASCII characters (eg. CJK paths) are
232
234
  // still percent-encoded exactly as before, since browsers encode those too.
233
235
  const encodeSegment = (s) => encodeURIComponent(s).replace(/%(2B|40|3A|24|26|2C|3B|3D)/g, m => decodeURIComponent(m));
236
+ // ---------------------------------------------------------------------------
237
+ // Lazy route subtrees
238
+ // ---------------------------------------------------------------------------
239
+ //
240
+ // A `children` thunk (`() => import("./feature/routes")`) is a *boundary*:
241
+ // until it resolves, the compiled tree carries a param-less catch-all
242
+ // placeholder branch under the boundary's pattern (splat-scored, so static
243
+ // siblings still win). Resolution is append-only and cached per thunk, then a
244
+ // module-level version signal bumps and every `branches()` consumer
245
+ // recompiles — matches, params, and route states all react. The placeholder's
246
+ // component reads a memo of the resolution promise, which keeps the enclosing
247
+ // navigation transition pending exactly like a `lazy()` route component; its
248
+ // `preload` *is* the resolver, so hover-intent preloading kicks the table
249
+ // load through the existing component-preload path.
250
+ const lazyBoundaries = new WeakMap();
251
+ // Module scope: boundary resolution is global, deterministic state (same
252
+ // thunk -> same routes), shared by every factory instance and the server's
253
+ // flight collector.
254
+ const [lazyTreeVersion, setLazyTreeVersion] = createSignal(0);
255
+ /** Reactive read of the lazy-subtree version — recompile compiled branches when it changes. */
256
+ export function trackLazySubtrees() {
257
+ return lazyTreeVersion();
258
+ }
259
+ /** Non-reactive read, for cache-busting outside the reactive graph (server collectors). */
260
+ export function peekLazySubtrees() {
261
+ return untrack(lazyTreeVersion);
262
+ }
263
+ function getLazyBoundary(thunk) {
264
+ let record = lazyBoundaries.get(thunk);
265
+ if (!record)
266
+ lazyBoundaries.set(thunk, (record = { thunk }));
267
+ return record;
268
+ }
269
+ /**
270
+ * Kicks (or joins) a boundary's resolution. Returns the resolved routes
271
+ * synchronously once available, the in-flight promise otherwise. Commit is
272
+ * always async — even for thunks returning arrays — so the version bump
273
+ * never writes a signal from inside a render computation.
274
+ */
275
+ export function resolveLazySubtree(record) {
276
+ if (record.resolved)
277
+ return record.resolved;
278
+ return (record.promise ||= Promise.resolve(record.thunk()).then(m => {
279
+ const routes = Array.isArray(m)
280
+ ? m
281
+ : m
282
+ .default ||
283
+ m.routes ||
284
+ [];
285
+ record.resolved = routes;
286
+ setLazyTreeVersion(v => v + 1);
287
+ return record.resolved;
288
+ }));
289
+ }
290
+ /**
291
+ * The unresolved boundaries in a match chain. Rendering gates on these (the
292
+ * route-states memo suspends until they land — see routers/components.tsx)
293
+ * and the server's flight collector awaits them before its preload pass.
294
+ */
295
+ export function unresolvedLazyMatches(matches) {
296
+ const pending = [];
297
+ for (const match of matches)
298
+ if (match.route.lazy && !match.route.lazy.resolved)
299
+ pending.push(match.route.lazy);
300
+ return pending;
301
+ }
302
+ function createLazyPlaceholder(pattern, record) {
303
+ // The placeholder never renders and needs no component — `matches` parks
304
+ // on unresolved boundaries before route contexts are created (kicking the
305
+ // resolver as it does), preloadRoute kicks it directly, and the version
306
+ // bump swaps in the real routes. `path + "/*"` with no splat name matches
307
+ // the boundary itself and everything beneath it without recording a param
308
+ // (createMatcher skips empty splat names).
309
+ const placeholderPattern = pattern + "/*";
310
+ return {
311
+ key: record,
312
+ originalPath: "*",
313
+ pattern: placeholderPattern,
314
+ matcher: createMatcher(placeholderPattern),
315
+ lazy: record
316
+ };
317
+ }
234
318
  export function createRoutes(routeDef, base = "") {
235
319
  const { component, preload, children, info } = routeDef;
236
320
  const isLeaf = !children || (Array.isArray(children) && !children.length);
@@ -284,16 +368,6 @@ export function createBranch(routes, index = 0) {
284
368
  function asArray(value) {
285
369
  return Array.isArray(value) ? value : [value];
286
370
  }
287
- /**
288
- * Resolves the route tree an app hands the router — JSX `<Route>` children
289
- * (`Route` is a data-holder component, evaluating to its merged props),
290
- * config objects, arrays, or thunks producing either — into route
291
- * definitions. Shared by the `<Router>` component (which keeps the
292
- * resolution live) and the server integration's preload runner (one-shot).
293
- */
294
- export function resolveRouteDefinitions(routes) {
295
- return children(() => routes);
296
- }
297
371
  export function createBranches(routeDef, base = "", stack = [], branches = []) {
298
372
  const routeDefs = asArray(routeDef);
299
373
  for (let i = 0, len = routeDefs.length; i < len; i++) {
@@ -304,9 +378,24 @@ export function createBranches(routeDef, base = "", stack = [], branches = []) {
304
378
  const routes = createRoutes(def, base);
305
379
  for (const route of routes) {
306
380
  stack.push(route);
307
- const isEmptyArray = Array.isArray(def.children) && def.children.length === 0;
308
- if (def.children && !isEmptyArray) {
309
- createBranches(def.children, route.pattern, stack, branches);
381
+ let children = def.children;
382
+ if (typeof children === "function") {
383
+ const record = getLazyBoundary(children);
384
+ if (record.resolved) {
385
+ children = record.resolved;
386
+ }
387
+ else {
388
+ // unresolved boundary: a catch-all placeholder holds its ground
389
+ stack.push(createLazyPlaceholder(route.pattern, record));
390
+ branches.push(createBranch([...stack], branches.length));
391
+ stack.pop();
392
+ stack.pop();
393
+ continue;
394
+ }
395
+ }
396
+ const isEmptyArray = Array.isArray(children) && children.length === 0;
397
+ if (children && !isEmptyArray) {
398
+ createBranches(children, route.pattern, stack, branches);
310
399
  }
311
400
  else {
312
401
  const branch = createBranch([...stack], branches.length);
@@ -375,6 +464,49 @@ function createLocation(path, state, queryWrapper) {
375
464
  query: queryWrapper ? queryWrapper(queryFn) : createMemoObject(queryFn)
376
465
  };
377
466
  }
467
+ /**
468
+ * Rendezvous between the router and the data layer's single-flight consumer.
469
+ * The Router registers itself at mount (unless `singleFlight={false}`); the
470
+ * action side provides the consumer factory when the first action is created
471
+ * (see data/action.ts). Whichever side arrives first waits for the other, so
472
+ * an action module loaded lazily (a code-split route) still attaches to the
473
+ * already-mounted router — and a router-only app, where no action ever
474
+ * loads, never subscribes to the transport, so the server is never asked to
475
+ * collect.
476
+ */
477
+ let flightConsumerFactory;
478
+ const flightRouters = new Map();
479
+ export function registerFlightRouter(router) {
480
+ flightRouters.set(router, flightConsumerFactory && flightConsumerFactory(router));
481
+ return () => {
482
+ const unsubscribe = flightRouters.get(router);
483
+ flightRouters.delete(router);
484
+ unsubscribe && unsubscribe();
485
+ };
486
+ }
487
+ export function provideFlightConsumer(factory) {
488
+ if (flightConsumerFactory)
489
+ return;
490
+ flightConsumerFactory = factory;
491
+ for (const [router, unsubscribe] of flightRouters) {
492
+ if (!unsubscribe)
493
+ flightRouters.set(router, factory(router));
494
+ }
495
+ }
496
+ /**
497
+ * The flash-cookie codec, provided by the action side (data/action.ts) so
498
+ * the router core never carries it: the core consumes the cookie eagerly
499
+ * per request (detection + one-shot clear via the tiny flashCookie.ts half)
500
+ * but defers decoding to this slot, read when the submissions signal
501
+ * initializes. Actions are created at module scope, so on the server the
502
+ * decoder is always installed before useSubmission can read — and a
503
+ * router-only app, where it never installs, has no actions that could have
504
+ * produced a flash cookie in the first place.
505
+ */
506
+ let flashDecoder;
507
+ export function provideFlashDecoder(decoder) {
508
+ flashDecoder || (flashDecoder = decoder);
509
+ }
378
510
  let intent;
379
511
  export function getIntent() {
380
512
  return intent;
@@ -390,7 +522,8 @@ export function createRouterContext(integration, branches, getContext, options =
390
522
  const { signal: [source, setSource], utils = {} } = integration;
391
523
  const parsePath = utils.parsePath || (p => p);
392
524
  const renderPath = utils.renderPath || (p => p);
393
- const beforeLeave = utils.beforeLeave || createBeforeLeave();
525
+ // An empty slot until `useBeforeLeave` installs the guard on first use.
526
+ const beforeLeave = utils.beforeLeave || {};
394
527
  const basePath = resolvePath("", options.base || "");
395
528
  const initialSource = untrack(source);
396
529
  if (basePath === undefined) {
@@ -411,14 +544,42 @@ export function createRouterContext(integration, branches, getContext, options =
411
544
  const effective = createMemo(() => navigateTarget() ?? source());
412
545
  const location = createLocation(() => effective().value, () => effective().state, utils.queryWrapper);
413
546
  const referrers = [];
414
- const submissions = createSignal(isServer ? initFromFlash() : [], {
415
- ownedWrite: true
416
- });
417
- const matches = createMemo(() => {
418
- if (typeof options.transformUrl === "function") {
419
- return getRouteMatches(branches(), options.transformUrl(location.pathname));
547
+ // The flash cookie is consumed eagerly: its one-shot clear (Set-Cookie)
548
+ // must be appended before streaming flushes the response headers, and an
549
+ // unread outcome must not haunt a later request's render. Only detection
550
+ // and clearing happen here (the tiny flashCookie.ts half); the raw header
551
+ // is stashed and decoding waits for the action-provided codec, read when
552
+ // the lazily allocated submissions signal below first initializes.
553
+ let flashCookieHeader;
554
+ if (isServer) {
555
+ const e = getRequestEvent();
556
+ if (e && !(e.router && e.router.submission)) {
557
+ const cookieHeader = e.request.headers.get("cookie");
558
+ if (hasFlashCookie(cookieHeader)) {
559
+ flashCookieHeader = cookieHeader;
560
+ // one-shot: clear it even when unreadable so it can't haunt later renders
561
+ if (e.response && e.response.headers)
562
+ e.response.headers.append("Set-Cookie", clearFlashCookie());
563
+ }
420
564
  }
421
- return getRouteMatches(branches(), location.pathname);
565
+ }
566
+ let submissions;
567
+ const matches = createMemo(() => {
568
+ const pathname = typeof options.transformUrl === "function"
569
+ ? options.transformUrl(location.pathname)
570
+ : location.pathname;
571
+ const m = getRouteMatches(branches(), pathname);
572
+ // An unresolved lazy subtree parks readers on not-ready semantics — the
573
+ // navigation transition (or the SSR stream) holds until the table lands.
574
+ // NotReadyError (not a returned promise) because a match chain is full
575
+ // of component functions the hydration serializer must never see. The
576
+ // recompute comes from the version-signal dependency on the client and
577
+ // from the carried promise's retry on the server; a boundary nested
578
+ // inside a boundary just parks the recomputed chain again.
579
+ const pending = unresolvedLazyMatches(m);
580
+ if (pending.length)
581
+ throw new NotReadyError(Promise.all(pending.map(resolveLazySubtree)));
582
+ return m;
422
583
  });
423
584
  const buildParams = () => mergeParams(matches());
424
585
  const wrapParams = utils.paramsWrapper
@@ -450,7 +611,9 @@ export function createRouterContext(integration, branches, getContext, options =
450
611
  beforeLeave,
451
612
  preloadRoute,
452
613
  singleFlight: options.singleFlight === undefined ? true : options.singleFlight,
453
- submissions
614
+ get submissions() {
615
+ return (submissions ||= createSignal(isServer ? initSubmissions() : [], { ownedWrite: true }));
616
+ }
454
617
  };
455
618
  function navigateFromRoute(route, to, options) {
456
619
  // Untrack in case someone navigates in an effect - don't want to track `reference` or route paths
@@ -467,6 +630,9 @@ export function createRouterContext(integration, branches, getContext, options =
467
630
  }
468
631
  return;
469
632
  }
633
+ // typed path proxy nodes coerce to their href
634
+ if (typeof to !== "string")
635
+ to = to.toString();
470
636
  const queryOnly = !to || to[0] === "?";
471
637
  const { replace, resolve, scroll, state: nextState } = {
472
638
  replace: false,
@@ -490,18 +656,21 @@ export function createRouterContext(integration, branches, getContext, options =
490
656
  e && (e.response = { status: 302, headers: new Headers({ Location: resolvedTo }) });
491
657
  setSource({ value: resolvedTo, replace, scroll, state: nextState });
492
658
  }
493
- else if (beforeLeave.confirm(resolvedTo, options)) {
659
+ else if (!beforeLeave.current || beforeLeave.current.confirm(resolvedTo, options)) {
494
660
  referrers.push({ value: current.value, replace, scroll, state: current.state });
495
661
  const newTarget = {
496
662
  value: resolvedTo,
497
663
  state: nextState
498
664
  };
499
- if (lastTransitionTarget === undefined) {
665
+ const firstNavigation = lastTransitionTarget === undefined;
666
+ intent = "navigate";
667
+ // assign the target before flushing so effects that run for the
668
+ // isRouting flip (e.g. pending link state) can read it
669
+ lastTransitionTarget = newTarget;
670
+ if (firstNavigation) {
500
671
  setIsRouting(true);
501
672
  flush();
502
673
  }
503
- intent = "navigate";
504
- lastTransitionTarget = newTarget;
505
674
  if (lastTransitionTarget === newTarget) {
506
675
  setNavigateTarget({ ...lastTransitionTarget });
507
676
  queueMicrotask(() => {
@@ -536,6 +705,12 @@ export function createRouterContext(integration, branches, getContext, options =
536
705
  }
537
706
  function preloadRoute(url, preloadData) {
538
707
  const matches = getRouteMatches(branches(), url.pathname);
708
+ // An unresolved lazy subtree in the chain: the placeholder's
709
+ // component.preload (below) kicks the table load; once it lands,
710
+ // preload again so the real inner routes warm too.
711
+ const boundary = matches.find(m => m.route.lazy && !m.route.lazy.resolved);
712
+ boundary &&
713
+ resolveLazySubtree(boundary.route.lazy).then(() => preloadRoute(url, preloadData));
539
714
  const prevIntent = intent;
540
715
  intent = "preload";
541
716
  for (let match in matches) {
@@ -565,22 +740,16 @@ export function createRouterContext(integration, branches, getContext, options =
565
740
  }
566
741
  // Seeds the initial submission from a no-JS form post: the server
567
742
  // function handler redirected back with the outcome in a one-shot flash
568
- // cookie (see src/server.ts's handleNoJS), which is read here — and
569
- // cleared — so the post-redirect SSR renders useSubmission() state
743
+ // cookie (see src/server.ts's handleNoJS), consumed eagerly above and
744
+ // decoded here — so the post-redirect SSR renders useSubmission() state
570
745
  // exactly as a scripted submission would. An explicitly pre-seeded
571
746
  // `event.router.submission` (framework integrations) takes precedence.
572
- function initFromFlash() {
747
+ function initSubmissions() {
573
748
  const e = getRequestEvent();
574
- if (!e)
575
- return [];
576
- let submission = e.router && e.router.submission;
577
- if (!submission) {
578
- const cookieHeader = e.request.headers.get("cookie");
579
- submission = decodeFlashCookie(cookieHeader);
580
- // one-shot: clear it even when unreadable so it can't haunt later renders
581
- if (hasFlashCookie(cookieHeader) && e.response && e.response.headers)
582
- e.response.headers.append("Set-Cookie", clearFlashCookie());
583
- }
749
+ const submission = (e && e.router && e.router.submission) ||
750
+ (flashDecoder && flashCookieHeader !== undefined
751
+ ? flashDecoder(flashCookieHeader)
752
+ : undefined);
584
753
  if (!submission)
585
754
  return [];
586
755
  return [
package/dist/server.d.ts CHANGED
@@ -5,30 +5,40 @@ export type { FlashSubmission } from "./data/flash.js";
5
5
  export type { CollectFlightDataHook, ServerFunctionOutcome };
6
6
  export interface FlightDataCollectorOptions {
7
7
  /**
8
- * The app's route tree — the same thing the `<Router>` receives as
9
- * children: JSX `<Route>` trees, config objects, arrays, or a thunk
10
- * producing either (for lazily/per-request built trees). Normalized to
11
- * route definitions the same way the `<Router>` normalizes its children
12
- * (on the server `Route` evaluates to its props, so no render pass is
13
- * needed). Flight data is produced by the pure preload runner: the target
14
- * URL is matched against the tree and the matched routes' `preload`
15
- * functions run in data-only mode.
8
+ * The app's route tree — the same config objects the `createRouter`
9
+ * factory receives, an array of them, or a thunk producing either (for
10
+ * lazily/per-request built trees). Flight data is produced by the pure
11
+ * preload runner: the target URL is matched against the tree and the
12
+ * matched routes' `preload` functions run in data-only mode.
16
13
  */
17
- routes: JSX.Element | RouteDefinition | RouteDefinition[] | (() => JSX.Element | RouteDefinition | RouteDefinition[]);
14
+ routes: RouteDefinition | readonly RouteDefinition[] | (() => RouteDefinition | readonly RouteDefinition[]);
18
15
  /**
19
- * The root layout's preload — the same function the app passes to
20
- * `<Router rootPreload>`. Runs before the matched routes' preloads with
21
- * the semantics the root gets during a real server render: the merged
22
- * params of every match and `intent: "initial"`.
16
+ * The root layout's preload — the same function the app passes to the
17
+ * `createRouter` factory's `preload` option. Runs before the matched
18
+ * routes' preloads with the semantics the root gets during a real server
19
+ * render: the merged params of every match and `intent: "initial"`.
23
20
  */
24
21
  rootPreload?: RoutePreloadFunc;
25
22
  /** The app's base path, for resolving redirect `Location`s and matching. */
26
23
  base?: string;
27
24
  }
25
+ /** A `createRouter` instance carries everything the collector needs. */
26
+ interface RouterInstanceLike {
27
+ (props: any): JSX.Element;
28
+ readonly routes: readonly RouteDefinition[];
29
+ readonly config: {
30
+ base?: string;
31
+ preload?: RoutePreloadFunc;
32
+ };
33
+ }
28
34
  /**
29
35
  * Produces the `collectFlightData` implementation for
30
36
  * `configureServerFunctionsServer` (or `handleServerFunctionRequest`
31
- * options). Strategy: rerun the route data for the URL the client will show
37
+ * options). Accepts a `createRouter` instance directly — its routes, base,
38
+ * and `preload` are the single source of truth — or an options object for
39
+ * trees not created through the factory.
40
+ *
41
+ * Strategy: rerun the route data for the URL the client will show
32
42
  * after the mutation — the redirect `Location` when the outcome carries
33
43
  * one, the referring page otherwise — collecting each `query` result under
34
44
  * its cache key, scoped to the outcome's `X-Revalidate` keys when present
@@ -36,7 +46,7 @@ export interface FlightDataCollectorOptions {
36
46
  * payload seeds the client router's cache through its registered
37
47
  * flight-data consumer.
38
48
  */
39
- export declare function createFlightDataCollector(options: FlightDataCollectorOptions): CollectFlightDataHook;
49
+ export declare function createFlightDataCollector(options: FlightDataCollectorOptions | RouterInstanceLike): CollectFlightDataHook;
40
50
  /**
41
51
  * The request headers for the flight-data collection pass: the source
42
52
  * request's headers with the mutation's `Set-Cookie` mutations folded into