@solidjs/router 1.0.0 → 2.0.0-next.13

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (56) hide show
  1. package/README.md +606 -697
  2. package/dist/claims.d.ts +21 -0
  3. package/dist/claims.js +115 -0
  4. package/dist/data/action.d.ts +40 -11
  5. package/dist/data/action.js +307 -101
  6. package/dist/data/events.d.ts +8 -0
  7. package/dist/data/events.js +24 -23
  8. package/dist/data/index.d.ts +2 -4
  9. package/dist/data/index.js +2 -4
  10. package/dist/data/query.d.ts +1 -3
  11. package/dist/data/query.js +75 -32
  12. package/dist/data/serverForms.d.ts +1 -0
  13. package/dist/data/serverForms.js +5 -0
  14. package/dist/fs.d.ts +94 -0
  15. package/dist/fs.js +51 -0
  16. package/dist/index.d.ts +24 -3
  17. package/dist/index.js +1949 -1204
  18. package/dist/index.jsx +2 -2
  19. package/dist/lifecycle.d.ts +29 -4
  20. package/dist/lifecycle.js +40 -37
  21. package/dist/paths.d.ts +117 -0
  22. package/dist/paths.js +41 -0
  23. package/dist/routers/components.d.ts +12 -26
  24. package/dist/routers/components.jsx +68 -54
  25. package/dist/routers/factory.d.ts +121 -0
  26. package/dist/routers/factory.jsx +154 -0
  27. package/dist/routers/history.d.ts +26 -0
  28. package/dist/routers/history.js +180 -0
  29. package/dist/routers/index.d.ts +5 -11
  30. package/dist/routers/index.js +2 -6
  31. package/dist/routers/scrollRestoration.d.ts +10 -1
  32. package/dist/routers/scrollRestoration.js +49 -11
  33. package/dist/routing.d.ts +96 -52
  34. package/dist/routing.js +429 -172
  35. package/dist/server.d.ts +47 -0
  36. package/dist/server.js +156 -0
  37. package/dist/types.d.ts +158 -44
  38. package/dist/utils.d.ts +2 -0
  39. package/dist/utils.js +2 -0
  40. package/package.json +12 -7
  41. package/dist/components.d.ts +0 -31
  42. package/dist/components.jsx +0 -40
  43. package/dist/data/createAsync.d.ts +0 -32
  44. package/dist/data/createAsync.js +0 -96
  45. package/dist/data/response.d.ts +0 -4
  46. package/dist/data/response.js +0 -42
  47. package/dist/routers/HashRouter.d.ts +0 -9
  48. package/dist/routers/HashRouter.js +0 -41
  49. package/dist/routers/MemoryRouter.d.ts +0 -24
  50. package/dist/routers/MemoryRouter.js +0 -57
  51. package/dist/routers/Router.d.ts +0 -17
  52. package/dist/routers/Router.js +0 -59
  53. package/dist/routers/StaticRouter.d.ts +0 -6
  54. package/dist/routers/StaticRouter.js +0 -15
  55. package/dist/routers/createRouter.d.ts +0 -10
  56. package/dist/routers/createRouter.js +0 -41
@@ -1,6 +1,7 @@
1
- import { createSignal, getListener, getOwner, onCleanup, sharedConfig, startTransition } from "solid-js";
2
- import { getRequestEvent, isServer } from "solid-js/web";
3
- import { useNavigate, getIntent, getInPreloadFn } from "../routing.js";
1
+ import { createSignal, getObserver, getOwner, onCleanup, sharedConfig } from "solid-js";
2
+ import { getRequestEvent, isResponseEnvelope, isServer, REVALIDATE_HEADER } from "@solidjs/web";
3
+ import { GET, decodeResponse, getServerFunctionMetadata, isServerFunction } from "@solidjs/web/server-functions";
4
+ import { useRouter, useRoute, getIntent, getInPreloadFn } from "../routing.js";
4
5
  const LocationHeader = "Location";
5
6
  const PRELOAD_TIMEOUT = 5000;
6
7
  const CACHE_TIMEOUT = 180000;
