navigation-stack 0.1.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/.babelrc.cjs +17 -0
- package/.eslintignore +8 -0
- package/.eslintrc.cjs +10 -0
- package/.github/workflows/main.yml +39 -0
- package/.yarn/install-state.gz +0 -0
- package/.yarnrc.yml +1 -0
- package/CODE_OF_CONDUCT.md +77 -0
- package/LICENSE +21 -0
- package/README.md +249 -0
- package/codecov.yml +1 -0
- package/karma.conf.cjs +63 -0
- package/lib/cjs/ActionTypes.js +14 -0
- package/lib/cjs/Actions.js +27 -0
- package/lib/cjs/LocationStateStorage.js +60 -0
- package/lib/cjs/addNavigationBlocker.js +7 -0
- package/lib/cjs/basePath.js +58 -0
- package/lib/cjs/createMiddlewares.js +43 -0
- package/lib/cjs/createSearchFromQuery.js +13 -0
- package/lib/cjs/environment/BrowserEnvironment.js +111 -0
- package/lib/cjs/environment/MemoryEnvironment.js +150 -0
- package/lib/cjs/environment/ServerEnvironment.js +53 -0
- package/lib/cjs/getLocationUrl.js +20 -0
- package/lib/cjs/index.js +30 -0
- package/lib/cjs/isPromise.js +9 -0
- package/lib/cjs/locationReducer.js +13 -0
- package/lib/cjs/middleware/createBasePathMiddleware.js +24 -0
- package/lib/cjs/middleware/createEnvironmentMiddleware.js +58 -0
- package/lib/cjs/middleware/createNavigationBlockerMiddleware.js +128 -0
- package/lib/cjs/middleware/createTransformLocationMiddleware.js +38 -0
- package/lib/cjs/middleware/navigationActionMiddleware.js +37 -0
- package/lib/cjs/middleware/normalizeInputLocationMiddleware.js +27 -0
- package/lib/cjs/navigationBlockers.js +146 -0
- package/lib/cjs/normalizeInputLocation.js +46 -0
- package/lib/cjs/onlyAllowedOnClientSide.js +10 -0
- package/lib/cjs/parseLocationUrl.js +39 -0
- package/lib/cjs/parseQueryFromSearch.js +16 -0
- package/lib/esm/ActionTypes.js +9 -0
- package/lib/esm/Actions.js +21 -0
- package/lib/esm/LocationStateStorage.js +53 -0
- package/lib/esm/addNavigationBlocker.js +2 -0
- package/lib/esm/basePath.js +53 -0
- package/lib/esm/createMiddlewares.js +37 -0
- package/lib/esm/createSearchFromQuery.js +8 -0
- package/lib/esm/environment/BrowserEnvironment.js +104 -0
- package/lib/esm/environment/MemoryEnvironment.js +143 -0
- package/lib/esm/environment/ServerEnvironment.js +46 -0
- package/lib/esm/getLocationUrl.js +15 -0
- package/lib/esm/index.js +12 -0
- package/lib/esm/isPromise.js +4 -0
- package/lib/esm/locationReducer.js +7 -0
- package/lib/esm/middleware/createBasePathMiddleware.js +19 -0
- package/lib/esm/middleware/createEnvironmentMiddleware.js +52 -0
- package/lib/esm/middleware/createNavigationBlockerMiddleware.js +123 -0
- package/lib/esm/middleware/createTransformLocationMiddleware.js +33 -0
- package/lib/esm/middleware/navigationActionMiddleware.js +32 -0
- package/lib/esm/middleware/normalizeInputLocationMiddleware.js +22 -0
- package/lib/esm/navigationBlockers.js +138 -0
- package/lib/esm/normalizeInputLocation.js +41 -0
- package/lib/esm/onlyAllowedOnClientSide.js +5 -0
- package/lib/esm/parseLocationUrl.js +33 -0
- package/lib/esm/parseQueryFromSearch.js +11 -0
- package/lib/index.d.ts +301 -0
- package/package.json +100 -0
- package/renovate.json +3 -0
- package/src/ActionTypes.js +9 -0
- package/src/Actions.js +26 -0
- package/src/LocationStateStorage.js +59 -0
- package/src/addNavigationBlocker.js +2 -0
- package/src/basePath.js +65 -0
- package/src/createMiddlewares.js +41 -0
- package/src/createSearchFromQuery.js +9 -0
- package/src/environment/BrowserEnvironment.js +109 -0
- package/src/environment/MemoryEnvironment.js +151 -0
- package/src/environment/ServerEnvironment.js +54 -0
- package/src/getLocationUrl.js +12 -0
- package/src/index.js +12 -0
- package/src/isPromise.js +8 -0
- package/src/locationReducer.js +8 -0
- package/src/middleware/createBasePathMiddleware.js +20 -0
- package/src/middleware/createEnvironmentMiddleware.js +57 -0
- package/src/middleware/createNavigationBlockerMiddleware.js +128 -0
- package/src/middleware/createTransformLocationMiddleware.js +29 -0
- package/src/middleware/navigationActionMiddleware.js +27 -0
- package/src/middleware/normalizeInputLocationMiddleware.js +21 -0
- package/src/navigationBlockers.js +158 -0
- package/src/normalizeInputLocation.js +44 -0
- package/src/onlyAllowedOnClientSide.js +5 -0
- package/src/parseLocationUrl.js +40 -0
- package/src/parseQueryFromSearch.js +12 -0
- package/test/.eslintrc.cjs +17 -0
- package/test/Action.test.js +72 -0
- package/test/ActionTypes.test.js +13 -0
- package/test/LocationStateStorage.test.js +75 -0
- package/test/basePath.test.js +158 -0
- package/test/createMiddlewares.test.js +62 -0
- package/test/environment/BrowserEnvironment.test.js +165 -0
- package/test/environment/MemoryEnvironment.test.js +218 -0
- package/test/environment/ServerEnvironment.test.js +23 -0
- package/test/getLocationUrl.test.js +33 -0
- package/test/helpers.js +34 -0
- package/test/index.js +44 -0
- package/test/index.test.js +20 -0
- package/test/locationReducer.test.js +42 -0
- package/test/middleware/createBasePathMiddleware.test.js +67 -0
- package/test/middleware/createNavigationBlockerMiddleware.test.js +472 -0
- package/test/middleware/createTransformLocationMiddleware.test.js +44 -0
- package/test/middleware/navigationActionMiddleware.test.js +74 -0
- package/test/middleware/normalizeInputLocationMiddleware.test.js +62 -0
- package/test/normalizeInputLocation.test.js +81 -0
- package/test/parseLocationUrl.test.js +30 -0
- package/types/.eslintrc.cjs +3 -0
- package/types/index.d.ts +301 -0
- package/types/tsconfig.json +14 -0
|
@@ -0,0 +1,41 @@
|
|
|
1
|
+
import createSearchFromQuery from './createSearchFromQuery';
|
|
2
|
+
import parseLocationUrl from './parseLocationUrl';
|
|
3
|
+
import parseQueryFromSearch from './parseQueryFromSearch';
|
|
4
|
+
|
|
5
|
+
// * If `location` is a string, it parses it into a `NormalizedInputLocation`.
|
|
6
|
+
// * If `location` is an object, it ensures that `search` and `hash` properties aren't `undefined`,
|
|
7
|
+
// i.e. it "ensures" that the `location` object can be used as a `NormalizedInputLocation`.
|
|
8
|
+
export default function normalizeInputLocation(location) {
|
|
9
|
+
if (typeof location === 'string') {
|
|
10
|
+
return parseLocationUrl(location);
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
// Convert `query` property values to strings.
|
|
14
|
+
if (location.query) {
|
|
15
|
+
for (const key of Object.keys(location.query)) {
|
|
16
|
+
location.query[key] = String(location.query[key]);
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// Create `query` from `search`.
|
|
21
|
+
if (location.search && !location.query) {
|
|
22
|
+
location = Object.assign({}, location, {
|
|
23
|
+
query: parseQueryFromSearch(location.search)
|
|
24
|
+
});
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// Convert `query` object into a `search` string
|
|
28
|
+
// if `query` is present but `search` is not.
|
|
29
|
+
if (location.query && !location.search) {
|
|
30
|
+
location = Object.assign({}, location, {
|
|
31
|
+
search: createSearchFromQuery(location.query)
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Set default values on `search` and `hash`
|
|
36
|
+
// if those properties are not present.
|
|
37
|
+
return Object.assign({}, location, {
|
|
38
|
+
search: location.search || '',
|
|
39
|
+
hash: location.hash || ''
|
|
40
|
+
});
|
|
41
|
+
}
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
import parseQueryFromSearch from './parseQueryFromSearch';
|
|
2
|
+
export default function parseLocationUrl(url) {
|
|
3
|
+
if (url[0] !== '/') {
|
|
4
|
+
throw new Error('Expected URL to start with a slash');
|
|
5
|
+
}
|
|
6
|
+
let remainingPath = url;
|
|
7
|
+
const hashIndex = remainingPath.indexOf('#');
|
|
8
|
+
let hash;
|
|
9
|
+
if (hashIndex !== -1) {
|
|
10
|
+
hash = remainingPath.slice(hashIndex);
|
|
11
|
+
remainingPath = remainingPath.slice(0, hashIndex);
|
|
12
|
+
} else {
|
|
13
|
+
hash = '';
|
|
14
|
+
}
|
|
15
|
+
const searchIndex = remainingPath.indexOf('?');
|
|
16
|
+
let search;
|
|
17
|
+
if (searchIndex !== -1) {
|
|
18
|
+
search = remainingPath.slice(searchIndex);
|
|
19
|
+
remainingPath = remainingPath.slice(0, searchIndex);
|
|
20
|
+
} else {
|
|
21
|
+
search = '';
|
|
22
|
+
}
|
|
23
|
+
const location = {
|
|
24
|
+
pathname: remainingPath,
|
|
25
|
+
search,
|
|
26
|
+
hash
|
|
27
|
+
};
|
|
28
|
+
const query = parseQueryFromSearch(search);
|
|
29
|
+
if (query) {
|
|
30
|
+
location.query = query;
|
|
31
|
+
}
|
|
32
|
+
return location;
|
|
33
|
+
}
|
|
@@ -0,0 +1,11 @@
|
|
|
1
|
+
import { parse as parseQuery } from 'query-string';
|
|
2
|
+
export default function parseQueryFromSearch(search) {
|
|
3
|
+
if (search.length > '?'.length) {
|
|
4
|
+
try {
|
|
5
|
+
return parseQuery(search.slice(1));
|
|
6
|
+
} catch (error) {
|
|
7
|
+
// Ignore any query parsing errors.
|
|
8
|
+
}
|
|
9
|
+
}
|
|
10
|
+
return undefined;
|
|
11
|
+
}
|
package/lib/index.d.ts
ADDED
|
@@ -0,0 +1,301 @@
|
|
|
1
|
+
// TypeScript Version: 3.0
|
|
2
|
+
|
|
3
|
+
export {};
|
|
4
|
+
|
|
5
|
+
export type Query = Record<string, string>;
|
|
6
|
+
|
|
7
|
+
// `InputLocationQuery` may specify query parameter values as any type of data.
|
|
8
|
+
// Those values will later be converted to strings.
|
|
9
|
+
export type InputLocationQuery = Record<
|
|
10
|
+
string,
|
|
11
|
+
string | number | boolean | null | undefined
|
|
12
|
+
>;
|
|
13
|
+
|
|
14
|
+
export interface Location<TState = any> {
|
|
15
|
+
/**
|
|
16
|
+
* 'PUSH' or 'REPLACE' if the location was reached via FarceActions.push or
|
|
17
|
+
* FarceActions.replace respectively; 'POP' on the initial location, or if
|
|
18
|
+
* the location was reached via the browser back or forward buttons or
|
|
19
|
+
* via FarceActions.shift
|
|
20
|
+
*/
|
|
21
|
+
action: 'PUSH' | 'REPLACE' | 'POP';
|
|
22
|
+
/**
|
|
23
|
+
* the path name; as on window.location e.g. '/foo'
|
|
24
|
+
*/
|
|
25
|
+
pathname: string;
|
|
26
|
+
/**
|
|
27
|
+
* map version of search string
|
|
28
|
+
*/
|
|
29
|
+
query: Query;
|
|
30
|
+
/**
|
|
31
|
+
* the search string; as on window.location e.g. '?bar=baz'
|
|
32
|
+
*/
|
|
33
|
+
search: string;
|
|
34
|
+
/**
|
|
35
|
+
* the location hash; as on window.location e.g. '#qux'
|
|
36
|
+
*/
|
|
37
|
+
hash: string;
|
|
38
|
+
/**
|
|
39
|
+
* if present, a unique key identifying the current history entry
|
|
40
|
+
*/
|
|
41
|
+
key?: string;
|
|
42
|
+
/**
|
|
43
|
+
* the current index of the history entry, starting at 0 for the initial
|
|
44
|
+
* entry; this increments on FarceActions.push but not on
|
|
45
|
+
* FarceActions.replace
|
|
46
|
+
*/
|
|
47
|
+
index: number;
|
|
48
|
+
/**
|
|
49
|
+
* the difference between the current index and the index of the previous
|
|
50
|
+
* location
|
|
51
|
+
*/
|
|
52
|
+
delta: number;
|
|
53
|
+
/**
|
|
54
|
+
* additional location state that is not part of the URL
|
|
55
|
+
*/
|
|
56
|
+
state: TState;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Location descriptor object used in #push and #replace.
|
|
61
|
+
*/
|
|
62
|
+
export interface InputLocationObject {
|
|
63
|
+
pathname: Location['pathname'];
|
|
64
|
+
search?: Location['search'];
|
|
65
|
+
query?: InputLocationQuery;
|
|
66
|
+
hash?: Location['hash'];
|
|
67
|
+
state?: Location['state'];
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
export interface LocationBase {
|
|
71
|
+
pathname: Location['pathname'];
|
|
72
|
+
search: Location['search'];
|
|
73
|
+
query?: Query;
|
|
74
|
+
hash: Location['hash'];
|
|
75
|
+
state?: Location['state'];
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Location descriptor string:
|
|
80
|
+
* store.dispatch(FarceActions.push('/foo?bar=baz#qux'));
|
|
81
|
+
*
|
|
82
|
+
* Equivalent location descriptor object:
|
|
83
|
+
* store.dispatch(FarceActions.push({
|
|
84
|
+
* pathname: '/foo',
|
|
85
|
+
* search: '?bar=baz',
|
|
86
|
+
* hash: '#qux',
|
|
87
|
+
* }));
|
|
88
|
+
*
|
|
89
|
+
* https://github.com/4Catalyzer/farce#locations-and-location-descriptors
|
|
90
|
+
*/
|
|
91
|
+
export type InputLocationString = string;
|
|
92
|
+
|
|
93
|
+
// Using an interface allows consumers to use object merging to add other
|
|
94
|
+
// location descriptor types.
|
|
95
|
+
export interface InputLocationTypes {
|
|
96
|
+
object: InputLocationObject;
|
|
97
|
+
string: InputLocationString;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
export type InputLocation = InputLocationTypes[keyof InputLocationTypes];
|
|
101
|
+
|
|
102
|
+
export interface CreateMiddlewaresOptions {
|
|
103
|
+
basePath?: string;
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
export type NavigationBlockerSyncResult = boolean | undefined;
|
|
107
|
+
export type NavigationBlockerResult =
|
|
108
|
+
| NavigationBlockerSyncResult
|
|
109
|
+
| Promise<NavigationBlockerSyncResult>;
|
|
110
|
+
|
|
111
|
+
/**
|
|
112
|
+
* The navigation listener function receives the `location` to which the user
|
|
113
|
+
* is attempting to navigate.
|
|
114
|
+
*
|
|
115
|
+
* The `location` argument is `null` when the web browser tab is about to be closed.
|
|
116
|
+
*/
|
|
117
|
+
export interface NavigationBlocker {
|
|
118
|
+
(location: Location | LocationBase | null): NavigationBlockerResult;
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
export function addBasePath<L extends InputLocation>(
|
|
122
|
+
location: L,
|
|
123
|
+
basePath?: string,
|
|
124
|
+
): L;
|
|
125
|
+
export function removeBasePath<L extends InputLocation>(
|
|
126
|
+
location: L,
|
|
127
|
+
basePath?: string,
|
|
128
|
+
): L;
|
|
129
|
+
|
|
130
|
+
export function getLocationUrl(location: InputLocationObject): string;
|
|
131
|
+
export function parseLocationUrl(locationUrl: string): LocationBase;
|
|
132
|
+
|
|
133
|
+
export function createMiddlewares(
|
|
134
|
+
environment: Environment,
|
|
135
|
+
options?: CreateMiddlewaresOptions,
|
|
136
|
+
): Middleware[];
|
|
137
|
+
|
|
138
|
+
export function addNavigationBlocker(
|
|
139
|
+
environment: EnvironmentBase,
|
|
140
|
+
blocker: NavigationBlocker,
|
|
141
|
+
): () => void;
|
|
142
|
+
|
|
143
|
+
export const ActionTypes: {
|
|
144
|
+
INIT: '@@navigation-stack/INIT';
|
|
145
|
+
PUSH: '@@navigation-stack/PUSH';
|
|
146
|
+
REPLACE: '@@navigation-stack/REPLACE';
|
|
147
|
+
NAVIGATE: '@@navigation-stack/NAVIGATE';
|
|
148
|
+
SHIFT: '@@navigation-stack/SHIFT';
|
|
149
|
+
UPDATE: '@@navigation-stack/UPDATE';
|
|
150
|
+
DISPOSE: '@@navigation-stack/DISPOSE';
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
export interface InitAction {
|
|
154
|
+
type: (typeof ActionTypes)['INIT'];
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
export interface PushAction {
|
|
158
|
+
type: (typeof ActionTypes)['PUSH'];
|
|
159
|
+
payload: InputLocation;
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
export interface ReplaceAction {
|
|
163
|
+
type: (typeof ActionTypes)['REPLACE'];
|
|
164
|
+
payload: InputLocation;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
export interface RewindAction {
|
|
168
|
+
type: (typeof ActionTypes)['SHIFT'];
|
|
169
|
+
payload: number;
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
export interface DisposeAction {
|
|
173
|
+
type: (typeof ActionTypes)['DISPOSE'];
|
|
174
|
+
}
|
|
175
|
+
|
|
176
|
+
export type Action =
|
|
177
|
+
| InitAction
|
|
178
|
+
| PushAction
|
|
179
|
+
| ReplaceAction
|
|
180
|
+
| RewindAction
|
|
181
|
+
| DisposeAction;
|
|
182
|
+
|
|
183
|
+
export const Actions: {
|
|
184
|
+
init(): InitAction;
|
|
185
|
+
push(location: InputLocation): PushAction;
|
|
186
|
+
replace(location: InputLocation): ReplaceAction;
|
|
187
|
+
go(delta: number): RewindAction;
|
|
188
|
+
dispose(): DisposeAction;
|
|
189
|
+
};
|
|
190
|
+
|
|
191
|
+
type BeforeDestroyListener = () => boolean | undefined;
|
|
192
|
+
|
|
193
|
+
export interface Environment {
|
|
194
|
+
init(): void;
|
|
195
|
+
|
|
196
|
+
// Subscribes to changes in location,
|
|
197
|
+
// excluding ones that happened as a result of calling `.navigate()`.
|
|
198
|
+
subscribe(listener: (location: Location) => void): () => void;
|
|
199
|
+
|
|
200
|
+
navigate(location: LocationBase): Location;
|
|
201
|
+
|
|
202
|
+
go(delta: number): void;
|
|
203
|
+
|
|
204
|
+
addBeforeDestroyListener(listener: BeforeDestroyListener): void;
|
|
205
|
+
|
|
206
|
+
getState(key: string): string | null;
|
|
207
|
+
removeState(key: string): void;
|
|
208
|
+
setState(key: string, value: string): void;
|
|
209
|
+
}
|
|
210
|
+
|
|
211
|
+
// This is just a copy-paste of the `Environment` interface above.
|
|
212
|
+
declare abstract class EnvironmentBase implements Environment {
|
|
213
|
+
init(): void;
|
|
214
|
+
|
|
215
|
+
// Subscribes to changes in location,
|
|
216
|
+
// excluding ones that happened as a result of calling `.navigate()`.
|
|
217
|
+
subscribe(listener: (location: Location) => void): () => void;
|
|
218
|
+
|
|
219
|
+
navigate(location: LocationBase): Location;
|
|
220
|
+
|
|
221
|
+
go(delta: number): void;
|
|
222
|
+
|
|
223
|
+
addBeforeDestroyListener(listener: BeforeDestroyListener): void;
|
|
224
|
+
|
|
225
|
+
getState(key: string): string | null;
|
|
226
|
+
removeState(key: string): void;
|
|
227
|
+
setState(key: string, value: string): void;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
export class BrowserEnvironment extends EnvironmentBase {}
|
|
231
|
+
|
|
232
|
+
export interface MemoryEnvironmentOptions<MemoryEnvironmentState = any> {
|
|
233
|
+
save?: (state: MemoryEnvironmentState) => void;
|
|
234
|
+
load?: () => MemoryEnvironmentState | undefined | null;
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
export class ServerEnvironment extends EnvironmentBase {
|
|
238
|
+
constructor(initialLocation: InputLocation);
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
export class MemoryEnvironment extends EnvironmentBase {
|
|
242
|
+
constructor(
|
|
243
|
+
initialLocation: InputLocation,
|
|
244
|
+
options?: MemoryEnvironmentOptions,
|
|
245
|
+
);
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
export interface QueryMiddlewareOptions {
|
|
249
|
+
stringify(query: InputLocationQuery): string;
|
|
250
|
+
parse(str: string): Query;
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
export function createQueryMiddleware(
|
|
254
|
+
options: QueryMiddlewareOptions,
|
|
255
|
+
): Middleware;
|
|
256
|
+
|
|
257
|
+
export const queryMiddleware: Middleware;
|
|
258
|
+
|
|
259
|
+
export function createBasePathMiddleware(basePath?: string): Middleware;
|
|
260
|
+
|
|
261
|
+
export const locationReducer: Reducer<Location, Action>;
|
|
262
|
+
|
|
263
|
+
export class LocationStateStorage {
|
|
264
|
+
constructor(environment: Environment, options?: { namespace?: string });
|
|
265
|
+
|
|
266
|
+
get(location: Location, key: string): any;
|
|
267
|
+
set(location: Location, key: string, value: any): void;
|
|
268
|
+
}
|
|
269
|
+
|
|
270
|
+
// The following types are copy-pasted from `redux`.
|
|
271
|
+
|
|
272
|
+
interface ReduxAction<T = any> {
|
|
273
|
+
type: T;
|
|
274
|
+
}
|
|
275
|
+
|
|
276
|
+
interface AnyAction extends ReduxAction {
|
|
277
|
+
// Allows any extra properties to be defined in an action.
|
|
278
|
+
[extraProps: string]: any;
|
|
279
|
+
}
|
|
280
|
+
|
|
281
|
+
interface Dispatch<A extends Action = AnyAction> {
|
|
282
|
+
<T extends A>(action: T): T;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
interface MiddlewareAPI<D extends Dispatch = Dispatch, S = any> {
|
|
286
|
+
dispatch: D;
|
|
287
|
+
getState(): S;
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
interface Middleware<
|
|
291
|
+
DispatchExt = {},
|
|
292
|
+
S = any,
|
|
293
|
+
D extends Dispatch = Dispatch,
|
|
294
|
+
> {
|
|
295
|
+
(api: MiddlewareAPI<D, S>): (next: Dispatch) => (action: any) => any;
|
|
296
|
+
}
|
|
297
|
+
|
|
298
|
+
type Reducer<S = any, A extends Action = AnyAction> = (
|
|
299
|
+
state: S | undefined,
|
|
300
|
+
action: A,
|
|
301
|
+
) => S;
|
package/package.json
ADDED
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
{
|
|
2
|
+
"name": "navigation-stack",
|
|
3
|
+
"version": "0.1.0",
|
|
4
|
+
"description": "Handles web browser navigation in a web application",
|
|
5
|
+
"keywords": [
|
|
6
|
+
"history",
|
|
7
|
+
"browser",
|
|
8
|
+
"navigation",
|
|
9
|
+
"router"
|
|
10
|
+
],
|
|
11
|
+
"homepage": "https://gitlab.com/catamphetamine/navigation-stack#readme",
|
|
12
|
+
"bugs": {
|
|
13
|
+
"url": "https://gitlab.com/catamphetamine/navigation-stack/issues"
|
|
14
|
+
},
|
|
15
|
+
"license": "MIT",
|
|
16
|
+
"author": "catamphetamine",
|
|
17
|
+
"main": "lib/cjs/index.js",
|
|
18
|
+
"module": "lib/esm/index.js",
|
|
19
|
+
"types": "lib/index.d.ts",
|
|
20
|
+
"repository": {
|
|
21
|
+
"type": "git",
|
|
22
|
+
"url": "https://gitlab.com/catamphetamine/navigation-stack.git"
|
|
23
|
+
},
|
|
24
|
+
"scripts": {
|
|
25
|
+
"build": "rimraf lib && 4c build --types false src && npm run build:pick && npm run build:types",
|
|
26
|
+
"build:pick": "cherry-pick --cjs-dir cjs --esm-dir esm --cwd lib ../src",
|
|
27
|
+
"build:types": "cpy types/*.d.ts lib",
|
|
28
|
+
"format": "4c format --prettier-ignore .eslintignore .",
|
|
29
|
+
"lint": "4c lint --prettier-ignore .eslintignore .",
|
|
30
|
+
"prepublish": "npm run build",
|
|
31
|
+
"release": "4c release",
|
|
32
|
+
"tdd": "cross-env NODE_ENV=test karma start karma.conf.cjs",
|
|
33
|
+
"test": "npm run lint && npm run test:ts && npm run testonly",
|
|
34
|
+
"test:ts": "dtslint types",
|
|
35
|
+
"testonly": "npm run tdd -- --single-run"
|
|
36
|
+
},
|
|
37
|
+
"husky": {
|
|
38
|
+
"hooks": {
|
|
39
|
+
"pre-commit": "lint-staged"
|
|
40
|
+
}
|
|
41
|
+
},
|
|
42
|
+
"lint-staged": {
|
|
43
|
+
"*": "yarn 4c lint --fix --prettier-ignore .eslintignore",
|
|
44
|
+
"README.md": "doctoc"
|
|
45
|
+
},
|
|
46
|
+
"prettier": "@4c/prettier-config",
|
|
47
|
+
"dependencies": {
|
|
48
|
+
"query-string": "^5.1.1"
|
|
49
|
+
},
|
|
50
|
+
"devDependencies": {
|
|
51
|
+
"@4c/babel-preset": "^9.1.0",
|
|
52
|
+
"@4c/cli": "^3.0.1",
|
|
53
|
+
"@4c/prettier-config": "^1.1.0",
|
|
54
|
+
"@babel/core": "^7.17.10",
|
|
55
|
+
"@typescript-eslint/eslint-plugin": "^5.23.0",
|
|
56
|
+
"@typescript-eslint/parser": "^5.23.0",
|
|
57
|
+
"babel-loader": "^8.2.5",
|
|
58
|
+
"babel-plugin-add-module-exports": "^1.0.4",
|
|
59
|
+
"babel-plugin-istanbul": "^6.1.1",
|
|
60
|
+
"chai": "^4.3.6",
|
|
61
|
+
"cherry-pick": "^0.5.0",
|
|
62
|
+
"codecov": "^3.8.3",
|
|
63
|
+
"cpy-cli": "^3.1.1",
|
|
64
|
+
"cross-env": "^7.0.3",
|
|
65
|
+
"delay": "^4.4.1",
|
|
66
|
+
"dirty-chai": "^2.0.1",
|
|
67
|
+
"doctoc": "^2.2.0",
|
|
68
|
+
"dtslint": "^4.2.1",
|
|
69
|
+
"eslint-config-4catalyzer": "^1.4.1",
|
|
70
|
+
"eslint-config-4catalyzer-typescript": "^3.2.1",
|
|
71
|
+
"eslint-config-prettier": "^8.5.0",
|
|
72
|
+
"eslint-plugin-import": "^2.26.0",
|
|
73
|
+
"eslint-plugin-prettier": "^4.0.0",
|
|
74
|
+
"husky": "^4.3.8",
|
|
75
|
+
"karma": "^6.3.19",
|
|
76
|
+
"karma-chrome-launcher": "^3.1.1",
|
|
77
|
+
"karma-coverage": "^2.2.0",
|
|
78
|
+
"karma-firefox-launcher": "^2.1.2",
|
|
79
|
+
"karma-mocha": "^2.0.1",
|
|
80
|
+
"karma-mocha-reporter": "^2.2.5",
|
|
81
|
+
"karma-sinon-chai": "^2.0.2",
|
|
82
|
+
"karma-sourcemap-loader": "^0.3.8",
|
|
83
|
+
"karma-webpack": "^5.0.0",
|
|
84
|
+
"lint-staged": "^12.4.1",
|
|
85
|
+
"mocha": "^9.2.2",
|
|
86
|
+
"p-defer": "^3.0.0",
|
|
87
|
+
"prettier": "^2.6.2",
|
|
88
|
+
"puppeteer": "^13.7.0",
|
|
89
|
+
"redux": "^4.1.2",
|
|
90
|
+
"rimraf": "^3.0.2",
|
|
91
|
+
"sinon": "^11.1.2",
|
|
92
|
+
"sinon-chai": "^3.7.0",
|
|
93
|
+
"webpack": "^5.72.1"
|
|
94
|
+
},
|
|
95
|
+
"resolutions": {
|
|
96
|
+
"dtslint/@definitelytyped/header-parser": "^0.0.41",
|
|
97
|
+
"dtslint/@definitelytyped/typescript-versions": "^0.0.41",
|
|
98
|
+
"dtslint/@definitelytyped/utils": "^0.0.41"
|
|
99
|
+
}
|
|
100
|
+
}
|
package/renovate.json
ADDED
|
@@ -0,0 +1,9 @@
|
|
|
1
|
+
export default {
|
|
2
|
+
INIT: '@@navigation-stack/INIT',
|
|
3
|
+
PUSH: '@@navigation-stack/PUSH',
|
|
4
|
+
REPLACE: '@@navigation-stack/REPLACE',
|
|
5
|
+
NAVIGATE: '@@navigation-stack/NAVIGATE',
|
|
6
|
+
SHIFT: '@@navigation-stack/SHIFT',
|
|
7
|
+
UPDATE: '@@navigation-stack/UPDATE',
|
|
8
|
+
DISPOSE: '@@navigation-stack/DISPOSE',
|
|
9
|
+
};
|
package/src/Actions.js
ADDED
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import ActionTypes from './ActionTypes';
|
|
2
|
+
|
|
3
|
+
export default {
|
|
4
|
+
init: () => ({
|
|
5
|
+
type: ActionTypes.INIT,
|
|
6
|
+
}),
|
|
7
|
+
|
|
8
|
+
push: (location) => ({
|
|
9
|
+
type: ActionTypes.PUSH,
|
|
10
|
+
payload: location,
|
|
11
|
+
}),
|
|
12
|
+
|
|
13
|
+
replace: (location) => ({
|
|
14
|
+
type: ActionTypes.REPLACE,
|
|
15
|
+
payload: location,
|
|
16
|
+
}),
|
|
17
|
+
|
|
18
|
+
shift: (delta) => ({
|
|
19
|
+
type: ActionTypes.SHIFT,
|
|
20
|
+
payload: delta,
|
|
21
|
+
}),
|
|
22
|
+
|
|
23
|
+
dispose: () => ({
|
|
24
|
+
type: ActionTypes.DISPOSE,
|
|
25
|
+
}),
|
|
26
|
+
};
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
import getLocationUrl from './getLocationUrl';
|
|
2
|
+
|
|
3
|
+
export default class LocationStateStorage {
|
|
4
|
+
constructor(environment, { namespace } = {}) {
|
|
5
|
+
this._environment = environment;
|
|
6
|
+
this._getFallbackLocationKey = getLocationUrl;
|
|
7
|
+
this._stateKeyPrefix = namespace ? `${namespace}|` : '';
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
get(location, key) {
|
|
11
|
+
const stateKey = this._getStateKey(location, key);
|
|
12
|
+
|
|
13
|
+
try {
|
|
14
|
+
const value = this._environment.getState(stateKey);
|
|
15
|
+
// === null is probably sufficient.
|
|
16
|
+
if (value === null) {
|
|
17
|
+
return undefined;
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
// We want to catch JSON parse errors in case someone separately threw
|
|
21
|
+
// junk into sessionStorage under our namespace.
|
|
22
|
+
return JSON.parse(value);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
// Pretend that the entry doesn't exist.
|
|
25
|
+
return undefined;
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
set(location, key, value) {
|
|
30
|
+
const stateKey = this._getStateKey(location, key);
|
|
31
|
+
|
|
32
|
+
if (value === undefined) {
|
|
33
|
+
try {
|
|
34
|
+
this._environment.removeState(stateKey);
|
|
35
|
+
} catch (error) {
|
|
36
|
+
// No need to handle errors here.
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
return;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Unlike with read, we want to fail on invalid values here, since the
|
|
43
|
+
// value here is provided by the caller of this method.
|
|
44
|
+
const valueString = JSON.stringify(value);
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
this._environment.setState(stateKey, valueString);
|
|
48
|
+
} catch (error) {
|
|
49
|
+
// No need to handle errors here either. If it didn't work, it didn't
|
|
50
|
+
// work. We make no guarantees about actually saving the value.
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
_getStateKey(location, key) {
|
|
55
|
+
const locationKey = location.key || this._getFallbackLocationKey(location);
|
|
56
|
+
const keyPrefix = `${this._stateKeyPrefix}${locationKey}`;
|
|
57
|
+
return `${keyPrefix}|${key}`;
|
|
58
|
+
}
|
|
59
|
+
}
|
package/src/basePath.js
ADDED
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
function normalizeBasePath(basePath) {
|
|
2
|
+
if (!basePath || basePath === '/') {
|
|
3
|
+
return undefined;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
// Validate `basePath`.
|
|
7
|
+
if (basePath[0] !== '/') {
|
|
8
|
+
throw new Error('`basePath` must start with a slash');
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
// Remove trailing slash from `basePath`.
|
|
12
|
+
if (basePath.slice(-1) === '/') {
|
|
13
|
+
basePath = basePath.slice(0, -1);
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
return basePath;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
function removeBasePathFromRelativeUrl(url, basePath) {
|
|
20
|
+
if (url.indexOf(basePath) === 0) {
|
|
21
|
+
// `farce` had a bug here:
|
|
22
|
+
// `location.pathname` is supposed to always be non-empty.
|
|
23
|
+
// If `basePath` is set to `/basePath` and the user navigates to `/basePath` URL,
|
|
24
|
+
// originally here it would simply strips the whole string from the URL
|
|
25
|
+
// and the result would be incorrect: `pathname: ""`.
|
|
26
|
+
// The fix below is adding `|| '/'` in the `return` statement.
|
|
27
|
+
// https://github.com/4Catalyzer/farce/issues/483
|
|
28
|
+
return url.slice(basePath.length) || '/';
|
|
29
|
+
}
|
|
30
|
+
return url;
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function addBasePath(location, basePath) {
|
|
34
|
+
basePath = normalizeBasePath(basePath);
|
|
35
|
+
|
|
36
|
+
if (!basePath) {
|
|
37
|
+
return location;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
if (typeof location === 'string') {
|
|
41
|
+
return `${basePath}${location}`;
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return {
|
|
45
|
+
...location,
|
|
46
|
+
pathname: `${basePath}${location.pathname}`,
|
|
47
|
+
};
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export function removeBasePath(location, basePath) {
|
|
51
|
+
basePath = normalizeBasePath(basePath);
|
|
52
|
+
|
|
53
|
+
if (!basePath) {
|
|
54
|
+
return location;
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
if (typeof location === 'string') {
|
|
58
|
+
return removeBasePathFromRelativeUrl(location, basePath);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
...location,
|
|
63
|
+
pathname: removeBasePathFromRelativeUrl(location.pathname, basePath),
|
|
64
|
+
};
|
|
65
|
+
}
|