@solidjs/router 0.17.0-next.5 → 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.
- package/README.md +526 -716
- package/dist/claims.d.ts +21 -0
- package/dist/claims.js +108 -0
- package/dist/data/action.d.ts +28 -1
- package/dist/data/action.js +184 -43
- package/dist/data/events.d.ts +8 -0
- package/dist/data/events.js +23 -22
- package/dist/data/flash.d.ts +21 -0
- package/dist/data/flash.js +78 -0
- package/dist/data/flashCookie.d.ts +7 -0
- package/dist/data/flashCookie.js +20 -0
- package/dist/data/query.js +11 -4
- package/dist/data/serverForms.d.ts +1 -0
- package/dist/data/serverForms.js +5 -0
- package/dist/index.d.ts +5 -3
- package/dist/index.js +1381 -686
- package/dist/index.jsx +2 -2
- package/dist/lifecycle.d.ts +29 -4
- package/dist/lifecycle.js +40 -37
- package/dist/paths.d.ts +117 -0
- package/dist/paths.js +41 -0
- package/dist/routers/components.d.ts +10 -21
- package/dist/routers/components.jsx +29 -44
- package/dist/routers/factory.d.ts +45 -0
- package/dist/routers/factory.jsx +143 -0
- package/dist/routers/history.d.ts +24 -0
- package/dist/routers/history.js +180 -0
- package/dist/routers/index.d.ts +4 -11
- package/dist/routers/index.js +2 -6
- package/dist/routing.d.ts +80 -44
- package/dist/routing.js +303 -107
- package/dist/server.d.ts +75 -0
- package/dist/server.js +264 -0
- package/dist/types.d.ts +75 -6
- package/dist/utils.d.ts +2 -0
- package/dist/utils.js +2 -0
- package/package.json +10 -8
- package/dist/components.d.ts +0 -31
- package/dist/components.jsx +0 -46
- package/dist/routers/HashRouter.d.ts +0 -9
- package/dist/routers/HashRouter.js +0 -41
- package/dist/routers/MemoryRouter.d.ts +0 -24
- package/dist/routers/MemoryRouter.js +0 -57
- package/dist/routers/Router.d.ts +0 -9
- package/dist/routers/Router.js +0 -45
- package/dist/routers/StaticRouter.d.ts +0 -6
- package/dist/routers/StaticRouter.js +0 -15
- package/dist/routers/createRouter.d.ts +0 -10
- package/dist/routers/createRouter.js +0 -40
package/dist/routing.js
CHANGED
|
@@ -1,13 +1,13 @@
|
|
|
1
1
|
import { flush, runWithOwner } from "solid-js";
|
|
2
|
-
import { createComponent, createContext, createMemo, createSignal,
|
|
2
|
+
import { createComponent, createContext, createMemo, createSignal, NotReadyError, untrack, useContext } from "solid-js";
|
|
3
3
|
import { isServer, getRequestEvent } from "@solidjs/web";
|
|
4
|
-
import {
|
|
5
|
-
import {
|
|
4
|
+
import { mockBase, comparablePath, createMemoObject, extractSearchParams, invariant, resolvePath, createMatcher, joinPaths, scoreRoute, mergeSearchString, expandOptionals } from "./utils.js";
|
|
5
|
+
import { clearFlashCookie, hasFlashCookie } from "./data/flashCookie.js";
|
|
6
6
|
const MAX_REDIRECTS = 100;
|
|
7
7
|
/** Consider this API opaque and internal. It is likely to change in the future. */
|
|
8
8
|
export const RouterContextObj = createContext();
|
|
9
9
|
export const RouteContextObj = createContext();
|
|
10
|
-
function useOptionalContext(context) {
|
|
10
|
+
export function useOptionalContext(context) {
|
|
11
11
|
try {
|
|
12
12
|
return useContext(context);
|
|
13
13
|
}
|
|
@@ -76,21 +76,6 @@ export const useLocation = () => useRouter().location;
|
|
|
76
76
|
* ```
|
|
77
77
|
*/
|
|
78
78
|
export const useIsRouting = () => useRouter().isRouting;
|
|
79
|
-
/**
|
|
80
|
-
* usePreloadRoute returns a function that can be used to preload a route manual.
|
|
81
|
-
* This is what happens automatically with link hovering and similar focus based behavior, but it is available here as an API.
|
|
82
|
-
*
|
|
83
|
-
* @example
|
|
84
|
-
* ```js
|
|
85
|
-
* const preload = usePreloadRoute();
|
|
86
|
-
*
|
|
87
|
-
* preload(`/users/settings`, { preloadData: true });
|
|
88
|
-
* ```
|
|
89
|
-
*/
|
|
90
|
-
export const usePreloadRoute = () => {
|
|
91
|
-
const pre = useRouter().preloadRoute;
|
|
92
|
-
return (url, options = {}) => pre(url instanceof URL ? url : new URL(url, mockBase), options.preloadData);
|
|
93
|
-
};
|
|
94
79
|
/**
|
|
95
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.
|
|
96
81
|
* Useful for determining if a given path matches the current route.
|
|
@@ -114,61 +99,44 @@ export const useMatch = (path, matchFilters) => {
|
|
|
114
99
|
});
|
|
115
100
|
};
|
|
116
101
|
/**
|
|
117
|
-
* `
|
|
118
|
-
*
|
|
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.
|
|
119
107
|
*
|
|
120
108
|
* @example
|
|
121
109
|
* ```js
|
|
122
|
-
* const matches =
|
|
110
|
+
* const matches = useRouteMatches();
|
|
123
111
|
*
|
|
124
|
-
* const breadcrumbs = createMemo(() => matches().map(m => m.route.info.breadcrumb))
|
|
112
|
+
* const breadcrumbs = createMemo(() => matches().map(m => m.route.info.breadcrumb));
|
|
125
113
|
* ```
|
|
126
114
|
*/
|
|
127
|
-
export const
|
|
115
|
+
export const useRouteMatches = () => {
|
|
128
116
|
const router = useRouter();
|
|
129
117
|
// return a copy so user mutations (eg. `.reverse()`) can't corrupt router state
|
|
130
118
|
return () => router.matches().slice();
|
|
131
119
|
};
|
|
132
120
|
/**
|
|
133
|
-
*
|
|
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.
|
|
134
124
|
*
|
|
135
125
|
* @example
|
|
136
126
|
* ```js
|
|
137
|
-
* const
|
|
138
|
-
*
|
|
139
|
-
* // fetch user based on the id path parameter
|
|
140
|
-
* const getUser = query(() => fetchUser(params.id), "user");
|
|
141
|
-
* ```
|
|
142
|
-
*/
|
|
143
|
-
export const useParams = () => useRoute().params;
|
|
144
|
-
/**
|
|
145
|
-
* Retrieves a tuple containing a reactive object to read the current location's query parameters and a method to update them.
|
|
146
|
-
* The object is a proxy so you must access properties to subscribe to reactive updates.
|
|
147
|
-
* **Note** that values will be strings and property names will retain their casing.
|
|
148
|
-
*
|
|
149
|
-
* The setter method accepts an object whose entries will be merged into the current query string.
|
|
150
|
-
* Values `''`, `undefined` and `null` will remove the key from the resulting query string.
|
|
151
|
-
* 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.
|
|
152
|
-
*
|
|
153
|
-
* @examples
|
|
154
|
-
* ```js
|
|
155
|
-
* const [searchParams, setSearchParams] = useSearchParams();
|
|
127
|
+
* const preload = usePreloadRoute();
|
|
156
128
|
*
|
|
157
|
-
*
|
|
158
|
-
* <div>
|
|
159
|
-
* <span>Page: {searchParams.page}</span>
|
|
160
|
-
* <button
|
|
161
|
-
* onClick={() =>
|
|
162
|
-
* setSearchParams({ page: (parseInt(searchParams.page) || 0) + 1 })
|
|
163
|
-
* }
|
|
164
|
-
* >
|
|
165
|
-
* Next Page
|
|
166
|
-
* </button>
|
|
167
|
-
* </div>
|
|
168
|
-
* );
|
|
129
|
+
* preload(paths.users(2).settings, { preloadData: true });
|
|
169
130
|
* ```
|
|
170
131
|
*/
|
|
171
|
-
export const
|
|
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) {
|
|
172
140
|
const router = useRouter();
|
|
173
141
|
const location = router.location;
|
|
174
142
|
const navigate = useNavigate();
|
|
@@ -188,48 +156,165 @@ export const useSearchParams = () => {
|
|
|
188
156
|
...options
|
|
189
157
|
});
|
|
190
158
|
};
|
|
191
|
-
|
|
192
|
-
|
|
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
|
+
}
|
|
193
183
|
/**
|
|
194
|
-
*
|
|
195
|
-
*
|
|
196
|
-
*
|
|
197
|
-
* - from (*Location*): current location (before change).
|
|
198
|
-
* - to (*string | number*): path passed to `navigate`.
|
|
199
|
-
* - options (*NavigateOptions*): options passed to navigate.
|
|
200
|
-
* - preventDefault (*function*): call to block the route change.
|
|
201
|
-
* - defaultPrevented (*readonly boolean*): `true` if any previously called leave handlers called `preventDefault`.
|
|
202
|
-
* - 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`).
|
|
203
187
|
*
|
|
204
188
|
* @example
|
|
205
|
-
* ```
|
|
206
|
-
*
|
|
207
|
-
*
|
|
208
|
-
*
|
|
209
|
-
*
|
|
210
|
-
*
|
|
211
|
-
*
|
|
212
|
-
*
|
|
213
|
-
*
|
|
214
|
-
* }
|
|
215
|
-
* }, 100);
|
|
216
|
-
* }
|
|
217
|
-
* });
|
|
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
|
+
* }
|
|
218
198
|
* ```
|
|
219
199
|
*/
|
|
220
|
-
export const
|
|
221
|
-
const
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
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_);
|
|
225
208
|
});
|
|
226
|
-
|
|
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
|
+
};
|
|
227
230
|
};
|
|
228
231
|
// Encodes a static path segment like `encodeURIComponent`, but leaves RFC 3986
|
|
229
232
|
// pchar characters (sub-delims / ":" / "@") literal, matching how browsers
|
|
230
233
|
// report them in `location.pathname`. Non-ASCII characters (eg. CJK paths) are
|
|
231
234
|
// still percent-encoded exactly as before, since browsers encode those too.
|
|
232
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
|
+
}
|
|
233
318
|
export function createRoutes(routeDef, base = "") {
|
|
234
319
|
const { component, preload, children, info } = routeDef;
|
|
235
320
|
const isLeaf = !children || (Array.isArray(children) && !children.length);
|
|
@@ -293,9 +378,24 @@ export function createBranches(routeDef, base = "", stack = [], branches = []) {
|
|
|
293
378
|
const routes = createRoutes(def, base);
|
|
294
379
|
for (const route of routes) {
|
|
295
380
|
stack.push(route);
|
|
296
|
-
|
|
297
|
-
if (
|
|
298
|
-
|
|
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);
|
|
299
399
|
}
|
|
300
400
|
else {
|
|
301
401
|
const branch = createBranch([...stack], branches.length);
|
|
@@ -317,7 +417,7 @@ export function getRouteMatches(branches, location) {
|
|
|
317
417
|
}
|
|
318
418
|
return [];
|
|
319
419
|
}
|
|
320
|
-
function mergeParams(matches) {
|
|
420
|
+
export function mergeParams(matches) {
|
|
321
421
|
const params = {};
|
|
322
422
|
for (let i = 0; i < matches.length; i++) {
|
|
323
423
|
Object.assign(params, matches[i].params);
|
|
@@ -364,6 +464,49 @@ function createLocation(path, state, queryWrapper) {
|
|
|
364
464
|
query: queryWrapper ? queryWrapper(queryFn) : createMemoObject(queryFn)
|
|
365
465
|
};
|
|
366
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
|
+
}
|
|
367
510
|
let intent;
|
|
368
511
|
export function getIntent() {
|
|
369
512
|
return intent;
|
|
@@ -379,7 +522,8 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
379
522
|
const { signal: [source, setSource], utils = {} } = integration;
|
|
380
523
|
const parsePath = utils.parsePath || (p => p);
|
|
381
524
|
const renderPath = utils.renderPath || (p => p);
|
|
382
|
-
|
|
525
|
+
// An empty slot until `useBeforeLeave` installs the guard on first use.
|
|
526
|
+
const beforeLeave = utils.beforeLeave || {};
|
|
383
527
|
const basePath = resolvePath("", options.base || "");
|
|
384
528
|
const initialSource = untrack(source);
|
|
385
529
|
if (basePath === undefined) {
|
|
@@ -400,14 +544,42 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
400
544
|
const effective = createMemo(() => navigateTarget() ?? source());
|
|
401
545
|
const location = createLocation(() => effective().value, () => effective().state, utils.queryWrapper);
|
|
402
546
|
const referrers = [];
|
|
403
|
-
|
|
404
|
-
|
|
405
|
-
|
|
406
|
-
|
|
407
|
-
|
|
408
|
-
|
|
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
|
+
}
|
|
409
564
|
}
|
|
410
|
-
|
|
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;
|
|
411
583
|
});
|
|
412
584
|
const buildParams = () => mergeParams(matches());
|
|
413
585
|
const wrapParams = utils.paramsWrapper
|
|
@@ -439,7 +611,9 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
439
611
|
beforeLeave,
|
|
440
612
|
preloadRoute,
|
|
441
613
|
singleFlight: options.singleFlight === undefined ? true : options.singleFlight,
|
|
442
|
-
submissions
|
|
614
|
+
get submissions() {
|
|
615
|
+
return (submissions ||= createSignal(isServer ? initSubmissions() : [], { ownedWrite: true }));
|
|
616
|
+
}
|
|
443
617
|
};
|
|
444
618
|
function navigateFromRoute(route, to, options) {
|
|
445
619
|
// Untrack in case someone navigates in an effect - don't want to track `reference` or route paths
|
|
@@ -456,6 +630,9 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
456
630
|
}
|
|
457
631
|
return;
|
|
458
632
|
}
|
|
633
|
+
// typed path proxy nodes coerce to their href
|
|
634
|
+
if (typeof to !== "string")
|
|
635
|
+
to = to.toString();
|
|
459
636
|
const queryOnly = !to || to[0] === "?";
|
|
460
637
|
const { replace, resolve, scroll, state: nextState } = {
|
|
461
638
|
replace: false,
|
|
@@ -479,18 +656,21 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
479
656
|
e && (e.response = { status: 302, headers: new Headers({ Location: resolvedTo }) });
|
|
480
657
|
setSource({ value: resolvedTo, replace, scroll, state: nextState });
|
|
481
658
|
}
|
|
482
|
-
else if (beforeLeave.confirm(resolvedTo, options)) {
|
|
659
|
+
else if (!beforeLeave.current || beforeLeave.current.confirm(resolvedTo, options)) {
|
|
483
660
|
referrers.push({ value: current.value, replace, scroll, state: current.state });
|
|
484
661
|
const newTarget = {
|
|
485
662
|
value: resolvedTo,
|
|
486
663
|
state: nextState
|
|
487
664
|
};
|
|
488
|
-
|
|
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) {
|
|
489
671
|
setIsRouting(true);
|
|
490
672
|
flush();
|
|
491
673
|
}
|
|
492
|
-
intent = "navigate";
|
|
493
|
-
lastTransitionTarget = newTarget;
|
|
494
674
|
if (lastTransitionTarget === newTarget) {
|
|
495
675
|
setNavigateTarget({ ...lastTransitionTarget });
|
|
496
676
|
queueMicrotask(() => {
|
|
@@ -525,6 +705,12 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
525
705
|
}
|
|
526
706
|
function preloadRoute(url, preloadData) {
|
|
527
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));
|
|
528
714
|
const prevIntent = intent;
|
|
529
715
|
intent = "preload";
|
|
530
716
|
for (let match in matches) {
|
|
@@ -552,13 +738,23 @@ export function createRouterContext(integration, branches, getContext, options =
|
|
|
552
738
|
}
|
|
553
739
|
intent = prevIntent;
|
|
554
740
|
}
|
|
555
|
-
|
|
741
|
+
// Seeds the initial submission from a no-JS form post: the server
|
|
742
|
+
// function handler redirected back with the outcome in a one-shot flash
|
|
743
|
+
// cookie (see src/server.ts's handleNoJS), consumed eagerly above and
|
|
744
|
+
// decoded here — so the post-redirect SSR renders useSubmission() state
|
|
745
|
+
// exactly as a scripted submission would. An explicitly pre-seeded
|
|
746
|
+
// `event.router.submission` (framework integrations) takes precedence.
|
|
747
|
+
function initSubmissions() {
|
|
556
748
|
const e = getRequestEvent();
|
|
557
|
-
|
|
749
|
+
const submission = (e && e.router && e.router.submission) ||
|
|
750
|
+
(flashDecoder && flashCookieHeader !== undefined
|
|
751
|
+
? flashDecoder(flashCookieHeader)
|
|
752
|
+
: undefined);
|
|
753
|
+
if (!submission)
|
|
558
754
|
return [];
|
|
559
755
|
return [
|
|
560
756
|
{
|
|
561
|
-
...
|
|
757
|
+
...submission,
|
|
562
758
|
clear() { },
|
|
563
759
|
retry() { }
|
|
564
760
|
}
|
package/dist/server.d.ts
ADDED
|
@@ -0,0 +1,75 @@
|
|
|
1
|
+
import type { JSX } from "@solidjs/web";
|
|
2
|
+
import type { CollectFlightDataHook, ServerFunctionOutcome } from "@solidjs/web/server-functions/server";
|
|
3
|
+
import type { RouteDefinition, RoutePreloadFunc } from "./types.js";
|
|
4
|
+
export type { FlashSubmission } from "./data/flash.js";
|
|
5
|
+
export type { CollectFlightDataHook, ServerFunctionOutcome };
|
|
6
|
+
export interface FlightDataCollectorOptions {
|
|
7
|
+
/**
|
|
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.
|
|
13
|
+
*/
|
|
14
|
+
routes: RouteDefinition | readonly RouteDefinition[] | (() => RouteDefinition | readonly RouteDefinition[]);
|
|
15
|
+
/**
|
|
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"`.
|
|
20
|
+
*/
|
|
21
|
+
rootPreload?: RoutePreloadFunc;
|
|
22
|
+
/** The app's base path, for resolving redirect `Location`s and matching. */
|
|
23
|
+
base?: string;
|
|
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
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Produces the `collectFlightData` implementation for
|
|
36
|
+
* `configureServerFunctionsServer` (or `handleServerFunctionRequest`
|
|
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
|
|
42
|
+
* after the mutation — the redirect `Location` when the outcome carries
|
|
43
|
+
* one, the referring page otherwise — collecting each `query` result under
|
|
44
|
+
* its cache key, scoped to the outcome's `X-Revalidate` keys when present
|
|
45
|
+
* (routes newly entered via redirect always collect fully). The returned
|
|
46
|
+
* payload seeds the client router's cache through its registered
|
|
47
|
+
* flight-data consumer.
|
|
48
|
+
*/
|
|
49
|
+
export declare function createFlightDataCollector(options: FlightDataCollectorOptions | RouterInstanceLike): CollectFlightDataHook;
|
|
50
|
+
/**
|
|
51
|
+
* The request headers for the flight-data collection pass: the source
|
|
52
|
+
* request's headers with the mutation's `Set-Cookie` mutations folded into
|
|
53
|
+
* the `Cookie` header, so preloads observe the post-mutation cookie state
|
|
54
|
+
* (deletions honored via Max-Age/Expires).
|
|
55
|
+
*/
|
|
56
|
+
export declare function createSingleFlightHeaders(sourceEvent: {
|
|
57
|
+
request: Request;
|
|
58
|
+
response?: {
|
|
59
|
+
headers?: Headers;
|
|
60
|
+
};
|
|
61
|
+
}): Headers;
|
|
62
|
+
export interface NoJSHandlerOptions {
|
|
63
|
+
/** The app's base path, for resolving redirect `Location`s. */
|
|
64
|
+
base?: string;
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Produces the `handleNoJS` implementation for
|
|
68
|
+
* `handleServerFunctionRequest` options: form posts made without the client
|
|
69
|
+
* runtime redirect back to the referring page (or to the result's
|
|
70
|
+
* `Location`) with the outcome riding a one-shot `flash` cookie. The
|
|
71
|
+
* router's SSR initialization reads the cookie into submission state, so
|
|
72
|
+
* `useSubmission()` renders the result exactly as a scripted submission
|
|
73
|
+
* would — progressive enhancement with no app code.
|
|
74
|
+
*/
|
|
75
|
+
export declare function createNoJSHandler(options?: NoJSHandlerOptions): (result: unknown, request: Request, args: unknown[], thrown?: boolean) => Response;
|