@@ -29,11 +30,9 @@ function getCache() {
29
30
  */
30
31
  export function revalidate(key, force = true) {
31
32
  const now = Date.now();
32
- // force the cache miss synchronously so a `refetch` in the same tick sees it —
33
- // startTransition defers its callback to a microtask (#497)
34
- force && cacheKeyOp(key, entry => (entry[0] = 0));
35
- return startTransition(() => {
36
- cacheKeyOp(key, entry => entry[4][1](now)); // retrigger live signals
33
+ cacheKeyOp(key, entry => {
34
+ force && (entry[0] = 0); //force cache miss
35
+ entry[4][1](now); // retrigger live signals
37
36
  });
38
37
  }
39
38
  export function cacheKeyOp(key, fn) {
@@ -44,15 +43,24 @@ export function cacheKeyOp(key, fn) {
44
43
  }
45
44
  }
46
45
  export function query(fn, name) {
47
- // prioritize GET for server functions
48
- if (fn.GET)
49
- fn = fn.GET;
46
+ // query implies GET: the router primitive is the declaration site, so a
47
+ // server function handed to query() is wrapped with core `GET(fn)` here,
48
+ // at query-creation (module scope) — the server half records the method
49
+ // declaration for dispatch, the client half swaps in the GET transport.
50
+ // An explicit `GET(fn)` already carries the declaration on the metadata
51
+ // channel (`getServerFunctionMetadata(fn)?.method === "GET"`) and passes
52
+ // through; non-server functions are untouched.
53
+ if (isServerFunction(fn) && !getServerFunctionMetadata(fn)?.method) {
54
+ fn = GET(fn);
55
+ }
50
56
  const cachedFn = ((...args) => {
51
57
  const cache = getCache();
52
58
  const intent = getIntent();
53
59
  const inPreloadFn = getInPreloadFn();
54
60
  const owner = getOwner();
55
- const navigate = owner ? useNavigate() : undefined;
61
+ const router = owner ? useRouter() : undefined;
62
+ const route = owner ? useRoute() : undefined;
63
+ const navigate = router && router.navigatorFactory();
56
64
  const now = Date.now();
57
65
  const key = name + hashKey(args);
58
66
  let cached = cache.get(key);
@@ -72,21 +80,31 @@ export function query(fn, name) {
72
80
  }
73
81
  }
74
82
  }
75
- if (getListener() && !isServer) {
83
+ if (getObserver() && !isServer) {
76
84
  tracking = true;
77
85
  onCleanup(() => cached[4].count--);
78
86
  }
87
+ // A read under a route section the in-flight navigation is leaving keeps
88
+ // its invalidated value instead of refetching: this render is disposed at
89
+ // commit and never paints, and refiring would put a second request on
90
+ // what single flight promises is one. The entry stays a cache miss, so
91
+ // the fetch happens at its next real use — a later navigation's preload
92
+ // or read — unless the flight payload seeds it first. Retained sections
93
+ // still refetch here, inside the navigation's transition, so the
94
+ // redirect commits with fresh data.
95
+ const leaving = cached && !cached[0] && route && router.leaving?.(route);
79
96
  if (cached &&
80
- cached[0] &&
81
- (isServer ||
82
- intent === "native" ||
83
- cached[4].count ||
84
- Date.now() - cached[0] < PRELOAD_TIMEOUT)) {
97
+ (cached[0]
98
+ ? isServer ||
99
+ intent === "native" ||
100
+ cached[4].count ||
101
+ Date.now() - cached[0] < PRELOAD_TIMEOUT
102
+ : leaving)) {
85
103
  if (tracking) {
86
104
  cached[4].count++;
87
105
  cached[4][0](); // track
88
106
  }
89
- if (cached[3] === "preload" && intent !== "preload") {
107
+ if (!leaving && cached[3] === "preload" && intent !== "preload") {
90
108
  cached[0] = now;
91
109
  }
92
110
  let res = cached[1];
@@ -95,7 +113,7 @@ export function query(fn, name) {
95
113
  "then" in cached[1]
96
114
  ? cached[1].then(handleResponse(false), handleResponse(true))
97
115
  : handleResponse(false)(cached[1]);
98
- !isServer && intent === "navigate" && startTransition(() => cached[4][1](cached[0])); // update version
116
+ !leaving && !isServer && intent === "navigate" && cached[4][1](cached[0]); // update version
99
117
  }
100
118
  inPreloadFn && "then" in res && res.catch(() => { });
101
119
  return res;
@@ -112,10 +130,10 @@ export function query(fn, name) {
112
130
  cached[0] = now;
113
131
  cached[1] = res;
114
132
  cached[3] = intent;
115
- !isServer && intent === "navigate" && startTransition(() => cached[4][1](cached[0])); // update version
133
+ !isServer && intent === "navigate" && cached[4][1](cached[0]); // update version
116
134
  }
117
135
  else {
118
- cache.set(key, (cached = [now, res, , intent, createSignal(now)]));
136
+ cache.set(key, (cached = [now, res, , intent, createSignal(now, { ownedWrite: true })]));
119
137
  cached[4].count = 0;
120
138
  }
121
139
  if (tracking) {
@@ -145,6 +163,14 @@ export function query(fn, name) {
145
163
  return res;
146
164
  function handleResponse(error) {
147
165
  return async (v) => {
166
+ let enveloped;
167
+ let hasEnveloped = false;
168
+ if (isResponseEnvelope(v)) {
169
+ // respond(): the value rides in memory beside the metadata
170
+ enveloped = v.value;
171
+ hasEnveloped = true;
172
+ v = v.response;
173
+ }
148
174
  if (v instanceof Response) {
149
175
  const e = getRequestEvent();
150
176
  if (e) {
@@ -157,19 +183,38 @@ export function query(fn, name) {
157
183
  }
158
184
  const url = v.headers.get(LocationHeader);
159
185
  if (url !== null) {
186
+ // invalidate the redirect's revalidation keys before navigating so
187
+ // the destination's preloads see the miss and fetch fresh (#580 thread)
188
+ const keys = !isServer && v.headers.get(REVALIDATE_HEADER)?.split(",");
189
+ keys && cacheKeyOp(keys, entry => (entry[0] = 0));
160
190
  // client + server relative redirect
161
- if (navigate && url.startsWith("/"))
162
- startTransition(() => {
163
- navigate(url, { replace: true });
164
- });
191
+ const soft = navigate && url.startsWith("/");
192
+ if (soft)
193
+ navigate(url, { replace: true });
165
194
  else if (!isServer)
166
195
  window.location.href = url;
167
196
  else if (e)
168
197
  e.response.status = 302;
169
- return;
198
+ // sweep the live signals inside the same transition as the
199
+ // navigation: surviving consumers (shared layouts) refetch and
200
+ // hold the commit, so the destination never paints stale data
201
+ keys && revalidate(keys, false);
202
+ // Hold the read pending on the client: the navigation unmounts this
203
+ // consumer, and resolving `undefined` instead hands a missing value
204
+ // to whatever renders before the transition commits (a downstream
205
+ // `.map()` crashes). The server keeps resolving — its render can't
206
+ // await a navigation and the 302 discards the body anyway.
207
+ return isServer ? undefined : new Promise(() => { });
208
+ }
209
+ if (hasEnveloped)
210
+ v = enveloped;
211
+ else if (v.body) {
212
+ // responses the transport hands over whole (revalidation) carry a
213
+ // codec-encoded body; anything else (a raw user Response) stays whole
214
+ const decoded = await decodeResponse(v);
215
+ if (decoded !== undefined)
216
+ v = decoded;
170
217
  }
171
- if (v.customBody)
172
- v = await v.customBody();
173
218
  }
174
219
  if (error)
175
220
  throw v;
@@ -197,14 +242,12 @@ query.set = (key, value) => {
197
242
  cached[3] = "preload";
198
243
  }
199
244
  else {
200
- cache.set(key, (cached = [now, Promise.resolve(value), value, "preload", createSignal(now)]));
245
+ cache.set(key, (cached = [now, Promise.resolve(value), value, "preload", createSignal(now, { ownedWrite: true })]));
201
246
  cached[4].count = 0;
202
247
  }
203
248
  };
204
249
  query.delete = (key) => getCache().delete(key);
205
250
  query.clear = () => getCache().clear();
206
- /** @deprecated use query instead */
207
- export const cache = query;
208
251
  function matchKey(key, keys) {
209
252
  for (let k of keys) {
210
253
  if (k && key.startsWith(k))
@@ -0,0 +1 @@
1
+ export { submitServerForm } from "./action.js";
@@ -0,0 +1,5 @@
1
+ // The delegation fallback's lazy entry (see data/events.ts). Nothing imports
2
+ // this module statically — it exists so the dynamic import has a target that
3
+ // bundlers can keep as a split point: router-only apps never load the action
4
+ // machinery unless a server-action form actually submits.
5
+ export { submitServerForm } from "./action.js";
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,7 +1,28 @@
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
- export * from "./components.jsx";
3
21
  export * from "./lifecycle.js";
4
- export { useHref, useIsRouting, useLocation, useMatch, useCurrentMatches, useNavigate, useParams, useResolvedPath, useSearchParams, useBeforeLeave, usePreloadRoute, RouterContextObj as RouterContext } from "./routing.js";
22
+ export { useHref, useIsRouting, useLinkState, useLocation, useMatch, useNavigate, usePreloadRoute, useParams, useResolvedPath, useRouteMatches, useSearchParams, RouterContextObj as RouterContext } from "./routing.js";
23
+ export type { LinkState } from "./routing.js";
5
24
  export { mergeSearchString as _mergeSearchString } from "./utils.js";
25
+ export { int } from "./paths.js";
26
+ export type { RoutePaths, PathParamsOf, PathEnd, TypedMatchFilter, DefaultSearchTypes } from "./paths.js";
6
27
  export * from "./data/index.js";
7
- export type { Location, LocationChange, SearchParams, MatchFilter, MatchFilters, NavigateOptions, Navigator, OutputMatch, Params, PathMatch, RouteSectionProps, RoutePreloadFunc, RoutePreloadFuncArgs, RouteDefinition, RouteDescription, RouteMatch, RouterIntegration, RouterUtils, SetParams, Submission, BeforeLeaveEventArgs, RouteLoadFunc, RouteLoadFuncArgs, RouterResponseInit, CustomResponse } 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";