@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/dist/server.js CHANGED
@@ -24,6 +24,7 @@
24
24
  import { provideRequestEvent } from "@solidjs/web/storage";
25
25
  import { createBranches, getRouteMatches, mergeParams, peekLazySubtrees, resolveLazySubtree } from "./routing.js";
26
26
  import { extractSearchParams } from "./utils.js";
27
+ import { serverRouteArgs, serverRouteOf } from "./serverRouteShared.js";
27
28
  // the instance is the provider component, so it (unlike an options object) is a function
28
29
  function isRouterInstance(options) {
29
30
  return typeof options === "function";
@@ -146,6 +147,11 @@ function runPreloads(event, branches, url, previousUrl, rootPreload) {
146
147
  if (!prevMatches[match] || matches[match].route !== prevMatches[match].route)
147
148
  event.router.dataOnly = true;
148
149
  const { route, params } = matches[match];
150
+ // A server component route's markup is collected like any query result:
151
+ // the same call (function id + derived args) the client is showing, so
152
+ // the response's region addresses what is mounted.
153
+ const server = serverRouteOf(route.component);
154
+ server && server.call(serverRouteArgs(route, params, location.query));
149
155
  route.preload &&
150
156
  route.preload({
151
157
  params,
@@ -0,0 +1,20 @@
1
+ import type { Component } from "solid-js";
2
+ import type { Params, RouteSectionProps, ServerRouteFunction, ServerRouteParams, TypedRouteConfig } from "./types.js";
3
+ /**
4
+ * Use a server component as a route (experimental). `source` is a function
5
+ * of the router-derived {@link ServerRouteArgs} — this route's `params`, and
6
+ * `search` when the route declares a schema — resolving to a server
7
+ * component: typically a `"use server"` function wrapped in `query()` or
8
+ * `liveQuery()`, whose key the app names and revalidates. The router mounts
9
+ * the result with the outlet as `children` and calls the same source under
10
+ * preload intent, so link hover and single-flight collection warm it.
11
+ *
12
+ * `children` is the only client position the router fills, so the server
13
+ * component may declare no other. One that takes client handlers, refs, or
14
+ * slots has a client half — write an ordinary route component for it.
15
+ *
16
+ * ```ts
17
+ * defineRoute({ path: "/stories/:id", component: serverRouteComponent(query(storyView, "story")) });
18
+ * ```
19
+ */
20
+ export declare function serverRouteComponent<P extends Params | TypedRouteConfig = Params, S = undefined>(source: ServerRouteFunction<P, S> | ((...args: never[]) => ReturnType<ServerRouteFunction<P, S>>)): Component<RouteSectionProps<unknown, ServerRouteParams<P>>>;
@@ -0,0 +1,90 @@
1
+ // Server component routes (experimental — rides the experimental server
2
+ // components surface in @solidjs/web; the arg shape may change).
3
+ //
4
+ // `serverRouteComponent(source)` turns a function of route arguments that
5
+ // resolves to a component into a route `component`. It replaces the client
6
+ // wrapper a server-component route used to need:
7
+ //
8
+ // // before: a client component exists only to make the call
9
+ // const getStory = query(storyView, "story");
10
+ // component: props => {
11
+ // const View = dynamic(() => getStory(props.params.id));
12
+ // return <View>{props.children}</View>;
13
+ // }
14
+ // // after
15
+ // component: serverRouteComponent(query(storyView, "story"))
16
+ //
17
+ // The source is the app's: `query(fn, key)` for a request/response server
18
+ // component, `liveQuery(fn, key)` for one that streams successive versions,
19
+ // or any function of the args. The router does not choose the cache
20
+ // strategy and does not own the key — `revalidate("story")` is the app's,
21
+ // as it is for any query. What the router adds is the URL → call
22
+ // translation: it derives the call's arguments from the match, mounts the
23
+ // resolved component with the outlet as `children`, and calls the same
24
+ // source under preload intent (link hover, `preloadRoute`, the single-flight
25
+ // collector), so the query or live channel is warm before the navigation
26
+ // renders against it.
27
+ //
28
+ // Arguments are derived, not read from a live location: the call's
29
+ // `(function, arguments)` address keys the frame store and the query cache,
30
+ // so the args must name precisely what the route depends on. They are the
31
+ // params this route's pattern (with its ancestors') declares — never a
32
+ // child's, so a layout does not refetch when a leaf param changes — and, only
33
+ // when the route declares a `search` schema, its validated output.
34
+ //
35
+ // The router fills exactly one client position: `children`, with the outlet.
36
+ // A server component that takes other client positions — handlers, refs,
37
+ // named slots — has a client half, and that half is a client component; the
38
+ // helper does not pretend otherwise. Write the wrapper for that route.
39
+ import { dynamic } from "@solidjs/web";
40
+ import { createComponent, DEV } from "solid-js";
41
+ import { SERVER_ROUTE } from "./serverRouteShared.js";
42
+ /**
43
+ * Use a server component as a route (experimental). `source` is a function
44
+ * of the router-derived {@link ServerRouteArgs} — this route's `params`, and
45
+ * `search` when the route declares a schema — resolving to a server
46
+ * component: typically a `"use server"` function wrapped in `query()` or
47
+ * `liveQuery()`, whose key the app names and revalidates. The router mounts
48
+ * the result with the outlet as `children` and calls the same source under
49
+ * preload intent, so link hover and single-flight collection warm it.
50
+ *
51
+ * `children` is the only client position the router fills, so the server
52
+ * component may declare no other. One that takes client handlers, refs, or
53
+ * slots has a client half — write an ordinary route component for it.
54
+ *
55
+ * ```ts
56
+ * defineRoute({ path: "/stories/:id", component: serverRouteComponent(query(storyView, "story")) });
57
+ * ```
58
+ */
59
+ export function serverRouteComponent(
60
+ // A source that ignores its args (an app shell) is `(...args: never[])`
61
+ // once `query()` has typed it; that is the same call, so accept it too.
62
+ source) {
63
+ const call = source;
64
+ // The value is a component only so it fits the `component` field; the
65
+ // router core never calls it — it mounts through the brand, with THIS
66
+ // level's params and the declared search output. A direct mount (a
67
+ // `lazy()` wrapper around the helper, a hand-written `<Route />`) would
68
+ // have only the merged params to call with: the same view under a second
69
+ // address, missing the cache and owning its own frame. Refuse it.
70
+ const route = () => {
71
+ throw new Error("serverRouteComponent(): mount it as a route's `component`" +
72
+ (DEV
73
+ ? " — the router derives the call from the match. It was rendered directly (through " +
74
+ "lazy(), as the <Router> root, or outside a route). For an app shell, make it a " +
75
+ "pathless layout route with children."
76
+ : ""));
77
+ };
78
+ function render(args, routeProps) {
79
+ // The source may answer a component, a promise of one, or (a live query)
80
+ // successive components; `dynamic`'s memo lands each the same way.
81
+ const View = dynamic(() => call(args()));
82
+ return createComponent(View, {
83
+ get children() {
84
+ return routeProps.children;
85
+ }
86
+ });
87
+ }
88
+ route[SERVER_ROUTE] = { call, render };
89
+ return route;
90
+ }
@@ -0,0 +1,33 @@
1
+ import type { Accessor } from "solid-js";
2
+ import type { JSX } from "@solidjs/web";
3
+ import type { Params, RouteDescription, RouteSectionProps, SearchParams, ServerRouteArgs } from "./types.js";
4
+ export declare const SERVER_ROUTE: unique symbol;
5
+ /**
6
+ * What `serverRouteComponent()` attaches to the route component it returns, and
7
+ * what the router core drives it through: `call` is the query-wrapped server
8
+ * function (preload and single-flight collection warm/collect through it),
9
+ * `render` mounts the resolved server component for the current args with
10
+ * the route's props (`children` is the outlet).
11
+ */
12
+ export interface ServerRouteBrand {
13
+ call: (args: ServerRouteArgs<Params, unknown>) => unknown;
14
+ render: (args: Accessor<ServerRouteArgs<Params, unknown>>, route: RouteSectionProps) => JSX.Element;
15
+ }
16
+ export type BrandedRouteComponent = Function & {
17
+ [SERVER_ROUTE]?: ServerRouteBrand;
18
+ };
19
+ /** The brand carried by a route component built with `serverRouteComponent()`, if any. */
20
+ export declare function serverRouteOf(component: unknown): ServerRouteBrand | undefined;
21
+ /**
22
+ * Derive a server route's call arguments from a match: the match's params
23
+ * (this route's pattern and its ancestors' — never a child's) and the route's
24
+ * `search` schema output when one is declared. Reads `query` only when a
25
+ * schema exists, so a search-agnostic route never tracks the query string.
26
+ */
27
+ export declare function serverRouteArgs(route: RouteDescription, params: Params, query: SearchParams): ServerRouteArgs<Params, unknown>;
28
+ /**
29
+ * Structural equality for the args memo: a navigation produces fresh match
30
+ * objects even when this level's params are unchanged, and an equal call
31
+ * must not re-enter the source (and refetch).
32
+ */
33
+ export declare function serverRouteArgsEqual(a: ServerRouteArgs<Params, unknown>, b: ServerRouteArgs<Params, unknown>): boolean;
@@ -0,0 +1,39 @@
1
+ import { validateSearch } from "./utils.js";
2
+ export const SERVER_ROUTE = Symbol("solid-router.serverRoute");
3
+ /** The brand carried by a route component built with `serverRouteComponent()`, if any. */
4
+ export function serverRouteOf(component) {
5
+ return component?.[SERVER_ROUTE];
6
+ }
7
+ /**
8
+ * Derive a server route's call arguments from a match: the match's params
9
+ * (this route's pattern and its ancestors' — never a child's) and the route's
10
+ * `search` schema output when one is declared. Reads `query` only when a
11
+ * schema exists, so a search-agnostic route never tracks the query string.
12
+ */
13
+ export function serverRouteArgs(route, params, query) {
14
+ const schema = route.key?.search;
15
+ let search;
16
+ if (schema) {
17
+ const raw = { ...query };
18
+ const outcome = validateSearch(schema, raw);
19
+ // Issues leave the raw values in place, matching useSearchParams: search
20
+ // strings are user input, so defaults belong in the schema itself.
21
+ search = outcome.issues ? raw : outcome.value;
22
+ }
23
+ return { params: { ...params }, search };
24
+ }
25
+ const shallowEqual = (a, b) => a === b ||
26
+ (!!a &&
27
+ !!b &&
28
+ typeof a === "object" &&
29
+ typeof b === "object" &&
30
+ Object.keys(a).length === Object.keys(b).length &&
31
+ Object.keys(a).every(k => a[k] === b[k]));
32
+ /**
33
+ * Structural equality for the args memo: a navigation produces fresh match
34
+ * objects even when this level's params are unchanged, and an equal call
35
+ * must not re-enter the source (and refetch).
36
+ */
37
+ export function serverRouteArgsEqual(a, b) {
38
+ return shallowEqual(a.params, b.params) && shallowEqual(a.search, b.search);
39
+ }
package/dist/types.d.ts CHANGED
@@ -141,6 +141,36 @@ export interface RouteSectionProps<T = unknown, P extends Params = Params> {
141
141
  children?: JSX.Element;
142
142
  }
143
143
  export type RouteSectionComponent<T = unknown, P extends Params = Params> = Component<RouteSectionProps<T, P>> | Component<Omit<RouteSectionProps<T, P>, "children">> | Component<{}>;
144
+ /**
145
+ * What a server component route is called with (experimental): the params
146
+ * the route's pattern (and its ancestors') declares, plus — only when the
147
+ * route declares a `search` schema — that schema's validated output.
148
+ */
149
+ export interface ServerRouteArgs<P extends Params | TypedRouteConfig = Params, S = undefined> {
150
+ params: ServerRouteParams<P>;
151
+ search: P extends TypedRouteConfig ? SearchOutputOf<P> : S;
152
+ }
153
+ /**
154
+ * `ServerRouteArgs<typeof route>`: a `defineFileRoute` config is a witness —
155
+ * params from its pattern, search from its schema — the way
156
+ * `RouteProps<typeof route>` reads it for a client page.
157
+ */
158
+ export type ServerRouteParams<P> = P extends TypedRouteConfig<infer Pattern> ? RouteParams<Pattern> : Extract<P, Params>;
159
+ type SearchOutputOf<Def> = Def extends {
160
+ search: infer Sch extends StandardSchemaV1<any, any>;
161
+ } ? NonNullable<Sch["~standard"]["types"]>["output"] : undefined;
162
+ /**
163
+ * The shape `serverRouteComponent()` accepts (experimental): a function of
164
+ * the router-derived {@link ServerRouteArgs} answering a server component —
165
+ * once (a `query`-wrapped `"use server"` function), or as successive
166
+ * versions (a `liveQuery`). The component's only client position is
167
+ * `children` — the route outlet, which the router fills.
168
+ */
169
+ export type ServerRouteFunction<P extends Params | TypedRouteConfig = Params, S = undefined> = (args: ServerRouteArgs<P, S>) => ServerRouteView | Promise<ServerRouteView> | AsyncIterable<ServerRouteView>;
170
+ /** The component a server route resolves to: `children` is its only client position. */
171
+ export type ServerRouteView = Component<{
172
+ children?: JSX.Element;
173
+ }>;
144
174
  declare const ROUTE_PATTERN: unique symbol;
145
175
  export interface TypedRouteConfig<S extends string = string> {
146
176
  readonly [ROUTE_PATTERN]: S;
@@ -336,12 +366,19 @@ export type Submission<T, U> = {
336
366
  readonly error: any;
337
367
  readonly url: string;
338
368
  clear: () => void;
339
- retry: () => void;
369
+ /** Re-run the action with the same input; resolves to its result, like the original call. */
370
+ retry: () => Promise<U>;
340
371
  };
341
372
  export interface MaybePreloadableComponent extends Component {
342
373
  preload?: () => void;
343
374
  }
344
- export type CacheEntry = [number, Promise<any>, any, Intent | undefined, Signal<number> & {
345
- count: number;
346
- }];
375
+ export type CacheEntry = [
376
+ number,
377
+ Promise<any>,
378
+ any,
379
+ Intent | undefined,
380
+ Signal<number> & {
381
+ count: number;
382
+ }
383
+ ];
347
384
  export type NarrowResponse<T> = T extends ResponseEnvelope<infer U> ? U : Exclude<T, Response>;
package/dist/utils.d.ts CHANGED
@@ -1,4 +1,4 @@
1
- import type { MatchFilters, PathMatch, RouteDescription, SearchParams, SetSearchParams } from "./types.js";
1
+ import type { StandardSchemaV1, MatchFilters, PathMatch, RouteDescription, SearchParams, SetSearchParams } from "./types.js";
2
2
  export declare const mockBase = "http://sr";
3
3
  export declare function normalizePath(path: string, omitSlash?: boolean): string;
4
4
  /** Pathname stripped of search/hash and trailing slash, lowercased — the form link matching compares. */
@@ -6,6 +6,8 @@ export declare const comparablePath: (path: string) => string;
6
6
  export declare function resolvePath(base: string, path: string, from?: string): string | undefined;
7
7
  export declare function invariant<T>(value: T | null | undefined, message: string): T;
8
8
  export declare function joinPaths(from: string, to: string): string;
9
+ /** Run a Standard Schema synchronously — the only mode search params support. */
10
+ export declare function validateSearch(schema: StandardSchemaV1<any, any>, raw: Record<string, any>): import("./types.js").StandardSchemaResult<any>;
9
11
  export declare function extractSearchParams(url: URL): SearchParams;
10
12
  export declare function createMatcher<S extends string>(path: S, partial?: boolean, matchFilters?: MatchFilters<S>): (location: string) => PathMatch | null;
11
13
  export declare function scoreRoute(route: RouteDescription): number;
package/dist/utils.js CHANGED
@@ -35,6 +35,13 @@ export function invariant(value, message) {
35
35
  export function joinPaths(from, to) {
36
36
  return normalizePath(from).replace(/\/*(\*.*)?$/g, "") + normalizePath(to);
37
37
  }
38
+ /** Run a Standard Schema synchronously — the only mode search params support. */
39
+ export function validateSearch(schema, raw) {
40
+ const outcome = schema["~standard"].validate(raw);
41
+ if (outcome instanceof Promise)
42
+ throw new Error("Async Standard Schema validation is not supported for search params");
43
+ return outcome;
44
+ }
38
45
  export function extractSearchParams(url) {
39
46
  const params = {};
40
47
  url.searchParams.forEach((value, key) => {
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "Ryan Turnquist"
7
7
  ],
8
8
  "license": "MIT",
9
- "version": "2.0.0-next.26",
9
+ "version": "2.0.0-next.28",
10
10
  "homepage": "https://github.com/solidjs/solid-router#readme",
11
11
  "repository": {
12
12
  "type": "git",
@@ -52,10 +52,16 @@
52
52
  },
53
53
  "peerDependencies": {
54
54
  "@solidjs/web": "^2.0.0-rc.9",
55
+ "filesystem-routing": ">=0.4.0",
55
56
  "solid-js": "^2.0.0-rc.9"
56
57
  },
58
+ "peerDependenciesMeta": {
59
+ "filesystem-routing": {
60
+ "optional": true
61
+ }
62
+ },
57
63
  "scripts": {
58
- "build": "rm -rf dist && tsc && rollup -c",
64
+ "build": "rm -rf dist && tsc && rollup -c && node scripts/check-fs-gate.mjs",
59
65
  "test": "vitest run && vitest run --config vitest.config.server.ts && npm run test:types",
60
66
  "test:watch": "vitest",
61
67
  "test:server": "vitest run --config vitest.config.server.ts",