@solidjs/router 0.10.9 → 0.11.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.
package/README.md CHANGED
@@ -565,13 +565,13 @@ Are used to injecting the optimistic updates while actions are in flight. They e
565
565
 
566
566
  ```jsx
567
567
  type Submission<T, U> = {
568
- input: T;
569
- result: U;
570
- error: any;
571
- pending: boolean
572
- clear: () => {}
573
- retry: () => {}
574
- }
568
+ readonly input: T;
569
+ readonly result?: U;
570
+ readonly pending: boolean;
571
+ readonly url: string;
572
+ clear: () => void;
573
+ retry: () => void;
574
+ };
575
575
 
576
576
  const submissions = useSubmissions(action, (input) => filter(input));
577
577
  const submission = useSubmission(action, (input) => filter(input));
@@ -721,7 +721,7 @@ This is the main Router component for the browser.
721
721
 
722
722
  ### `<A>`
723
723
 
724
- Like the `<a>` tag but supports relative paths and active class styling (requires client side JavaScript).
724
+ Like the `<a>` tag but supports automatic apply of base path + relative paths and active class styling (requires client side JavaScript).
725
725
 
726
726
  The `<A>` tag has an `active` class if its href matches the current location, and `inactive` otherwise. **Note:** By default matching includes locations that are descendents (eg. href `/users` matches locations `/users` and `/users/123`), use the boolean `end` prop to prevent matching these. This is particularly useful for links to the root route `/` which would match everything.
727
727
 
@@ -1,5 +1,5 @@
1
1
  import type { JSX } from "solid-js";
2
- import type { Location, Navigator } from "./types";
2
+ import type { Location, Navigator } from "./types.js";
3
3
  declare module "solid-js" {
4
4
  namespace JSX {
5
5
  interface AnchorHTMLAttributes<T> {
@@ -1,6 +1,6 @@
1
1
  import { createMemo, mergeProps, splitProps } from "solid-js";
2
- import { useHref, useLocation, useNavigate, useResolvedPath } from "./routing";
3
- import { normalizePath } from "./utils";
2
+ import { useHref, useLocation, useNavigate, useResolvedPath } from "./routing.js";
3
+ import { normalizePath } from "./utils.js";
4
4
  export function A(props) {
5
5
  props = mergeProps({ inactiveClass: "inactive", activeClass: "active" }, props);
6
6
  const [, rest] = splitProps(props, [
@@ -17,17 +17,17 @@ export function A(props) {
17
17
  const isActive = createMemo(() => {
18
18
  const to_ = to();
19
19
  if (to_ === undefined)
20
- return false;
20
+ return [false, false];
21
21
  const path = normalizePath(to_.split(/[?#]/, 1)[0]).toLowerCase();
22
22
  const loc = normalizePath(location.pathname).toLowerCase();
23
- return props.end ? path === loc : loc.startsWith(path);
23
+ return [props.end ? path === loc : loc.startsWith(path), path === loc];
24
24
  });
25
25
  return (<a {...rest} href={href() || props.href} state={JSON.stringify(props.state)} classList={{
26
26
  ...(props.class && { [props.class]: true }),
27
- [props.inactiveClass]: !isActive(),
28
- [props.activeClass]: isActive(),
27
+ [props.inactiveClass]: !isActive()[0],
28
+ [props.activeClass]: isActive()[0],
29
29
  ...rest.classList
30
- }} link aria-current={isActive() ? "page" : undefined}/>);
30
+ }} link aria-current={isActive()[1] ? "page" : undefined}/>);
31
31
  }
32
32
  export function Navigate(props) {
33
33
  const navigate = useNavigate();
@@ -1,5 +1,5 @@
1
1
  import { JSX } from "solid-js";
2
- import { Submission } from "../types";
2
+ import { Submission } from "../types.js";
3
3
  export type Action<T extends Array<any>, U> = (T extends [FormData] | [] ? JSX.SerializableAttributeValue : unknown) & ((...vars: T) => Promise<U>) & {
4
4
  url: string;
5
5
  with<A extends any[], B extends any[]>(this: (this: any, ...args: [...A, ...B]) => Promise<U>, ...args: A): Action<B, U>;
@@ -1,8 +1,8 @@
1
1
  import { $TRACK, createMemo, createSignal, onCleanup, getOwner } from "solid-js";
2
2
  import { isServer } from "solid-js/web";
3
- import { useRouter } from "../routing";
4
- import { redirectStatusCodes } from "../utils";
5
- import { cacheKeyOp, hashKey, revalidate } from "./cache";
3
+ import { useRouter } from "../routing.js";
4
+ import { redirectStatusCodes, mockBase } from "../utils.js";
5
+ import { cacheKeyOp, hashKey, revalidate } from "./cache.js";
6
6
  export const actions = /* #__PURE__ */ new Map();
7
7
  export function useSubmissions(fn, filter) {
8
8
  const router = useRouter();
@@ -80,7 +80,7 @@ function toAction(fn, url) {
80
80
  const newFn = function (...passedArgs) {
81
81
  return fn.call(this, ...args, ...passedArgs);
82
82
  };
83
- const uri = new URL(url, "http://sar");
83
+ const uri = new URL(url, mockBase);
84
84
  uri.searchParams.set("args", hashKey(args));
85
85
  return toAction(newFn, (uri.origin === "https://action" ? uri.origin : "") + uri.pathname + uri.search);
86
86
  };
@@ -1,6 +1,5 @@
1
- import { type Signal } from "solid-js";
2
1
  import { type ReconcileOptions } from "solid-js/store";
3
- type CacheEntry = [number, any, string, Set<Signal<number>>];
2
+ import { CacheEntry } from "../types.js";
4
3
  export declare function revalidate(key?: string | string[] | void, force?: boolean): Promise<void>;
5
4
  export declare function cacheKeyOp(key: string | string[] | void, fn: (cacheEntry: CacheEntry) => void): void;
6
5
  export type CachedFunction<T extends (...args: any) => U | Response, U> = T & {
@@ -10,7 +9,6 @@ export type CachedFunction<T extends (...args: any) => U | Response, U> = T & {
10
9
  export declare function cache<T extends (...args: any) => U | Response, U>(fn: T, name: string, options?: ReconcileOptions): CachedFunction<T, U>;
11
10
  export declare namespace cache {
12
11
  var set: (key: string, value: any) => void;
13
- var clear: () => any;
12
+ var clear: () => void;
14
13
  }
15
14
  export declare function hashKey<T extends Array<any>>(args: T): string;
16
- export {};
@@ -1,8 +1,8 @@
1
- import { createSignal, getOwner, onCleanup, sharedConfig, startTransition } from "solid-js";
1
+ import { createSignal, getListener, getOwner, onCleanup, sharedConfig, startTransition } from "solid-js";
2
2
  import { createStore, reconcile } from "solid-js/store";
3
3
  import { getRequestEvent, isServer } from "solid-js/web";
4
- import { useNavigate, getIntent } from "../routing";
5
- import { redirectStatusCodes } from "../utils";
4
+ import { useNavigate, getIntent } from "../routing.js";
5
+ import { redirectStatusCodes } from "../utils.js";
6
6
  const LocationHeader = "Location";
7
7
  const PRELOAD_TIMEOUT = 5000;
8
8
  const CACHE_TIMEOUT = 180000;
@@ -12,7 +12,7 @@ if (!isServer) {
12
12
  setInterval(() => {
13
13
  const now = Date.now();
14
14
  for (let [k, v] of cacheMap.entries()) {
15
- if (!v[3].size && now - v[0] > CACHE_TIMEOUT) {
15
+ if (!v[3].count && now - v[0] > CACHE_TIMEOUT) {
16
16
  cacheMap.delete(k);
17
17
  }
18
18
  }
@@ -21,17 +21,17 @@ if (!isServer) {
21
21
  function getCache() {
22
22
  if (!isServer)
23
23
  return cacheMap;
24
- const req = getRequestEvent() || sharedConfig.context;
24
+ const req = getRequestEvent();
25
25
  if (!req)
26
26
  throw new Error("Cannot find cache context");
27
- return req.routerCache || (req.routerCache = new Map());
27
+ return (req.router || (req.router = {})).cache || (req.router.cache = new Map());
28
28
  }
29
29
  export function revalidate(key, force = true) {
30
30
  return startTransition(() => {
31
31
  const now = Date.now();
32
32
  cacheKeyOp(key, entry => {
33
33
  force && (entry[0] = 0); //force cache miss
34
- revalidateSignals(entry[3], now); // retrigger live signals
34
+ entry[3][1](now); // retrigger live signals
35
35
  });
36
36
  });
37
37
  }
@@ -42,12 +42,11 @@ export function cacheKeyOp(key, fn) {
42
42
  fn(cacheMap.get(k));
43
43
  }
44
44
  }
45
- function revalidateSignals(set, time) {
46
- for (let s of set)
47
- s[1](time);
48
- }
49
45
  export function cache(fn, name, options) {
50
46
  const [store, setStore] = createStore({});
47
+ // prioritize GET for server functions
48
+ if (fn.GET)
49
+ fn = fn.GET;
51
50
  const cachedFn = ((...args) => {
52
51
  const cache = getCache();
53
52
  const intent = getIntent();
@@ -56,16 +55,20 @@ export function cache(fn, name, options) {
56
55
  const now = Date.now();
57
56
  const key = name + hashKey(args);
58
57
  let cached = cache.get(key);
59
- let version;
60
- if (owner && !isServer) {
61
- version = createSignal(now, {
62
- equals: (p, v) => v - p < 50 // margin of error
63
- });
64
- onCleanup(() => cached[3].delete(version));
65
- version[0](); // track it;
58
+ let tracking;
59
+ if (getListener() && !isServer) {
60
+ tracking = true;
61
+ onCleanup(() => cached[3].count--);
66
62
  }
67
- if (cached && (isServer || intent === "native" || Date.now() - cached[0] < PRELOAD_TIMEOUT)) {
68
- version && cached[3].add(version);
63
+ if (cached &&
64
+ (isServer ||
65
+ intent === "native" ||
66
+ (cached[0] && cached[3].count) ||
67
+ Date.now() - cached[0] < PRELOAD_TIMEOUT)) {
68
+ if (tracking) {
69
+ cached[3].count++;
70
+ cached[3][0](); // track
71
+ }
69
72
  if (cached[2] === "preload" && intent !== "preload") {
70
73
  cached[0] = now;
71
74
  }
@@ -75,7 +78,7 @@ export function cache(fn, name, options) {
75
78
  "then" in cached[1]
76
79
  ? cached[1].then(handleResponse(false), handleResponse(true))
77
80
  : handleResponse(false)(cached[1]);
78
- !isServer && intent === "navigate" && startTransition(() => revalidateSignals(cached[3], cached[0])); // update version
81
+ !isServer && intent === "navigate" && startTransition(() => cached[3][1](cached[0])); // update version
79
82
  }
80
83
  return res;
81
84
  }
@@ -91,13 +94,16 @@ export function cache(fn, name, options) {
91
94
  cached[0] = now;
92
95
  cached[1] = res;
93
96
  cached[2] = intent;
94
- version && cached[3].add(version);
95
- if (!isServer && intent === "navigate") {
96
- startTransition(() => revalidateSignals(cached[3], cached[0])); // update version
97
- }
97
+ !isServer && intent === "navigate" && startTransition(() => cached[3][1](cached[0])); // update version
98
+ }
99
+ else {
100
+ cache.set(key, (cached = [now, res, intent, createSignal(now)]));
101
+ cached[3].count = 0;
102
+ }
103
+ if (tracking) {
104
+ cached[3].count++;
105
+ cached[3][0](); // track
98
106
  }
99
- else
100
- cache.set(key, (cached = [now, res, intent, new Set(version ? [version] : [])]));
101
107
  if (intent !== "preload") {
102
108
  res =
103
109
  "then" in res
@@ -106,22 +112,26 @@ export function cache(fn, name, options) {
106
112
  }
107
113
  return res;
108
114
  function handleResponse(error) {
109
- return (v) => {
110
- if (v instanceof Response && redirectStatusCodes.has(v.status)) {
111
- if (navigate) {
112
- startTransition(() => {
113
- let url = v.headers.get(LocationHeader);
114
- if (url && url.startsWith("/")) {
115
- navigate(url, {
116
- replace: true
117
- });
118
- }
119
- else if (!isServer && url) {
120
- window.location.href = url;
121
- }
122
- });
115
+ return async (v) => {
116
+ if (v instanceof Response) {
117
+ if (redirectStatusCodes.has(v.status)) {
118
+ if (navigate) {
119
+ startTransition(() => {
120
+ let url = v.headers.get(LocationHeader);
121
+ if (url && url.startsWith("/")) {
122
+ navigate(url, {
123
+ replace: true
124
+ });
125
+ }
126
+ else if (!isServer && url) {
127
+ window.location.href = url;
128
+ }
129
+ });
130
+ }
131
+ return;
123
132
  }
124
- return;
133
+ if (v.customBody)
134
+ v = await v.customBody();
125
135
  }
126
136
  if (error)
127
137
  throw v;
@@ -140,21 +150,15 @@ cache.set = (key, value) => {
140
150
  const cache = getCache();
141
151
  const now = Date.now();
142
152
  let cached = cache.get(key);
143
- let version;
144
- if (getOwner()) {
145
- version = createSignal(now, {
146
- equals: (p, v) => v - p < 50 // margin of error
147
- });
148
- onCleanup(() => cached[3].delete(version));
149
- }
150
153
  if (cached) {
151
154
  cached[0] = now;
152
155
  cached[1] = value;
153
156
  cached[2] = "preload";
154
- version && cached[3].add(version);
155
157
  }
156
- else
157
- cache.set(key, (cached = [now, value, , new Set(version ? [version] : [])]));
158
+ else {
159
+ cache.set(key, (cached = [now, value, , createSignal(now)]));
160
+ cached[3].count = 0;
161
+ }
158
162
  };
159
163
  cache.clear = () => getCache().clear();
160
164
  function matchKey(key, keys) {
@@ -1,2 +1,2 @@
1
- import type { RouterContext } from "../types";
1
+ import type { RouterContext } from "../types.js";
2
2
  export declare function setupNativeEvents(preload?: boolean, explicitLinks?: boolean, actionBase?: string): (router: RouterContext) => void;
@@ -1,6 +1,7 @@
1
1
  import { delegateEvents } from "solid-js/web";
2
2
  import { onCleanup } from "solid-js";
3
- import { actions } from "./action";
3
+ import { actions } from "./action.js";
4
+ import { mockBase } from "../utils.js";
4
5
  export function setupNativeEvents(preload = true, explicitLinks = false, actionBase = "/_server") {
5
6
  return (router) => {
6
7
  const basePath = router.base.path();
@@ -20,7 +21,7 @@ export function setupNativeEvents(preload = true, explicitLinks = false, actionB
20
21
  const a = evt
21
22
  .composedPath()
22
23
  .find(el => el instanceof Node && el.nodeName.toUpperCase() === "A");
23
- if (!a || (explicitLinks && !a.getAttribute("link")))
24
+ if (!a || (explicitLinks && !a.hasAttribute("link")))
24
25
  return;
25
26
  const svg = isSvg(a);
26
27
  const href = svg ? a.href.baseVal : a.href;
@@ -83,12 +84,13 @@ export function setupNativeEvents(preload = true, explicitLinks = false, actionB
83
84
  }
84
85
  function handleFormSubmit(evt) {
85
86
  let actionRef = evt.submitter && evt.submitter.hasAttribute("formaction")
86
- ? evt.submitter.formAction
87
- : evt.target.action;
87
+ ? evt.submitter.getAttribute("formaction")
88
+ : evt.target.getAttribute("action");
88
89
  if (!actionRef)
89
90
  return;
90
91
  if (!actionRef.startsWith("https://action/")) {
91
- const url = new URL(actionRef);
92
+ // normalize server actions
93
+ const url = new URL(actionRef, mockBase);
92
94
  actionRef = router.parsePath(url.pathname + url.search);
93
95
  if (!actionRef.startsWith(actionBase))
94
96
  return;
@@ -1,4 +1,4 @@
1
- export { createAsync } from "./createAsync";
2
- export { action, useSubmission, useSubmissions, useAction, type Action } from "./action";
3
- export { cache, revalidate, type CachedFunction } from "./cache";
4
- export { redirect, reload } from "./response";
1
+ export { createAsync } from "./createAsync.js";
2
+ export { action, useSubmission, useSubmissions, useAction, type Action } from "./action.js";
3
+ export { cache, revalidate, type CachedFunction } from "./cache.js";
4
+ export { redirect, reload, json } from "./response.js";
@@ -1,4 +1,4 @@
1
- export { createAsync } from "./createAsync";
2
- export { action, useSubmission, useSubmissions, useAction } from "./action";
3
- export { cache, revalidate } from "./cache";
4
- export { redirect, reload } from "./response";
1
+ export { createAsync } from "./createAsync.js";
2
+ export { action, useSubmission, useSubmissions, useAction } from "./action.js";
3
+ export { cache, revalidate } from "./cache.js";
4
+ export { redirect, reload, json } from "./response.js";
@@ -1,5 +1,6 @@
1
1
  export type RouterResponseInit = ResponseInit & {
2
2
  revalidate?: string | string[];
3
3
  };
4
- export declare function redirect(url: string, init?: number | RouterResponseInit): Response;
5
- export declare function reload(init: RouterResponseInit): Response;
4
+ export declare function redirect(url: string, init?: number | RouterResponseInit): never;
5
+ export declare function reload(init: RouterResponseInit): never;
6
+ export declare function json<T>(data: T, init?: Omit<ResponseInit, "body">): T;
@@ -23,6 +23,18 @@ export function reload(init) {
23
23
  const { revalidate, ...responseInit } = init;
24
24
  return new Response(null, {
25
25
  ...responseInit,
26
- ...(revalidate ? { headers: new Headers(responseInit.headers).set("X-Revalidate", revalidate.toString()) } : {})
26
+ ...(revalidate
27
+ ? { headers: new Headers(responseInit.headers).set("X-Revalidate", revalidate.toString()) }
28
+ : {})
27
29
  });
28
30
  }
31
+ export function json(data, init) {
32
+ const headers = new Headers((init || {}).headers);
33
+ headers.set("Content-Type", "application/json");
34
+ const response = new Response(JSON.stringify(data), {
35
+ ...init,
36
+ headers
37
+ });
38
+ response.customBody = () => data;
39
+ return response;
40
+ }
package/dist/index.d.ts CHANGED
@@ -1,7 +1,7 @@
1
- export * from "./routers";
2
- export * from "./components";
3
- export * from "./lifecycle";
4
- export { useHref, useIsRouting, useLocation, useMatch, useNavigate, useParams, useResolvedPath, useSearchParams, useBeforeLeave, } from "./routing";
5
- export { mergeSearchString as _mergeSearchString } from "./utils";
6
- export * from "./data";
7
- export type { Location, LocationChange, NavigateOptions, Navigator, OutputMatch, Params, RouteSectionProps, RouteLoadFunc, RouteLoadFuncArgs, RouteDefinition, RouterIntegration, RouterUtils, SetParams, BeforeLeaveEventArgs } from "./types";
1
+ export * from "./routers/index.js";
2
+ export * from "./components.jsx";
3
+ export * from "./lifecycle.js";
4
+ export { useHref, useIsRouting, useLocation, useMatch, useNavigate, useParams, useResolvedPath, useSearchParams, useBeforeLeave, } from "./routing.js";
5
+ export { mergeSearchString as _mergeSearchString } from "./utils.js";
6
+ export * from "./data/index.js";
7
+ export type { Location, LocationChange, NavigateOptions, Navigator, OutputMatch, Params, RouteSectionProps, RouteLoadFunc, RouteLoadFuncArgs, RouteDefinition, RouterIntegration, RouterUtils, SetParams, BeforeLeaveEventArgs } from "./types.js";