@dharmax/state-router 3.1.2 → 3.2.1

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.
@@ -8,13 +8,22 @@ export class StateManager {
8
8
  stateContext;
9
9
  static dispatcher = dispatcher;
10
10
  changeAuthorities = [];
11
- constructor(mode = 'hash', autostart = true) {
11
+ router;
12
+ beforeChangeHandlers = [];
13
+ afterChangeHandlers = [];
14
+ constructor(mode = 'hash', autostart = true, routerInstance = router) {
12
15
  this.mode = mode;
16
+ this.router = routerInstance;
13
17
  if (autostart)
14
- router.listen(mode);
18
+ this.router.listen(mode);
15
19
  }
16
20
  start() {
17
- router.listen(this.mode);
21
+ this.router.listen(this.mode);
22
+ }
23
+ stop() {
24
+ // unlisten router to cleanup event listeners
25
+ // @ts-ignore - older Router versions may not have unlisten
26
+ this.router.unlisten && this.router.unlisten();
18
27
  }
19
28
  onChange(handler) {
20
29
  return StateManager.dispatcher.on('state:changed', handler);
@@ -50,9 +59,9 @@ export class StateManager {
50
59
  }
51
60
  /** attempts to restore state from current url. Currently, works only in hash mode */
52
61
  restoreState(defaultState) {
53
- if (router.navigate(window.location.pathname))
62
+ if (this.router.navigate(window.location.pathname))
54
63
  return;
55
- router.navigate(defaultState);
64
+ this.router.navigate(defaultState);
56
65
  }
57
66
  /**
58
67
  *
@@ -67,13 +76,18 @@ export class StateManager {
67
76
  }
68
77
  // check if the state change was declined by any change authority and if so - don't do it and return false
69
78
  const changeConfirmations = await Promise.all(this.changeAuthorities.map(authority => authority(newState)));
70
- if (changeConfirmations.includes(false))
79
+ const vetoes = await Promise.all(this.beforeChangeHandlers.map(h => Promise.resolve(h(newState, context))));
80
+ if (changeConfirmations.includes(false) || vetoes.includes(false))
71
81
  return false;
72
82
  // perform the change
73
83
  this.previousState = this.appState;
74
84
  this.stateContext = context;
75
85
  this.appState = newState;
76
86
  dispatcher.trigger('state-manager', 'state', 'changed', this.appState);
87
+ // afterChange hooks
88
+ for (const cb of this.afterChangeHandlers) {
89
+ await cb(this.appState, context, this.previousState);
90
+ }
77
91
  return true;
78
92
  }
79
93
  /**
@@ -86,8 +100,9 @@ export class StateManager {
86
100
  addState(name, pageName, route, mode) {
87
101
  pageName = pageName || name;
88
102
  route = route || pageName;
89
- if (typeof route === "string") {
90
- let newRoute = route.split('%').join('?(.*)');
103
+ if (typeof route === 'string' && route.includes('%')) {
104
+ // keep colon-based string patterns intact for Router to compile named params
105
+ const newRoute = route.split('%').join('?(.*)');
91
106
  route = new RegExp(`^${newRoute}$`);
92
107
  }
93
108
  this.registerStateByState({
@@ -99,8 +114,13 @@ export class StateManager {
99
114
  }
100
115
  registerStateByState(state) {
101
116
  this.allStates[state.name] = state;
102
- router.add(state.route, async (context) => {
103
- if (await this.setState(state.name, context)) {
117
+ const self = this;
118
+ this.router.add(state.route, async function (...captures) {
119
+ // prefer named params (from router string patterns), else multi-captures array, else first capture
120
+ // @ts-ignore
121
+ const named = (this && this.params) || null;
122
+ const context = named && Object.keys(named).length ? named : (captures.length <= 1 ? captures[0] : captures);
123
+ if (await self.setState(state.name, context)) {
104
124
  // @ts-ignore
105
125
  window.pageChangeHandler && window.pageChangeHandler('send', 'pageview', `/${state.name}/${context || ''}`);
106
126
  // @ts-ignore
@@ -108,4 +128,18 @@ export class StateManager {
108
128
  }
109
129
  });
110
130
  }
131
+ onBeforeChange(handler) {
132
+ this.beforeChangeHandlers.push(handler);
133
+ }
134
+ onAfterChange(handler) {
135
+ this.afterChangeHandlers.push(handler);
136
+ }
137
+ onNotFound(handler) {
138
+ // proxy to router-level notFound
139
+ // @ts-ignore - older Router versions may not have onNotFound
140
+ this.router.onNotFound && this.router.onNotFound(handler);
141
+ }
142
+ }
143
+ export function createStateManager(mode = 'hash', autostart = true, routerInstance = router) {
144
+ return new StateManager(mode, autostart, routerInstance);
111
145
  }
@@ -0,0 +1,18 @@
1
+ "use strict";
2
+ var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
3
+ if (k2 === undefined) k2 = k;
4
+ var desc = Object.getOwnPropertyDescriptor(m, k);
5
+ if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
6
+ desc = { enumerable: true, get: function() { return m[k]; } };
7
+ }
8
+ Object.defineProperty(o, k2, desc);
9
+ }) : (function(o, m, k, k2) {
10
+ if (k2 === undefined) k2 = k;
11
+ o[k2] = m[k];
12
+ }));
13
+ var __exportStar = (this && this.__exportStar) || function(m, exports) {
14
+ for (var p in m) if (p !== "default" && !Object.prototype.hasOwnProperty.call(exports, p)) __createBinding(exports, m, p);
15
+ };
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ __exportStar(require("./router"), exports);
18
+ __exportStar(require("./state-manager"), exports);
@@ -0,0 +1,265 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.router = exports.Router = void 0;
4
+ exports.createRouter = createRouter;
5
+ class Router {
6
+ mode = 'hash';
7
+ routes = [];
8
+ root = '/';
9
+ rootCompare = '';
10
+ baseLocation = null;
11
+ staticFilters = [];
12
+ isListening = false;
13
+ bound = {};
14
+ notFoundHandler;
15
+ decodeParams = false;
16
+ constructor() {
17
+ this.staticFilters.push(url => {
18
+ const staticFileExtensions = ['.json', '.css', '.js', '.png', '.jpg', '.svg', '.webp', '.md', '.ejs', '.jsm', '.txt'];
19
+ return staticFileExtensions.some(ext => url.endsWith(ext));
20
+ });
21
+ }
22
+ cleanPathString(path) {
23
+ path = path.replace(/\/$/, '').replace(/^\//, '');
24
+ return path = path.replace(/#{2,}/g, '#');
25
+ }
26
+ clearQuery(url) {
27
+ const [path, query] = url.split('?');
28
+ if (!query)
29
+ return path;
30
+ const [_, hash] = query.split('#');
31
+ return hash ? `${path}#${hash}` : path;
32
+ }
33
+ isStaticFile(url) {
34
+ return (this.staticFilters || []).some(filter => filter(url));
35
+ }
36
+ resetRoot(root) {
37
+ const cleaned = this.cleanPathString(root);
38
+ this.root = '/' + cleaned + '/';
39
+ this.rootCompare = cleaned ? cleaned + '/' : '';
40
+ }
41
+ getLocation() {
42
+ if (!this.isBrowser())
43
+ return '';
44
+ if (this.mode === 'history') {
45
+ let fragment = decodeURI(window.location.pathname + window.location.search);
46
+ fragment = this.clearQuery(fragment);
47
+ // strip leading slash for comparison convenience
48
+ fragment = fragment.replace(/^\//, '');
49
+ if (this.root !== '/' && this.rootCompare && fragment.startsWith(this.rootCompare)) {
50
+ fragment = fragment.slice(this.rootCompare.length);
51
+ }
52
+ fragment = this.cleanPathString(fragment);
53
+ return fragment;
54
+ }
55
+ else {
56
+ const match = window.location.href.match(/#(.*)$/);
57
+ return match ? this.clearQuery(match[1]) : '';
58
+ }
59
+ }
60
+ add(pattern, handler) {
61
+ let paramNames;
62
+ if (typeof pattern === 'function') {
63
+ handler = pattern;
64
+ pattern = /^.*$/; // Match any path
65
+ }
66
+ else if (typeof pattern === 'string') {
67
+ const compiled = this.compilePattern(pattern);
68
+ pattern = compiled.regex;
69
+ paramNames = compiled.paramNames;
70
+ }
71
+ this.routes.push({ pattern: pattern, handler: handler, paramNames });
72
+ return this;
73
+ }
74
+ onNotFound(handler) {
75
+ this.notFoundHandler = handler;
76
+ return this;
77
+ }
78
+ setDecodeParams(decode) {
79
+ this.decodeParams = decode;
80
+ return this;
81
+ }
82
+ getQueryParams(search) {
83
+ if (!this.isBrowser() && !search)
84
+ return {};
85
+ const qs = typeof search === 'string' ? search : window.location.search || '';
86
+ const usp = new URLSearchParams(qs);
87
+ const obj = {};
88
+ usp.forEach((v, k) => { obj[k] = v; });
89
+ return obj;
90
+ }
91
+ isBrowser() {
92
+ return typeof window !== 'undefined' && typeof document !== 'undefined';
93
+ }
94
+ compilePattern(pattern) {
95
+ // normalize leading slash to align with cleanPathString behavior
96
+ const normalized = pattern.replace(/^\//, '');
97
+ const paramNames = [];
98
+ // convert /users/:id -> ^users/([^/]+)$
99
+ const reStr = normalized
100
+ .replace(/([.*+?^${}()|[\]\\])/g, '\\$1') // escape regex specials
101
+ .replace(/:([a-zA-Z_][a-zA-Z0-9_]*)/g, (_m, p1) => {
102
+ paramNames.push(p1);
103
+ return '([^/]+)';
104
+ });
105
+ return { regex: new RegExp(`^${reStr}$`), paramNames };
106
+ }
107
+ /**
108
+ *
109
+ * @param location
110
+ * @return true if it was intercepted or false if not handled
111
+ */
112
+ handleChange(location) {
113
+ const path = (location ?? this.getLocation()) || '';
114
+ if (this.isStaticFile(path))
115
+ return false; // Bypass routing for static files
116
+ for (const route of this.routes) {
117
+ const match = route.pattern ? path.match(route.pattern) : null;
118
+ if (match) {
119
+ match.shift(); // Remove the full match element
120
+ const queryParams = this.getQueryParams();
121
+ const captures = this.decodeParams ? match.map(v => safeDecode(v)) : match;
122
+ let params;
123
+ if (route.paramNames && route.paramNames.length) {
124
+ params = {};
125
+ route.paramNames.forEach((n, i) => params[n] = captures[i]);
126
+ }
127
+ route.handler.call({ queryParams, params }, ...captures);
128
+ return true;
129
+ }
130
+ }
131
+ if (this.notFoundHandler) {
132
+ this.notFoundHandler(path);
133
+ return true;
134
+ }
135
+ if (path)
136
+ console.warn(`No routing found for ${path}`);
137
+ return false;
138
+ }
139
+ listen(mode = 'hash') {
140
+ if (!this.isBrowser())
141
+ return;
142
+ // avoid duplicate listeners
143
+ if (this.isListening)
144
+ this.unlisten();
145
+ const self = this;
146
+ this.mode = mode;
147
+ const handler = (path) => {
148
+ const p = path || location.href.split('#')[0];
149
+ if (self.isStaticFile(p))
150
+ return;
151
+ const currentLocation = self.getLocation();
152
+ if (self.baseLocation !== currentLocation) {
153
+ self.baseLocation = currentLocation;
154
+ self.handleChange(currentLocation);
155
+ }
156
+ };
157
+ const handleInternalNavigation = (event) => {
158
+ // modified clicks or non-left clicks
159
+ const me = event;
160
+ if (event.type === 'click') {
161
+ if (me.button !== 0)
162
+ return; // left-click only
163
+ if (me.metaKey || me.ctrlKey || me.shiftKey)
164
+ return;
165
+ }
166
+ const target = event.target;
167
+ if (!target || !('closest' in target))
168
+ return;
169
+ const anchor = target.closest?.('a');
170
+ if (!anchor)
171
+ return;
172
+ if (event.type === 'keydown') {
173
+ const ke = event;
174
+ if (ke.key !== 'Enter')
175
+ return;
176
+ }
177
+ const href = anchor.getAttribute('href') || '';
178
+ if (!href)
179
+ return;
180
+ const url = new URL(href, window.location.href);
181
+ // ignore external origins or different hostnames
182
+ if (url.origin !== window.location.origin)
183
+ return;
184
+ // ignore target=_blank, download, or rel=noreferrer
185
+ const t = anchor.getAttribute('target');
186
+ if (t && t.toLowerCase() === '_blank')
187
+ return;
188
+ if (anchor.hasAttribute('download'))
189
+ return;
190
+ const rel = anchor.getAttribute('rel');
191
+ if (rel && /\bnoreferrer\b/i.test(rel))
192
+ return;
193
+ const pathWithQuery = url.pathname + (url.search || '');
194
+ if (self.isStaticFile(pathWithQuery) || self.isStaticFile(url.pathname))
195
+ return;
196
+ event.preventDefault();
197
+ history.pushState({ path: pathWithQuery }, '', pathWithQuery);
198
+ handler(pathWithQuery);
199
+ };
200
+ switch (mode) {
201
+ case 'hash':
202
+ this.bound.hashchange = () => handler();
203
+ window.addEventListener('hashchange', this.bound.hashchange);
204
+ break;
205
+ case 'history':
206
+ this.bound.popstate = (event) => handler(event.state?.path);
207
+ this.bound.click = handleInternalNavigation;
208
+ this.bound.keydown = (event) => handleInternalNavigation(event);
209
+ window.addEventListener('popstate', this.bound.popstate);
210
+ document.addEventListener('click', this.bound.click);
211
+ document.addEventListener('keydown', this.bound.keydown);
212
+ break;
213
+ }
214
+ this.isListening = true;
215
+ handler();
216
+ }
217
+ unlisten() {
218
+ if (!this.isBrowser() || !this.isListening)
219
+ return;
220
+ switch (this.mode) {
221
+ case 'hash':
222
+ if (this.bound.hashchange)
223
+ window.removeEventListener('hashchange', this.bound.hashchange);
224
+ break;
225
+ case 'history':
226
+ if (this.bound.popstate)
227
+ window.removeEventListener('popstate', this.bound.popstate);
228
+ if (this.bound.click)
229
+ document.removeEventListener('click', this.bound.click);
230
+ if (this.bound.keydown)
231
+ document.removeEventListener('keydown', this.bound.keydown);
232
+ break;
233
+ }
234
+ this.bound = {};
235
+ this.isListening = false;
236
+ }
237
+ navigate(path = '', opts) {
238
+ if (!this.isBrowser())
239
+ return false;
240
+ if (this.mode === 'history') {
241
+ const url = this.root + this.cleanPathString(path);
242
+ if (opts?.replace)
243
+ history.replaceState(null, '', url);
244
+ else
245
+ history.pushState(null, '', url);
246
+ }
247
+ else
248
+ window.location.hash = this.cleanPathString(path);
249
+ return this.handleChange();
250
+ }
251
+ replace(path = '') {
252
+ return this.navigate(path, { replace: true });
253
+ }
254
+ }
255
+ exports.Router = Router;
256
+ exports.router = new Router();
257
+ function createRouter() { return new Router(); }
258
+ function safeDecode(v) {
259
+ try {
260
+ return decodeURIComponent(v);
261
+ }
262
+ catch {
263
+ return v;
264
+ }
265
+ }
@@ -0,0 +1,41 @@
1
+ type RouteHandler = (...args: string[]) => void;
2
+ export type RoutingMode = 'history' | 'hash';
3
+ export declare class Router {
4
+ private mode;
5
+ private routes;
6
+ private root;
7
+ private rootCompare;
8
+ private baseLocation;
9
+ staticFilters: ((url: string) => boolean)[];
10
+ private isListening;
11
+ private bound;
12
+ private notFoundHandler?;
13
+ private decodeParams;
14
+ constructor();
15
+ private cleanPathString;
16
+ private clearQuery;
17
+ private isStaticFile;
18
+ resetRoot(root: string): void;
19
+ getLocation(): string;
20
+ add(pattern: RegExp | string | RouteHandler, handler?: RouteHandler): Router;
21
+ onNotFound(handler: (path: string) => void): Router;
22
+ setDecodeParams(decode: boolean): Router;
23
+ getQueryParams(search?: string): Record<string, string>;
24
+ private isBrowser;
25
+ private compilePattern;
26
+ /**
27
+ *
28
+ * @param location
29
+ * @return true if it was intercepted or false if not handled
30
+ */
31
+ handleChange(location?: string): boolean;
32
+ listen(mode?: RoutingMode): void;
33
+ unlisten(): void;
34
+ navigate(path?: string, opts?: {
35
+ replace?: boolean;
36
+ }): boolean;
37
+ replace(path?: string): boolean;
38
+ }
39
+ export declare const router: Router;
40
+ export declare function createRouter(): Router;
41
+ export {};
@@ -0,0 +1,150 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.StateManager = void 0;
4
+ exports.createStateManager = createStateManager;
5
+ const router_1 = require("./router");
6
+ const pubsub_1 = require("@dharmax/pubsub");
7
+ class StateManager {
8
+ mode;
9
+ allStates = {};
10
+ appState;
11
+ previousState;
12
+ stateContext;
13
+ static dispatcher = pubsub_1.default;
14
+ changeAuthorities = [];
15
+ router;
16
+ beforeChangeHandlers = [];
17
+ afterChangeHandlers = [];
18
+ constructor(mode = 'hash', autostart = true, routerInstance = router_1.router) {
19
+ this.mode = mode;
20
+ this.router = routerInstance;
21
+ if (autostart)
22
+ this.router.listen(mode);
23
+ }
24
+ start() {
25
+ this.router.listen(this.mode);
26
+ }
27
+ stop() {
28
+ // unlisten router to cleanup event listeners
29
+ // @ts-ignore - older Router versions may not have unlisten
30
+ this.router.unlisten && this.router.unlisten();
31
+ }
32
+ onChange(handler) {
33
+ return StateManager.dispatcher.on('state:changed', handler);
34
+ }
35
+ /*
36
+ Add a hook which enable conditional approval of state change. It can be more than one; when a state
37
+ change is requested, all the registered authorities must return true (asynchronously) otherwise the change
38
+ requested doesn't happen.
39
+ **/
40
+ registerChangeAuthority(authorityCallback) {
41
+ this.changeAuthorities.push(authorityCallback);
42
+ }
43
+ getState() {
44
+ return this.appState || {};
45
+ }
46
+ get previous() {
47
+ return this.previousState;
48
+ }
49
+ get context() {
50
+ return this.stateContext;
51
+ }
52
+ /**
53
+ * set current page state
54
+ * @param state can be either just a state or a state and context (which can be sub-state, or anything else)
55
+ */
56
+ set state(state) {
57
+ if (Array.isArray(state)) {
58
+ const sName = state.shift();
59
+ this.setState(sName, state);
60
+ }
61
+ else
62
+ this.setState(state);
63
+ }
64
+ /** attempts to restore state from current url. Currently, works only in hash mode */
65
+ restoreState(defaultState) {
66
+ if (this.router.navigate(window.location.pathname))
67
+ return;
68
+ this.router.navigate(defaultState);
69
+ }
70
+ /**
71
+ *
72
+ * @param stateName state
73
+ * @param context extra context (e.g. sub-state)
74
+ */
75
+ async setState(stateName, context) {
76
+ const newState = this.allStates[stateName];
77
+ if (!newState) {
78
+ alert(`Undefined app state ${stateName}`);
79
+ return false;
80
+ }
81
+ // check if the state change was declined by any change authority and if so - don't do it and return false
82
+ const changeConfirmations = await Promise.all(this.changeAuthorities.map(authority => authority(newState)));
83
+ const vetoes = await Promise.all(this.beforeChangeHandlers.map(h => Promise.resolve(h(newState, context))));
84
+ if (changeConfirmations.includes(false) || vetoes.includes(false))
85
+ return false;
86
+ // perform the change
87
+ this.previousState = this.appState;
88
+ this.stateContext = context;
89
+ this.appState = newState;
90
+ pubsub_1.default.trigger('state-manager', 'state', 'changed', this.appState);
91
+ // afterChange hooks
92
+ for (const cb of this.afterChangeHandlers) {
93
+ await cb(this.appState, context, this.previousState);
94
+ }
95
+ return true;
96
+ }
97
+ /**
98
+ * Define an application state
99
+ * @param name
100
+ * @param pageName by default it equals the name (you can null it)
101
+ * @param route by default it equals the pageName (ditto)
102
+ * @param mode optional
103
+ */
104
+ addState(name, pageName, route, mode) {
105
+ pageName = pageName || name;
106
+ route = route || pageName;
107
+ if (typeof route === 'string' && route.includes('%')) {
108
+ // keep colon-based string patterns intact for Router to compile named params
109
+ const newRoute = route.split('%').join('?(.*)');
110
+ route = new RegExp(`^${newRoute}$`);
111
+ }
112
+ this.registerStateByState({
113
+ name,
114
+ pageName,
115
+ route,
116
+ mode
117
+ });
118
+ }
119
+ registerStateByState(state) {
120
+ this.allStates[state.name] = state;
121
+ const self = this;
122
+ this.router.add(state.route, async function (...captures) {
123
+ // prefer named params (from router string patterns), else multi-captures array, else first capture
124
+ // @ts-ignore
125
+ const named = (this && this.params) || null;
126
+ const context = named && Object.keys(named).length ? named : (captures.length <= 1 ? captures[0] : captures);
127
+ if (await self.setState(state.name, context)) {
128
+ // @ts-ignore
129
+ window.pageChangeHandler && window.pageChangeHandler('send', 'pageview', `/${state.name}/${context || ''}`);
130
+ // @ts-ignore
131
+ window.ga && window.ga('send', 'pageview', `/${state.name}/${context || ''}`);
132
+ }
133
+ });
134
+ }
135
+ onBeforeChange(handler) {
136
+ this.beforeChangeHandlers.push(handler);
137
+ }
138
+ onAfterChange(handler) {
139
+ this.afterChangeHandlers.push(handler);
140
+ }
141
+ onNotFound(handler) {
142
+ // proxy to router-level notFound
143
+ // @ts-ignore - older Router versions may not have onNotFound
144
+ this.router.onNotFound && this.router.onNotFound(handler);
145
+ }
146
+ }
147
+ exports.StateManager = StateManager;
148
+ function createStateManager(mode = 'hash', autostart = true, routerInstance = router_1.router) {
149
+ return new StateManager(mode, autostart, routerInstance);
150
+ }
@@ -0,0 +1,56 @@
1
+ import { RoutingMode, Router as RouterType } from "./router";
2
+ import { IPubSubHandle, PubSubEvent } from "@dharmax/pubsub";
3
+ export type ApplicationStateName = string;
4
+ export type ApplicationState = {
5
+ name: ApplicationStateName;
6
+ pageName: string;
7
+ route: RegExp | string;
8
+ mode?: string | string[];
9
+ };
10
+ export type ChangeAuthority = (state: ApplicationState) => Promise<boolean>;
11
+ export declare class StateManager {
12
+ private mode;
13
+ private allStates;
14
+ private appState;
15
+ private previousState;
16
+ private stateContext;
17
+ static dispatcher: import("@dharmax/pubsub").PubSub;
18
+ private changeAuthorities;
19
+ private router;
20
+ private beforeChangeHandlers;
21
+ private afterChangeHandlers;
22
+ constructor(mode?: RoutingMode, autostart?: boolean, routerInstance?: RouterType);
23
+ start(): void;
24
+ stop(): void;
25
+ onChange(handler: (event: PubSubEvent, data: any) => void): IPubSubHandle;
26
+ registerChangeAuthority(authorityCallback: (targetState: ApplicationState) => Promise<boolean>): void;
27
+ getState(): ApplicationState;
28
+ get previous(): ApplicationState;
29
+ get context(): any;
30
+ /**
31
+ * set current page state
32
+ * @param state can be either just a state or a state and context (which can be sub-state, or anything else)
33
+ */
34
+ set state(state: ApplicationStateName | [ApplicationStateName, ...any]);
35
+ /** attempts to restore state from current url. Currently, works only in hash mode */
36
+ restoreState(defaultState: ApplicationStateName): void;
37
+ /**
38
+ *
39
+ * @param stateName state
40
+ * @param context extra context (e.g. sub-state)
41
+ */
42
+ setState(stateName: ApplicationStateName, context?: any): Promise<boolean>;
43
+ /**
44
+ * Define an application state
45
+ * @param name
46
+ * @param pageName by default it equals the name (you can null it)
47
+ * @param route by default it equals the pageName (ditto)
48
+ * @param mode optional
49
+ */
50
+ addState(name: string, pageName?: string, route?: RegExp | string, mode?: string | string[]): void;
51
+ registerStateByState(state: ApplicationState): void;
52
+ onBeforeChange(handler: (target: ApplicationState, context?: any) => boolean | Promise<boolean>): void;
53
+ onAfterChange(handler: (state: ApplicationState, context?: any, previous?: ApplicationState) => void | Promise<void>): void;
54
+ onNotFound(handler: (path: string) => void): void;
55
+ }
56
+ export declare function createStateManager(mode?: RoutingMode, autostart?: boolean, routerInstance?: RouterType): StateManager;
package/package.json CHANGED
@@ -1,14 +1,27 @@
1
1
  {
2
2
  "name": "@dharmax/state-router",
3
- "version": "3.1.2",
3
+ "version": "3.2.1",
4
4
  "description": "A cute and tight router and application state controller",
5
- "main": "dist/index.js",
5
+ "type": "module",
6
+ "main": "dist-cjs/index.cjs",
7
+ "module": "dist/index.js",
6
8
  "types": "dist/index.d.ts",
9
+ "exports": {
10
+ ".": {
11
+ "types": "./dist/index.d.ts",
12
+ "import": "./dist/index.js",
13
+ "require": "./dist-cjs/index.cjs"
14
+ },
15
+ "./package.json": "./package.json"
16
+ },
7
17
 
8
18
  "scripts": {
9
- "test": "echo \"Error: no test specified\" && exit 1",
10
- "build": "npx tsc",
11
- "prepublish": "npm run build"
19
+ "test": "vitest run --coverage",
20
+ "test:watch": "vitest",
21
+ "build": "npm run build:esm && npm run build:cjs",
22
+ "build:esm": "npx tsc",
23
+ "build:cjs": "npx tsc -p tsconfig.cjs.json && node scripts/rename-cjs.mjs",
24
+ "prepublishOnly": "npm run build"
12
25
  },
13
26
  "repository": {
14
27
  "type": "git",
@@ -23,9 +36,18 @@
23
36
  "bugs": {
24
37
  "url": "https://github.com/dharmax/state-router/issues"
25
38
  },
26
- "homepage": "https://github.com/dharmax/state-routerr",
39
+ "homepage": "https://github.com/dharmax/state-router",
40
+ "sideEffects": false,
41
+ "files": [
42
+ "dist",
43
+ "dist-cjs",
44
+ "ReadMe.md"
45
+ ],
27
46
  "devDependencies": {
28
- "typescript": "^4.9.5"
47
+ "typescript": "^5.9.2",
48
+ "vitest": "^2.0.5",
49
+ "jsdom": "^24.1.0",
50
+ "@vitest/coverage-v8": "^2.0.5"
29
51
  },
30
52
  "dependencies": {
31
53
  "@dharmax/pubsub": "^1.1.0"