@solidjs/router 0.15.4 → 0.16.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/dist/data/response.d.ts +1 -1
- package/dist/index.js +37 -6
- package/dist/routers/HashRouter.d.ts +1 -1
- package/dist/routers/createRouter.d.ts +1 -1
- package/dist/src/components.d.ts +31 -0
- package/dist/src/components.jsx +39 -0
- package/dist/src/data/action.d.ts +17 -0
- package/dist/src/data/action.js +163 -0
- package/dist/src/data/action.spec.d.ts +1 -0
- package/dist/src/data/action.spec.js +297 -0
- package/dist/src/data/createAsync.d.ts +32 -0
- package/dist/src/data/createAsync.js +96 -0
- package/dist/src/data/createAsync.spec.d.ts +1 -0
- package/dist/src/data/createAsync.spec.js +196 -0
- package/dist/src/data/events.d.ts +9 -0
- package/dist/src/data/events.js +123 -0
- package/dist/src/data/events.spec.d.ts +1 -0
- package/dist/src/data/events.spec.js +567 -0
- package/dist/src/data/index.d.ts +4 -0
- package/dist/src/data/index.js +4 -0
- package/dist/src/data/query.d.ts +23 -0
- package/dist/src/data/query.js +232 -0
- package/dist/src/data/query.spec.d.ts +1 -0
- package/dist/src/data/query.spec.js +354 -0
- package/dist/src/data/response.d.ts +4 -0
- package/dist/src/data/response.js +42 -0
- package/dist/src/data/response.spec.d.ts +1 -0
- package/dist/src/data/response.spec.js +165 -0
- package/dist/src/index.d.ts +7 -0
- package/dist/src/index.jsx +6 -0
- package/dist/src/lifecycle.d.ts +5 -0
- package/dist/src/lifecycle.js +69 -0
- package/dist/src/routers/HashRouter.d.ts +9 -0
- package/dist/src/routers/HashRouter.js +41 -0
- package/dist/src/routers/MemoryRouter.d.ts +24 -0
- package/dist/src/routers/MemoryRouter.js +57 -0
- package/dist/src/routers/Router.d.ts +9 -0
- package/dist/src/routers/Router.js +45 -0
- package/dist/src/routers/StaticRouter.d.ts +6 -0
- package/dist/src/routers/StaticRouter.js +15 -0
- package/dist/src/routers/components.d.ts +27 -0
- package/dist/src/routers/components.jsx +118 -0
- package/dist/src/routers/createRouter.d.ts +10 -0
- package/dist/src/routers/createRouter.js +41 -0
- package/dist/src/routers/index.d.ts +11 -0
- package/dist/src/routers/index.js +6 -0
- package/dist/src/routing.d.ts +175 -0
- package/dist/src/routing.js +560 -0
- package/dist/src/types.d.ts +200 -0
- package/dist/src/types.js +1 -0
- package/dist/src/utils.d.ts +13 -0
- package/dist/src/utils.js +185 -0
- package/dist/test/helpers.d.ts +6 -0
- package/dist/test/helpers.js +50 -0
- package/dist/utils.d.ts +1 -1
- package/package.json +1 -1
package/dist/data/response.d.ts
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { RouterResponseInit, CustomResponse } from "../types";
|
|
1
|
+
import type { RouterResponseInit, CustomResponse } from "../types.js";
|
|
2
2
|
export declare function redirect(url: string, init?: number | RouterResponseInit): CustomResponse<never>;
|
|
3
3
|
export declare function reload(init?: RouterResponseInit): CustomResponse<never>;
|
|
4
4
|
export declare function json<T>(data: T, init?: RouterResponseInit): CustomResponse<T>;
|
package/dist/index.js
CHANGED
|
@@ -237,8 +237,18 @@ function expandOptionals(pattern) {
|
|
|
237
237
|
}
|
|
238
238
|
return expandOptionals(suffix).reduce((results, expansion) => [...results, ...prefixes.map(p => p + expansion)], []);
|
|
239
239
|
}
|
|
240
|
+
function setFunctionName(obj, value) {
|
|
241
|
+
Object.defineProperty(obj, "name", {
|
|
242
|
+
value,
|
|
243
|
+
writable: false,
|
|
244
|
+
configurable: false
|
|
245
|
+
});
|
|
246
|
+
return obj;
|
|
247
|
+
}
|
|
240
248
|
|
|
241
249
|
const MAX_REDIRECTS = 100;
|
|
250
|
+
|
|
251
|
+
/** Consider this API opaque and internal. It is likely to change in the future. */
|
|
242
252
|
const RouterContextObj = createContext();
|
|
243
253
|
const RouteContextObj = createContext();
|
|
244
254
|
const useRouter = () => invariant(useContext(RouterContextObj), "<A> and 'use' router primitives can be only used inside a Route.");
|
|
@@ -1323,8 +1333,10 @@ function action(fn, options = {}) {
|
|
|
1323
1333
|
const o = typeof options === "string" ? {
|
|
1324
1334
|
name: options
|
|
1325
1335
|
} : options;
|
|
1326
|
-
const
|
|
1336
|
+
const name = o.name || (!isServer ? String(hashString(fn.toString())) : undefined);
|
|
1337
|
+
const url = fn.url || name && `https://action/${name}` || "";
|
|
1327
1338
|
mutate.base = url;
|
|
1339
|
+
if (name) setFunctionName(mutate, name);
|
|
1328
1340
|
return toAction(mutate, url);
|
|
1329
1341
|
}
|
|
1330
1342
|
function toAction(fn, url) {
|
|
@@ -1386,7 +1398,12 @@ async function handleResponse(response, error, navigate) {
|
|
|
1386
1398
|
} : undefined;
|
|
1387
1399
|
}
|
|
1388
1400
|
|
|
1389
|
-
function setupNativeEvents(
|
|
1401
|
+
function setupNativeEvents({
|
|
1402
|
+
preload = true,
|
|
1403
|
+
explicitLinks = false,
|
|
1404
|
+
actionBase = "/_server",
|
|
1405
|
+
transformUrl
|
|
1406
|
+
} = {}) {
|
|
1390
1407
|
return router => {
|
|
1391
1408
|
const basePath = router.base.path();
|
|
1392
1409
|
const navigateFromRoute = router.navigatorFactory(router.base);
|
|
@@ -1528,7 +1545,12 @@ function Router(props) {
|
|
|
1528
1545
|
});
|
|
1529
1546
|
}
|
|
1530
1547
|
})),
|
|
1531
|
-
create: setupNativeEvents(
|
|
1548
|
+
create: setupNativeEvents({
|
|
1549
|
+
preload: props.preload,
|
|
1550
|
+
explicitLinks: props.explicitLinks,
|
|
1551
|
+
actionBase: props.actionBase,
|
|
1552
|
+
transformUrl: props.transformUrl
|
|
1553
|
+
}),
|
|
1532
1554
|
utils: {
|
|
1533
1555
|
go: delta => window.history.go(delta),
|
|
1534
1556
|
beforeLeave
|
|
@@ -1569,7 +1591,11 @@ function HashRouter(props) {
|
|
|
1569
1591
|
saveCurrentDepth();
|
|
1570
1592
|
},
|
|
1571
1593
|
init: notify => bindEvent(window, "hashchange", notifyIfNotBlocked(notify, delta => !beforeLeave.confirm(delta && delta < 0 ? delta : getSource()))),
|
|
1572
|
-
create: setupNativeEvents(
|
|
1594
|
+
create: setupNativeEvents({
|
|
1595
|
+
preload: props.preload,
|
|
1596
|
+
explicitLinks: props.explicitLinks,
|
|
1597
|
+
actionBase: props.actionBase
|
|
1598
|
+
}),
|
|
1573
1599
|
utils: {
|
|
1574
1600
|
go: delta => window.history.go(delta),
|
|
1575
1601
|
renderPath: path => `#${path}`,
|
|
@@ -1631,7 +1657,11 @@ function MemoryRouter(props) {
|
|
|
1631
1657
|
get: memoryHistory.get,
|
|
1632
1658
|
set: memoryHistory.set,
|
|
1633
1659
|
init: memoryHistory.listen,
|
|
1634
|
-
create: setupNativeEvents(
|
|
1660
|
+
create: setupNativeEvents({
|
|
1661
|
+
preload: props.preload,
|
|
1662
|
+
explicitLinks: props.explicitLinks,
|
|
1663
|
+
actionBase: props.actionBase
|
|
1664
|
+
}),
|
|
1635
1665
|
utils: {
|
|
1636
1666
|
go: memoryHistory.go
|
|
1637
1667
|
}
|
|
@@ -1715,6 +1745,7 @@ function createAsync(fn, options) {
|
|
|
1715
1745
|
let prev = () => !resource || resource.state === "unresolved" ? undefined : resource.latest;
|
|
1716
1746
|
[resource] = createResource(() => subFetch(fn, catchError(() => untrack(prev), () => undefined)), v => v, options);
|
|
1717
1747
|
const resultAccessor = () => resource();
|
|
1748
|
+
if (options?.name) setFunctionName(resultAccessor, options.name);
|
|
1718
1749
|
Object.defineProperty(resultAccessor, "latest", {
|
|
1719
1750
|
get() {
|
|
1720
1751
|
return resource.latest;
|
|
@@ -1845,4 +1876,4 @@ function json(data, init = {}) {
|
|
|
1845
1876
|
return response;
|
|
1846
1877
|
}
|
|
1847
1878
|
|
|
1848
|
-
export { A, HashRouter, MemoryRouter, Navigate, Route, Router, StaticRouter, mergeSearchString as _mergeSearchString, action, cache, createAsync, createAsyncStore, createBeforeLeave, createMemoryHistory, createRouter, json, keepDepth, notifyIfNotBlocked, query, redirect, reload, revalidate, saveCurrentDepth, useAction, useBeforeLeave, useCurrentMatches, useHref, useIsRouting, useLocation, useMatch, useNavigate, useParams, usePreloadRoute, useResolvedPath, useSearchParams, useSubmission, useSubmissions };
|
|
1879
|
+
export { A, HashRouter, MemoryRouter, Navigate, Route, Router, RouterContextObj as RouterContext, StaticRouter, mergeSearchString as _mergeSearchString, action, cache, createAsync, createAsyncStore, createBeforeLeave, createMemoryHistory, createRouter, json, keepDepth, notifyIfNotBlocked, query, redirect, reload, revalidate, saveCurrentDepth, useAction, useBeforeLeave, useCurrentMatches, useHref, useIsRouting, useLocation, useMatch, useNavigate, useParams, usePreloadRoute, useResolvedPath, useSearchParams, useSubmission, useSubmissions };
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import type { JSX } from "solid-js";
|
|
2
|
-
import type { BaseRouterProps } from "./components.
|
|
2
|
+
import type { BaseRouterProps } from "./components.jsx";
|
|
3
3
|
export declare function hashParser(str: string): string;
|
|
4
4
|
export type HashRouterProps = BaseRouterProps & {
|
|
5
5
|
actionBase?: string;
|
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import type { LocationChange, RouterContext, RouterUtils } from "../types.
|
|
1
|
+
import type { LocationChange, RouterContext, RouterUtils } from "../types.js";
|
|
2
2
|
export declare function createRouter(config: {
|
|
3
3
|
get: () => string | LocationChange;
|
|
4
4
|
set: (next: LocationChange) => void;
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
import type { JSX } from "solid-js";
|
|
2
|
+
import type { Location, Navigator } from "./types.js";
|
|
3
|
+
declare module "solid-js" {
|
|
4
|
+
namespace JSX {
|
|
5
|
+
interface AnchorHTMLAttributes<T> {
|
|
6
|
+
state?: string;
|
|
7
|
+
noScroll?: boolean;
|
|
8
|
+
replace?: boolean;
|
|
9
|
+
preload?: boolean;
|
|
10
|
+
link?: boolean;
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
export interface AnchorProps extends Omit<JSX.AnchorHTMLAttributes<HTMLAnchorElement>, "state"> {
|
|
15
|
+
href: string;
|
|
16
|
+
replace?: boolean | undefined;
|
|
17
|
+
noScroll?: boolean | undefined;
|
|
18
|
+
state?: unknown | undefined;
|
|
19
|
+
inactiveClass?: string | undefined;
|
|
20
|
+
activeClass?: string | undefined;
|
|
21
|
+
end?: boolean | undefined;
|
|
22
|
+
}
|
|
23
|
+
export declare function A(props: AnchorProps): JSX.Element;
|
|
24
|
+
export interface NavigateProps {
|
|
25
|
+
href: ((args: {
|
|
26
|
+
navigate: Navigator;
|
|
27
|
+
location: Location;
|
|
28
|
+
}) => string) | string;
|
|
29
|
+
state?: unknown;
|
|
30
|
+
}
|
|
31
|
+
export declare function Navigate(props: NavigateProps): null;
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { createMemo, mergeProps, splitProps } from "solid-js";
|
|
2
|
+
import { useHref, useLocation, useNavigate, useResolvedPath } from "./routing.js";
|
|
3
|
+
import { normalizePath } from "./utils.js";
|
|
4
|
+
export function A(props) {
|
|
5
|
+
props = mergeProps({ inactiveClass: "inactive", activeClass: "active" }, props);
|
|
6
|
+
const [, rest] = splitProps(props, [
|
|
7
|
+
"href",
|
|
8
|
+
"state",
|
|
9
|
+
"class",
|
|
10
|
+
"activeClass",
|
|
11
|
+
"inactiveClass",
|
|
12
|
+
"end"
|
|
13
|
+
]);
|
|
14
|
+
const to = useResolvedPath(() => props.href);
|
|
15
|
+
const href = useHref(to);
|
|
16
|
+
const location = useLocation();
|
|
17
|
+
const isActive = createMemo(() => {
|
|
18
|
+
const to_ = to();
|
|
19
|
+
if (to_ === undefined)
|
|
20
|
+
return [false, false];
|
|
21
|
+
const path = normalizePath(to_.split(/[?#]/, 1)[0]).toLowerCase();
|
|
22
|
+
const loc = decodeURI(normalizePath(location.pathname).toLowerCase());
|
|
23
|
+
return [props.end ? path === loc : loc.startsWith(path + "/") || loc === path, path === loc];
|
|
24
|
+
});
|
|
25
|
+
return (<a {...rest} href={href() || props.href} state={JSON.stringify(props.state)} classList={{
|
|
26
|
+
...(props.class && { [props.class]: true }),
|
|
27
|
+
[props.inactiveClass]: !isActive()[0],
|
|
28
|
+
[props.activeClass]: isActive()[0],
|
|
29
|
+
...rest.classList
|
|
30
|
+
}} link aria-current={isActive()[1] ? "page" : undefined}/>);
|
|
31
|
+
}
|
|
32
|
+
export function Navigate(props) {
|
|
33
|
+
const navigate = useNavigate();
|
|
34
|
+
const location = useLocation();
|
|
35
|
+
const { href, state } = props;
|
|
36
|
+
const path = typeof href === "function" ? href({ navigate, location }) : href;
|
|
37
|
+
navigate(path, { replace: true, state });
|
|
38
|
+
return null;
|
|
39
|
+
}
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
import { JSX } from "solid-js";
|
|
2
|
+
import type { Submission, SubmissionStub, NarrowResponse } from "../types.js";
|
|
3
|
+
export type Action<T extends Array<any>, U, V = T> = (T extends [FormData | URLSearchParams] | [] ? JSX.SerializableAttributeValue : unknown) & ((...vars: T) => Promise<NarrowResponse<U>>) & {
|
|
4
|
+
url: string;
|
|
5
|
+
with<A extends any[], B extends any[]>(this: (this: any, ...args: [...A, ...B]) => Promise<NarrowResponse<U>>, ...args: A): Action<B, U, V>;
|
|
6
|
+
};
|
|
7
|
+
export declare const actions: Map<string, Action<any, any, any>>;
|
|
8
|
+
export declare function useSubmissions<T extends Array<any>, U, V>(fn: Action<T, U, V>, filter?: (input: V) => boolean): Submission<T, NarrowResponse<U>>[] & {
|
|
9
|
+
pending: boolean;
|
|
10
|
+
};
|
|
11
|
+
export declare function useSubmission<T extends Array<any>, U, V>(fn: Action<T, U, V>, filter?: (input: V) => boolean): Submission<T, NarrowResponse<U>> | SubmissionStub;
|
|
12
|
+
export declare function useAction<T extends Array<any>, U, V>(action: Action<T, U, V>): (...args: Parameters<Action<T, U, V>>) => Promise<NarrowResponse<U>>;
|
|
13
|
+
export declare function action<T extends Array<any>, U = void>(fn: (...args: T) => Promise<U>, name?: string): Action<T, U>;
|
|
14
|
+
export declare function action<T extends Array<any>, U = void>(fn: (...args: T) => Promise<U>, options?: {
|
|
15
|
+
name?: string;
|
|
16
|
+
onComplete?: (s: Submission<T, U>) => void;
|
|
17
|
+
}): Action<T, U>;
|
|
@@ -0,0 +1,163 @@
|
|
|
1
|
+
import { $TRACK, createMemo, createSignal, onCleanup, getOwner } from "solid-js";
|
|
2
|
+
import { isServer } from "solid-js/web";
|
|
3
|
+
import { useRouter } from "../routing.js";
|
|
4
|
+
import { mockBase, setFunctionName } from "../utils.js";
|
|
5
|
+
import { cacheKeyOp, hashKey, revalidate, query } from "./query.js";
|
|
6
|
+
export const actions = /* #__PURE__ */ new Map();
|
|
7
|
+
export function useSubmissions(fn, filter) {
|
|
8
|
+
const router = useRouter();
|
|
9
|
+
const subs = createMemo(() => router.submissions[0]().filter(s => s.url === fn.base && (!filter || filter(s.input))));
|
|
10
|
+
return new Proxy([], {
|
|
11
|
+
get(_, property) {
|
|
12
|
+
if (property === $TRACK)
|
|
13
|
+
return subs();
|
|
14
|
+
if (property === "pending")
|
|
15
|
+
return subs().some(sub => !sub.result);
|
|
16
|
+
return subs()[property];
|
|
17
|
+
},
|
|
18
|
+
has(_, property) {
|
|
19
|
+
return property in subs();
|
|
20
|
+
}
|
|
21
|
+
});
|
|
22
|
+
}
|
|
23
|
+
export function useSubmission(fn, filter) {
|
|
24
|
+
const submissions = useSubmissions(fn, filter);
|
|
25
|
+
return new Proxy({}, {
|
|
26
|
+
get(_, property) {
|
|
27
|
+
if ((submissions.length === 0 && property === "clear") || property === "retry")
|
|
28
|
+
return () => { };
|
|
29
|
+
return submissions[submissions.length - 1]?.[property];
|
|
30
|
+
}
|
|
31
|
+
});
|
|
32
|
+
}
|
|
33
|
+
export function useAction(action) {
|
|
34
|
+
const r = useRouter();
|
|
35
|
+
return (...args) => action.apply({ r }, args);
|
|
36
|
+
}
|
|
37
|
+
export function action(fn, options = {}) {
|
|
38
|
+
function mutate(...variables) {
|
|
39
|
+
const router = this.r;
|
|
40
|
+
const form = this.f;
|
|
41
|
+
const p = (router.singleFlight && fn.withOptions
|
|
42
|
+
? fn.withOptions({ headers: { "X-Single-Flight": "true" } })
|
|
43
|
+
: fn)(...variables);
|
|
44
|
+
const [result, setResult] = createSignal();
|
|
45
|
+
let submission;
|
|
46
|
+
function handler(error) {
|
|
47
|
+
return async (res) => {
|
|
48
|
+
const result = await handleResponse(res, error, router.navigatorFactory());
|
|
49
|
+
let retry = null;
|
|
50
|
+
o.onComplete?.({
|
|
51
|
+
...submission,
|
|
52
|
+
result: result?.data,
|
|
53
|
+
error: result?.error,
|
|
54
|
+
pending: false,
|
|
55
|
+
retry() {
|
|
56
|
+
return (retry = submission.retry());
|
|
57
|
+
}
|
|
58
|
+
});
|
|
59
|
+
if (retry)
|
|
60
|
+
return retry;
|
|
61
|
+
if (!result)
|
|
62
|
+
return submission.clear();
|
|
63
|
+
setResult(result);
|
|
64
|
+
if (result.error && !form)
|
|
65
|
+
throw result.error;
|
|
66
|
+
return result.data;
|
|
67
|
+
};
|
|
68
|
+
}
|
|
69
|
+
router.submissions[1](s => [
|
|
70
|
+
...s,
|
|
71
|
+
(submission = {
|
|
72
|
+
input: variables,
|
|
73
|
+
url,
|
|
74
|
+
get result() {
|
|
75
|
+
return result()?.data;
|
|
76
|
+
},
|
|
77
|
+
get error() {
|
|
78
|
+
return result()?.error;
|
|
79
|
+
},
|
|
80
|
+
get pending() {
|
|
81
|
+
return !result();
|
|
82
|
+
},
|
|
83
|
+
clear() {
|
|
84
|
+
router.submissions[1](v => v.filter(i => i !== submission));
|
|
85
|
+
},
|
|
86
|
+
retry() {
|
|
87
|
+
setResult(undefined);
|
|
88
|
+
const p = fn(...variables);
|
|
89
|
+
return p.then(handler(), handler(true));
|
|
90
|
+
}
|
|
91
|
+
})
|
|
92
|
+
]);
|
|
93
|
+
return p.then(handler(), handler(true));
|
|
94
|
+
}
|
|
95
|
+
const o = typeof options === "string" ? { name: options } : options;
|
|
96
|
+
const name = o.name || (!isServer ? String(hashString(fn.toString())) : undefined);
|
|
97
|
+
const url = fn.url || (name && `https://action/${name}`) || "";
|
|
98
|
+
mutate.base = url;
|
|
99
|
+
if (name)
|
|
100
|
+
setFunctionName(mutate, name);
|
|
101
|
+
return toAction(mutate, url);
|
|
102
|
+
}
|
|
103
|
+
function toAction(fn, url) {
|
|
104
|
+
fn.toString = () => {
|
|
105
|
+
if (!url)
|
|
106
|
+
throw new Error("Client Actions need explicit names if server rendered");
|
|
107
|
+
return url;
|
|
108
|
+
};
|
|
109
|
+
fn.with = function (...args) {
|
|
110
|
+
const newFn = function (...passedArgs) {
|
|
111
|
+
return fn.call(this, ...args, ...passedArgs);
|
|
112
|
+
};
|
|
113
|
+
newFn.base = fn.base;
|
|
114
|
+
const uri = new URL(url, mockBase);
|
|
115
|
+
uri.searchParams.set("args", hashKey(args));
|
|
116
|
+
return toAction(newFn, (uri.origin === "https://action" ? uri.origin : "") + uri.pathname + uri.search);
|
|
117
|
+
};
|
|
118
|
+
fn.url = url;
|
|
119
|
+
if (!isServer) {
|
|
120
|
+
actions.set(url, fn);
|
|
121
|
+
getOwner() && onCleanup(() => actions.delete(url));
|
|
122
|
+
}
|
|
123
|
+
return fn;
|
|
124
|
+
}
|
|
125
|
+
const hashString = (s) => s.split("").reduce((a, b) => ((a << 5) - a + b.charCodeAt(0)) | 0, 0);
|
|
126
|
+
async function handleResponse(response, error, navigate) {
|
|
127
|
+
let data;
|
|
128
|
+
let custom;
|
|
129
|
+
let keys;
|
|
130
|
+
let flightKeys;
|
|
131
|
+
if (response instanceof Response) {
|
|
132
|
+
if (response.headers.has("X-Revalidate"))
|
|
133
|
+
keys = response.headers.get("X-Revalidate").split(",");
|
|
134
|
+
if (response.customBody) {
|
|
135
|
+
data = custom = await response.customBody();
|
|
136
|
+
if (response.headers.has("X-Single-Flight")) {
|
|
137
|
+
data = data._$value;
|
|
138
|
+
delete custom._$value;
|
|
139
|
+
flightKeys = Object.keys(custom);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
if (response.headers.has("Location")) {
|
|
143
|
+
const locationUrl = response.headers.get("Location") || "/";
|
|
144
|
+
if (locationUrl.startsWith("http")) {
|
|
145
|
+
window.location.href = locationUrl;
|
|
146
|
+
}
|
|
147
|
+
else {
|
|
148
|
+
navigate(locationUrl);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
else if (error)
|
|
153
|
+
return { error: response };
|
|
154
|
+
else
|
|
155
|
+
data = response;
|
|
156
|
+
// invalidate
|
|
157
|
+
cacheKeyOp(keys, entry => (entry[0] = 0));
|
|
158
|
+
// set cache
|
|
159
|
+
flightKeys && flightKeys.forEach(k => query.set(k, custom[k]));
|
|
160
|
+
// trigger revalidation
|
|
161
|
+
await revalidate(keys, false);
|
|
162
|
+
return data != null ? { data } : undefined;
|
|
163
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export {};
|
|
@@ -0,0 +1,297 @@
|
|
|
1
|
+
import { createRoot } from "solid-js";
|
|
2
|
+
import { vi } from "vitest";
|
|
3
|
+
import { action, useAction, useSubmission, useSubmissions, actions } from "./action.js";
|
|
4
|
+
import { createMockRouter } from "../../test/helpers.js";
|
|
5
|
+
vi.mock("../src/utils.js", () => ({
|
|
6
|
+
mockBase: "https://action"
|
|
7
|
+
}));
|
|
8
|
+
let mockRouterContext;
|
|
9
|
+
vi.mock("../routing.js", () => ({
|
|
10
|
+
useRouter: () => mockRouterContext,
|
|
11
|
+
createRouterContext: () => createMockRouter(),
|
|
12
|
+
RouterContextObj: {},
|
|
13
|
+
RouteContextObj: {},
|
|
14
|
+
useRoute: () => mockRouterContext.base,
|
|
15
|
+
useResolvedPath: () => "/",
|
|
16
|
+
useHref: () => "/",
|
|
17
|
+
useNavigate: () => vi.fn(),
|
|
18
|
+
useLocation: () => mockRouterContext.location,
|
|
19
|
+
useRouteData: () => undefined,
|
|
20
|
+
useMatch: () => null,
|
|
21
|
+
useParams: () => ({}),
|
|
22
|
+
useSearchParams: () => [{}, vi.fn()],
|
|
23
|
+
useIsRouting: () => false,
|
|
24
|
+
usePreloadRoute: () => vi.fn(),
|
|
25
|
+
useBeforeLeave: () => vi.fn()
|
|
26
|
+
}));
|
|
27
|
+
describe("action", () => {
|
|
28
|
+
beforeEach(() => {
|
|
29
|
+
actions.clear();
|
|
30
|
+
mockRouterContext = createMockRouter();
|
|
31
|
+
});
|
|
32
|
+
test("should create an action function with `url` property", () => {
|
|
33
|
+
const testAction = action(async (data) => {
|
|
34
|
+
return `processed: ${data}`;
|
|
35
|
+
}, "test-action");
|
|
36
|
+
expect(typeof testAction).toBe("function");
|
|
37
|
+
expect(testAction.url).toBe("https://action/test-action");
|
|
38
|
+
});
|
|
39
|
+
test("should create action with auto-generated hash when no `name` provided", () => {
|
|
40
|
+
const testFn = async (data) => `result: ${data}`;
|
|
41
|
+
const testAction = action(testFn);
|
|
42
|
+
expect(testAction.url).toMatch(/^https:\/\/action\/-?\d+$/);
|
|
43
|
+
expect(testAction.name).toMatch(/^-?\d+$/);
|
|
44
|
+
});
|
|
45
|
+
test("should use it as `name` when `options` are provided as a string", () => {
|
|
46
|
+
const testFn = async (data) => `result: ${data}`;
|
|
47
|
+
const testAction = action(testFn, "test-action");
|
|
48
|
+
expect(testAction.url).toMatch("https://action/test-action");
|
|
49
|
+
expect(testAction.name).toBe("test-action");
|
|
50
|
+
});
|
|
51
|
+
test("should use `name` when provided in object options", () => {
|
|
52
|
+
const testFn = async (data) => `result: ${data}`;
|
|
53
|
+
const testAction = action(testFn, { name: "test-action" });
|
|
54
|
+
expect(testAction.url).toMatch("https://action/test-action");
|
|
55
|
+
expect(testAction.name).toBe("test-action");
|
|
56
|
+
});
|
|
57
|
+
test("should register action in actions map", () => {
|
|
58
|
+
const testAction = action(async () => "result", "register-test");
|
|
59
|
+
expect(actions.has(testAction.url)).toBe(true);
|
|
60
|
+
expect(actions.get(testAction.url)).toBe(testAction);
|
|
61
|
+
});
|
|
62
|
+
test("should support `.with` method for currying arguments", () => {
|
|
63
|
+
const baseAction = action(async (prefix, data) => {
|
|
64
|
+
return `${prefix}: ${data}`;
|
|
65
|
+
}, "with-test");
|
|
66
|
+
const curriedAction = baseAction.with("PREFIX");
|
|
67
|
+
expect(typeof curriedAction).toBe("function");
|
|
68
|
+
expect(curriedAction.url).toMatch(/with-test\?args=/);
|
|
69
|
+
});
|
|
70
|
+
test("should execute action and create submission", async () => {
|
|
71
|
+
return createRoot(async () => {
|
|
72
|
+
const testAction = action(async (data) => {
|
|
73
|
+
return `processed: ${data}`;
|
|
74
|
+
}, "execute-test");
|
|
75
|
+
const boundAction = useAction(testAction);
|
|
76
|
+
const promise = boundAction("test-data");
|
|
77
|
+
const submissions = mockRouterContext.submissions[0]();
|
|
78
|
+
expect(submissions).toHaveLength(1);
|
|
79
|
+
expect(submissions[0].input).toEqual(["test-data"]);
|
|
80
|
+
expect(submissions[0].pending).toBe(true);
|
|
81
|
+
const result = await promise;
|
|
82
|
+
expect(result).toBe("processed: test-data");
|
|
83
|
+
});
|
|
84
|
+
});
|
|
85
|
+
test("should handle action errors", async () => {
|
|
86
|
+
return createRoot(async () => {
|
|
87
|
+
const errorAction = action(async () => {
|
|
88
|
+
throw new Error("Test error");
|
|
89
|
+
}, "error-test");
|
|
90
|
+
const boundAction = useAction(errorAction);
|
|
91
|
+
try {
|
|
92
|
+
await boundAction();
|
|
93
|
+
}
|
|
94
|
+
catch (error) {
|
|
95
|
+
expect(error.message).toBe("Test error");
|
|
96
|
+
}
|
|
97
|
+
const submissions = mockRouterContext.submissions[0]();
|
|
98
|
+
expect(submissions[0].error.message).toBe("Test error");
|
|
99
|
+
});
|
|
100
|
+
});
|
|
101
|
+
test("should support `onComplete` callback", async () => {
|
|
102
|
+
return createRoot(async () => {
|
|
103
|
+
const onComplete = vi.fn();
|
|
104
|
+
const testAction = action(async (data) => `result: ${data}`, {
|
|
105
|
+
name: "callback-test",
|
|
106
|
+
onComplete
|
|
107
|
+
});
|
|
108
|
+
const boundAction = useAction(testAction);
|
|
109
|
+
await boundAction("test");
|
|
110
|
+
expect(onComplete).toHaveBeenCalledWith(expect.objectContaining({
|
|
111
|
+
result: "result: test",
|
|
112
|
+
error: undefined,
|
|
113
|
+
pending: false
|
|
114
|
+
}));
|
|
115
|
+
});
|
|
116
|
+
});
|
|
117
|
+
});
|
|
118
|
+
describe("useSubmissions", () => {
|
|
119
|
+
beforeEach(() => {
|
|
120
|
+
mockRouterContext = createMockRouter();
|
|
121
|
+
});
|
|
122
|
+
test("should return submissions for specific action", () => {
|
|
123
|
+
return createRoot(() => {
|
|
124
|
+
const testAction = action(async () => "result", "submissions-test");
|
|
125
|
+
mockRouterContext.submissions[1](submissions => [
|
|
126
|
+
...submissions,
|
|
127
|
+
{
|
|
128
|
+
input: ["data1"],
|
|
129
|
+
url: testAction.url,
|
|
130
|
+
result: "result1",
|
|
131
|
+
error: undefined,
|
|
132
|
+
pending: false,
|
|
133
|
+
clear: vi.fn(),
|
|
134
|
+
retry: vi.fn()
|
|
135
|
+
},
|
|
136
|
+
{
|
|
137
|
+
input: ["data2"],
|
|
138
|
+
url: testAction.url,
|
|
139
|
+
result: undefined,
|
|
140
|
+
error: undefined,
|
|
141
|
+
pending: true,
|
|
142
|
+
clear: vi.fn(),
|
|
143
|
+
retry: vi.fn()
|
|
144
|
+
}
|
|
145
|
+
]);
|
|
146
|
+
const submissions = useSubmissions(testAction);
|
|
147
|
+
expect(submissions).toHaveLength(2);
|
|
148
|
+
expect(submissions[0].input).toEqual(["data1"]);
|
|
149
|
+
expect(submissions[1].input).toEqual(["data2"]);
|
|
150
|
+
expect(submissions.pending).toBe(true);
|
|
151
|
+
});
|
|
152
|
+
});
|
|
153
|
+
test("should filter submissions when filter function provided", () => {
|
|
154
|
+
return createRoot(() => {
|
|
155
|
+
const testAction = action(async (data) => data, "filter-test");
|
|
156
|
+
mockRouterContext.submissions[1](submissions => [
|
|
157
|
+
...submissions,
|
|
158
|
+
{
|
|
159
|
+
input: ["keep"],
|
|
160
|
+
url: testAction.url,
|
|
161
|
+
result: "result1",
|
|
162
|
+
error: undefined,
|
|
163
|
+
pending: false,
|
|
164
|
+
clear: vi.fn(),
|
|
165
|
+
retry: vi.fn()
|
|
166
|
+
},
|
|
167
|
+
{
|
|
168
|
+
input: ["skip"],
|
|
169
|
+
url: testAction.url,
|
|
170
|
+
result: "result2",
|
|
171
|
+
error: undefined,
|
|
172
|
+
pending: false,
|
|
173
|
+
clear: vi.fn(),
|
|
174
|
+
retry: vi.fn()
|
|
175
|
+
}
|
|
176
|
+
]);
|
|
177
|
+
const submissions = useSubmissions(testAction, input => input[0] === "keep");
|
|
178
|
+
expect(submissions).toHaveLength(1);
|
|
179
|
+
expect(submissions[0].input).toEqual(["keep"]);
|
|
180
|
+
});
|
|
181
|
+
});
|
|
182
|
+
test("should return pending false when no pending submissions", () => {
|
|
183
|
+
return createRoot(() => {
|
|
184
|
+
const testAction = action(async () => "result", "no-pending-test");
|
|
185
|
+
mockRouterContext.submissions[1](submissions => [
|
|
186
|
+
...submissions,
|
|
187
|
+
{
|
|
188
|
+
input: ["data"],
|
|
189
|
+
url: testAction.url,
|
|
190
|
+
result: "result",
|
|
191
|
+
error: undefined,
|
|
192
|
+
pending: false,
|
|
193
|
+
clear: vi.fn(),
|
|
194
|
+
retry: vi.fn()
|
|
195
|
+
}
|
|
196
|
+
]);
|
|
197
|
+
const submissions = useSubmissions(testAction);
|
|
198
|
+
expect(submissions.pending).toBe(false);
|
|
199
|
+
});
|
|
200
|
+
});
|
|
201
|
+
});
|
|
202
|
+
describe("useSubmission", () => {
|
|
203
|
+
beforeEach(() => {
|
|
204
|
+
mockRouterContext = createMockRouter();
|
|
205
|
+
});
|
|
206
|
+
test("should return latest submission for action", () => {
|
|
207
|
+
return createRoot(() => {
|
|
208
|
+
const testAction = action(async () => "result", "latest-test");
|
|
209
|
+
mockRouterContext.submissions[1](submissions => [
|
|
210
|
+
...submissions,
|
|
211
|
+
{
|
|
212
|
+
input: ["data1"],
|
|
213
|
+
url: testAction.url,
|
|
214
|
+
result: "result1",
|
|
215
|
+
error: undefined,
|
|
216
|
+
pending: false,
|
|
217
|
+
clear: vi.fn(),
|
|
218
|
+
retry: vi.fn()
|
|
219
|
+
},
|
|
220
|
+
{
|
|
221
|
+
input: ["data2"],
|
|
222
|
+
url: testAction.url,
|
|
223
|
+
result: "result2",
|
|
224
|
+
error: undefined,
|
|
225
|
+
pending: false,
|
|
226
|
+
clear: vi.fn(),
|
|
227
|
+
retry: vi.fn()
|
|
228
|
+
}
|
|
229
|
+
]);
|
|
230
|
+
const submission = useSubmission(testAction);
|
|
231
|
+
expect(submission.input).toEqual(["data2"]);
|
|
232
|
+
expect(submission.result).toBe("result2");
|
|
233
|
+
});
|
|
234
|
+
});
|
|
235
|
+
test("should return stub when no submissions exist", () => {
|
|
236
|
+
return createRoot(() => {
|
|
237
|
+
const testAction = action(async () => "result", "stub-test");
|
|
238
|
+
const submission = useSubmission(testAction);
|
|
239
|
+
expect(submission.clear).toBeDefined();
|
|
240
|
+
expect(submission.retry).toBeDefined();
|
|
241
|
+
expect(typeof submission.clear).toBe("function");
|
|
242
|
+
expect(typeof submission.retry).toBe("function");
|
|
243
|
+
});
|
|
244
|
+
});
|
|
245
|
+
test("should filter submissions when filter function provided", () => {
|
|
246
|
+
return createRoot(() => {
|
|
247
|
+
const testAction = action(async (data) => data, "filter-submission-test");
|
|
248
|
+
mockRouterContext.submissions[1](submissions => [
|
|
249
|
+
...submissions,
|
|
250
|
+
{
|
|
251
|
+
input: ["skip"],
|
|
252
|
+
url: testAction.url,
|
|
253
|
+
result: "result1",
|
|
254
|
+
error: undefined,
|
|
255
|
+
pending: false,
|
|
256
|
+
clear: vi.fn(),
|
|
257
|
+
retry: vi.fn()
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
input: ["keep"],
|
|
261
|
+
url: testAction.url,
|
|
262
|
+
result: "result2",
|
|
263
|
+
error: undefined,
|
|
264
|
+
pending: false,
|
|
265
|
+
clear: vi.fn(),
|
|
266
|
+
retry: vi.fn()
|
|
267
|
+
}
|
|
268
|
+
]);
|
|
269
|
+
const submission = useSubmission(testAction, input => input[0] === "keep");
|
|
270
|
+
expect(submission.input).toEqual(["keep"]);
|
|
271
|
+
expect(submission.result).toBe("result2");
|
|
272
|
+
});
|
|
273
|
+
});
|
|
274
|
+
});
|
|
275
|
+
describe("useAction", () => {
|
|
276
|
+
beforeEach(() => {
|
|
277
|
+
mockRouterContext = createMockRouter();
|
|
278
|
+
});
|
|
279
|
+
test("should return bound action function", () => {
|
|
280
|
+
return createRoot(() => {
|
|
281
|
+
const testAction = action(async (data) => `result: ${data}`, "bound-test");
|
|
282
|
+
const boundAction = useAction(testAction);
|
|
283
|
+
expect(typeof boundAction).toBe("function");
|
|
284
|
+
});
|
|
285
|
+
});
|
|
286
|
+
test("should execute action through useAction", async () => {
|
|
287
|
+
return createRoot(async () => {
|
|
288
|
+
const testAction = action(async (data) => {
|
|
289
|
+
await new Promise(resolve => setTimeout(resolve, 1));
|
|
290
|
+
return `result: ${data}`;
|
|
291
|
+
}, "context-test");
|
|
292
|
+
const boundAction = useAction(testAction);
|
|
293
|
+
const result = await boundAction("test-data");
|
|
294
|
+
expect(result).toBe("result: test-data");
|
|
295
|
+
});
|
|
296
|
+
});
|
|
297
|
+
});
|