@solidjs/router 1.0.0-next.9 → 2.0.0-next.12
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 +100 -7
- package/dist/data/action.js +34 -25
- package/dist/fs.d.ts +94 -0
- package/dist/fs.js +51 -0
- package/dist/index.d.ts +20 -1
- package/dist/index.js +233 -116
- package/dist/routers/factory.d.ts +81 -5
- package/dist/routers/factory.jsx +25 -14
- package/dist/routers/history.d.ts +2 -0
- package/dist/routers/history.js +2 -2
- package/dist/routers/index.d.ts +3 -2
- package/dist/routers/index.js +1 -1
- package/dist/routers/scrollRestoration.d.ts +41 -0
- package/dist/routers/scrollRestoration.js +134 -0
- package/dist/routing.d.ts +11 -3
- package/dist/routing.js +25 -8
- package/dist/server.d.ts +7 -35
- package/dist/server.js +35 -143
- package/dist/types.d.ts +66 -12
- package/package.json +8 -7
- package/dist/data/flash.d.ts +0 -21
- package/dist/data/flash.js +0 -78
- package/dist/data/flashCookie.d.ts +0 -7
- package/dist/data/flashCookie.js +0 -20
package/README.md
CHANGED
|
@@ -29,12 +29,14 @@ Explore the official [documentation](https://docs.solidjs.com/solid-router) for
|
|
|
29
29
|
- [The Mental Model: Instance vs Hooks](#the-mental-model-instance-vs-hooks)
|
|
30
30
|
- [Route Definitions](#route-definitions)
|
|
31
31
|
- [Dynamic Routes](#dynamic-routes)
|
|
32
|
+
- [Typed Route Params](#typed-route-params)
|
|
32
33
|
- [Match Filters](#match-filters)
|
|
33
34
|
- [Optional Parameters](#optional-parameters)
|
|
34
35
|
- [Wildcard Routes](#wildcard-routes)
|
|
35
36
|
- [Multiple Paths](#multiple-paths)
|
|
36
37
|
- [Nested Routes](#nested-routes)
|
|
37
38
|
- [Lazy Route Subtrees](#lazy-route-subtrees)
|
|
39
|
+
- [File-System Routes](#file-system-routes)
|
|
38
40
|
- [Typed Paths](#typed-paths)
|
|
39
41
|
- [Links](#links)
|
|
40
42
|
- [Preload Functions](#preload-functions)
|
|
@@ -94,6 +96,8 @@ export const adminRoutes = defineRoutes([
|
|
|
94
96
|
export const Router = createRouter({ routes: [...appRoutes, ...adminRoutes] });
|
|
95
97
|
```
|
|
96
98
|
|
|
99
|
+
Its single-route sibling `defineRoute` also types `params` inside the route's own `component` and `preload` — see [Typed Route Params](#typed-route-params).
|
|
100
|
+
|
|
97
101
|
Mount it by rendering the instance. The render-prop child is your root layout — it always stays mounted, receives the matched content as `props.children`, and is the ideal place for top-level navigation and context providers:
|
|
98
102
|
|
|
99
103
|
```tsx
|
|
@@ -188,6 +192,48 @@ As long as the URL fits the pattern, the `User` component shows, and `id` is ava
|
|
|
188
192
|
</Show>
|
|
189
193
|
```
|
|
190
194
|
|
|
195
|
+
### Typed Route Params
|
|
196
|
+
|
|
197
|
+
By default `params` is an open record — every key is `string | undefined`, even when the pattern guarantees it. Wrap a route in `defineRoute` and its `component` and `preload` are typed from the route's own `path`:
|
|
198
|
+
|
|
199
|
+
```tsx
|
|
200
|
+
import { defineRoute } from "@solidjs/router";
|
|
201
|
+
|
|
202
|
+
const story = defineRoute({
|
|
203
|
+
path: "/stories/:id/:tab?",
|
|
204
|
+
preload: ({ params }) => getStory(params.id), // params.id: string
|
|
205
|
+
component: props => (
|
|
206
|
+
<Story
|
|
207
|
+
id={props.params.id} // string — the pattern guarantees it
|
|
208
|
+
tab={props.params.tab} // string | undefined — optional param
|
|
209
|
+
/>
|
|
210
|
+
)
|
|
211
|
+
});
|
|
212
|
+
```
|
|
213
|
+
|
|
214
|
+
`defineRoute` is an identity function at runtime — the route object drops into `routes` (or a parent's `children`) like any plain object, and `path`, `children`, `matchFilters`, and `search` still flow into `paths` and the typed hooks. Params inherited from parent routes stay accessible as `string | undefined`; nested `children` type their own params only if they use `defineRoute` themselves.
|
|
215
|
+
|
|
216
|
+
For components declared away from their route, `RouteProps` takes a path witness — the same `paths` node you navigate with (`import type` keeps the instance out of the runtime graph, so no cycle) — plus an optional data type:
|
|
217
|
+
|
|
218
|
+
```tsx
|
|
219
|
+
import type { RouteComponent, RouteProps } from "@solidjs/router";
|
|
220
|
+
import type { Router } from "./app/router";
|
|
221
|
+
|
|
222
|
+
function Story(props: RouteProps<typeof Router.paths.stories, StoryData>) {
|
|
223
|
+
props.params.id; // string
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
// component-type form — props infer contextually
|
|
227
|
+
const Story: RouteComponent<typeof Router.paths.stories, StoryData> = props => (
|
|
228
|
+
<h1>{props.params.id}</h1>
|
|
229
|
+
);
|
|
230
|
+
|
|
231
|
+
// or, anywhere under the route:
|
|
232
|
+
const params = useParams(paths.stories); // typed from the tree
|
|
233
|
+
```
|
|
234
|
+
|
|
235
|
+
When no instance is in scope at the definition site — most notably [file-system route files](#file-system-routes), where the pattern lives in the filename — the witness can be the pattern string itself: `RouteProps<"/stories/:id">`.
|
|
236
|
+
|
|
191
237
|
### Match Filters
|
|
192
238
|
|
|
193
239
|
Each parameter can be validated with a `MatchFilter` — an enum array, a regex, or a predicate. If validation fails, the route doesn't match:
|
|
@@ -316,6 +362,37 @@ The import only fires when something needs the subtree — hovering a link into
|
|
|
316
362
|
|
|
317
363
|
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.
|
|
318
364
|
|
|
365
|
+
### File-System Routes
|
|
366
|
+
|
|
367
|
+
The `@solidjs/router/fs` adapter turns a `file-routes` manifest into route definitions — the app imports the virtual module, the adapter maps it:
|
|
368
|
+
|
|
369
|
+
```tsx
|
|
370
|
+
import { pageRoutes } from "virtual:file-routes";
|
|
371
|
+
import { fileRoutes } from "@solidjs/router/fs";
|
|
372
|
+
|
|
373
|
+
export const Router = createRouter({ routes: fileRoutes(pageRoutes) });
|
|
374
|
+
```
|
|
375
|
+
|
|
376
|
+
Route files export their component as `default` and everything else as a `route` config export, which is spread into the definition. Inside a route file the pattern lives in the filename, so there's no `paths` node to witness with at the definition site — `defineFileRoute` takes the pattern string instead, types `preload`'s params from it, and validates `matchFilters` along the way. The config then doubles as the component's [`RouteProps`](#typed-route-params) witness, typing `params` from the pattern and `data` from the `preload`'s return type:
|
|
377
|
+
|
|
378
|
+
```tsx
|
|
379
|
+
// routes/blog/[id].tsx
|
|
380
|
+
import { int } from "@solidjs/router";
|
|
381
|
+
import { defineFileRoute } from "@solidjs/router/fs";
|
|
382
|
+
|
|
383
|
+
export const route = defineFileRoute("/blog/:id", {
|
|
384
|
+
matchFilters: { id: int },
|
|
385
|
+
preload: ({ params }) => getPost(params.id) // params.id: string
|
|
386
|
+
});
|
|
387
|
+
|
|
388
|
+
export default function Post(props: RouteProps<typeof route>) {
|
|
389
|
+
props.params.id; // string
|
|
390
|
+
props.data; // ReturnType of the preload above
|
|
391
|
+
}
|
|
392
|
+
```
|
|
393
|
+
|
|
394
|
+
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
|
+
|
|
319
396
|
## Typed Paths
|
|
320
397
|
|
|
321
398
|
`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:
|
|
@@ -598,9 +675,11 @@ Retrieves a reactive, store-like object of the current route's path parameters.
|
|
|
598
675
|
|
|
599
676
|
```tsx
|
|
600
677
|
const params = useParams(); // Params (strings)
|
|
601
|
-
const params = useParams(paths.users); // { id:
|
|
678
|
+
const params = useParams(paths.users); // { id: string } — typed from the tree
|
|
602
679
|
```
|
|
603
680
|
|
|
681
|
+
Inside a route's own `component`/`preload`, [`defineRoute`](#typed-route-params) types `props.params` without a witness.
|
|
682
|
+
|
|
604
683
|
### useNavigate
|
|
605
684
|
|
|
606
685
|
Retrieves a method to navigate. Accepts a string or a typed path node, plus options:
|
|
@@ -641,11 +720,14 @@ return <div classList={{ "grey-out": isRouting() }}>...</div>;
|
|
|
641
720
|
|
|
642
721
|
### useMatch
|
|
643
722
|
|
|
644
|
-
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:
|
|
723
|
+
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):
|
|
645
724
|
|
|
646
725
|
```tsx
|
|
647
726
|
const match = useMatch(() => "/admin/*rest");
|
|
727
|
+
match()?.params.rest; // string
|
|
648
728
|
return <Show when={match()}>...</Show>;
|
|
729
|
+
|
|
730
|
+
const here = useMatch(() => paths.users(2));
|
|
649
731
|
```
|
|
650
732
|
|
|
651
733
|
### useRouteMatches
|
|
@@ -654,7 +736,17 @@ Returns an accessor of the router's *resolved* matches for the current location
|
|
|
654
736
|
|
|
655
737
|
```tsx
|
|
656
738
|
const matches = useRouteMatches();
|
|
657
|
-
const breadcrumbs = createMemo(() => matches().map(m => m.route.info
|
|
739
|
+
const breadcrumbs = createMemo(() => matches().map(m => m.route.info?.breadcrumb));
|
|
740
|
+
```
|
|
741
|
+
|
|
742
|
+
`info` is freeform by default; augment `RouteInfo` to type it app-wide — declared keys are checked at route definitions and typed on reads:
|
|
743
|
+
|
|
744
|
+
```ts
|
|
745
|
+
declare module "@solidjs/router" {
|
|
746
|
+
interface RouteInfo {
|
|
747
|
+
breadcrumb?: string;
|
|
748
|
+
}
|
|
749
|
+
}
|
|
658
750
|
```
|
|
659
751
|
|
|
660
752
|
### usePreloadRoute
|
|
@@ -738,17 +830,18 @@ Router.match("/users/2/settings?tab=x");
|
|
|
738
830
|
|
|
739
831
|
## Server Integration
|
|
740
832
|
|
|
741
|
-
Framework handler wiring lives in `@solidjs/router/server`.
|
|
833
|
+
Framework handler wiring lives in `@solidjs/router/server`. The integration accepts the router instance directly — its routes, base, and preload are the single source of truth:
|
|
742
834
|
|
|
743
835
|
```tsx
|
|
744
|
-
import { createFlightDataCollector
|
|
836
|
+
import { createFlightDataCollector } from "@solidjs/router/server";
|
|
745
837
|
import { Router } from "./app/router";
|
|
746
838
|
|
|
747
839
|
const collectFlightData = createFlightDataCollector(Router);
|
|
748
|
-
const handleNoJS = createNoJSHandler();
|
|
749
840
|
```
|
|
750
841
|
|
|
751
|
-
`createFlightDataCollector` produces the single-flight hook: after a mutation it reruns the route data the mutation invalidated for the page the client is on (or is redirected to), folding fresh data into the same response.
|
|
842
|
+
`createFlightDataCollector` produces the single-flight hook: after a mutation it reruns the route data the mutation invalidated for the page the client is on (or is redirected to), folding fresh data into the same response. This policy previously lived inside SolidStart; the router now owns it, so custom server setups get single-flight mutations without a framework.
|
|
843
|
+
|
|
844
|
+
The no-JS form convention needs no wiring at all: the server function runtime answers form posts made without the client runtime by redirecting back with the outcome in a one-shot flash cookie, and the router's SSR reads it into submission state. To configure it (e.g. a base path), pass `createNoJSHandler(options)` from `@solidjs/web/server-functions/server` as the handler's `handleNoJS`.
|
|
752
845
|
|
|
753
846
|
## Migration from 0.x
|
|
754
847
|
|
package/dist/data/action.js
CHANGED
|
@@ -1,9 +1,11 @@
|
|
|
1
1
|
import { $TRACK, action as createSolidAction, createMemo, onCleanup, getOwner } from "solid-js";
|
|
2
|
-
import { isResponseEnvelope, isServer } from "@solidjs/web";
|
|
3
|
-
import { createServerReference,
|
|
2
|
+
import { isResponseEnvelope, isServer, REVALIDATE_HEADER } from "@solidjs/web";
|
|
3
|
+
import { createServerReference, decodeResponsePayload, subscribeFlightData } from "@solidjs/web/server-functions";
|
|
4
|
+
// The explicit /server specifier is safe here: the only call site is
|
|
5
|
+
// server-guarded, so client builds tree-shake the codec away.
|
|
6
|
+
import { decodeFlashCookie } from "@solidjs/web/server-functions/server";
|
|
4
7
|
import { provideFlashDecoder, provideFlightConsumer, useRouter } from "../routing.js";
|
|
5
8
|
import { setRouterFormHandler } from "./events.js";
|
|
6
|
-
import { decodeFlashCookie } from "./flash.js";
|
|
7
9
|
import { mockBase, setFunctionName } from "../utils.js";
|
|
8
10
|
import { cacheKeyOp, hashKey, revalidate, query } from "./query.js";
|
|
9
11
|
const submitHooksSymbol = Symbol("routerActionSubmitHooks");
|
|
@@ -108,8 +110,12 @@ function installRouterIntegrations() {
|
|
|
108
110
|
if (integrationsInstalled)
|
|
109
111
|
return;
|
|
110
112
|
integrationsInstalled = true;
|
|
111
|
-
|
|
112
|
-
|
|
113
|
+
if (isServer) {
|
|
114
|
+
// Server-only: initSubmissions only decodes during SSR, so client builds
|
|
115
|
+
// tree-shake the codec (which now lives behind the runtime's server entry).
|
|
116
|
+
provideFlashDecoder(decodeFlashCookie);
|
|
117
|
+
}
|
|
118
|
+
else {
|
|
113
119
|
setRouterFormHandler(handleFormAction);
|
|
114
120
|
provideFlightConsumer(setupFlightDataConsumer);
|
|
115
121
|
}
|
|
@@ -179,14 +185,12 @@ function actionImpl(fn, options = {}) {
|
|
|
179
185
|
finally {
|
|
180
186
|
form && setFormBusy(form, -1);
|
|
181
187
|
}
|
|
182
|
-
if (!response)
|
|
183
|
-
return undefined;
|
|
184
188
|
let submission;
|
|
185
189
|
submission = {
|
|
186
190
|
input: variables,
|
|
187
191
|
url,
|
|
188
|
-
result: response.data,
|
|
189
|
-
error: response.error,
|
|
192
|
+
result: response && response.data,
|
|
193
|
+
error: response && response.error,
|
|
190
194
|
clear() {
|
|
191
195
|
router.submissions[1](entries => entries.filter(entry => entry !== submission));
|
|
192
196
|
},
|
|
@@ -195,12 +199,20 @@ function actionImpl(fn, options = {}) {
|
|
|
195
199
|
return current[invokeSymbol].call({ r: router, f: form }, variables, current);
|
|
196
200
|
}
|
|
197
201
|
};
|
|
198
|
-
|
|
202
|
+
// Book-keeping is intentional: only outcomes worth showing or retrying
|
|
203
|
+
// (a result or an error) enter the submissions list, so the typical void
|
|
204
|
+
// mutation leaves nothing behind. Settled hooks still see every
|
|
205
|
+
// completion — void, metadata-only, and redirects included — one
|
|
206
|
+
// `onSettled` per invocation (#580).
|
|
207
|
+
response && router.submissions[1](entries => [...entries, submission]);
|
|
199
208
|
for (const hook of settledHooks.values())
|
|
200
209
|
hook(submission);
|
|
201
|
-
if (response
|
|
202
|
-
|
|
203
|
-
|
|
210
|
+
if (response) {
|
|
211
|
+
if (response.error && !form)
|
|
212
|
+
throw response.error;
|
|
213
|
+
return response.data;
|
|
214
|
+
}
|
|
215
|
+
return undefined;
|
|
204
216
|
}
|
|
205
217
|
const o = typeof options === "string" ? { name: options } : options;
|
|
206
218
|
const name = o.name || (!isServer ? String(hashString(fn.toString())) : undefined);
|
|
@@ -299,11 +311,11 @@ export function setupFlightDataConsumer(router) {
|
|
|
299
311
|
* the flight-data consumer and the action response path (which still sees
|
|
300
312
|
* metadata-bearing responses when no flight data was collected).
|
|
301
313
|
*/
|
|
302
|
-
|
|
314
|
+
function applyResponseMetadata(metadata, navigate, flightData) {
|
|
303
315
|
let keys;
|
|
304
316
|
if (metadata) {
|
|
305
|
-
if (metadata.headers.has(
|
|
306
|
-
keys = metadata.headers.get(
|
|
317
|
+
if (metadata.headers.has(REVALIDATE_HEADER))
|
|
318
|
+
keys = metadata.headers.get(REVALIDATE_HEADER).split(",");
|
|
307
319
|
if (metadata.headers.has("Location")) {
|
|
308
320
|
const locationUrl = metadata.headers.get("Location") || "/";
|
|
309
321
|
if (locationUrl.startsWith("http")) {
|
|
@@ -319,7 +331,7 @@ async function applyResponseMetadata(metadata, navigate, flightData) {
|
|
|
319
331
|
// set cache
|
|
320
332
|
flightData && Object.keys(flightData).forEach(k => query.set(k, flightData[k]));
|
|
321
333
|
// trigger revalidation
|
|
322
|
-
|
|
334
|
+
revalidate(keys, false);
|
|
323
335
|
}
|
|
324
336
|
async function handleResponse(response, error, navigate, metadataHandled) {
|
|
325
337
|
let data;
|
|
@@ -336,14 +348,11 @@ async function handleResponse(response, error, navigate, metadataHandled) {
|
|
|
336
348
|
// carry a codec-encoded body the router decodes itself. With the
|
|
337
349
|
// flight-data consumer registered single-flight payloads never reach
|
|
338
350
|
// this path, but a manually opted-in call (no consumer) still can —
|
|
339
|
-
//
|
|
351
|
+
// the runtime splits its own envelope shape.
|
|
340
352
|
if (response.body) {
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
data = payload.value;
|
|
345
|
-
flightData = payload.data;
|
|
346
|
-
}
|
|
353
|
+
const payload = await decodeResponsePayload(response);
|
|
354
|
+
data = payload.value;
|
|
355
|
+
flightData = payload.flightData;
|
|
347
356
|
}
|
|
348
357
|
}
|
|
349
358
|
else if (error)
|
|
@@ -354,6 +363,6 @@ async function handleResponse(response, error, navigate, metadataHandled) {
|
|
|
354
363
|
// function's unwrapped value. Do not treat that value as a second plain
|
|
355
364
|
// action response and invalidate the freshly seeded query cache again.
|
|
356
365
|
if (!metadataHandled || metadata || flightData)
|
|
357
|
-
|
|
366
|
+
applyResponseMetadata(metadata, navigate, flightData);
|
|
358
367
|
return data != null ? { data } : undefined;
|
|
359
368
|
}
|
package/dist/fs.d.ts
ADDED
|
@@ -0,0 +1,94 @@
|
|
|
1
|
+
import type { DefinedRouteFilters, RouteParams, RoutePreloadFunc, RoutePreloadFuncArgs, RouteInfo, RouteSectionComponent, StandardSchemaV1, TypedRouteConfig, ValidFilters } from "./types.js";
|
|
2
|
+
/**
|
|
3
|
+
* The type `defineFileRoute` hands back: `matchFilters` and `search` stay
|
|
4
|
+
* literal — and only present when provided precisely, which is what the
|
|
5
|
+
* `paths` machinery keys on once the manifest spreads this into the
|
|
6
|
+
* definition — while `preload` widens back to the plain contract. The
|
|
7
|
+
* pattern rides along as a phantom brand so `RouteProps<typeof route>`
|
|
8
|
+
* can type the file's component from the same witness.
|
|
9
|
+
*/
|
|
10
|
+
export type FileRouteConfig<S extends string = string, T = unknown, F = undefined, Sch = undefined> = TypedRouteConfig<S> & ([F] extends [undefined] ? {} : DefinedRouteFilters<S> extends F ? {} : {
|
|
11
|
+
matchFilters: F;
|
|
12
|
+
}) & ([Sch] extends [undefined] ? {} : {
|
|
13
|
+
search: Sch;
|
|
14
|
+
}) & {
|
|
15
|
+
preload?: RoutePreloadFunc<T>;
|
|
16
|
+
info?: RouteInfo;
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Identity helper for a route file's `route` export that types `preload`'s
|
|
20
|
+
* `args.params` from a pattern witness — the file's path pattern, which the
|
|
21
|
+
* manifest (not this string) still provides at runtime:
|
|
22
|
+
*
|
|
23
|
+
* ```ts
|
|
24
|
+
* // routes/blog/[id].tsx
|
|
25
|
+
* export const route = defineFileRoute("/blog/:id", {
|
|
26
|
+
* matchFilters: { id: int },
|
|
27
|
+
* preload: ({ params }) => getPost(params.id) // params.id: string
|
|
28
|
+
* });
|
|
29
|
+
*
|
|
30
|
+
* export default function Post(props: RouteProps<typeof route>) {} // params *and* data typed
|
|
31
|
+
* ```
|
|
32
|
+
*
|
|
33
|
+
* `matchFilters` are validated against the pattern's params, and their
|
|
34
|
+
* types (and any `search` schema) flow through the manifest into `paths`.
|
|
35
|
+
*/
|
|
36
|
+
export declare function defineFileRoute<S extends string, T = unknown, const F = DefinedRouteFilters<S>, Sch extends StandardSchemaV1<any, any> | undefined = undefined>(path: S, config: {
|
|
37
|
+
matchFilters?: F & ValidFilters<F, S>;
|
|
38
|
+
preload?: (args: RoutePreloadFuncArgs<RouteParams<S>>) => T;
|
|
39
|
+
/** Standard Schema validator for this route's search params; its input type flows into the typed path proxy. */
|
|
40
|
+
search?: Sch;
|
|
41
|
+
info?: RouteInfo;
|
|
42
|
+
}): FileRouteConfig<S, T, F, Sch>;
|
|
43
|
+
/** A code-split module ref: delivered as a dynamic import. */
|
|
44
|
+
export interface FileRouteLazyRef<M = Record<string, unknown>> {
|
|
45
|
+
src: string;
|
|
46
|
+
import(): Promise<M>;
|
|
47
|
+
}
|
|
48
|
+
/** An eager module ref: its picked exports are imported statically. */
|
|
49
|
+
export interface FileRouteEagerRef<M = Record<string, unknown>> {
|
|
50
|
+
require(): M;
|
|
51
|
+
}
|
|
52
|
+
/** The shape of a nested route-manifest entry the adapter consumes. */
|
|
53
|
+
export interface FileRouteEntry {
|
|
54
|
+
path: string;
|
|
55
|
+
page?: boolean;
|
|
56
|
+
$component?: FileRouteLazyRef<any> | undefined;
|
|
57
|
+
$$route?: FileRouteEagerRef<any> | undefined;
|
|
58
|
+
children?: readonly FileRouteEntry[] | undefined;
|
|
59
|
+
}
|
|
60
|
+
type RouteConfigOf<E> = E extends {
|
|
61
|
+
$$route: {
|
|
62
|
+
require(): {
|
|
63
|
+
route: infer R;
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
} ? R : {};
|
|
67
|
+
/**
|
|
68
|
+
* One manifest entry as a route definition. `children` is a required key on
|
|
69
|
+
* purpose: an optional key infers as `C | undefined`, and that union
|
|
70
|
+
* distributes `RoutePaths` into an uncallable shape.
|
|
71
|
+
*/
|
|
72
|
+
export type FileRouteFrom<E> = RouteConfigOf<E> & {
|
|
73
|
+
path: E extends {
|
|
74
|
+
path: infer P extends string;
|
|
75
|
+
} ? P : never;
|
|
76
|
+
component: E extends {
|
|
77
|
+
$component: object;
|
|
78
|
+
} ? RouteSectionComponent : undefined;
|
|
79
|
+
children: E extends {
|
|
80
|
+
children: infer C extends readonly FileRouteEntry[];
|
|
81
|
+
} ? FileRoutesFrom<C> : undefined;
|
|
82
|
+
};
|
|
83
|
+
export type FileRoutesFrom<T extends readonly FileRouteEntry[]> = {
|
|
84
|
+
[K in keyof T]: FileRouteFrom<T[K]>;
|
|
85
|
+
};
|
|
86
|
+
/**
|
|
87
|
+
* Converts the nested page entries of a file-system route manifest into
|
|
88
|
+
* route definitions: `$component` refs become code-split `lazy` components
|
|
89
|
+
* (their `src` doubles as the `moduleUrl` core resolves assets and islands
|
|
90
|
+
* against), and each entry's `route` config export is spread into its
|
|
91
|
+
* definition. Pass the result to `createRouter({ routes })`.
|
|
92
|
+
*/
|
|
93
|
+
export declare function fileRoutes<const T extends readonly FileRouteEntry[]>(entries: T): FileRoutesFrom<T>;
|
|
94
|
+
export {};
|
package/dist/fs.js
ADDED
|
@@ -0,0 +1,51 @@
|
|
|
1
|
+
import { lazy } from "solid-js";
|
|
2
|
+
/**
|
|
3
|
+
* Identity helper for a route file's `route` export that types `preload`'s
|
|
4
|
+
* `args.params` from a pattern witness — the file's path pattern, which the
|
|
5
|
+
* manifest (not this string) still provides at runtime:
|
|
6
|
+
*
|
|
7
|
+
* ```ts
|
|
8
|
+
* // routes/blog/[id].tsx
|
|
9
|
+
* export const route = defineFileRoute("/blog/:id", {
|
|
10
|
+
* matchFilters: { id: int },
|
|
11
|
+
* preload: ({ params }) => getPost(params.id) // params.id: string
|
|
12
|
+
* });
|
|
13
|
+
*
|
|
14
|
+
* export default function Post(props: RouteProps<typeof route>) {} // params *and* data typed
|
|
15
|
+
* ```
|
|
16
|
+
*
|
|
17
|
+
* `matchFilters` are validated against the pattern's params, and their
|
|
18
|
+
* types (and any `search` schema) flow through the manifest into `paths`.
|
|
19
|
+
*/
|
|
20
|
+
export function defineFileRoute(path, config) {
|
|
21
|
+
return config;
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Converts the nested page entries of a file-system route manifest into
|
|
25
|
+
* route definitions: `$component` refs become code-split `lazy` components
|
|
26
|
+
* (their `src` doubles as the `moduleUrl` core resolves assets and islands
|
|
27
|
+
* against), and each entry's `route` config export is spread into its
|
|
28
|
+
* definition. Pass the result to `createRouter({ routes })`.
|
|
29
|
+
*/
|
|
30
|
+
export function fileRoutes(entries) {
|
|
31
|
+
const components = new Map();
|
|
32
|
+
const componentOf = (ref) => {
|
|
33
|
+
let component = components.get(ref.src);
|
|
34
|
+
if (!component) {
|
|
35
|
+
component = lazy(ref.import, ref.src);
|
|
36
|
+
components.set(ref.src, component);
|
|
37
|
+
}
|
|
38
|
+
return component;
|
|
39
|
+
};
|
|
40
|
+
const toRoute = (entry) => {
|
|
41
|
+
const config = (entry.$$route?.require().route ?? {});
|
|
42
|
+
return {
|
|
43
|
+
...config,
|
|
44
|
+
path: entry.path,
|
|
45
|
+
component: entry.$component ? componentOf(entry.$component) : undefined,
|
|
46
|
+
info: { ...config.info, filesystem: true },
|
|
47
|
+
children: entry.children ? entry.children.map(toRoute) : undefined
|
|
48
|
+
};
|
|
49
|
+
};
|
|
50
|
+
return entries.map(toRoute);
|
|
51
|
+
}
|
package/dist/index.d.ts
CHANGED
|
@@ -1,3 +1,22 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Arbitrary metadata attached to routes via `info`, surfaced through
|
|
3
|
+
* `useRouteMatches` and the instance's `match()`. Declared here — in the
|
|
4
|
+
* package entry — so apps can augment it for typed, discoverable metadata:
|
|
5
|
+
*
|
|
6
|
+
* ```ts
|
|
7
|
+
* declare module "@solidjs/router" {
|
|
8
|
+
* interface RouteInfo {
|
|
9
|
+
* breadcrumb?: string;
|
|
10
|
+
* }
|
|
11
|
+
* }
|
|
12
|
+
* ```
|
|
13
|
+
*
|
|
14
|
+
* Declared keys are type-checked at route definitions and typed on reads;
|
|
15
|
+
* undeclared keys remain freeform.
|
|
16
|
+
*/
|
|
17
|
+
export interface RouteInfo {
|
|
18
|
+
[key: string]: any;
|
|
19
|
+
}
|
|
1
20
|
export * from "./routers/index.js";
|
|
2
21
|
export * from "./lifecycle.js";
|
|
3
22
|
export { useHref, useIsRouting, useLinkState, useLocation, useMatch, useNavigate, usePreloadRoute, useParams, useResolvedPath, useRouteMatches, useSearchParams, RouterContextObj as RouterContext } from "./routing.js";
|
|
@@ -6,4 +25,4 @@ export { mergeSearchString as _mergeSearchString } from "./utils.js";
|
|
|
6
25
|
export { int } from "./paths.js";
|
|
7
26
|
export type { RoutePaths, PathParamsOf, PathEnd, TypedMatchFilter, DefaultSearchTypes } from "./paths.js";
|
|
8
27
|
export * from "./data/index.js";
|
|
9
|
-
export type { Location, LocationChange, SearchParams, MatchFilter, MatchFilters, NavigateOptions, Navigator, OutputMatch, Params, PathMatch, RouteSectionProps, RoutePreloadFunc, RoutePreloadFuncArgs, RouteDefinition, RouteDescription, RouteMatch, RouterIntegration, RouterUtils, SetParams, SetSearchParams, Submission, BeforeLeaveEventArgs, TypedPath, TypedSearchPath, StandardSchemaV1 } from "./types.js";
|
|
28
|
+
export type { Location, LocationChange, SearchParams, MatchFilter, MatchFilters, NavigateOptions, Navigator, OutputMatch, Params, PathMatch, RouteComponent, RouteParams, RouteProps, RouteSectionProps, RoutePreloadFunc, RoutePreloadFuncArgs, RouteDefinition, RouteDescription, RouteMatch, RouterIntegration, RouterUtils, SetParams, SetSearchParams, Submission, BeforeLeaveEventArgs, TypedPath, TypedSearchPath, StandardSchemaV1 } from "./types.js";
|