@thi.ng/router 3.1.8 → 3.2.2

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/CHANGELOG.md CHANGED
@@ -1,6 +1,6 @@
1
1
  # Change Log
2
2
 
3
- - **Last updated**: 2022-06-09T16:14:01Z
3
+ - **Last updated**: 2022-08-01T14:54:00Z
4
4
  - **Generator**: [thi.ng/monopub](https://thi.ng/monopub)
5
5
 
6
6
  All notable changes to this project will be documented in this file.
@@ -9,6 +9,19 @@ See [Conventional Commits](https://conventionalcommits.org/) for commit guidelin
9
9
  **Note:** Unlisted _patch_ versions only involve non-code or otherwise excluded changes
10
10
  and/or version bumps of transitive dependencies.
11
11
 
12
+ ## [3.2.0](https://github.com/thi-ng/umbrella/tree/@thi.ng/router@3.2.0) (2022-06-17)
13
+
14
+ #### 🚀 Features
15
+
16
+ - update format(), hash/prefix handling ([724b3ad](https://github.com/thi-ng/umbrella/commit/724b3ad))
17
+ - update HTMLRouter default prefix to "#/" if `useFragment` is true
18
+ - remove obsolete `HTMLRouter.format()` (now the same as BasicRouter)
19
+ - update BasicRouter.format() to throw error for missing route param value
20
+ - add trailing slash option, optimize routeForID() ([c003dc2](https://github.com/thi-ng/umbrella/commit/c003dc2))
21
+ - update BasicRouter default config init
22
+ - pre-build `routeIndex` in ctor
23
+ - optimize `routeForID()` to use new `routeIndex`
24
+
12
25
  ### [3.1.5](https://github.com/thi-ng/umbrella/tree/@thi.ng/router@3.1.5) (2022-04-07)
13
26
 
14
27
  #### ♻️ Refactoring
package/README.md CHANGED
@@ -70,7 +70,7 @@ node --experimental-repl-await
70
70
  > const router = await import("@thi.ng/router");
71
71
  ```
72
72
 
73
- Package sizes (gzipped, pre-treeshake): ESM: 1.56 KB
73
+ Package sizes (gzipped, pre-treeshake): ESM: 1.54 KB
74
74
 
75
75
  ## Dependencies
76
76
 
@@ -114,10 +114,10 @@ const config = {
114
114
  // Optional route path component separator. Default: `/`
115
115
  separator: "/",
116
116
 
117
- // Route prefix. Default: `/`. All routes to be parsed by `route()`
118
- // are assumed to have this prefix. All routes returned by
119
- // `format()` will include this prefix.
120
- prefix: "/",
117
+ // Route prefix. Default: `/` (or `#/` if `useFragment` is enabled).
118
+ // All routes to be parsed by `route()` are assumed to have this prefix.
119
+ // All routes returned by `format()` will include this prefix.
120
+ prefix: "#/",
121
121
 
122
122
  // actual route defs
123
123
  // these are checked in given order
@@ -185,9 +185,6 @@ router.addListener(EVENT_ROUTE_CHANGED, console.log);
185
185
  router.start();
186
186
  ```
187
187
 
188
- See [further comments in source
189
- code](https://github.com/thi-ng/umbrella/blob/develop/packages/router/src/api.ts)
190
-
191
188
  ## Authors
192
189
 
193
190
  Karsten Schmidt
package/api.d.ts CHANGED
@@ -1,16 +1,16 @@
1
1
  import type { Fn, IID, IObjectOf } from "@thi.ng/api";
2
2
  /**
3
- * A validation function to for authenticated routes. If this function
4
- * determines that the user is not allowed to access this route, it
5
- * should return nothing or a {@link RouteMatch} object for redirecting (e.g.
6
- * to a login, home page or other non-protected route). If nothing is
7
- * returned and no other routes can be matched, the router will
8
- * eventually return the configure default fallback route.
3
+ * A validation function for to-be authenticated routes. If this function
4
+ * determines that the user is not allowed to access this route, it should
5
+ * return nothing or a {@link RouteMatch} object for redirecting (e.g. to a
6
+ * login, home page or other non-protected route). If nothing is returned and no
7
+ * other routes can be matched, the router will eventually return the configured
8
+ * default fallback route (see {@link RouterConfig.defaultRouteID}).
9
9
  */
10
10
  export declare type RouteAuthenticator = (route: Route, curr: string[], params: any) => RouteMatch;
11
11
  /**
12
- * Route validator subspecs are optional and used to coerce and/or
13
- * validate individual route parameters.
12
+ * Route validator subspecs are optional and used to coerce and/or validate
13
+ * individual route parameters.
14
14
  */
15
15
  export interface RouteParamValidator {
16
16
  /**
@@ -18,30 +18,28 @@ export interface RouteParamValidator {
18
18
  */
19
19
  coerce?: Fn<string, any>;
20
20
  /**
21
- * Optional arbitrary value validation. If any validator
22
- * returns non-true result, the currently checked route
23
- * becomes unmatched/invalid and the router continues
24
- * checking other routes.
21
+ * Optional arbitrary value validation (applied *after* coercion, if any).
22
+ * If a validator returns non-true result, the currently checked route
23
+ * becomes unmatched/invalid and the router continues checking other routes.
25
24
  */
26
25
  check: Fn<any, boolean>;
27
26
  }
28
27
  /**
29
- * A Route describes an application path (possibly parameterized),
30
- * incl. parameter coercion, validation and overall route
31
- * authentication. Apart from `id` and `match` all other fields
32
- * are optional.
28
+ * A Route describes an application path (possibly parameterized), incl.
29
+ * parameter coercion, validation and overall route authentication. Apart from
30
+ * `id` and `match` all other fields are optional.
33
31
  */
34
32
  export interface Route extends IID<string> {
35
33
  /**
36
- * Array of path components. If a value is prefixed with `?` this
37
- * path component will be captured under that name. E.g.
38
- * `["projects", "?id"]` will match any of these routes:
34
+ * Array of path components. If a value is prefixed with `?` this path
35
+ * component will be captured under that name. E.g. `["projects", "?id"]`
36
+ * will match any of these routes:
39
37
  *
40
38
  * - `projects/123`
41
39
  * - `projects/abcde`
42
40
  *
43
- * `validate` options can then be used to further restrict the
44
- * possible value range of the `id` value...
41
+ * `validate` options can then be used to further restrict the possible
42
+ * value range of the `id` value...
45
43
  */
46
44
  match: string[];
47
45
  /**
@@ -57,14 +55,14 @@ export interface Route extends IID<string> {
57
55
  * }
58
56
  * ```
59
57
  *
60
- * This will first coerce the `id` route param to a number and then
61
- * only allow the route to be matched if `id < 100`.
58
+ * This will first coerce the `id` route param to a number and then only
59
+ * allow the route to be matched if `id < 100`.
62
60
  */
63
61
  validate?: IObjectOf<RouteParamValidator>;
64
62
  /**
65
63
  * Flag to indicate if this route should be passed to the globally
66
- * configured authentication function. Only matched and validated
67
- * routes are processed.
64
+ * configured authentication function. Only matched and validated routes are
65
+ * processed.
68
66
  */
69
67
  auth?: boolean;
70
68
  /**
@@ -94,26 +92,25 @@ export interface RouteMatch extends IID<string> {
94
92
  */
95
93
  export interface RouterConfig {
96
94
  /**
97
- * An array of route specs, which are being attempted to be matched
98
- * in order of appearance.
95
+ * An array of route specs, which are being attempted to be matched in order
96
+ * of appearance.
99
97
  */
100
98
  routes: Route[];
101
99
  /**
102
- * Fallback route ID (MUST exist in `routes`), used if none of the
103
- * defined routes could be matched against user input, e.g. a home
104
- * or error page.
100
+ * Fallback route ID (MUST exist in `routes`), used if none of the defined
101
+ * routes could be matched against user input, e.g. a home or error page.
105
102
  */
106
103
  defaultRouteID: string;
107
104
  /**
108
- * Optional initial route to trigger when router starts. If given,
109
- * this MUST be a route without params.
105
+ * Optional initial route to trigger when router starts. If given, this MUST
106
+ * be a route without params.
110
107
  */
111
108
  initialRouteID?: string;
112
109
  /**
113
110
  * Optional route authentication function. See {@link RouteAuthenticator}
114
- * for further details. If no authenticator is given, all matched
115
- * routes will always succeed, regardless if a rule's `auth` flag is
116
- * enabled or not.
111
+ * for further details. If no authenticator is given, all matched routes
112
+ * will always succeed, regardless if a rule's `auth` flag is enabled or
113
+ * not.
117
114
  */
118
115
  authenticator?: RouteAuthenticator;
119
116
  /**
@@ -122,16 +119,27 @@ export interface RouterConfig {
122
119
  separator?: string;
123
120
  /**
124
121
  * Route prefix. Default: `/`. All routes to be parsed by
125
- * {@link BasicRouter.route} are assumed to have this prefix. All
126
- * routes returned by {@link (BasicRouter.format:1)} will include
127
- * this prefix.
122
+ * {@link BasicRouter.route} are assumed to have this prefix. All routes
123
+ * returned by {@link (BasicRouter.format:1)} will include this prefix.
128
124
  */
129
125
  prefix?: string;
126
+ /**
127
+ * If true (default), the trailing slash (actually
128
+ * {@link RouterConfig.separator}) of a given route string will be removed
129
+ * before matching.
130
+ */
131
+ removeTrailingSlash?: boolean;
130
132
  }
131
133
  export interface HTMLRouterConfig extends RouterConfig {
132
134
  /**
133
- * Optional flag to indicate if URL hash fragment should be used for
134
- * routes.
135
+ * Same as {@link RouterConfig.prefix}. If
136
+ * {@link HTMLRouterConfig.useFragment} is true, then the default changes to
137
+ * `#/`. If `useFragment` is enabled and a custom prefix is given, it MUST
138
+ * include the leading `#` as well.
139
+ */
140
+ prefix?: string;
141
+ /**
142
+ * Optional flag to indicate if URL hash fragment should be used for routes.
135
143
  */
136
144
  useFragment?: boolean;
137
145
  }
package/basic.d.ts CHANGED
@@ -3,6 +3,7 @@ import { Route, RouteMatch, RouteParamValidator, RouterConfig } from "./api.js";
3
3
  export declare class BasicRouter implements INotify {
4
4
  config: RouterConfig;
5
5
  current: RouteMatch | undefined;
6
+ routeIndex: Record<string, Route>;
6
7
  constructor(config: RouterConfig);
7
8
  /** {@inheritDoc @thi.ng/api#INotify.addListener} */
8
9
  addListener(id: string, fn: Listener, scope?: any): boolean;
@@ -17,19 +18,19 @@ export declare class BasicRouter implements INotify {
17
18
  * to default route. Before returning, triggers event with
18
19
  * return value as well.
19
20
  *
20
- * @param raw - route path to match
21
+ * @param src - route path to match
21
22
  */
22
23
  route(src: string): RouteMatch | undefined;
23
24
  /**
24
- * Returns a formatted version of given {@link RouteMatch}, incl. any params.
25
- * Throw an error if an invalid route `id` is provided.
25
+ * Returns a formatted version of given {@link RouteMatch}, incl. any
26
+ * params, or alternatively a registered route ID (and optional route
27
+ * params). Throws an error if an invalid route `id` is provided.
26
28
  *
27
- * @param match -
29
+ * @param id -
28
30
  * @param params -
29
- * @param hash - if true, prepends `#` to results
30
31
  */
31
- format(id: string, params?: any, hash?: boolean): string;
32
- format(match: Partial<RouteMatch>, hash?: boolean): string;
32
+ format(id: string, params?: any): string;
33
+ format(match: Partial<RouteMatch>): string;
33
34
  routeForID(id: string): Route | undefined;
34
35
  protected matchRoutes(src: string): RouteMatch | undefined;
35
36
  protected matchRoute(curr: string[], route: Route): RouteMatch | undefined;
package/basic.js CHANGED
@@ -8,16 +8,18 @@ import { illegalArity } from "@thi.ng/errors/illegal-arity";
8
8
  import { EVENT_ROUTE_CHANGED, } from "./api.js";
9
9
  let BasicRouter = class BasicRouter {
10
10
  constructor(config) {
11
- config.authenticator =
12
- config.authenticator ||
13
- ((route, _, params) => ({
14
- id: route.id,
15
- title: route.title,
16
- params,
17
- }));
18
- config.prefix = config.prefix === undefined ? "/" : config.prefix;
19
- config.separator = config.separator || "/";
20
- this.config = config;
11
+ this.config = {
12
+ authenticator: (route, _, params) => ({
13
+ id: route.id,
14
+ title: route.title,
15
+ params,
16
+ }),
17
+ prefix: "/",
18
+ separator: "/",
19
+ removeTrailingSlash: true,
20
+ ...config,
21
+ };
22
+ this.routeIndex = this.config.routes.reduce((acc, r) => ((acc[r.id] = r), acc), {});
21
23
  assert(this.routeForID(this.config.defaultRouteID) !== undefined, `missing config for default route: '${this.config.defaultRouteID}'`);
22
24
  if (config.initialRouteID) {
23
25
  const route = this.routeForID(config.initialRouteID);
@@ -47,11 +49,12 @@ let BasicRouter = class BasicRouter {
47
49
  * to default route. Before returning, triggers event with
48
50
  * return value as well.
49
51
  *
50
- * @param raw - route path to match
52
+ * @param src - route path to match
51
53
  */
52
54
  route(src) {
53
- if (src.charAt(0) === "#") {
54
- src = src.substring(1);
55
+ if (this.config.removeTrailingSlash &&
56
+ src.charAt(src.length - 1) === this.config.separator) {
57
+ src = src.substring(0, src.length - 1);
55
58
  }
56
59
  src = src.substring(this.config.prefix.length);
57
60
  let match = this.matchRoutes(src);
@@ -69,20 +72,11 @@ let BasicRouter = class BasicRouter {
69
72
  return match;
70
73
  }
71
74
  format(...args) {
72
- let [id, params, hash] = args;
75
+ let [id, params] = args;
73
76
  let match;
74
77
  switch (args.length) {
75
- case 3:
76
- match = { id, params };
77
- break;
78
78
  case 2:
79
- if (isString(id)) {
80
- match = { id, params };
81
- }
82
- else {
83
- hash = params;
84
- match = id;
85
- }
79
+ match = { id, params };
86
80
  break;
87
81
  case 1:
88
82
  match = isString(id) ? { id } : id;
@@ -93,14 +87,18 @@ let BasicRouter = class BasicRouter {
93
87
  const route = this.routeForID(match.id);
94
88
  if (route) {
95
89
  const params = match.params || {};
96
- return ((hash ? "#" : "") +
97
- this.config.prefix +
90
+ return (this.config.prefix +
98
91
  route.match
99
- .map((x) => x.charAt(0) === "?"
100
- ? (x = params[x.substring(1)]) != null
101
- ? x
102
- : "NULL"
103
- : x)
92
+ .map((x) => {
93
+ if (isRouteParam(x)) {
94
+ const id = x.substring(1);
95
+ const p = params[id];
96
+ if (p != null)
97
+ return p;
98
+ illegalArgs(`missing value for param '${id}'`);
99
+ }
100
+ return x;
101
+ })
104
102
  .join(this.config.separator));
105
103
  }
106
104
  else {
@@ -108,7 +106,7 @@ let BasicRouter = class BasicRouter {
108
106
  }
109
107
  }
110
108
  routeForID(id) {
111
- return this.config.routes.find((route) => route.id === id);
109
+ return this.routeIndex[id];
112
110
  }
113
111
  matchRoutes(src) {
114
112
  const routes = this.config.routes;
@@ -121,12 +119,13 @@ let BasicRouter = class BasicRouter {
121
119
  }
122
120
  }
123
121
  matchRoute(curr, route) {
124
- const match = route.match, n = match.length;
122
+ const match = route.match;
123
+ const n = match.length;
125
124
  if (curr.length === n) {
126
125
  const params = {};
127
126
  for (let i = 0; i < n; i++) {
128
127
  const m = match[i];
129
- if (m.charAt(0) === "?") {
128
+ if (isRouteParam(m)) {
130
129
  params[m.substring(1)] = curr[i];
131
130
  }
132
131
  else if (curr[i] !== m) {
@@ -164,4 +163,5 @@ BasicRouter = __decorate([
164
163
  INotifyMixin
165
164
  ], BasicRouter);
166
165
  export { BasicRouter };
167
- const isParametricRoute = (route) => route.match.some((p) => p.charAt(0) === "?");
166
+ const isParametricRoute = (route) => route.match.some(isRouteParam);
167
+ const isRouteParam = (x) => x[0] === "?";
package/history.d.ts CHANGED
@@ -1,5 +1,5 @@
1
1
  import type { Fn } from "@thi.ng/api";
2
- import type { HTMLRouterConfig, RouteMatch } from "./api.js";
2
+ import type { HTMLRouterConfig } from "./api.js";
3
3
  import { BasicRouter } from "./basic.js";
4
4
  export declare class HTMLRouter extends BasicRouter {
5
5
  protected currentPath: string;
@@ -16,13 +16,11 @@ export declare class HTMLRouter extends BasicRouter {
16
16
  * If called from userland, this normally is true. However, we want
17
17
  * to avoid this if called from this router's own event handlers.
18
18
  *
19
- * @param raw -
19
+ * @param src -
20
20
  * @param pushState -
21
21
  */
22
- route(src: string, pushState?: boolean): RouteMatch | undefined;
22
+ route(src: string, pushState?: boolean): import("./api.js").RouteMatch | undefined;
23
23
  routeTo(route: string): void;
24
- format(id: PropertyKey, params?: any): string;
25
- format(match: Partial<RouteMatch>): string;
26
24
  protected handlePopChange(): Fn<PopStateEvent, void>;
27
25
  protected handleHashChange(): EventListener;
28
26
  protected handleRouteFailure(): boolean;
package/history.js CHANGED
@@ -1,10 +1,8 @@
1
- import { isString } from "@thi.ng/checks/is-string";
2
1
  import { equiv } from "@thi.ng/equiv";
3
- import { illegalArity } from "@thi.ng/errors/illegal-arity";
4
2
  import { BasicRouter } from "./basic.js";
5
3
  export class HTMLRouter extends BasicRouter {
6
4
  constructor(config) {
7
- super(config);
5
+ super({ prefix: config.useFragment ? "#/" : "/", ...config });
8
6
  this.useFragment = config.useFragment !== false;
9
7
  this.ignoreHashChange = false;
10
8
  }
@@ -36,7 +34,7 @@ export class HTMLRouter extends BasicRouter {
36
34
  * If called from userland, this normally is true. However, we want
37
35
  * to avoid this if called from this router's own event handlers.
38
36
  *
39
- * @param raw -
37
+ * @param src -
40
38
  * @param pushState -
41
39
  */
42
40
  route(src, pushState = true) {
@@ -56,20 +54,6 @@ export class HTMLRouter extends BasicRouter {
56
54
  }
57
55
  this.route(route);
58
56
  }
59
- format(...args) {
60
- let match;
61
- switch (args.length) {
62
- case 2:
63
- match = { id: args[0], params: args[1] };
64
- break;
65
- case 1:
66
- match = isString(args[0]) ? { id: args[0] } : args[0];
67
- break;
68
- default:
69
- illegalArity(args.length);
70
- }
71
- return super.format(match, this.useFragment);
72
- }
73
57
  handlePopChange() {
74
58
  return (this.popHandler =
75
59
  this.popHandler ||
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@thi.ng/router",
3
- "version": "3.1.8",
3
+ "version": "3.2.2",
4
4
  "description": "Generic router for browser & non-browser based applications",
5
5
  "type": "module",
6
6
  "module": "./index.js",
@@ -34,19 +34,19 @@
34
34
  "test": "testament test"
35
35
  },
36
36
  "dependencies": {
37
- "@thi.ng/api": "^8.3.7",
38
- "@thi.ng/checks": "^3.2.1",
39
- "@thi.ng/equiv": "^2.1.7",
40
- "@thi.ng/errors": "^2.1.7",
37
+ "@thi.ng/api": "^8.3.9",
38
+ "@thi.ng/checks": "^3.2.3",
39
+ "@thi.ng/equiv": "^2.1.9",
40
+ "@thi.ng/errors": "^2.1.9",
41
41
  "tslib": "^2.4.0"
42
42
  },
43
43
  "devDependencies": {
44
44
  "@microsoft/api-extractor": "^7.25.0",
45
- "@thi.ng/testament": "^0.2.8",
45
+ "@thi.ng/testament": "^0.2.10",
46
46
  "rimraf": "^3.0.2",
47
47
  "tools": "^0.0.1",
48
48
  "typedoc": "^0.22.17",
49
- "typescript": "^4.7.3"
49
+ "typescript": "^4.7.4"
50
50
  },
51
51
  "keywords": [
52
52
  "browser",
@@ -91,5 +91,5 @@
91
91
  ],
92
92
  "year": 2014
93
93
  },
94
- "gitHead": "ab0188234419f2d9f471de80871df930e5555bd6\n"
94
+ "gitHead": "976ccd698cedaa60dcef2e69030a5eb98898cc4a\n"
95
95
  }