@solidjs/router 0.11.3 → 0.11.5

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.
@@ -1,8 +1,8 @@
1
1
  import { $TRACK, createMemo, createSignal, onCleanup, getOwner } from "solid-js";
2
2
  import { isServer } from "solid-js/web";
3
3
  import { useRouter } from "../routing.js";
4
- import { redirectStatusCodes, mockBase } from "../utils.js";
5
- import { cacheKeyOp, hashKey, revalidate } from "./cache.js";
4
+ import { mockBase } from "../utils.js";
5
+ import { cacheKeyOp, hashKey, revalidate, cache } from "./cache.js";
6
6
  export const actions = /* #__PURE__ */ new Map();
7
7
  export function useSubmissions(fn, filter) {
8
8
  const router = useRouter();
@@ -31,10 +31,12 @@ export function useAction(action) {
31
31
  }
32
32
  export function action(fn, name) {
33
33
  function mutate(...variables) {
34
- const p = fn(...variables);
34
+ const router = this;
35
+ const p = (router.singleFlight && fn.withOptions
36
+ ? fn.withOptions({ headers: { "X-Single-Flight": "true" } })
37
+ : fn)(...variables);
35
38
  const [result, setResult] = createSignal();
36
39
  let submission;
37
- const router = this;
38
40
  async function handler(res) {
39
41
  const data = await handleResponse(res, router.navigatorFactory());
40
42
  data ? setResult({ data }) : submission.clear();
@@ -95,12 +97,25 @@ const hashString = (s) => s.split("").reduce((a, b) => ((a << 5) - a + b.charCod
95
97
  async function handleResponse(response, navigate) {
96
98
  let data;
97
99
  let keys;
100
+ let invalidateKeys;
98
101
  if (response instanceof Response) {
99
102
  if (response.headers.has("X-Revalidate"))
100
- keys = response.headers.get("X-Revalidate").split(",");
101
- if (response.customBody)
103
+ keys = invalidateKeys = response.headers.get("X-Revalidate").split(",");
104
+ if (response.customBody) {
102
105
  data = await response.customBody();
103
- if (redirectStatusCodes.has(response.status)) {
106
+ if (response.headers.has("X-Single-Flight")) {
107
+ keys || (keys = []);
108
+ invalidateKeys || (invalidateKeys = []);
109
+ Object.keys(data).forEach(key => {
110
+ if (key === "_$value")
111
+ return;
112
+ keys.push(key);
113
+ cache.set(key, data[key]);
114
+ });
115
+ data = data._$value;
116
+ }
117
+ }
118
+ if (response.headers.has("Location")) {
104
119
  const locationUrl = response.headers.get("Location") || "/";
105
120
  if (locationUrl.startsWith("http")) {
106
121
  window.location.href = locationUrl;
@@ -113,7 +128,7 @@ async function handleResponse(response, navigate) {
113
128
  else
114
129
  data = response;
115
130
  // invalidate
116
- cacheKeyOp(keys, entry => (entry[0] = 0));
131
+ cacheKeyOp(invalidateKeys, entry => (entry[0] = 0));
117
132
  // trigger revalidation
118
133
  await revalidate(keys, false);
119
134
  return data;
@@ -2,7 +2,6 @@ import { createSignal, getListener, getOwner, onCleanup, sharedConfig, startTran
2
2
  import { createStore, reconcile } from "solid-js/store";
3
3
  import { getRequestEvent, isServer } from "solid-js/web";
4
4
  import { useNavigate, getIntent } from "../routing.js";
5
- import { redirectStatusCodes } from "../utils.js";
6
5
  const LocationHeader = "Location";
7
6
  const PRELOAD_TIMEOUT = 5000;
8
7
  const CACHE_TIMEOUT = 180000;
@@ -56,6 +55,21 @@ export function cache(fn, name, options) {
56
55
  const key = name + hashKey(args);
57
56
  let cached = cache.get(key);
58
57
  let tracking;
58
+ if (isServer) {
59
+ const e = getRequestEvent();
60
+ if (e) {
61
+ const dataOnly = (e.router || (e.router = {})).dataOnly;
62
+ if (dataOnly) {
63
+ const data = e && (e.router.data || (e.router.data = {}));
64
+ if (data && key in data)
65
+ return data[key];
66
+ if (Array.isArray(dataOnly) && !dataOnly.includes(key)) {
67
+ data[key] = undefined;
68
+ return Promise.resolve();
69
+ }
70
+ }
71
+ }
72
+ }
59
73
  if (getListener() && !isServer) {
60
74
  tracking = true;
61
75
  onCleanup(() => cached[3].count--);
@@ -91,6 +105,7 @@ export function cache(fn, name, options) {
91
105
  sharedConfig.context.async &&
92
106
  !sharedConfig.context.noHydrate) {
93
107
  const e = getRequestEvent();
108
+ e && e.router.dataOnly && (e.router.data[key] = res);
94
109
  (!e || !e.serverOnly) && sharedConfig.context.serialize(key, res);
95
110
  }
96
111
  if (cached) {
@@ -117,7 +132,7 @@ export function cache(fn, name, options) {
117
132
  function handleResponse(error) {
118
133
  return async (v) => {
119
134
  if (v instanceof Response) {
120
- if (redirectStatusCodes.has(v.status)) {
135
+ if (v.headers.has("Location")) {
121
136
  if (navigate) {
122
137
  startTransition(() => {
123
138
  let url = v.headers.get(LocationHeader);
@@ -1,6 +1,6 @@
1
- export type RouterResponseInit = ResponseInit & {
1
+ export type RouterResponseInit = Omit<ResponseInit, "body"> & {
2
2
  revalidate?: string | string[];
3
3
  };
4
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;
5
+ export declare function reload(init?: RouterResponseInit): never;
6
+ export declare function json<T>(data: T, init?: RouterResponseInit): T;
@@ -19,20 +19,22 @@ export function redirect(url, init = 302) {
19
19
  });
20
20
  return response;
21
21
  }
22
- export function reload(init) {
22
+ export function reload(init = {}) {
23
23
  const { revalidate, ...responseInit } = init;
24
+ const headers = new Headers(responseInit.headers);
25
+ revalidate && headers.set("X-Revalidate", revalidate.toString());
24
26
  return new Response(null, {
25
27
  ...responseInit,
26
- ...(revalidate
27
- ? { headers: new Headers(responseInit.headers).set("X-Revalidate", revalidate.toString()) }
28
- : {})
28
+ headers
29
29
  });
30
30
  }
31
- export function json(data, init) {
32
- const headers = new Headers((init || {}).headers);
31
+ export function json(data, init = {}) {
32
+ const { revalidate, ...responseInit } = init;
33
+ const headers = new Headers(responseInit.headers);
34
+ revalidate && headers.set("X-Revalidate", revalidate.toString());
33
35
  headers.set("Content-Type", "application/json");
34
36
  const response = new Response(JSON.stringify(data), {
35
- ...init,
37
+ ...responseInit,
36
38
  headers
37
39
  });
38
40
  response.customBody = () => data;
package/dist/index.js CHANGED
@@ -79,7 +79,6 @@ function notifyIfNotBlocked(notify, block) {
79
79
  const hasSchemeRegex = /^(?:[a-z0-9]+:)?\/\//i;
80
80
  const trimPathRegex = /^\/+|(\/)\/+$/g;
81
81
  const mockBase = "http://sr";
82
- const redirectStatusCodes = new Set([204, 301, 302, 303, 307, 308]);
83
82
  function normalizePath(path, omitSlash = false) {
84
83
  const s = path.replace(trimPathRegex, "$1");
85
84
  return s ? omitSlash || /^[?#]/.test(s) ? s : "/" + s : "";
@@ -477,6 +476,7 @@ function createRouterContext(integration, getBranches, options = {}) {
477
476
  navigatorFactory,
478
477
  beforeLeave,
479
478
  preloadRoute,
479
+ singleFlight: options.singleFlight === undefined ? true : options.singleFlight,
480
480
  submissions
481
481
  };
482
482
  function navigateFromRoute(route, to, options) {
@@ -641,10 +641,12 @@ const createRouterComponent = router => props => {
641
641
  const routeDefs = children(() => props.children);
642
642
  const branches = createMemo(() => createBranches(props.root ? {
643
643
  component: props.root,
644
+ load: props.rootLoad,
644
645
  children: routeDefs()
645
646
  } : routeDefs(), props.base || ""));
646
647
  const routerState = createRouterContext(router, branches, {
647
- base
648
+ base,
649
+ singleFlight: props.singleFlight
648
650
  });
649
651
  router.create && router.create(routerState);
650
652
  return createComponent$1(RouterContextObj.Provider, {
@@ -663,6 +665,10 @@ function Routes(props) {
663
665
  const matches = createMemo(() => getRouteMatches(props.branches, props.routerState.location.pathname));
664
666
  if (isServer) {
665
667
  const e = getRequestEvent();
668
+ if (e && e.router && e.router.dataOnly) {
669
+ dataOnly(e, props.branches);
670
+ return;
671
+ }
666
672
  e && ((e.router || (e.router = {})).matches || (e.router.matches = matches().map(({
667
673
  route,
668
674
  path,
@@ -747,6 +753,32 @@ const Route = props => {
747
753
  });
748
754
  };
749
755
 
756
+ // for data only mode with single flight mutations
757
+ function dataOnly(event, branches) {
758
+ const url = new URL(event.request.url);
759
+ const prevMatches = getRouteMatches(branches, new URL(event.router.previousUrl || event.request.url).pathname);
760
+ const matches = getRouteMatches(branches, url.pathname);
761
+ for (let match = 0; match < matches.length; match++) {
762
+ if (!prevMatches[match] || matches[match].route !== prevMatches[match].route) event.router.dataOnly = true;
763
+ const {
764
+ route,
765
+ params
766
+ } = matches[match];
767
+ route.load && route.load({
768
+ params,
769
+ location: {
770
+ pathname: url.pathname,
771
+ search: url.search,
772
+ hash: url.hash,
773
+ query: extractSearchParams(url),
774
+ state: null,
775
+ key: ""
776
+ },
777
+ intent: "preload"
778
+ });
779
+ }
780
+ }
781
+
750
782
  function intercept([value, setValue], get, set) {
751
783
  return [get ? () => get(value()) : value, set ? v => setValue(set(v)) : setValue];
752
784
  }
@@ -860,6 +892,20 @@ function cache(fn, name, options) {
860
892
  const key = name + hashKey(args);
861
893
  let cached = cache.get(key);
862
894
  let tracking;
895
+ if (isServer) {
896
+ const e = getRequestEvent();
897
+ if (e) {
898
+ const dataOnly = (e.router || (e.router = {})).dataOnly;
899
+ if (dataOnly) {
900
+ const data = e && (e.router.data || (e.router.data = {}));
901
+ if (data && key in data) return data[key];
902
+ if (Array.isArray(dataOnly) && !dataOnly.includes(key)) {
903
+ data[key] = undefined;
904
+ return Promise.resolve();
905
+ }
906
+ }
907
+ }
908
+ }
863
909
  if (getListener() && !isServer) {
864
910
  tracking = true;
865
911
  onCleanup(() => cached[3].count--);
@@ -885,6 +931,7 @@ function cache(fn, name, options) {
885
931
  // serialize on server
886
932
  if (isServer && sharedConfig.context && sharedConfig.context.async && !sharedConfig.context.noHydrate) {
887
933
  const e = getRequestEvent();
934
+ e && e.router.dataOnly && (e.router.data[key] = res);
888
935
  (!e || !e.serverOnly) && sharedConfig.context.serialize(key, res);
889
936
  }
890
937
  if (cached) {
@@ -907,7 +954,7 @@ function cache(fn, name, options) {
907
954
  function handleResponse(error) {
908
955
  return async v => {
909
956
  if (v instanceof Response) {
910
- if (redirectStatusCodes.has(v.status)) {
957
+ if (v.headers.has("Location")) {
911
958
  if (navigate) {
912
959
  startTransition(() => {
913
960
  let url = v.headers.get(LocationHeader);
@@ -995,10 +1042,14 @@ function useAction(action) {
995
1042
  }
996
1043
  function action(fn, name) {
997
1044
  function mutate(...variables) {
998
- const p = fn(...variables);
1045
+ const router = this;
1046
+ const p = (router.singleFlight && fn.withOptions ? fn.withOptions({
1047
+ headers: {
1048
+ "X-Single-Flight": "true"
1049
+ }
1050
+ }) : fn)(...variables);
999
1051
  const [result, setResult] = createSignal();
1000
1052
  let submission;
1001
- const router = this;
1002
1053
  async function handler(res) {
1003
1054
  const data = await handleResponse(res, router.navigatorFactory());
1004
1055
  data ? setResult({
@@ -1055,10 +1106,23 @@ const hashString = s => s.split("").reduce((a, b) => (a << 5) - a + b.charCodeAt
1055
1106
  async function handleResponse(response, navigate) {
1056
1107
  let data;
1057
1108
  let keys;
1109
+ let invalidateKeys;
1058
1110
  if (response instanceof Response) {
1059
- if (response.headers.has("X-Revalidate")) keys = response.headers.get("X-Revalidate").split(",");
1060
- if (response.customBody) data = await response.customBody();
1061
- if (redirectStatusCodes.has(response.status)) {
1111
+ if (response.headers.has("X-Revalidate")) keys = invalidateKeys = response.headers.get("X-Revalidate").split(",");
1112
+ if (response.customBody) {
1113
+ data = await response.customBody();
1114
+ if (response.headers.has("X-Single-Flight")) {
1115
+ keys || (keys = []);
1116
+ invalidateKeys || (invalidateKeys = []);
1117
+ Object.keys(data).forEach(key => {
1118
+ if (key === "_$value") return;
1119
+ keys.push(key);
1120
+ cache.set(key, data[key]);
1121
+ });
1122
+ data = data._$value;
1123
+ }
1124
+ }
1125
+ if (response.headers.has("Location")) {
1062
1126
  const locationUrl = response.headers.get("Location") || "/";
1063
1127
  if (locationUrl.startsWith("http")) {
1064
1128
  window.location.href = locationUrl;
@@ -1068,7 +1132,7 @@ async function handleResponse(response, navigate) {
1068
1132
  }
1069
1133
  } else data = response;
1070
1134
  // invalidate
1071
- cacheKeyOp(keys, entry => entry[0] = 0);
1135
+ cacheKeyOp(invalidateKeys, entry => entry[0] = 0);
1072
1136
  // trigger revalidation
1073
1137
  await revalidate(keys, false);
1074
1138
  return data;
@@ -1310,6 +1374,7 @@ function MemoryRouter(props) {
1310
1374
  get: memoryHistory.get,
1311
1375
  set: memoryHistory.set,
1312
1376
  init: memoryHistory.listen,
1377
+ create: setupNativeEvents(props.preload, props.explicitLinks, props.actionBase),
1313
1378
  utils: {
1314
1379
  go: memoryHistory.go
1315
1380
  }
@@ -1455,23 +1520,28 @@ function redirect(url, init = 302) {
1455
1520
  });
1456
1521
  return response;
1457
1522
  }
1458
- function reload(init) {
1523
+ function reload(init = {}) {
1459
1524
  const {
1460
1525
  revalidate,
1461
1526
  ...responseInit
1462
1527
  } = init;
1528
+ const headers = new Headers(responseInit.headers);
1529
+ revalidate && headers.set("X-Revalidate", revalidate.toString());
1463
1530
  return new Response(null, {
1464
1531
  ...responseInit,
1465
- ...(revalidate ? {
1466
- headers: new Headers(responseInit.headers).set("X-Revalidate", revalidate.toString())
1467
- } : {})
1532
+ headers
1468
1533
  });
1469
1534
  }
1470
- function json(data, init) {
1471
- const headers = new Headers((init || {}).headers);
1535
+ function json(data, init = {}) {
1536
+ const {
1537
+ revalidate,
1538
+ ...responseInit
1539
+ } = init;
1540
+ const headers = new Headers(responseInit.headers);
1541
+ revalidate && headers.set("X-Revalidate", revalidate.toString());
1472
1542
  headers.set("Content-Type", "application/json");
1473
1543
  const response = new Response(JSON.stringify(data), {
1474
- ...init,
1544
+ ...responseInit,
1475
1545
  headers
1476
1546
  });
1477
1547
  response.customBody = () => data;
@@ -17,5 +17,8 @@ export declare function createMemoryHistory(): {
17
17
  };
18
18
  export type MemoryRouterProps = BaseRouterProps & {
19
19
  history?: MemoryHistory;
20
+ actionBase?: string;
21
+ explicitLinks?: boolean;
22
+ preload?: boolean;
20
23
  };
21
24
  export declare function MemoryRouter(props: MemoryRouterProps): JSX.Element;
@@ -1,4 +1,5 @@
1
1
  import { createRouter, scrollToHash } from "./createRouter.js";
2
+ import { setupNativeEvents } from "../data/events.js";
2
3
  export function createMemoryHistory() {
3
4
  const entries = ["/"];
4
5
  let index = 0;
@@ -45,6 +46,7 @@ export function MemoryRouter(props) {
45
46
  get: memoryHistory.get,
46
47
  set: memoryHistory.set,
47
48
  init: memoryHistory.listen,
49
+ create: setupNativeEvents(props.preload, props.explicitLinks, props.actionBase),
48
50
  utils: {
49
51
  go: memoryHistory.go
50
52
  }
@@ -1,11 +1,13 @@
1
1
  import type { Component, JSX } from "solid-js";
2
- import type { MatchFilters, RouteLoadFunc, RouteDefinition, RouterIntegration, RouteSectionProps } from "../types.ts";
2
+ import type { MatchFilters, RouteLoadFunc, RouteDefinition, RouterIntegration, RouteSectionProps } from "../types.js";
3
3
  export type BaseRouterProps = {
4
4
  base?: string;
5
5
  /**
6
6
  * A component that wraps the content of every route.
7
7
  */
8
8
  root?: Component<RouteSectionProps>;
9
+ rootLoad?: RouteLoadFunc;
10
+ singleFlight?: boolean;
9
11
  children?: JSX.Element | RouteDefinition | RouteDefinition[];
10
12
  };
11
13
  export declare const createRouterComponent: (router: RouterIntegration) => (props: BaseRouterProps) => JSX.Element;
@@ -2,12 +2,12 @@
2
2
  import { getRequestEvent, isServer } from "solid-js/web";
3
3
  import { children, createMemo, createRoot, mergeProps, on, Show } from "solid-js";
4
4
  import { createBranches, createRouteContext, createRouterContext, getRouteMatches, RouteContextObj, RouterContextObj } from "../routing.js";
5
- import { createMemoObject } from "../utils.js";
5
+ import { createMemoObject, extractSearchParams } from "../utils.js";
6
6
  export const createRouterComponent = (router) => (props) => {
7
7
  const { base } = props;
8
8
  const routeDefs = children(() => props.children);
9
- const branches = createMemo(() => createBranches(props.root ? { component: props.root, children: routeDefs() } : routeDefs(), props.base || ""));
10
- const routerState = createRouterContext(router, branches, { base });
9
+ const branches = createMemo(() => createBranches(props.root ? { component: props.root, load: props.rootLoad, children: routeDefs() } : routeDefs(), props.base || ""));
10
+ const routerState = createRouterContext(router, branches, { base, singleFlight: props.singleFlight });
11
11
  router.create && router.create(routerState);
12
12
  return (<RouterContextObj.Provider value={routerState}>
13
13
  <Routes routerState={routerState} branches={branches()}/>
@@ -17,6 +17,10 @@ function Routes(props) {
17
17
  const matches = createMemo(() => getRouteMatches(props.branches, props.routerState.location.pathname));
18
18
  if (isServer) {
19
19
  const e = getRequestEvent();
20
+ if (e && e.router && e.router.dataOnly) {
21
+ dataOnly(e, props.branches);
22
+ return;
23
+ }
20
24
  e &&
21
25
  ((e.router || (e.router = {})).matches ||
22
26
  (e.router.matches = matches().map(({ route, path, params }) => ({
@@ -81,3 +85,27 @@ export const Route = (props) => {
81
85
  }
82
86
  });
83
87
  };
88
+ // for data only mode with single flight mutations
89
+ function dataOnly(event, branches) {
90
+ const url = new URL(event.request.url);
91
+ const prevMatches = getRouteMatches(branches, new URL(event.router.previousUrl || event.request.url).pathname);
92
+ const matches = getRouteMatches(branches, url.pathname);
93
+ for (let match = 0; match < matches.length; match++) {
94
+ if (!prevMatches[match] || matches[match].route !== prevMatches[match].route)
95
+ event.router.dataOnly = true;
96
+ const { route, params } = matches[match];
97
+ route.load &&
98
+ route.load({
99
+ params,
100
+ location: {
101
+ pathname: url.pathname,
102
+ search: url.search,
103
+ hash: url.hash,
104
+ query: extractSearchParams(url),
105
+ state: null,
106
+ key: ""
107
+ },
108
+ intent: "preload"
109
+ });
110
+ }
111
+ }
package/dist/routing.d.ts CHANGED
@@ -21,5 +21,6 @@ export declare function createLocation(path: Accessor<string>, state: Accessor<a
21
21
  export declare function getIntent(): Intent | undefined;
22
22
  export declare function createRouterContext(integration: RouterIntegration, getBranches?: () => Branch[], options?: {
23
23
  base?: string;
24
+ singleFlight?: boolean;
24
25
  }): RouterContext;
25
26
  export declare function createRouteContext(router: RouterContext, parent: RouteContext, outlet: () => JSX.Element, match: () => RouteMatch, params: Params): RouteContext;
package/dist/routing.js CHANGED
@@ -245,6 +245,7 @@ export function createRouterContext(integration, getBranches, options = {}) {
245
245
  navigatorFactory,
246
246
  beforeLeave,
247
247
  preloadRoute,
248
+ singleFlight: options.singleFlight === undefined ? true : options.singleFlight,
248
249
  submissions
249
250
  };
250
251
  function navigateFromRoute(route, to, options) {
package/dist/types.d.ts CHANGED
@@ -10,6 +10,9 @@ declare module "solid-js/web" {
10
10
  matches?: OutputMatch[];
11
11
  cache?: Map<string, CacheEntry>;
12
12
  submission?: Submission<any, any>;
13
+ dataOnly?: boolean | string[];
14
+ data?: Record<string, any>;
15
+ previousUrl?: string;
13
16
  };
14
17
  serverOnly?: boolean;
15
18
  }
@@ -127,6 +130,7 @@ export interface RouterContext {
127
130
  parsePath(str: string): string;
128
131
  beforeLeave: BeforeLeaveLifecycle;
129
132
  preloadRoute: (url: URL, preloadData: boolean) => void;
133
+ singleFlight: boolean;
130
134
  submissions: Signal<Submission<any, any>[]>;
131
135
  }
132
136
  export interface BeforeLeaveEventArgs {
package/dist/utils.d.ts CHANGED
@@ -1,6 +1,5 @@
1
1
  import type { MatchFilters, Params, PathMatch, Route, SetParams } from "./types.ts";
2
2
  export declare const mockBase = "http://sr";
3
- export declare const redirectStatusCodes: Set<number>;
4
3
  export declare function normalizePath(path: string, omitSlash?: boolean): string;
5
4
  export declare function resolvePath(base: string, path: string, from?: string): string | undefined;
6
5
  export declare function invariant<T>(value: T | null | undefined, message: string): T;
package/dist/utils.js CHANGED
@@ -2,7 +2,6 @@ import { createMemo, getOwner, runWithOwner } from "solid-js";
2
2
  const hasSchemeRegex = /^(?:[a-z0-9]+:)?\/\//i;
3
3
  const trimPathRegex = /^\/+|(\/)\/+$/g;
4
4
  export const mockBase = "http://sr";
5
- export const redirectStatusCodes = new Set([204, 301, 302, 303, 307, 308]);
6
5
  export function normalizePath(path, omitSlash = false) {
7
6
  const s = path.replace(trimPathRegex, "$1");
8
7
  return s ? (omitSlash || /^[?#]/.test(s) ? s : "/" + s) : "";
package/package.json CHANGED
@@ -6,7 +6,7 @@
6
6
  "Ryan Turnquist"
7
7
  ],
8
8
  "license": "MIT",
9
- "version": "0.11.3",
9
+ "version": "0.11.5",
10
10
  "homepage": "https://github.com/solidjs/solid-router#readme",
11
11
  "repository": {
12
12
  "type": "git",