@solidjs/router 1.0.0-next.8 → 1.0.0

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 (58) hide show
  1. package/README.md +715 -531
  2. package/dist/components.d.ts +31 -0
  3. package/dist/components.jsx +40 -0
  4. package/dist/data/action.d.ts +11 -40
  5. package/dist/data/action.js +100 -274
  6. package/dist/data/createAsync.d.ts +32 -0
  7. package/dist/data/createAsync.js +96 -0
  8. package/dist/data/events.d.ts +0 -8
  9. package/dist/data/events.js +23 -24
  10. package/dist/data/index.d.ts +4 -2
  11. package/dist/data/index.js +4 -2
  12. package/dist/data/query.d.ts +3 -1
  13. package/dist/data/query.js +22 -39
  14. package/dist/data/response.d.ts +4 -0
  15. package/dist/data/response.js +42 -0
  16. package/dist/index.d.ts +3 -5
  17. package/dist/index.js +1024 -1578
  18. package/dist/index.jsx +2 -2
  19. package/dist/lifecycle.d.ts +4 -29
  20. package/dist/lifecycle.js +37 -40
  21. package/dist/routers/HashRouter.d.ts +9 -0
  22. package/dist/routers/HashRouter.js +41 -0
  23. package/dist/routers/MemoryRouter.d.ts +24 -0
  24. package/dist/routers/MemoryRouter.js +57 -0
  25. package/dist/routers/Router.d.ts +17 -0
  26. package/dist/routers/Router.js +59 -0
  27. package/dist/routers/StaticRouter.d.ts +6 -0
  28. package/dist/routers/StaticRouter.js +15 -0
  29. package/dist/routers/components.d.ts +26 -12
  30. package/dist/routers/components.jsx +54 -68
  31. package/dist/routers/createRouter.d.ts +10 -0
  32. package/dist/routers/createRouter.js +41 -0
  33. package/dist/routers/index.d.ts +11 -4
  34. package/dist/routers/index.js +6 -2
  35. package/dist/routers/scrollRestoration.d.ts +32 -0
  36. package/dist/routers/scrollRestoration.js +96 -0
  37. package/dist/routing.d.ts +52 -88
  38. package/dist/routing.js +166 -381
  39. package/dist/types.d.ts +33 -83
  40. package/dist/utils.d.ts +0 -2
  41. package/dist/utils.js +0 -2
  42. package/package.json +7 -11
  43. package/dist/claims.d.ts +0 -21
  44. package/dist/claims.js +0 -115
  45. package/dist/data/flash.d.ts +0 -21
  46. package/dist/data/flash.js +0 -78
  47. package/dist/data/flashCookie.d.ts +0 -7
  48. package/dist/data/flashCookie.js +0 -20
  49. package/dist/data/serverForms.d.ts +0 -1
  50. package/dist/data/serverForms.js +0 -5
  51. package/dist/paths.d.ts +0 -117
  52. package/dist/paths.js +0 -41
  53. package/dist/routers/factory.d.ts +0 -45
  54. package/dist/routers/factory.jsx +0 -143
  55. package/dist/routers/history.d.ts +0 -24
  56. package/dist/routers/history.js +0 -180
  57. package/dist/server.d.ts +0 -75
  58. package/dist/server.js +0 -264
