@solidjs/router 2.0.0-next.26 → 2.0.0-next.28
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 +168 -70
- package/dist/data/liveQuery.d.ts +5 -5
- package/dist/data/liveQuery.js +13 -11
- package/dist/fs.d.ts +7 -1
- package/dist/fs.js +29 -5
- package/dist/fsServer.d.ts +15 -0
- package/dist/fsServer.js +10 -0
- package/dist/index.d.ts +2 -1
- package/dist/index.js +247 -38
- package/dist/index.jsx +1 -0
- package/dist/routers/components.jsx +4 -0
- package/dist/routers/factory.d.ts +10 -1
- package/dist/routers/factory.jsx +14 -2
- package/dist/routing.js +65 -26
- package/dist/server.js +6 -0
- package/dist/serverRouteComponent.d.ts +20 -0
- package/dist/serverRouteComponent.js +90 -0
- package/dist/serverRouteShared.d.ts +33 -0
- package/dist/serverRouteShared.js +39 -0
- package/dist/types.d.ts +41 -4
- package/dist/utils.d.ts +3 -1
- package/dist/utils.js +7 -0
- package/package.json +8 -2
package/README.md
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
|
|
11
11
|
</div>
|
|
12
12
|
|
|
13
|
-
**Solid Router** brings fine-grained reactivity to route navigation. Routes are config objects — the single source of truth for matching
|
|
13
|
+
**Solid Router** brings fine-grained reactivity to route navigation. Routes are config objects — the single source of truth for matching _and_ types — and the router upgrades HTML's own interaction verbs instead of wrapping them: `<a href={path}>` and `<form action={action}>` carry typed, URL-addressable values on real platform elements, intercepted by delegation, decorated with a shared attribute vocabulary, and fully functional without JavaScript.
|
|
14
14
|
|
|
15
15
|
Explore the official [documentation](https://docs.solidjs.com/solid-router) for detailed guides and examples.
|
|
16
16
|
|
|
@@ -36,6 +36,7 @@ Explore the official [documentation](https://docs.solidjs.com/solid-router) for
|
|
|
36
36
|
- [Multiple Paths](#multiple-paths)
|
|
37
37
|
- [Nested Routes](#nested-routes)
|
|
38
38
|
- [Lazy Route Subtrees](#lazy-route-subtrees)
|
|
39
|
+
- [Server Component Routes (experimental)](#server-component-routes-experimental)
|
|
39
40
|
- [File-System Routes](#file-system-routes)
|
|
40
41
|
- [Typed Paths](#typed-paths)
|
|
41
42
|
- [Links](#links)
|
|
@@ -89,7 +90,13 @@ When a tree is composed across files (feature subtrees), wrap the extracted arra
|
|
|
89
90
|
```tsx
|
|
90
91
|
// features/admin/routes.ts
|
|
91
92
|
export const adminRoutes = defineRoutes([
|
|
92
|
-
{
|
|
93
|
+
{
|
|
94
|
+
path: "/admin",
|
|
95
|
+
component: Admin,
|
|
96
|
+
children: [
|
|
97
|
+
/* ... */
|
|
98
|
+
]
|
|
99
|
+
}
|
|
93
100
|
]);
|
|
94
101
|
|
|
95
102
|
// app/router.ts
|
|
@@ -138,19 +145,19 @@ The API splits across two surfaces, and the line between them is precise: **coul
|
|
|
138
145
|
|
|
139
146
|
The instance is shared — one module-level object serving every mount, every request, every test. It is deliberately non-stateful (on the server there are many "current locations" at once), so it carries only the app's static routing vocabulary. Hooks read the live session from context.
|
|
140
147
|
|
|
141
|
-
| Instance — facts about the
|
|
142
|
-
|
|
|
143
|
-
| `paths` — how to spell URLs
|
|
144
|
-
| `match(url)` — how would a URL match | `useNavigate`, `usePreloadRoute` — move / warm
|
|
145
|
-
| `routes`, `config` — what exists
|
|
148
|
+
| Instance — facts about the _app_ | Hooks — facts about the _session_ |
|
|
149
|
+
| ------------------------------------ | ----------------------------------------------------------------- |
|
|
150
|
+
| `paths` — how to spell URLs | `useLocation`, `useParams` — where am I |
|
|
151
|
+
| `match(url)` — how would a URL match | `useNavigate`, `usePreloadRoute` — move / warm |
|
|
152
|
+
| `routes`, `config` — what exists | `useIsRouting`, `useRouteMatches`, `useSearchParams` — live state |
|
|
146
153
|
|
|
147
154
|
They compose as noun and verb — the instance supplies a typed URL, the hook acts on the current session:
|
|
148
155
|
|
|
149
156
|
```tsx
|
|
150
157
|
const navigate = useNavigate();
|
|
151
|
-
navigate(paths.users(2));
|
|
158
|
+
navigate(paths.users(2)); // verb(noun)
|
|
152
159
|
|
|
153
|
-
const params = useParams(paths.users);
|
|
160
|
+
const params = useParams(paths.users); // hook, typed by the instance
|
|
154
161
|
```
|
|
155
162
|
|
|
156
163
|
**Hooks are the default; import the router only when you need typed URLs or matching outside a render.** Components that only read their session (params, location, string-path navigation) never need the instance — which also means component files don't form import cycles with the router module that references them in its config.
|
|
@@ -159,17 +166,17 @@ const params = useParams(paths.users); // hook, typed by the instance
|
|
|
159
166
|
|
|
160
167
|
A route definition supports:
|
|
161
168
|
|
|
162
|
-
| key | type
|
|
163
|
-
| -------------- |
|
|
164
|
-
| `path` | `string \| string[]`
|
|
165
|
-
| `component` | `Component`
|
|
169
|
+
| key | type | description |
|
|
170
|
+
| -------------- | ------------------------------------------------------------ | ------------------------------------------------------------------------------- |
|
|
171
|
+
| `path` | `string \| string[]` | Path partial for this route segment |
|
|
172
|
+
| `component` | `Component` | Component rendered for the matched segment |
|
|
166
173
|
| `children` | `RouteDefinition \| RouteDefinition[] \| () => Promise<...>` | Nested route definitions, or a thunk for a [lazy subtree](#lazy-route-subtrees) |
|
|
167
|
-
| `preload` | `RoutePreloadFunc`
|
|
168
|
-
| `matchFilters` | `MatchFilters`
|
|
169
|
-
| `search` | `StandardSchemaV1`
|
|
170
|
-
| `info` | `Record<string, any>`
|
|
174
|
+
| `preload` | `RoutePreloadFunc` | Called on preload intent (hover/focus) and navigation |
|
|
175
|
+
| `matchFilters` | `MatchFilters` | Additional constraints for matching parameters |
|
|
176
|
+
| `search` | `StandardSchemaV1` | Search-param validator; its types flow into `paths` and hooks |
|
|
177
|
+
| `info` | `Record<string, any>` | Arbitrary metadata, readable via `useRouteMatches` |
|
|
171
178
|
|
|
172
|
-
The tree is **immutable and there is one router per app** — that's what makes `paths` and the typed hooks truthful, it lets matching compile once and be shared by every mount, request, and `match()` call, and it means delegation, link state, and preloading all have a single owner. Compose large apps by spreading subtrees into the config (see `defineRoutes` above); mounting a router inside another router is not supported (nested `<Routes>` has been gone since 0.10) and warns in development. Sections whose
|
|
179
|
+
The tree is **immutable and there is one router per app** — that's what makes `paths` and the typed hooks truthful, it lets matching compile once and be shared by every mount, request, and `match()` call, and it means delegation, link state, and preloading all have a single owner. Compose large apps by spreading subtrees into the config (see `defineRoutes` above); mounting a router inside another router is not supported (nested `<Routes>` has been gone since 0.10) and warns in development. Sections whose _code_ shouldn't load up front are [lazy route subtrees](#lazy-route-subtrees) — still one tree, still typed.
|
|
173
180
|
|
|
174
181
|
### Dynamic Routes
|
|
175
182
|
|
|
@@ -204,8 +211,8 @@ const story = defineRoute({
|
|
|
204
211
|
preload: ({ params }) => getStory(params.id), // params.id: string
|
|
205
212
|
component: props => (
|
|
206
213
|
<Story
|
|
207
|
-
id={props.params.id}
|
|
208
|
-
tab={props.params.tab}
|
|
214
|
+
id={props.params.id} // string — the pattern guarantees it
|
|
215
|
+
tab={props.params.tab} // string | undefined — optional param
|
|
209
216
|
/>
|
|
210
217
|
)
|
|
211
218
|
});
|
|
@@ -242,8 +249,8 @@ Each parameter can be validated with a `MatchFilter` — an enum array, a regex,
|
|
|
242
249
|
import { int, type MatchFilters } from "@solidjs/router";
|
|
243
250
|
|
|
244
251
|
const filters: MatchFilters = {
|
|
245
|
-
parent: ["mom", "dad"],
|
|
246
|
-
id: /^\d+$/,
|
|
252
|
+
parent: ["mom", "dad"], // enum values
|
|
253
|
+
id: /^\d+$/, // only numbers
|
|
247
254
|
withHtmlExtension: (v: string) => v.length > 5 && v.endsWith(".html")
|
|
248
255
|
};
|
|
249
256
|
|
|
@@ -254,7 +261,7 @@ const routes = defineRoutes([
|
|
|
254
261
|
|
|
255
262
|
So `/users/mom/123/contact.html` matches, while `/users/aunt/123/contact.html` (invalid `parent`) and `/users/mom/me/contact.html` (non-numeric `id`) don't.
|
|
256
263
|
|
|
257
|
-
The built-in `int` filter is
|
|
264
|
+
The built-in `int` filter is _typed_: it constrains matching to integers at runtime and types the param as `number` at `paths` callsites:
|
|
258
265
|
|
|
259
266
|
```tsx
|
|
260
267
|
{ path: "/users/:id", matchFilters: { id: int }, component: User }
|
|
@@ -362,6 +369,64 @@ The import only fires when something needs the subtree — hovering a link into
|
|
|
362
369
|
|
|
363
370
|
Resolution is cached per thunk and append-only: the tree never changes shape after a subtree lands, it just gets more specific. Keep thunks deterministic — `() => import(...)` — rather than switching tables on runtime state.
|
|
364
371
|
|
|
372
|
+
### Server Component Routes (experimental)
|
|
373
|
+
|
|
374
|
+
Solid's experimental [server components](https://github.com/solidjs/solid/blob/main/documentation/solid-2.0/11-server-components.md) are `"use server"` functions that return a component. `serverRouteComponent` takes a `query` over one and uses it as a route's `component` directly — the router owns the URL → call translation a client wrapper used to spell out:
|
|
375
|
+
|
|
376
|
+
```tsx
|
|
377
|
+
// story.tsx — the source owns its key
|
|
378
|
+
import { query, type ServerRouteArgs } from "@solidjs/router";
|
|
379
|
+
|
|
380
|
+
export const getStory = query(async ({ params }: ServerRouteArgs<{ id: string }>) => {
|
|
381
|
+
"use server";
|
|
382
|
+
const story = await db.stories.get(params.id);
|
|
383
|
+
return props => (
|
|
384
|
+
<article>
|
|
385
|
+
<h1>{story.title}</h1>
|
|
386
|
+
{props.children}
|
|
387
|
+
</article>
|
|
388
|
+
);
|
|
389
|
+
}, "story");
|
|
390
|
+
|
|
391
|
+
// routes.ts
|
|
392
|
+
defineRoute({ path: "/stories/:id", component: serverRouteComponent(getStory) });
|
|
393
|
+
```
|
|
394
|
+
|
|
395
|
+
The source is called with **derived** arguments, not a live location — the call's `(function, arguments)` address keys both the query cache and the frame store, so the args name exactly what the route depends on:
|
|
396
|
+
|
|
397
|
+
- `params`: the params this route's pattern (and its ancestors') declares — never a child's, so a layout does not refetch when a leaf param changes. `defineRoute` checks them against the pattern.
|
|
398
|
+
- `search`: the validated output of the route's [`search` schema](#typed-search-params), only when one is declared. Otherwise `undefined`, and the route never tracks the query string.
|
|
399
|
+
|
|
400
|
+
A route view is route-shaped on purpose — the address stays stable and `defineRoute` can check its params against the pattern — which means it is only callable as a route. When the same server component is also used elsewhere, keep it a plain (non-exported, non-endpoint) function and have the route view call it with `params.id`. The value `serverRouteComponent` returns is a component only so it fits the `component` field; mounting it any other way (through `lazy()`, or by hand) throws, since outside the match there are only merged params to call with.
|
|
401
|
+
|
|
402
|
+
The router mounts the resolved component with the outlet as `children`, so a server component can be a layout, and it calls the same source under preload intent — link hover, `preloadRoute`, the [single-flight collector](#server-integration) — with the same derived args. What that call _means_ is the source's: the router does not choose the cache strategy or own the key.
|
|
403
|
+
|
|
404
|
+
- `query(fn, key)`: link intent warms the entry the render reads, `revalidate("story")` and action responses refetch it, and the collector reproduces it so a mutation's response carries the route's fresh markup. Argument changes deliver into the mounted boundary — it morphs in place rather than remounting.
|
|
405
|
+
- [`liveQuery(fn, key)`](#livequery-experimental): the frame stream stays open and the channel owns it — hover connects it (held through the preload window, so a hovered link is an open stream), `revalidate(key)` reconnects, and the mutation sweep and the single-flight collector both leave it alone — nothing pulls it server-side, and the stream is its own freshness.
|
|
406
|
+
|
|
407
|
+
Anything else callable with the args works too; wrapping is what gives dedupe, preload, and revalidation.
|
|
408
|
+
|
|
409
|
+
Mutations reach these routes the way they reach any query, and the cost model is worth knowing: a swept server route is a server-side re-render plus its markup in the response, not a small data blob. From a server action, `redirect(url)` re-collects everything the target shows and a bare `reload()` everything the current page shows — server routes included, shell and layouts too. On a server-component-heavy page, name what a mutation actually changed instead. `Router.keysFor(url)` answers the query keys of the server routes a URL shows, root to leaf — the app's own keys, from pure matching, so it works in an action and never spells a key:
|
|
410
|
+
|
|
411
|
+
```ts
|
|
412
|
+
export const addComment = action(async (form: FormData) => {
|
|
413
|
+
"use server";
|
|
414
|
+
await db.comments.add(form);
|
|
415
|
+
return reload({ revalidate: Router.keysFor(paths.stories(form.get("id"))) });
|
|
416
|
+
});
|
|
417
|
+
```
|
|
418
|
+
|
|
419
|
+
Route keys, not addresses: every story page is refetched, which is what prefix matching gives a hand-written `revalidate("story")` too.
|
|
420
|
+
|
|
421
|
+
An app shell is a pathless layout route whose `component` is a server component. With no pattern, its args are the constant `{ params: {}, search: undefined }` — one call, one address, persisting across every navigation — and as a route it gets preload, collection, and `revalidate` like any other. The `<Router>` root slot exists for client providers that need router context without following route rules; a server component has neither, so it does not go there:
|
|
422
|
+
|
|
423
|
+
```tsx
|
|
424
|
+
const routes = [{ component: serverRouteComponent(query(appShell, "shell")), children: pages }];
|
|
425
|
+
render(() => <Router routes={routes} />, document.body);
|
|
426
|
+
```
|
|
427
|
+
|
|
428
|
+
`children` is the only client position the router fills, and the helper's type says so: a server component that requires other props — event handlers, refs, named slots — is rejected. Those come from the client, so that route has a client half; write it as an ordinary route component around `dynamic()`. Interaction that lives on the server — form posts to server actions via `action={addTodo}` — needs no client component at all.
|
|
429
|
+
|
|
365
430
|
### File-System Routes
|
|
366
431
|
|
|
367
432
|
The `@solidjs/router/fs` adapter turns a `file-routes` manifest into route definitions — the app imports the virtual module, the adapter maps it:
|
|
@@ -387,22 +452,46 @@ export const route = defineFileRoute("/blog/:id", {
|
|
|
387
452
|
|
|
388
453
|
export default function Post(props: RouteProps<typeof route>) {
|
|
389
454
|
props.params.id; // string
|
|
390
|
-
props.data;
|
|
455
|
+
props.data; // ReturnType of the preload above
|
|
391
456
|
}
|
|
392
457
|
```
|
|
393
458
|
|
|
394
459
|
The pattern string is a typing witness — at runtime the manifest's path (from the filename) is the source of truth. With the plugin's `types` option generating a literal declaration for the virtual module, the file paths flow into `paths` and the typed hooks like a hand-written tree — `paths.blog(42)` typechecks, filters and search schemas included, and `useParams(paths.blog)` works as usual anywhere under the route.
|
|
395
460
|
|
|
461
|
+
#### Server pages
|
|
462
|
+
|
|
463
|
+
A route file's page can be a [server component](#server-component-routes-experimental): make the default export a `"use server"` function of the route args. With `fileRoutes({ serverComponents: true })` on the `file-routes` plugin, the scanner flags such a file and the adapter builds the route the hand-written tree spells out — `serverRouteComponent(query(fn, key))` with the file's path as the key. No `query`, key, or wrapper in the route file. (That option turns on server component _routes_; server components themselves are Solid's plugin's `serverFunctions: { components: true }`, the same switch a hand-written server route needs.)
|
|
464
|
+
|
|
465
|
+
```tsx
|
|
466
|
+
// routes/stories/[id].tsx
|
|
467
|
+
import { defineFileRoute } from "@solidjs/router/fs";
|
|
468
|
+
import type { ServerRouteArgs } from "@solidjs/router";
|
|
469
|
+
|
|
470
|
+
export const route = defineFileRoute("/stories/:id", { search: storySearch });
|
|
471
|
+
|
|
472
|
+
export default async function Story({ params, search }: ServerRouteArgs<typeof route>) {
|
|
473
|
+
"use server";
|
|
474
|
+
const story = await db.stories.get(params.id);
|
|
475
|
+
return props => <article>{story.title}{props.children}</article>;
|
|
476
|
+
}
|
|
477
|
+
```
|
|
478
|
+
|
|
479
|
+
`ServerRouteArgs<typeof route>` reads the config as a witness like `RouteProps` does: `params` from the pattern, `search` as the schema's output (`undefined` with no schema). The key is the file — `Router.keysFor(paths.stories(7))` answers `["src/routes/stories/[id].tsx"]` (plus any server layouts above it), so an action names the page without spelling a path; because keys match by prefix, `revalidate("src/routes/stories")` refetches every page under the directory (pathless layouts have no route path of their own, so the file is what tells them apart). The directive has to be the first statement of the inline default export — behind a wrapper call or a re-export the scanner cannot see it, the page is code-split like a client page, and in development the adapter throws a directed error when the chunk resolves.
|
|
480
|
+
|
|
481
|
+
A live page names its wrapper in the `route` config — `defineFileRoute("/feed", { query: liveQuery })` — and the adapter sources through that instead of `query`. The route file imports `liveQuery`, so only an app with a live page carries it.
|
|
482
|
+
|
|
483
|
+
Apps with no server page pay nothing for any of this. The plugin folds the scan into `filesystem-routing/flags`, and the adapter's only path to `serverRouteComponent` and `query` is a module gated on that constant, so the branch — and with it the frames runtime — tree-shakes away. `pnpm build` asserts it.
|
|
484
|
+
|
|
396
485
|
## Typed Paths
|
|
397
486
|
|
|
398
487
|
`paths` is a proxy inferred from the route tree. Property access descends into static segments, calls bind params, and it mirrors URL anatomy — params, then a search object, then a hash string:
|
|
399
488
|
|
|
400
489
|
```tsx
|
|
401
|
-
paths.users(123)
|
|
402
|
-
paths.users(2).settings
|
|
403
|
-
paths.users(2, { tab: "x" }, "comments")
|
|
404
|
-
paths.about()
|
|
405
|
-
paths()
|
|
490
|
+
paths.users(123); // ok — matchFilters flow into the callsite
|
|
491
|
+
paths.users(2).settings; // chainable into children
|
|
492
|
+
paths.users(2, { tab: "x" }, "comments"); // "/users/2?tab=x#comments"
|
|
493
|
+
paths.about(); // zero-arg/search calls terminate to a plain string
|
|
494
|
+
paths(); // "/" — the root
|
|
406
495
|
```
|
|
407
496
|
|
|
408
497
|
Every node coerces via `toString`, so nodes drop straight into `href`, `navigate()`, and `redirect()` without explicit termination. Accessing a segment that doesn't exist in the tree, or binding a param with the wrong type, is a compile error.
|
|
@@ -413,14 +502,14 @@ There is no link component. Use `<a>`; the router intercepts same-origin clicks
|
|
|
413
502
|
|
|
414
503
|
Behavior modifiers are attributes, so they work identically in client, server-rendered, and third-party markup:
|
|
415
504
|
|
|
416
|
-
| attribute | description
|
|
417
|
-
| ---------- |
|
|
418
|
-
| `replace` | Replace the history entry instead of pushing
|
|
419
|
-
| `noscroll` | Turn off scrolling to the top after navigation
|
|
505
|
+
| attribute | description |
|
|
506
|
+
| ---------- | --------------------------------------------------------------------------------------------------------------- |
|
|
507
|
+
| `replace` | Replace the history entry instead of pushing |
|
|
508
|
+
| `noscroll` | Turn off scrolling to the top after navigation |
|
|
420
509
|
| `state` | JSON string [pushed](https://developer.mozilla.org/en-US/docs/Web/API/History/pushState) onto the history stack |
|
|
421
|
-
| `preload` | Set to `"false"` to opt this link out of hover/focus preloading
|
|
422
|
-
| `link` | Marks a router link when `explicitLinks` is enabled
|
|
423
|
-
| `target` | Any value (e.g. `_self`) opts the anchor out of router handling
|
|
510
|
+
| `preload` | Set to `"false"` to opt this link out of hover/focus preloading |
|
|
511
|
+
| `link` | Marks a router link when `explicitLinks` is enabled |
|
|
512
|
+
| `target` | Any value (e.g. `_self`) opts the anchor out of router handling |
|
|
424
513
|
|
|
425
514
|
```tsx
|
|
426
515
|
<a href={paths.login} replace>Log in</a>
|
|
@@ -431,9 +520,15 @@ Behavior modifiers are attributes, so they work identically in client, server-re
|
|
|
431
520
|
Active and pending state is styled with CSS — one vocabulary for every kind of link:
|
|
432
521
|
|
|
433
522
|
```css
|
|
434
|
-
nav a[aria-current="page"] {
|
|
435
|
-
|
|
436
|
-
|
|
523
|
+
nav a[aria-current="page"] {
|
|
524
|
+
font-weight: 600;
|
|
525
|
+
} /* exact match */
|
|
526
|
+
nav a[data-active] {
|
|
527
|
+
color: var(--accent);
|
|
528
|
+
} /* exact or prefix match */
|
|
529
|
+
a[data-pending] {
|
|
530
|
+
opacity: 0.6;
|
|
531
|
+
} /* target of in-flight navigation */
|
|
437
532
|
```
|
|
438
533
|
|
|
439
534
|
(The root path only ever matches exactly, so `href={paths()}` doesn't light up on every page.)
|
|
@@ -471,10 +566,10 @@ const routes = defineRoutes([{ path: "/users/:id", component: User, preload: pre
|
|
|
471
566
|
|
|
472
567
|
The preload function receives:
|
|
473
568
|
|
|
474
|
-
| key | type
|
|
475
|
-
| -------- |
|
|
476
|
-
| params | object
|
|
477
|
-
| location | `{ pathname, search, hash, query, state, key }`
|
|
569
|
+
| key | type | description |
|
|
570
|
+
| -------- | -------------------------------------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
|
|
571
|
+
| params | object | The route parameters (same value as `useParams()` inside the route component) |
|
|
572
|
+
| location | `{ pathname, search, hash, query, state, key }` | Path information (corresponds to [`useLocation()`](#uselocation)) |
|
|
478
573
|
| intent | `"initial" \| "navigate" \| "native" \| "preload"` | Why this is being called: `initial` — first render; `navigate` — router navigation; `native` — browser back/forward; `preload` — link hover/focus, not navigating |
|
|
479
574
|
|
|
480
575
|
The factory-level `preload` option is the app-wide counterpart: it runs once per mount/request with the merged params of every match, and its result reaches the root render-prop as `props.data`.
|
|
@@ -513,7 +608,7 @@ const todos = createProjection(() => getTodos(), []);
|
|
|
513
608
|
Keys support targeted invalidation:
|
|
514
609
|
|
|
515
610
|
```ts
|
|
516
|
-
getUser.key;
|
|
611
|
+
getUser.key; // "users"
|
|
517
612
|
getUser.keyFor(5); // "users[5]"
|
|
518
613
|
```
|
|
519
614
|
|
|
@@ -549,13 +644,13 @@ Live queries ride the router's existing machinery rather than adding their own:
|
|
|
549
644
|
- **Preload** — calling one in a preload function warms the connection, so navigation renders against an already-delivered value instead of holding the transition on connect.
|
|
550
645
|
- **SSR** — the document face renders the first value; hydration adopts it and reconnects. Server-side, consumers of a key within one request observe the same value.
|
|
551
646
|
- **Revalidation** — explicit `revalidate(key)` reconnects (the producer re-yields current state by contract). The post-mutation sweep leaves healthy connections alone: the stream is its own freshness mechanism.
|
|
552
|
-
- **Single-flight** — a mutation
|
|
647
|
+
- **Single-flight** — live keys are neither collected nor swept: the stream is the freshness mechanism, and a mutation reaches a live query through its producer (the `watch` yielding the changed state), not through the mutation response. The flight-data seam that pushes payload values into open channels stays available to an integration that collects live keys itself.
|
|
553
648
|
|
|
554
649
|
The callable carries the `query` conventions (`key`, `keyFor`) plus a reactive `status(...args)` read (`"idle" | "connecting" | "connected" | "reconnecting" | "closed"`) for surfacing connection state in UI.
|
|
555
650
|
|
|
556
651
|
### `action`
|
|
557
652
|
|
|
558
|
-
A router action is
|
|
653
|
+
A router action is _an action with a URL_ — Solid's mutation primitive plus URL addressability, submission tracking, and response handling. Data helpers come from the router; response helpers (`redirect`, `reload`) come from `@solidjs/web` — they're protocol-level and work without the router:
|
|
559
654
|
|
|
560
655
|
```tsx
|
|
561
656
|
import { action } from "@solidjs/router";
|
|
@@ -580,7 +675,10 @@ const updateUser = action(async (form: FormData) => {
|
|
|
580
675
|
Actions only work with POST requests, so put `method="post"` on your form. Submitting forms get `aria-busy="true"` automatically while the action (including its revalidation) is in flight — the same CSS story as links:
|
|
581
676
|
|
|
582
677
|
```css
|
|
583
|
-
form[aria-busy] button {
|
|
678
|
+
form[aria-busy] button {
|
|
679
|
+
pointer-events: none;
|
|
680
|
+
opacity: 0.6;
|
|
681
|
+
}
|
|
584
682
|
```
|
|
585
683
|
|
|
586
684
|
Forms work without JavaScript: a real POST, a redirect back, and the result seeded into submission state through a one-shot flash cookie. Single-flight mutations are on by default — the mutation response carries the refreshed route data in the same round trip.
|
|
@@ -664,7 +762,7 @@ const routes = defineRoutes([
|
|
|
664
762
|
|
|
665
763
|
```tsx
|
|
666
764
|
const [search, setSearch] = useSearchParams(paths.search);
|
|
667
|
-
search.page;
|
|
765
|
+
search.page; // number (parsed, not "2")
|
|
668
766
|
setSearch({ page: search.page + 1 }); // typed setter
|
|
669
767
|
|
|
670
768
|
<a href={paths.search({ q: "solid", page: 2 })}>Search</a>; // typed builder
|
|
@@ -678,26 +776,26 @@ Without a schema, `useSearchParams()` behaves as before: raw string values, merg
|
|
|
678
776
|
createRouter(config);
|
|
679
777
|
```
|
|
680
778
|
|
|
681
|
-
| option | type
|
|
682
|
-
| --------------- |
|
|
683
|
-
| `routes` | `RouteDefinition[]`
|
|
684
|
-
| `base` | `string`
|
|
685
|
-
| `preload` | `RoutePreloadFunc`
|
|
686
|
-
| `history` | `RouterHistory`
|
|
687
|
-
| `singleFlight` | `boolean`
|
|
688
|
-
| `actionBase` | `string`
|
|
689
|
-
| `preloadLinks` | `boolean`
|
|
690
|
-
| `explicitLinks` | `boolean`
|
|
691
|
-
| `transformUrl` | `(url: string) => string`
|
|
779
|
+
| option | type | description |
|
|
780
|
+
| --------------- | ------------------------- | ----------------------------------------------------------------------------------------------------- |
|
|
781
|
+
| `routes` | `RouteDefinition[]` | The route tree — inline arrays infer literally; wrap extracted trees in `defineRoutes` |
|
|
782
|
+
| `base` | `string` | Base url to use for matching routes |
|
|
783
|
+
| `preload` | `RoutePreloadFunc` | App-wide preload: once per mount/request, result reaches the root render-prop as `props.data` |
|
|
784
|
+
| `history` | `RouterHistory` | History adapter; defaults to browser history on the client and the request URL on the server |
|
|
785
|
+
| `singleFlight` | `boolean` | Single-flight mutations, default `true` |
|
|
786
|
+
| `actionBase` | `string` | Root url for server actions, default `/_server` |
|
|
787
|
+
| `preloadLinks` | `boolean` | Preload route code/data on link hover and focus, default `true` |
|
|
788
|
+
| `explicitLinks` | `boolean` | Require the `link` attribute for router handling instead of intercepting all anchors, default `false` |
|
|
789
|
+
| `transformUrl` | `(url: string) => string` | Rewrite URLs before matching |
|
|
692
790
|
|
|
693
791
|
The returned instance is the provider component and carries the static surface:
|
|
694
792
|
|
|
695
|
-
| member
|
|
696
|
-
|
|
|
697
|
-
| `paths`
|
|
698
|
-
| `match`
|
|
699
|
-
| `routes`
|
|
700
|
-
| `config`
|
|
793
|
+
| member | description |
|
|
794
|
+
| -------- | ------------------------------------------------------------------------------------------------- |
|
|
795
|
+
| `paths` | The [typed path proxy](#typed-paths) |
|
|
796
|
+
| `match` | Pure matching against an arbitrary URL — no rendering or request context; root→leaf, `[]` if none |
|
|
797
|
+
| `routes` | The config tree |
|
|
798
|
+
| `config` | The full config — lets server integrations consume the instance directly |
|
|
701
799
|
|
|
702
800
|
## Router Primitives
|
|
703
801
|
|
|
@@ -708,7 +806,7 @@ Hooks read the live session off router context.
|
|
|
708
806
|
Retrieves a reactive, store-like object of the current route's path parameters. Pass a paths node for typing:
|
|
709
807
|
|
|
710
808
|
```tsx
|
|
711
|
-
const params = useParams();
|
|
809
|
+
const params = useParams(); // Params (strings)
|
|
712
810
|
const params = useParams(paths.users); // { id: string } — typed from the tree
|
|
713
811
|
```
|
|
714
812
|
|
|
@@ -756,7 +854,7 @@ In Solid's dev and observe builds the router also declares every navigation to t
|
|
|
756
854
|
|
|
757
855
|
### useMatch
|
|
758
856
|
|
|
759
|
-
Tests a path
|
|
857
|
+
Tests a path _pattern you supply_ against the current location; returns a memo of match information or `undefined`. It never consults the route tree — the pattern doesn't have to correspond to a defined route. The match's `params` are typed from the pattern, and a typed path node works too (a concrete URL — useful for "am I here" checks):
|
|
760
858
|
|
|
761
859
|
```tsx
|
|
762
860
|
const match = useMatch(() => "/admin/*rest");
|
|
@@ -768,7 +866,7 @@ const here = useMatch(() => paths.users(2));
|
|
|
768
866
|
|
|
769
867
|
### useRouteMatches
|
|
770
868
|
|
|
771
|
-
Returns an accessor of the router's
|
|
869
|
+
Returns an accessor of the router's _resolved_ matches for the current location — the chain of route definitions producing the current render, outermost first. This is the counterpart to `useMatch`: one reflects the route tree, the other tests a pattern. Useful for reading `info` metadata:
|
|
772
870
|
|
|
773
871
|
```tsx
|
|
774
872
|
const matches = useRouteMatches();
|
|
@@ -890,7 +988,7 @@ This guide maps from the stable 0.x releases (Solid 1). 1.0 removes the componen
|
|
|
890
988
|
<Router root={App}>
|
|
891
989
|
<Route path="/users" component={Users} />
|
|
892
990
|
<Route path="/users/:id" component={User} />
|
|
893
|
-
</Router
|
|
991
|
+
</Router>;
|
|
894
992
|
|
|
895
993
|
// 1.0
|
|
896
994
|
const Router = createRouter({
|
|
@@ -900,7 +998,7 @@ const Router = createRouter({
|
|
|
900
998
|
]
|
|
901
999
|
});
|
|
902
1000
|
|
|
903
|
-
<Router>{props => <App {...props} />}</Router
|
|
1001
|
+
<Router>{props => <App {...props} />}</Router>;
|
|
904
1002
|
```
|
|
905
1003
|
|
|
906
1004
|
- `<HashRouter>` → `createRouter({ routes, history: hashHistory() })`
|
package/dist/data/liveQuery.d.ts
CHANGED
|
@@ -18,11 +18,11 @@ export type LiveFunction<T extends (...args: any) => any> = T extends (...args:
|
|
|
18
18
|
* definite rejection (4xx: the transport stamps HTTP statuses onto
|
|
19
19
|
* failures) ends the channel and surfaces the error to consumers, as does
|
|
20
20
|
* a first-connect failure.
|
|
21
|
-
* - Server functions are declared GET at creation (like `query`). Live
|
|
22
|
-
*
|
|
23
|
-
*
|
|
24
|
-
*
|
|
25
|
-
*
|
|
21
|
+
* - Server functions are declared GET at creation (like `query`). Live keys
|
|
22
|
+
* are neither collected for single-flight nor swept after a mutation: the
|
|
23
|
+
* stream is the freshness mechanism, and the mutation reaches a live
|
|
24
|
+
* query through its producer. (The flight-data hook below still adopts a
|
|
25
|
+
* live key an integration collects itself.)
|
|
26
26
|
* - Calling under preload intent warms the channel (a temporary hold keeps
|
|
27
27
|
* it open through the preload window), so navigation renders against an
|
|
28
28
|
* already-connected stream instead of holding the transition on connect
|
package/dist/data/liveQuery.js
CHANGED
|
@@ -205,7 +205,7 @@ function subscriberIterable(open) {
|
|
|
205
205
|
}
|
|
206
206
|
};
|
|
207
207
|
return {
|
|
208
|
-
next: () =>
|
|
208
|
+
next: () => released ? Promise.resolve({ done: true, value: undefined }) : pull(),
|
|
209
209
|
return(value) {
|
|
210
210
|
release();
|
|
211
211
|
return Promise.resolve({ done: true, value });
|
|
@@ -236,10 +236,12 @@ function push(ch, value) {
|
|
|
236
236
|
// connection is alive and is itself the freshness mechanism — a
|
|
237
237
|
// push-driven producer yields the mutated state on its own, and tearing
|
|
238
238
|
// down healthy connections on every mutation defeats the model.
|
|
239
|
-
// -
|
|
240
|
-
//
|
|
241
|
-
//
|
|
242
|
-
// for
|
|
239
|
+
// - A single-flight payload carrying a live key delivers INTO the open
|
|
240
|
+
// channel rather than reconnecting it. The router's own collector does not
|
|
241
|
+
// produce live keys (the stream is the freshness mechanism; collection
|
|
242
|
+
// for live server components is an open question — a mutation's region
|
|
243
|
+
// would outrank the standing frame stream under the transport's version
|
|
244
|
+
// policy), so this path serves an integration that collects them itself.
|
|
243
245
|
let hooked = false;
|
|
244
246
|
function hookRevalidate() {
|
|
245
247
|
if (hooked)
|
|
@@ -293,11 +295,11 @@ function hookRevalidate() {
|
|
|
293
295
|
* definite rejection (4xx: the transport stamps HTTP statuses onto
|
|
294
296
|
* failures) ends the channel and surfaces the error to consumers, as does
|
|
295
297
|
* a first-connect failure.
|
|
296
|
-
* - Server functions are declared GET at creation (like `query`). Live
|
|
297
|
-
*
|
|
298
|
-
*
|
|
299
|
-
*
|
|
300
|
-
*
|
|
298
|
+
* - Server functions are declared GET at creation (like `query`). Live keys
|
|
299
|
+
* are neither collected for single-flight nor swept after a mutation: the
|
|
300
|
+
* stream is the freshness mechanism, and the mutation reaches a live
|
|
301
|
+
* query through its producer. (The flight-data hook below still adopts a
|
|
302
|
+
* live key an integration collects itself.)
|
|
301
303
|
* - Calling under preload intent warms the channel (a temporary hold keeps
|
|
302
304
|
* it open through the preload window), so navigation renders against an
|
|
303
305
|
* already-connected stream instead of holding the transition on connect
|
|
@@ -349,7 +351,7 @@ export function liveQuery(fn, name) {
|
|
|
349
351
|
// reads the same guarantee). Channels live in the event, retained
|
|
350
352
|
// past teardown so a later consumer replays the settled value instead
|
|
351
353
|
// of reinvoking; the producer still closes with its last consumer.
|
|
352
|
-
const router =
|
|
354
|
+
const router = e.router || (e.router = {});
|
|
353
355
|
const channels = router.liveChannels || (router.liveChannels = new Map());
|
|
354
356
|
return subscriberIterable(() => openChannel(channels, key, () => fn(...args), false, true));
|
|
355
357
|
}
|
package/dist/fs.d.ts
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import type { ServerPageQuery } from "./fsServer.js";
|
|
1
2
|
import type { DefinedRouteFilters, RouteParams, RoutePreloadFunc, RoutePreloadFuncArgs, RouteInfo, RouteSectionComponent, StandardSchemaV1, TypedRouteConfig, ValidFilters } from "./types.js";
|
|
2
3
|
/**
|
|
3
4
|
* The type `defineFileRoute` hands back: `matchFilters` and `search` stay
|
|
@@ -14,6 +15,7 @@ export type FileRouteConfig<S extends string = string, T = unknown, F = undefine
|
|
|
14
15
|
}) & {
|
|
15
16
|
preload?: RoutePreloadFunc<T> | undefined;
|
|
16
17
|
info?: RouteInfo | undefined;
|
|
18
|
+
query?: ServerPageQuery | undefined;
|
|
17
19
|
};
|
|
18
20
|
/**
|
|
19
21
|
* Identity helper for a route file's `route` export that types `preload`'s
|
|
@@ -39,6 +41,8 @@ export declare function defineFileRoute<S extends string, T = unknown, const F =
|
|
|
39
41
|
/** Standard Schema validator for this route's search params; its input type flows into the typed path proxy. */
|
|
40
42
|
search?: Sch;
|
|
41
43
|
info?: RouteInfo | undefined;
|
|
44
|
+
/** For a server page: the wrapper its source goes through — `query` unless named, e.g. `liveQuery`. */
|
|
45
|
+
query?: ServerPageQuery | undefined;
|
|
42
46
|
}): FileRouteConfig<S, T, F, Sch>;
|
|
43
47
|
/** A code-split module ref: delivered as a dynamic import. */
|
|
44
48
|
export interface FileRouteLazyRef<M = Record<string, unknown>> {
|
|
@@ -54,7 +58,9 @@ export interface FileRouteEagerRef<M = Record<string, unknown>> {
|
|
|
54
58
|
export interface FileRouteEntry {
|
|
55
59
|
path: string;
|
|
56
60
|
page?: boolean;
|
|
57
|
-
/**
|
|
61
|
+
/** The page component is a `"use server"` function; its `$component` is an eager ref to the stub. */
|
|
62
|
+
server?: boolean;
|
|
63
|
+
/** Code-split by default; an eager ref when delivered with `codeSplitting: false` or for a server page. */
|
|
58
64
|
$component?: FileRouteLazyRef<any> | FileRouteEagerRef<any> | undefined;
|
|
59
65
|
$$route?: FileRouteEagerRef<any> | undefined;
|
|
60
66
|
children?: readonly FileRouteEntry[] | undefined;
|
package/dist/fs.js
CHANGED
|
@@ -1,4 +1,7 @@
|
|
|
1
|
-
import { lazy } from "solid-js";
|
|
1
|
+
import { DEV, lazy } from "solid-js";
|
|
2
|
+
import { isServerFunction } from "@solidjs/web";
|
|
3
|
+
import { serverRoutes } from "filesystem-routing/flags";
|
|
4
|
+
import * as server from "./fsServer.js";
|
|
2
5
|
/**
|
|
3
6
|
* Identity helper for a route file's `route` export that types `preload`'s
|
|
4
7
|
* `args.params` from a pattern witness — the file's path pattern, which the
|
|
@@ -31,15 +34,36 @@ export function defineFileRoute(path, config) {
|
|
|
31
34
|
*/
|
|
32
35
|
export function fileRoutes(entries) {
|
|
33
36
|
const components = new Map();
|
|
34
|
-
const componentOf = (
|
|
37
|
+
const componentOf = (entry, config) => {
|
|
38
|
+
const ref = entry.$component;
|
|
35
39
|
if ("require" in ref) {
|
|
36
|
-
|
|
40
|
+
const component = ref.require()
|
|
41
|
+
.default;
|
|
42
|
+
// A server page: the stub is the source; the file is the key (unique,
|
|
43
|
+
// and a directory prefix is a subtree for `revalidate`).
|
|
44
|
+
return serverRoutes && entry.server
|
|
45
|
+
? server.serverRoute(component, (ref.src || entry.path).split("?")[0], config)
|
|
46
|
+
: component;
|
|
37
47
|
}
|
|
38
48
|
let component = components.get(ref.src);
|
|
39
49
|
if (!component) {
|
|
40
50
|
// moduleUrl is lazy()'s third argument as of solid 2.0.0-rc.1 (options
|
|
41
51
|
// moved second); route components are default exports, so no { export }.
|
|
42
|
-
component = lazy(
|
|
52
|
+
component = lazy(
|
|
53
|
+
// A `"use server"` default the scanner did not see (behind a wrapper
|
|
54
|
+
// call or a re-export) — or scanned without the plugin's
|
|
55
|
+
// `serverComponents` option — was code-split like a client page:
|
|
56
|
+
// say so in development rather than mount a stub as a component.
|
|
57
|
+
DEV
|
|
58
|
+
? () => ref.import().then(mod => {
|
|
59
|
+
if (isServerFunction(mod.default))
|
|
60
|
+
throw new Error(`Route module "${ref.src}" exports a server function as its page, but it ` +
|
|
61
|
+
"was delivered as a client page. Enable `serverComponents: true` on the " +
|
|
62
|
+
'file-routes plugin and make the `"use server"` directive the first ' +
|
|
63
|
+
"statement of the inline default export.");
|
|
64
|
+
return mod;
|
|
65
|
+
})
|
|
66
|
+
: ref.import, undefined, ref.src);
|
|
43
67
|
components.set(ref.src, component);
|
|
44
68
|
}
|
|
45
69
|
return component;
|
|
@@ -49,7 +73,7 @@ export function fileRoutes(entries) {
|
|
|
49
73
|
return {
|
|
50
74
|
...config,
|
|
51
75
|
path: entry.path,
|
|
52
|
-
component: entry.$component ? componentOf(entry
|
|
76
|
+
component: entry.$component ? componentOf(entry, config) : undefined,
|
|
53
77
|
info: { ...config.info, filesystem: true },
|
|
54
78
|
children: entry.children ? entry.children.map(toRoute) : undefined
|
|
55
79
|
};
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
import type { RouteSectionComponent } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* The wrapper a server page's source goes through: `query` unless the route
|
|
4
|
+
* config names another — `liveQuery`, imported by the route file that wants
|
|
5
|
+
* it, so only that app carries it.
|
|
6
|
+
*/
|
|
7
|
+
export type ServerPageQuery = (fn: (...args: any[]) => any, key: string) => (...args: any[]) => any;
|
|
8
|
+
/**
|
|
9
|
+
* The server page half of the fs adapter — the only importer of `query` and
|
|
10
|
+
* `serverRouteComponent`. fs.ts reaches it behind `serverRoutes` from
|
|
11
|
+
* `filesystem-routing/flags`, so an app with no server page never bundles it.
|
|
12
|
+
*/
|
|
13
|
+
export declare function serverRoute(fn: (...args: any[]) => any, key: string, config?: {
|
|
14
|
+
query?: ServerPageQuery | undefined;
|
|
15
|
+
}): RouteSectionComponent;
|
package/dist/fsServer.js
ADDED
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import { query } from "./data/query.js";
|
|
2
|
+
import { serverRouteComponent } from "./serverRouteComponent.js";
|
|
3
|
+
/**
|
|
4
|
+
* The server page half of the fs adapter — the only importer of `query` and
|
|
5
|
+
* `serverRouteComponent`. fs.ts reaches it behind `serverRoutes` from
|
|
6
|
+
* `filesystem-routing/flags`, so an app with no server page never bundles it.
|
|
7
|
+
*/
|
|
8
|
+
export function serverRoute(fn, key, config) {
|
|
9
|
+
return serverRouteComponent((config?.query ?? query)(fn, key));
|
|
10
|
+
}
|