@@ -0,0 +1,96 @@
1
+ /**
2
+ * This is mock of the eventual Solid 2.0 primitive. It is not fully featured.
3
+ */
4
+ import { createResource, sharedConfig, untrack, catchError } from "solid-js";
5
+ import { createStore, reconcile, unwrap } from "solid-js/store";
6
+ import { isServer } from "solid-js/web";
7
+ import { setFunctionName } from "../utils.js";
8
+ export function createAsync(fn, options) {
9
+ let resource;
10
+ let prev = () => !resource || resource.state === "unresolved" ? undefined : resource.latest;
11
+ [resource] = createResource(() => subFetch(fn, catchError(() => untrack(prev), () => undefined)), v => v, options);
12
+ const resultAccessor = (() => resource());
13
+ if (options?.name)
14
+ setFunctionName(resultAccessor, options.name);
15
+ Object.defineProperty(resultAccessor, "latest", {
16
+ get() {
17
+ return resource.latest;
18
+ }
19
+ });
20
+ return resultAccessor;
21
+ }
22
+ export function createAsyncStore(fn, options = {}) {
23
+ let resource;
24
+ let prev = () => !resource || resource.state === "unresolved"
25
+ ? undefined
26
+ : unwrap(resource.latest);
27
+ [resource] = createResource(() => subFetch(fn, catchError(() => untrack(prev), () => undefined)), v => v, {
28
+ ...options,
29
+ storage: (init) => createDeepSignal(init, options.reconcile)
30
+ });
31
+ const resultAccessor = (() => resource());
32
+ Object.defineProperty(resultAccessor, "latest", {
33
+ get() {
34
+ return resource.latest;
35
+ }
36
+ });
37
+ return resultAccessor;
38
+ }
39
+ function createDeepSignal(value, options) {
40
+ const [store, setStore] = createStore({
41
+ value: structuredClone(value)
42
+ });
43
+ return [
44
+ () => store.value,
45
+ (v) => {
46
+ typeof v === "function" && (v = v());
47
+ setStore("value", reconcile(structuredClone(v), options));
48
+ return store.value;
49
+ }
50
+ ];
51
+ }
52
+ // mock promise while hydrating to prevent fetching
53
+ class MockPromise {
54
+ static all() {
55
+ return new MockPromise();
56
+ }
57
+ static allSettled() {
58
+ return new MockPromise();
59
+ }
60
+ static any() {
61
+ return new MockPromise();
62
+ }
63
+ static race() {
64
+ return new MockPromise();
65
+ }
66
+ static reject() {
67
+ return new MockPromise();
68
+ }
69
+ static resolve() {
70
+ return new MockPromise();
71
+ }
72
+ catch() {
73
+ return new MockPromise();
74
+ }
75
+ then() {
76
+ return new MockPromise();
77
+ }
78
+ finally() {
79
+ return new MockPromise();
80
+ }
81
+ }
82
+ function subFetch(fn, prev) {
83
+ if (isServer || !sharedConfig.context)
84
+ return fn(prev);
85
+ const ogFetch = fetch;
86
+ const ogPromise = Promise;
87
+ try {
88
+ window.fetch = () => new MockPromise();
89
+ Promise = MockPromise;
90
+ return fn(prev);
91
+ }
92
+ finally {
93
+ window.fetch = ogFetch;
94
+ Promise = ogPromise;
95
+ }
96
+ }
@@ -1,12 +1,4 @@
1
1
  import type { RouterContext } from "../types.js";
2
- /**
3
- * The submit delegation consults this slot instead of importing the action
4
- * module: the action side installs its handler on first action creation
5
- * (see data/action.ts), so an app that never creates an action never pulls
6
- * the data layer into its bundle through the router's event wiring.
7
- */
8
- export type RouterFormHandler = (evt: SubmitEvent, router: RouterContext, actionBase: string) => void;
9
- export declare function setRouterFormHandler(handler: RouterFormHandler | undefined): void;
10
2
  type NativeEventConfig = {
11
3
  preload?: boolean;
12
4
  explicitLinks?: boolean;
@@ -1,9 +1,7 @@
1
- import { delegateEvents } from "@solidjs/web";
1
+ import { delegateEvents } from "solid-js/web";
2
2
  import { onCleanup } from "solid-js";
3
- let formHandler;
4
- export function setRouterFormHandler(handler) {
5
- formHandler = handler;
6
- }
3
+ import { actions } from "./action.js";
4
+ import { mockBase } from "../utils.js";
7
5
  export function setupNativeEvents({ preload = true, explicitLinks = false, actionBase = "/_server", transformUrl } = {}) {
8
6
  return (router) => {
9
7
  const basePath = router.base.path();
@@ -78,29 +76,30 @@ export function setupNativeEvents({ preload = true, explicitLinks = false, actio
78
76
  }, 20);
79
77
  }
80
78
  function handleFormSubmit(evt) {
81
- if (formHandler)
82
- return formHandler(evt, router, actionBase);
83
- // No form handler means no action module in the client graph at all
84
- // (e.g. server components binding forms straight to server functions).
85
- // A POST to a url under actionBase is self-describing, so delegation
86
- // is still sufficient: intercept synchronously — the no-JS treatment
87
- // is reserved for clients with no JS — capture the FormData, and load
88
- // the handler lazily. Apps that never submit one never load it.
89
79
  if (evt.defaultPrevented)
90
80
  return;
91
- const form = evt.target;
92
- const ref = evt.submitter && evt.submitter.hasAttribute("formaction")
81
+ let actionRef = evt.submitter && evt.submitter.hasAttribute("formaction")
93
82
  ? evt.submitter.getAttribute("formaction")
94
- : form.getAttribute("action");
95
- if (!ref || ref.startsWith("https://action/"))
96
- return;
97
- const url = new URL(ref, document.baseURI);
98
- const path = router.parsePath(url.pathname + url.search);
99
- if (!path.startsWith(actionBase) || form.method.toUpperCase() !== "POST")
83
+ : evt.target.getAttribute("action");
84
+ if (!actionRef)
100
85
  return;
101
- evt.preventDefault();
102
- const data = new FormData(form, evt.submitter);
103
- import("./serverForms.js").then(m => m.submitServerForm(router, path, form, data));
86
+ if (!actionRef.startsWith("https://action/")) {
87
+ // normalize server actions
88
+ const url = new URL(actionRef, mockBase);
89
+ actionRef = router.parsePath(url.pathname + url.search);
90
+ if (!actionRef.startsWith(actionBase))
91
+ return;
92
+ }
93
+ if (evt.target.method.toUpperCase() !== "POST")
94
+ throw new Error("Only POST forms are supported for Actions");
95
+ const handler = actions.get(actionRef);
96
+ if (handler) {
97
+ evt.preventDefault();
98
+ const data = new FormData(evt.target, evt.submitter);
99
+ handler.call({ r: router, f: evt.target }, evt.target.enctype === "multipart/form-data"
100
+ ? data
101
+ : new URLSearchParams(data));
102
+ }
104
103
  }
105
104
  // ensure delegated event run first
106
105
  delegateEvents(["click", "submit"]);
@@ -1,2 +1,4 @@
1
- export { action, useSubmissions, useAction, type Action } from "./action.js";
2
- export { query, revalidate, type CachedFunction } from "./query.js";
1
+ export { createAsync, createAsyncStore, type AccessorWithLatest } from "./createAsync.js";
2
+ export { action, useSubmission, useSubmissions, useAction, type Action } from "./action.js";
3
+ export { query, revalidate, cache, type CachedFunction } from "./query.js";
4
+ export { redirect, reload, json } from "./response.js";
@@ -1,2 +1,4 @@
1
- export { action, useSubmissions, useAction } from "./action.js";
2
- export { query, revalidate } from "./query.js";
1
+ export { createAsync, createAsyncStore } from "./createAsync.js";
2
+ export { action, useSubmission, useSubmissions, useAction } from "./action.js";
3
+ export { query, revalidate, cache } from "./query.js";
4
+ export { redirect, reload, json } from "./response.js";
@@ -2,7 +2,7 @@ import type { CacheEntry, NarrowResponse } from "../types.js";
2
2
  /**
3
3
  * Revalidates the given cache entry/entries.
4
4
  */
5
- export declare function revalidate(key?: string | string[] | void, force?: boolean): void;
5
+ export declare function revalidate(key?: string | string[] | void, force?: boolean): Promise<void>;
6
6
  export declare function cacheKeyOp(key: string | string[] | void, fn: (cacheEntry: CacheEntry) => void): void;
7
7
  export type CachedFunction<T extends (...args: any) => any> = T extends (...args: infer A) => infer R ? ([] extends {
8
8
  [K in keyof A]-?: A[K];
@@ -18,4 +18,6 @@ export declare namespace query {
18
18
  export var clear: () => void;
19
19
  export { _a as delete };
20
20
  }
21
+ /** @deprecated use query instead */
22
+ export declare const cache: typeof query;
21
23
  export declare function hashKey<T extends Array<any>>(args: T): string;
@@ -1,6 +1,5 @@
1
- import { createSignal, getObserver, getOwner, onCleanup, sharedConfig } from "solid-js";
2
- import { getRequestEvent, isResponseEnvelope, isServer } from "@solidjs/web";
3
- import { GET, decodeResponse, getServerFunctionMetadata, isServerFunction } from "@solidjs/web/server-functions";
1
+ import { createSignal, getListener, getOwner, onCleanup, sharedConfig, startTransition } from "solid-js";
2
+ import { getRequestEvent, isServer } from "solid-js/web";
4
3
  import { useNavigate, getIntent, getInPreloadFn } from "../routing.js";
5
4
  const LocationHeader = "Location";
6
5
  const PRELOAD_TIMEOUT = 5000;
@@ -30,9 +29,11 @@ function getCache() {
30
29
  */
31
30
  export function revalidate(key, force = true) {
32
31
  const now = Date.now();
33
- cacheKeyOp(key, entry => {
34
- force && (entry[0] = 0); //force cache miss
35
- entry[4][1](now); // retrigger live signals
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
36
37
  });
37
38
  }
38
39
  export function cacheKeyOp(key, fn) {
@@ -43,16 +44,9 @@ export function cacheKeyOp(key, fn) {
43
44
  }
44
45
  }
45
46
  export function query(fn, name) {
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
- }
47
+ // prioritize GET for server functions
48
+ if (fn.GET)
49
+ fn = fn.GET;
56
50
  const cachedFn = ((...args) => {
57
51
  const cache = getCache();
58
52
  const intent = getIntent();
@@ -78,7 +72,7 @@ export function query(fn, name) {
78
72
  }
79
73
  }
80
74
  }
81
- if (getObserver() && !isServer) {
75
+ if (getListener() && !isServer) {
82
76
  tracking = true;
83
77
  onCleanup(() => cached[4].count--);
84
78
  }
@@ -101,7 +95,7 @@ export function query(fn, name) {
101
95
  "then" in cached[1]
102
96
  ? cached[1].then(handleResponse(false), handleResponse(true))
103
97
  : handleResponse(false)(cached[1]);
104
- !isServer && intent === "navigate" && cached[4][1](cached[0]); // update version
98
+ !isServer && intent === "navigate" && startTransition(() => cached[4][1](cached[0])); // update version
105
99
  }
106
100
  inPreloadFn && "then" in res && res.catch(() => { });
107
101
  return res;
@@ -118,10 +112,10 @@ export function query(fn, name) {
118
112
  cached[0] = now;
119
113
  cached[1] = res;
120
114
  cached[3] = intent;
121
- !isServer && intent === "navigate" && cached[4][1](cached[0]); // update version
115
+ !isServer && intent === "navigate" && startTransition(() => cached[4][1](cached[0])); // update version
122
116
  }
123
117
  else {
124
- cache.set(key, (cached = [now, res, , intent, createSignal(now, { ownedWrite: true })]));
118
+ cache.set(key, (cached = [now, res, , intent, createSignal(now)]));
125
119
  cached[4].count = 0;
126
120
  }
127
121
  if (tracking) {
@@ -151,14 +145,6 @@ export function query(fn, name) {
151
145
  return res;
152
146
  function handleResponse(error) {
153
147
  return async (v) => {
154
- let enveloped;
155
- let hasEnveloped = false;
156
- if (isResponseEnvelope(v)) {
157
- // respond(): the value rides in memory beside the metadata
158
- enveloped = v.value;
159
- hasEnveloped = true;
160
- v = v.response;
161
- }
162
148
  if (v instanceof Response) {
163
149
  const e = getRequestEvent();
164
150
  if (e) {
@@ -173,22 +159,17 @@ export function query(fn, name) {
173
159
  if (url !== null) {
174
160
  // client + server relative redirect
175
161
  if (navigate && url.startsWith("/"))
176
- navigate(url, { replace: true });
162
+ startTransition(() => {
163
+ navigate(url, { replace: true });
164
+ });
177
165
  else if (!isServer)
178
166
  window.location.href = url;
179
167
  else if (e)
180
168
  e.response.status = 302;
181
169
  return;
182
170
  }
183
- if (hasEnveloped)
184
- v = enveloped;
185
- else if (v.body) {
186
- // responses the transport hands over whole (revalidation) carry a
187
- // codec-encoded body; anything else (a raw user Response) stays whole
188
- const decoded = await decodeResponse(v);
189
- if (decoded !== undefined)
190
- v = decoded;
191
- }
171
+ if (v.customBody)
172
+ v = await v.customBody();
192
173
  }
193
174
  if (error)
194
175
  throw v;
@@ -216,12 +197,14 @@ query.set = (key, value) => {
216
197
  cached[3] = "preload";
217
198
  }
218
199
  else {
219
- cache.set(key, (cached = [now, Promise.resolve(value), value, "preload", createSignal(now, { ownedWrite: true })]));
200
+ cache.set(key, (cached = [now, Promise.resolve(value), value, "preload", createSignal(now)]));
220
201
  cached[4].count = 0;
221
202
  }
222
203
  };
223
204
  query.delete = (key) => getCache().delete(key);
224
205
  query.clear = () => getCache().clear();
206
+ /** @deprecated use query instead */
207
+ export const cache = query;
225
208
  function matchKey(key, keys) {
226
209
  for (let k of keys) {
227
210
  if (k && key.startsWith(k))
@@ -0,0 +1,4 @@
1
+ import type { RouterResponseInit, CustomResponse } from "../types.js";
2
+ export declare function redirect(url: string, init?: number | RouterResponseInit): CustomResponse<never>;
3
+ export declare function reload(init?: RouterResponseInit): CustomResponse<never>;
4
+ export declare function json<T>(data: T, init?: RouterResponseInit): CustomResponse<T>;
@@ -0,0 +1,42 @@
1
+ export function redirect(url, init = 302) {
2
+ let responseInit;
3
+ let revalidate;
4
+ if (typeof init === "number") {
5
+ responseInit = { status: init };
6
+ }
7
+ else {
8
+ ({ revalidate, ...responseInit } = init);
9
+ if (typeof responseInit.status === "undefined") {
10
+ responseInit.status = 302;
11
+ }
12
+ }
13
+ const headers = new Headers(responseInit.headers);
14
+ headers.set("Location", url);
15
+ revalidate !== undefined && headers.set("X-Revalidate", revalidate.toString());
16
+ const response = new Response(null, {
17
+ ...responseInit,
18
+ headers: headers
19
+ });
20
+ return response;
21
+ }
22
+ export function reload(init = {}) {
23
+ const { revalidate, ...responseInit } = init;
24
+ const headers = new Headers(responseInit.headers);
25
+ revalidate !== undefined && headers.set("X-Revalidate", revalidate.toString());
26
+ return new Response(null, {
27
+ ...responseInit,
28
+ headers
29
+ });
30
+ }
31
+ export function json(data, init = {}) {
32
+ const { revalidate, ...responseInit } = init;
33
+ const headers = new Headers(responseInit.headers);
34
+ revalidate !== undefined && headers.set("X-Revalidate", revalidate.toString());
35
+ headers.set("Content-Type", "application/json");
36
+ const response = new Response(JSON.stringify(data), {
37
+ ...responseInit,
38
+ headers
39
+ });
40
+ response.customBody = () => data;
41
+ return response;
42
+ }
package/dist/index.d.ts CHANGED
@@ -1,9 +1,7 @@
1
1
  export * from "./routers/index.js";
2
+ export * from "./components.jsx";
2
3
  export * from "./lifecycle.js";
3
- export { useHref, useIsRouting, useLinkState, useLocation, useMatch, useNavigate, usePreloadRoute, useParams, useResolvedPath, useRouteMatches, useSearchParams, RouterContextObj as RouterContext } from "./routing.js";
4
- export type { LinkState } from "./routing.js";
4
+ export { useHref, useIsRouting, useLocation, useMatch, useCurrentMatches, useNavigate, useParams, useResolvedPath, useSearchParams, useBeforeLeave, usePreloadRoute, RouterContextObj as RouterContext } from "./routing.js";
5
5
  export { mergeSearchString as _mergeSearchString } from "./utils.js";
6
- export { int } from "./paths.js";
7
- export type { RoutePaths, PathParamsOf, PathEnd, TypedMatchFilter } from "./paths.js";
8
6
  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, Submission, BeforeLeaveEventArgs, TypedPath, TypedSearchPath, StandardSchemaV1 } from "./types.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";