@ktjs/router 0.6.6 → 0.13.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/README.md +5 -1
- package/dist/index.iife.js +406 -1
- package/dist/index.legacy.js +520 -1
- package/dist/index.mjs +399 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -2,10 +2,14 @@
|
|
|
2
2
|
|
|
3
3
|
<img src="https://raw.githubusercontent.com/baendlorel/kt.js/dev/.assets/ktjs-0.0.1.svg" alt="KT.js Logo" width="150"/>
|
|
4
4
|
|
|
5
|
-
|
|
5
|
+
[](https://www.npmjs.com/package/@ktjs/router)
|
|
6
|
+
|
|
7
|
+
> 📦 Part of [KT.js](https://github.com/baendlorel/kt.js) - A simple and easy-to-use web framework that never re-renders.
|
|
6
8
|
|
|
7
9
|
Client-side router with navigation guards for KT.js.
|
|
8
10
|
|
|
11
|
+
**Current Version:** 0.13.0
|
|
12
|
+
|
|
9
13
|
## Overview
|
|
10
14
|
|
|
11
15
|
`@ktjs/router` is a lightweight, hash-based client-side router with powerful navigation guards and async/sync auto-adaptation. It provides all the essential routing features you need without the bloat.
|
package/dist/index.iife.js
CHANGED
|
@@ -1 +1,406 @@
|
|
|
1
|
-
var __ktjs_router__
|
|
1
|
+
var __ktjs_router__ = (function (exports) {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* Default guard that always returns true
|
|
6
|
+
*/
|
|
7
|
+
const defaultHook = () => true;
|
|
8
|
+
const throws = (m) => {
|
|
9
|
+
throw new Error(`@ktjs/router: ${m}`);
|
|
10
|
+
};
|
|
11
|
+
const normalizePath = (...paths) => {
|
|
12
|
+
const p = paths
|
|
13
|
+
.map((p) => p.split('/'))
|
|
14
|
+
.flat()
|
|
15
|
+
.filter(Boolean);
|
|
16
|
+
return '/' + p.join('/');
|
|
17
|
+
};
|
|
18
|
+
/**
|
|
19
|
+
* Parse query string into object
|
|
20
|
+
*/
|
|
21
|
+
const parseQuery = (queryString) => {
|
|
22
|
+
const query = {};
|
|
23
|
+
if (!queryString || queryString === '?') {
|
|
24
|
+
return query;
|
|
25
|
+
}
|
|
26
|
+
const params = queryString.replace(/^\?/, '').split('&');
|
|
27
|
+
for (const param of params) {
|
|
28
|
+
const [key, value] = param.split('=');
|
|
29
|
+
if (key) {
|
|
30
|
+
query[decodeURIComponent(key)] = value ? decodeURIComponent(value) : '';
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
return query;
|
|
34
|
+
};
|
|
35
|
+
/**
|
|
36
|
+
* Build query string from object
|
|
37
|
+
*/
|
|
38
|
+
const buildQuery = (query) => {
|
|
39
|
+
const keys = Object.keys(query);
|
|
40
|
+
if (keys.length === 0)
|
|
41
|
+
return '';
|
|
42
|
+
const params = keys.map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(query[key])}`).join('&');
|
|
43
|
+
return `?${params}`;
|
|
44
|
+
};
|
|
45
|
+
/**
|
|
46
|
+
* Substitute params into path pattern
|
|
47
|
+
* @example '/user/:id' + {id: '123'} => '/user/123'
|
|
48
|
+
*/
|
|
49
|
+
const emplaceParams = (path, params) => {
|
|
50
|
+
let result = path;
|
|
51
|
+
for (const key in params) {
|
|
52
|
+
result = result.replace(`:${key}`, params[key]);
|
|
53
|
+
}
|
|
54
|
+
return result;
|
|
55
|
+
};
|
|
56
|
+
/**
|
|
57
|
+
* Extract dynamic params from path using pattern
|
|
58
|
+
* @example pattern: '/user/:id', path: '/user/123' => {id: '123'}
|
|
59
|
+
*/
|
|
60
|
+
const extractParams = (pattern, path) => {
|
|
61
|
+
const params = {};
|
|
62
|
+
const patternParts = pattern.split('/');
|
|
63
|
+
const pathParts = path.split('/');
|
|
64
|
+
if (patternParts.length !== pathParts.length) {
|
|
65
|
+
return null;
|
|
66
|
+
}
|
|
67
|
+
for (let i = 0; i < patternParts.length; i++) {
|
|
68
|
+
const patternPart = patternParts[i];
|
|
69
|
+
const pathPart = pathParts[i];
|
|
70
|
+
if (patternPart.startsWith(':')) {
|
|
71
|
+
const paramName = patternPart.slice(1);
|
|
72
|
+
params[paramName] = pathPart;
|
|
73
|
+
}
|
|
74
|
+
else if (patternPart !== pathPart) {
|
|
75
|
+
return null;
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
return params;
|
|
79
|
+
};
|
|
80
|
+
|
|
81
|
+
/**
|
|
82
|
+
* Route matcher for finding matching routes and extracting params
|
|
83
|
+
*/
|
|
84
|
+
const createMatcher = (routes) => {
|
|
85
|
+
const nameMap = {};
|
|
86
|
+
for (let i = 0; i < routes.length; i++) {
|
|
87
|
+
const route = routes[i];
|
|
88
|
+
if (route.name !== undefined) {
|
|
89
|
+
if (route.name in nameMap) {
|
|
90
|
+
throws(`Duplicate route name detected: '${route.name}'`);
|
|
91
|
+
}
|
|
92
|
+
nameMap[route.name] = route;
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
/**
|
|
96
|
+
* Find route by name
|
|
97
|
+
*/
|
|
98
|
+
const findByName = (name) => {
|
|
99
|
+
return nameMap[name] ?? null;
|
|
100
|
+
};
|
|
101
|
+
/**
|
|
102
|
+
* Match path against all routes
|
|
103
|
+
*/
|
|
104
|
+
const match = (path) => {
|
|
105
|
+
const normalizedPath = normalizePath(path);
|
|
106
|
+
// Try exact match first
|
|
107
|
+
for (const route of routes) {
|
|
108
|
+
if (route.path === normalizedPath) {
|
|
109
|
+
return {
|
|
110
|
+
route,
|
|
111
|
+
params: {},
|
|
112
|
+
result: getMatchedChain(route),
|
|
113
|
+
};
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
// Try dynamic routes
|
|
117
|
+
for (const route of routes) {
|
|
118
|
+
if (route.path.includes(':')) {
|
|
119
|
+
const params = extractParams(route.path, normalizedPath);
|
|
120
|
+
if (params) {
|
|
121
|
+
return {
|
|
122
|
+
route,
|
|
123
|
+
params,
|
|
124
|
+
result: getMatchedChain(route),
|
|
125
|
+
};
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
return null;
|
|
130
|
+
};
|
|
131
|
+
/**
|
|
132
|
+
* Get chain of matched routes (for nested routes)
|
|
133
|
+
* - parent roots ahead
|
|
134
|
+
*/
|
|
135
|
+
const getMatchedChain = (route) => {
|
|
136
|
+
const matched = [route];
|
|
137
|
+
const path = route.path;
|
|
138
|
+
// Find parent routes by path prefix matching
|
|
139
|
+
for (let i = 0; i < routes.length; i++) {
|
|
140
|
+
const r = routes[i];
|
|
141
|
+
if (r !== route && path.startsWith(r.path) && path !== r.path) {
|
|
142
|
+
matched.push(r);
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
return matched.reverse();
|
|
146
|
+
};
|
|
147
|
+
return {
|
|
148
|
+
findByName,
|
|
149
|
+
match,
|
|
150
|
+
};
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
/**
|
|
154
|
+
* Create a new router instance
|
|
155
|
+
*/
|
|
156
|
+
const createRouter = (config) => {
|
|
157
|
+
// # default configs
|
|
158
|
+
const beforeEach = config.beforeEach ?? defaultHook;
|
|
159
|
+
const afterEach = config.afterEach ?? defaultHook;
|
|
160
|
+
const onNotFound = config.onNotFound ?? defaultHook;
|
|
161
|
+
const onError = config.onError ?? defaultHook;
|
|
162
|
+
const asyncGuards = config.asyncGuards ?? true;
|
|
163
|
+
// # private values
|
|
164
|
+
const routes = [];
|
|
165
|
+
/**
|
|
166
|
+
* Normalize routes by adding default guards
|
|
167
|
+
*/
|
|
168
|
+
const normalize = (rawRoutes, parentPath) => rawRoutes.map((route) => {
|
|
169
|
+
const path = normalizePath(parentPath, route.path);
|
|
170
|
+
const normalized = {
|
|
171
|
+
path,
|
|
172
|
+
name: route.name ?? '',
|
|
173
|
+
meta: route.meta ?? {},
|
|
174
|
+
beforeEnter: route.beforeEnter ?? defaultHook,
|
|
175
|
+
after: route.after ?? defaultHook,
|
|
176
|
+
children: route.children ? normalize(route.children, path) : [],
|
|
177
|
+
};
|
|
178
|
+
// directly push the normalized route to the list
|
|
179
|
+
// avoid flatten them again
|
|
180
|
+
routes.push(normalized);
|
|
181
|
+
return normalized;
|
|
182
|
+
});
|
|
183
|
+
// Normalize routes with default guards
|
|
184
|
+
normalize(config.routes, '/');
|
|
185
|
+
const { findByName, match } = createMatcher(routes);
|
|
186
|
+
let current = null;
|
|
187
|
+
const history = [];
|
|
188
|
+
// # methods
|
|
189
|
+
const executeGuardsSync = (to, from, guardLevel) => {
|
|
190
|
+
try {
|
|
191
|
+
if (guardLevel === 0 /* GuardLevel.None */) {
|
|
192
|
+
return true;
|
|
193
|
+
}
|
|
194
|
+
if (guardLevel & 1 /* GuardLevel.Global */) {
|
|
195
|
+
const result = beforeEach(to, from);
|
|
196
|
+
if (result === false) {
|
|
197
|
+
return false;
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
if (guardLevel & 2 /* GuardLevel.Route */) {
|
|
201
|
+
const targetRoute = to.matched[to.matched.length - 1];
|
|
202
|
+
const result = targetRoute.beforeEnter(to);
|
|
203
|
+
if (result === false) {
|
|
204
|
+
return false;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
return true;
|
|
208
|
+
}
|
|
209
|
+
catch (error) {
|
|
210
|
+
onError(error);
|
|
211
|
+
return false;
|
|
212
|
+
}
|
|
213
|
+
};
|
|
214
|
+
const executeGuards = async (to, from, guardLevel) => {
|
|
215
|
+
try {
|
|
216
|
+
if (guardLevel === 0 /* GuardLevel.None */) {
|
|
217
|
+
return true;
|
|
218
|
+
}
|
|
219
|
+
if (guardLevel & 1 /* GuardLevel.Global */) {
|
|
220
|
+
const result = await beforeEach(to, from);
|
|
221
|
+
if (result === false) {
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
if (guardLevel & 2 /* GuardLevel.Route */) {
|
|
226
|
+
const targetRoute = to.matched[to.matched.length - 1];
|
|
227
|
+
const result = targetRoute.beforeEnter(to);
|
|
228
|
+
return result !== false;
|
|
229
|
+
}
|
|
230
|
+
return true;
|
|
231
|
+
}
|
|
232
|
+
catch (error) {
|
|
233
|
+
onError(error);
|
|
234
|
+
return false;
|
|
235
|
+
}
|
|
236
|
+
};
|
|
237
|
+
const navigatePrepare = (options) => {
|
|
238
|
+
// Resolve target route
|
|
239
|
+
let targetPath;
|
|
240
|
+
let targetRoute;
|
|
241
|
+
if (options.name) {
|
|
242
|
+
targetRoute = findByName(options.name);
|
|
243
|
+
if (!targetRoute) {
|
|
244
|
+
throws(`Route not found: ${options.name}`);
|
|
245
|
+
}
|
|
246
|
+
targetPath = targetRoute.path;
|
|
247
|
+
}
|
|
248
|
+
else if (options.path) {
|
|
249
|
+
targetPath = normalizePath(options.path);
|
|
250
|
+
targetRoute = match(targetPath)?.route;
|
|
251
|
+
}
|
|
252
|
+
else {
|
|
253
|
+
throws(`Either path or name must be provided`);
|
|
254
|
+
}
|
|
255
|
+
// Substitute params
|
|
256
|
+
if (options.params) {
|
|
257
|
+
targetPath = emplaceParams(targetPath, options.params);
|
|
258
|
+
}
|
|
259
|
+
// Match final path
|
|
260
|
+
const matched = match(targetPath);
|
|
261
|
+
if (!matched) {
|
|
262
|
+
onNotFound(targetPath);
|
|
263
|
+
return null;
|
|
264
|
+
}
|
|
265
|
+
// Build route context
|
|
266
|
+
const queryString = options.query ? buildQuery(options.query) : '';
|
|
267
|
+
const fullPath = targetPath + queryString;
|
|
268
|
+
const to = {
|
|
269
|
+
path: targetPath,
|
|
270
|
+
name: matched.route.name,
|
|
271
|
+
params: { ...matched.params, ...(options.params ?? {}) },
|
|
272
|
+
query: options.query ?? {},
|
|
273
|
+
meta: matched.route.meta ?? {},
|
|
274
|
+
matched: matched.result,
|
|
275
|
+
};
|
|
276
|
+
return {
|
|
277
|
+
guardLevel: options.guardLevel ?? 15 /* GuardLevel.Default */,
|
|
278
|
+
replace: options.replace ?? false,
|
|
279
|
+
to,
|
|
280
|
+
fullPath,
|
|
281
|
+
};
|
|
282
|
+
};
|
|
283
|
+
const navigateSync = (options) => {
|
|
284
|
+
try {
|
|
285
|
+
const prep = navigatePrepare(options);
|
|
286
|
+
if (!prep) {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
const { guardLevel, replace, to, fullPath } = prep;
|
|
290
|
+
// Execute guards
|
|
291
|
+
if (!executeGuardsSync(to, current, guardLevel)) {
|
|
292
|
+
return false;
|
|
293
|
+
}
|
|
294
|
+
// Update browser history
|
|
295
|
+
const url = fullPath;
|
|
296
|
+
if (replace) {
|
|
297
|
+
window.history.replaceState({ path: to.path }, '', url);
|
|
298
|
+
}
|
|
299
|
+
else {
|
|
300
|
+
window.history.pushState({ path: to.path }, '', url);
|
|
301
|
+
}
|
|
302
|
+
// Update current route
|
|
303
|
+
current = to;
|
|
304
|
+
history.push(to);
|
|
305
|
+
// Execute after hooks
|
|
306
|
+
executeAfterHooksSync(to, history[history.length - 2] ?? null);
|
|
307
|
+
return true;
|
|
308
|
+
}
|
|
309
|
+
catch (error) {
|
|
310
|
+
onError(error);
|
|
311
|
+
return false;
|
|
312
|
+
}
|
|
313
|
+
};
|
|
314
|
+
const navigateAsync = async (options) => {
|
|
315
|
+
try {
|
|
316
|
+
const prep = navigatePrepare(options);
|
|
317
|
+
if (!prep) {
|
|
318
|
+
return false;
|
|
319
|
+
}
|
|
320
|
+
const { guardLevel, replace, to, fullPath } = prep;
|
|
321
|
+
const passed = await executeGuards(to, current, guardLevel);
|
|
322
|
+
if (!passed) {
|
|
323
|
+
return false;
|
|
324
|
+
}
|
|
325
|
+
// ---- Guards passed ----
|
|
326
|
+
const url = fullPath;
|
|
327
|
+
if (replace) {
|
|
328
|
+
window.history.replaceState({ path: to.path }, '', url);
|
|
329
|
+
}
|
|
330
|
+
else {
|
|
331
|
+
window.history.pushState({ path: to.path }, '', url);
|
|
332
|
+
}
|
|
333
|
+
executeAfterHooks(to, history[history.length - 2] ?? null);
|
|
334
|
+
return true;
|
|
335
|
+
}
|
|
336
|
+
catch (error) {
|
|
337
|
+
onError(error);
|
|
338
|
+
return false;
|
|
339
|
+
}
|
|
340
|
+
};
|
|
341
|
+
const navigate = asyncGuards ? navigateSync : navigateAsync;
|
|
342
|
+
const executeAfterHooksSync = (to, from) => {
|
|
343
|
+
const targetRoute = to.matched[to.matched.length - 1];
|
|
344
|
+
targetRoute.after(to);
|
|
345
|
+
afterEach(to, from);
|
|
346
|
+
};
|
|
347
|
+
const executeAfterHooks = async (to, from) => {
|
|
348
|
+
const targetRoute = to.matched[to.matched.length - 1];
|
|
349
|
+
await targetRoute.after(to);
|
|
350
|
+
await afterEach(to, from);
|
|
351
|
+
};
|
|
352
|
+
/**
|
|
353
|
+
* Normalize navigation argument
|
|
354
|
+
*/
|
|
355
|
+
const normalizeLocation = (loc) => {
|
|
356
|
+
if (typeof loc === 'string') {
|
|
357
|
+
// Parse path and query
|
|
358
|
+
const [path, queryString] = loc.split('?');
|
|
359
|
+
return {
|
|
360
|
+
path,
|
|
361
|
+
query: queryString ? parseQuery(queryString) : undefined,
|
|
362
|
+
};
|
|
363
|
+
}
|
|
364
|
+
return loc;
|
|
365
|
+
};
|
|
366
|
+
// # register events
|
|
367
|
+
// Listen to browser back/forward
|
|
368
|
+
window.addEventListener('popstate', (event) => {
|
|
369
|
+
if (event.state?.path) {
|
|
370
|
+
navigate({ path: event.state.path, guardLevel: 1 /* GuardLevel.Global */, replace: true });
|
|
371
|
+
}
|
|
372
|
+
});
|
|
373
|
+
// Router instance
|
|
374
|
+
return {
|
|
375
|
+
get current() {
|
|
376
|
+
return current;
|
|
377
|
+
},
|
|
378
|
+
get history() {
|
|
379
|
+
return history.concat();
|
|
380
|
+
},
|
|
381
|
+
push(location) {
|
|
382
|
+
const options = normalizeLocation(location);
|
|
383
|
+
return navigate(options);
|
|
384
|
+
},
|
|
385
|
+
silentPush(location) {
|
|
386
|
+
const options = normalizeLocation(location);
|
|
387
|
+
return navigate({ ...options, guardLevel: 2 /* GuardLevel.Route */ });
|
|
388
|
+
},
|
|
389
|
+
replace(location) {
|
|
390
|
+
const options = normalizeLocation(location);
|
|
391
|
+
return navigate({ ...options, replace: true });
|
|
392
|
+
},
|
|
393
|
+
back() {
|
|
394
|
+
window.history.back();
|
|
395
|
+
},
|
|
396
|
+
forward() {
|
|
397
|
+
window.history.forward();
|
|
398
|
+
},
|
|
399
|
+
};
|
|
400
|
+
};
|
|
401
|
+
|
|
402
|
+
exports.createRouter = createRouter;
|
|
403
|
+
|
|
404
|
+
return exports;
|
|
405
|
+
|
|
406
|
+
})({});
|
package/dist/index.legacy.js
CHANGED
|
@@ -1 +1,520 @@
|
|
|
1
|
-
var __ktjs_router__
|
|
1
|
+
var __ktjs_router__ = (function (exports) {
|
|
2
|
+
'use strict';
|
|
3
|
+
|
|
4
|
+
/******************************************************************************
|
|
5
|
+
Copyright (c) Microsoft Corporation.
|
|
6
|
+
|
|
7
|
+
Permission to use, copy, modify, and/or distribute this software for any
|
|
8
|
+
purpose with or without fee is hereby granted.
|
|
9
|
+
|
|
10
|
+
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES WITH
|
|
11
|
+
REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF MERCHANTABILITY
|
|
12
|
+
AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR ANY SPECIAL, DIRECT,
|
|
13
|
+
INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES WHATSOEVER RESULTING FROM
|
|
14
|
+
LOSS OF USE, DATA OR PROFITS, WHETHER IN AN ACTION OF CONTRACT, NEGLIGENCE OR
|
|
15
|
+
OTHER TORTIOUS ACTION, ARISING OUT OF OR IN CONNECTION WITH THE USE OR
|
|
16
|
+
PERFORMANCE OF THIS SOFTWARE.
|
|
17
|
+
***************************************************************************** */
|
|
18
|
+
/* global Reflect, Promise, SuppressedError, Symbol, Iterator */
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
var __assign = function() {
|
|
22
|
+
__assign = Object.assign || function __assign(t) {
|
|
23
|
+
for (var s, i = 1, n = arguments.length; i < n; i++) {
|
|
24
|
+
s = arguments[i];
|
|
25
|
+
for (var p in s) if (Object.prototype.hasOwnProperty.call(s, p)) t[p] = s[p];
|
|
26
|
+
}
|
|
27
|
+
return t;
|
|
28
|
+
};
|
|
29
|
+
return __assign.apply(this, arguments);
|
|
30
|
+
};
|
|
31
|
+
|
|
32
|
+
function __awaiter(thisArg, _arguments, P, generator) {
|
|
33
|
+
function adopt(value) { return value instanceof P ? value : new P(function (resolve) { resolve(value); }); }
|
|
34
|
+
return new (P || (P = Promise))(function (resolve, reject) {
|
|
35
|
+
function fulfilled(value) { try { step(generator.next(value)); } catch (e) { reject(e); } }
|
|
36
|
+
function rejected(value) { try { step(generator["throw"](value)); } catch (e) { reject(e); } }
|
|
37
|
+
function step(result) { result.done ? resolve(result.value) : adopt(result.value).then(fulfilled, rejected); }
|
|
38
|
+
step((generator = generator.apply(thisArg, _arguments || [])).next());
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function __generator(thisArg, body) {
|
|
43
|
+
var _ = { label: 0, sent: function() { if (t[0] & 1) throw t[1]; return t[1]; }, trys: [], ops: [] }, f, y, t, g = Object.create((typeof Iterator === "function" ? Iterator : Object).prototype);
|
|
44
|
+
return g.next = verb(0), g["throw"] = verb(1), g["return"] = verb(2), typeof Symbol === "function" && (g[Symbol.iterator] = function() { return this; }), g;
|
|
45
|
+
function verb(n) { return function (v) { return step([n, v]); }; }
|
|
46
|
+
function step(op) {
|
|
47
|
+
if (f) throw new TypeError("Generator is already executing.");
|
|
48
|
+
while (g && (g = 0, op[0] && (_ = 0)), _) try {
|
|
49
|
+
if (f = 1, y && (t = op[0] & 2 ? y["return"] : op[0] ? y["throw"] || ((t = y["return"]) && t.call(y), 0) : y.next) && !(t = t.call(y, op[1])).done) return t;
|
|
50
|
+
if (y = 0, t) op = [op[0] & 2, t.value];
|
|
51
|
+
switch (op[0]) {
|
|
52
|
+
case 0: case 1: t = op; break;
|
|
53
|
+
case 4: _.label++; return { value: op[1], done: false };
|
|
54
|
+
case 5: _.label++; y = op[1]; op = [0]; continue;
|
|
55
|
+
case 7: op = _.ops.pop(); _.trys.pop(); continue;
|
|
56
|
+
default:
|
|
57
|
+
if (!(t = _.trys, t = t.length > 0 && t[t.length - 1]) && (op[0] === 6 || op[0] === 2)) { _ = 0; continue; }
|
|
58
|
+
if (op[0] === 3 && (!t || (op[1] > t[0] && op[1] < t[3]))) { _.label = op[1]; break; }
|
|
59
|
+
if (op[0] === 6 && _.label < t[1]) { _.label = t[1]; t = op; break; }
|
|
60
|
+
if (t && _.label < t[2]) { _.label = t[2]; _.ops.push(op); break; }
|
|
61
|
+
if (t[2]) _.ops.pop();
|
|
62
|
+
_.trys.pop(); continue;
|
|
63
|
+
}
|
|
64
|
+
op = body.call(thisArg, _);
|
|
65
|
+
} catch (e) { op = [6, e]; y = 0; } finally { f = t = 0; }
|
|
66
|
+
if (op[0] & 5) throw op[1]; return { value: op[0] ? op[1] : void 0, done: true };
|
|
67
|
+
}
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
typeof SuppressedError === "function" ? SuppressedError : function (error, suppressed, message) {
|
|
71
|
+
var e = new Error(message);
|
|
72
|
+
return e.name = "SuppressedError", e.error = error, e.suppressed = suppressed, e;
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Default guard that always returns true
|
|
77
|
+
*/
|
|
78
|
+
var defaultHook = function () { return true; };
|
|
79
|
+
var throws = function (m) {
|
|
80
|
+
throw new Error("@ktjs/router: ".concat(m));
|
|
81
|
+
};
|
|
82
|
+
var normalizePath = function () {
|
|
83
|
+
var paths = [];
|
|
84
|
+
for (var _i = 0; _i < arguments.length; _i++) {
|
|
85
|
+
paths[_i] = arguments[_i];
|
|
86
|
+
}
|
|
87
|
+
var p = paths
|
|
88
|
+
.map(function (p) { return p.split('/'); })
|
|
89
|
+
.flat()
|
|
90
|
+
.filter(Boolean);
|
|
91
|
+
return '/' + p.join('/');
|
|
92
|
+
};
|
|
93
|
+
/**
|
|
94
|
+
* Parse query string into object
|
|
95
|
+
*/
|
|
96
|
+
var parseQuery = function (queryString) {
|
|
97
|
+
var query = {};
|
|
98
|
+
if (!queryString || queryString === '?') {
|
|
99
|
+
return query;
|
|
100
|
+
}
|
|
101
|
+
var params = queryString.replace(/^\?/, '').split('&');
|
|
102
|
+
for (var _i = 0, params_1 = params; _i < params_1.length; _i++) {
|
|
103
|
+
var param = params_1[_i];
|
|
104
|
+
var _a = param.split('='), key = _a[0], value = _a[1];
|
|
105
|
+
if (key) {
|
|
106
|
+
query[decodeURIComponent(key)] = value ? decodeURIComponent(value) : '';
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
return query;
|
|
110
|
+
};
|
|
111
|
+
/**
|
|
112
|
+
* Build query string from object
|
|
113
|
+
*/
|
|
114
|
+
var buildQuery = function (query) {
|
|
115
|
+
var keys = Object.keys(query);
|
|
116
|
+
if (keys.length === 0)
|
|
117
|
+
return '';
|
|
118
|
+
var params = keys.map(function (key) { return "".concat(encodeURIComponent(key), "=").concat(encodeURIComponent(query[key])); }).join('&');
|
|
119
|
+
return "?".concat(params);
|
|
120
|
+
};
|
|
121
|
+
/**
|
|
122
|
+
* Substitute params into path pattern
|
|
123
|
+
* @example '/user/:id' + {id: '123'} => '/user/123'
|
|
124
|
+
*/
|
|
125
|
+
var emplaceParams = function (path, params) {
|
|
126
|
+
var result = path;
|
|
127
|
+
for (var key in params) {
|
|
128
|
+
result = result.replace(":".concat(key), params[key]);
|
|
129
|
+
}
|
|
130
|
+
return result;
|
|
131
|
+
};
|
|
132
|
+
/**
|
|
133
|
+
* Extract dynamic params from path using pattern
|
|
134
|
+
* @example pattern: '/user/:id', path: '/user/123' => {id: '123'}
|
|
135
|
+
*/
|
|
136
|
+
var extractParams = function (pattern, path) {
|
|
137
|
+
var params = {};
|
|
138
|
+
var patternParts = pattern.split('/');
|
|
139
|
+
var pathParts = path.split('/');
|
|
140
|
+
if (patternParts.length !== pathParts.length) {
|
|
141
|
+
return null;
|
|
142
|
+
}
|
|
143
|
+
for (var i = 0; i < patternParts.length; i++) {
|
|
144
|
+
var patternPart = patternParts[i];
|
|
145
|
+
var pathPart = pathParts[i];
|
|
146
|
+
if (patternPart.startsWith(':')) {
|
|
147
|
+
var paramName = patternPart.slice(1);
|
|
148
|
+
params[paramName] = pathPart;
|
|
149
|
+
}
|
|
150
|
+
else if (patternPart !== pathPart) {
|
|
151
|
+
return null;
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
return params;
|
|
155
|
+
};
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Route matcher for finding matching routes and extracting params
|
|
159
|
+
*/
|
|
160
|
+
var createMatcher = function (routes) {
|
|
161
|
+
var nameMap = {};
|
|
162
|
+
for (var i = 0; i < routes.length; i++) {
|
|
163
|
+
var route = routes[i];
|
|
164
|
+
if (route.name !== undefined) {
|
|
165
|
+
if (route.name in nameMap) {
|
|
166
|
+
throws("Duplicate route name detected: '".concat(route.name, "'"));
|
|
167
|
+
}
|
|
168
|
+
nameMap[route.name] = route;
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
/**
|
|
172
|
+
* Find route by name
|
|
173
|
+
*/
|
|
174
|
+
var findByName = function (name) {
|
|
175
|
+
var _a;
|
|
176
|
+
return (_a = nameMap[name]) !== null && _a !== void 0 ? _a : null;
|
|
177
|
+
};
|
|
178
|
+
/**
|
|
179
|
+
* Match path against all routes
|
|
180
|
+
*/
|
|
181
|
+
var match = function (path) {
|
|
182
|
+
var normalizedPath = normalizePath(path);
|
|
183
|
+
// Try exact match first
|
|
184
|
+
for (var _i = 0, routes_1 = routes; _i < routes_1.length; _i++) {
|
|
185
|
+
var route = routes_1[_i];
|
|
186
|
+
if (route.path === normalizedPath) {
|
|
187
|
+
return {
|
|
188
|
+
route: route,
|
|
189
|
+
params: {},
|
|
190
|
+
result: getMatchedChain(route),
|
|
191
|
+
};
|
|
192
|
+
}
|
|
193
|
+
}
|
|
194
|
+
// Try dynamic routes
|
|
195
|
+
for (var _a = 0, routes_2 = routes; _a < routes_2.length; _a++) {
|
|
196
|
+
var route = routes_2[_a];
|
|
197
|
+
if (route.path.includes(':')) {
|
|
198
|
+
var params = extractParams(route.path, normalizedPath);
|
|
199
|
+
if (params) {
|
|
200
|
+
return {
|
|
201
|
+
route: route,
|
|
202
|
+
params: params,
|
|
203
|
+
result: getMatchedChain(route),
|
|
204
|
+
};
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
return null;
|
|
209
|
+
};
|
|
210
|
+
/**
|
|
211
|
+
* Get chain of matched routes (for nested routes)
|
|
212
|
+
* - parent roots ahead
|
|
213
|
+
*/
|
|
214
|
+
var getMatchedChain = function (route) {
|
|
215
|
+
var matched = [route];
|
|
216
|
+
var path = route.path;
|
|
217
|
+
// Find parent routes by path prefix matching
|
|
218
|
+
for (var i = 0; i < routes.length; i++) {
|
|
219
|
+
var r = routes[i];
|
|
220
|
+
if (r !== route && path.startsWith(r.path) && path !== r.path) {
|
|
221
|
+
matched.push(r);
|
|
222
|
+
}
|
|
223
|
+
}
|
|
224
|
+
return matched.reverse();
|
|
225
|
+
};
|
|
226
|
+
return {
|
|
227
|
+
findByName: findByName,
|
|
228
|
+
match: match,
|
|
229
|
+
};
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Create a new router instance
|
|
234
|
+
*/
|
|
235
|
+
var createRouter = function (config) {
|
|
236
|
+
var _a, _b, _c, _d, _e;
|
|
237
|
+
// # default configs
|
|
238
|
+
var beforeEach = (_a = config.beforeEach) !== null && _a !== void 0 ? _a : defaultHook;
|
|
239
|
+
var afterEach = (_b = config.afterEach) !== null && _b !== void 0 ? _b : defaultHook;
|
|
240
|
+
var onNotFound = (_c = config.onNotFound) !== null && _c !== void 0 ? _c : defaultHook;
|
|
241
|
+
var onError = (_d = config.onError) !== null && _d !== void 0 ? _d : defaultHook;
|
|
242
|
+
var asyncGuards = (_e = config.asyncGuards) !== null && _e !== void 0 ? _e : true;
|
|
243
|
+
// # private values
|
|
244
|
+
var routes = [];
|
|
245
|
+
/**
|
|
246
|
+
* Normalize routes by adding default guards
|
|
247
|
+
*/
|
|
248
|
+
var normalize = function (rawRoutes, parentPath) {
|
|
249
|
+
return rawRoutes.map(function (route) {
|
|
250
|
+
var _a, _b, _c, _d;
|
|
251
|
+
var path = normalizePath(parentPath, route.path);
|
|
252
|
+
var normalized = {
|
|
253
|
+
path: path,
|
|
254
|
+
name: (_a = route.name) !== null && _a !== void 0 ? _a : '',
|
|
255
|
+
meta: (_b = route.meta) !== null && _b !== void 0 ? _b : {},
|
|
256
|
+
beforeEnter: (_c = route.beforeEnter) !== null && _c !== void 0 ? _c : defaultHook,
|
|
257
|
+
after: (_d = route.after) !== null && _d !== void 0 ? _d : defaultHook,
|
|
258
|
+
children: route.children ? normalize(route.children, path) : [],
|
|
259
|
+
};
|
|
260
|
+
// directly push the normalized route to the list
|
|
261
|
+
// avoid flatten them again
|
|
262
|
+
routes.push(normalized);
|
|
263
|
+
return normalized;
|
|
264
|
+
});
|
|
265
|
+
};
|
|
266
|
+
// Normalize routes with default guards
|
|
267
|
+
normalize(config.routes, '/');
|
|
268
|
+
var _f = createMatcher(routes), findByName = _f.findByName, match = _f.match;
|
|
269
|
+
var current = null;
|
|
270
|
+
var history = [];
|
|
271
|
+
// # methods
|
|
272
|
+
var executeGuardsSync = function (to, from, guardLevel) {
|
|
273
|
+
try {
|
|
274
|
+
if (guardLevel === 0 /* GuardLevel.None */) {
|
|
275
|
+
return true;
|
|
276
|
+
}
|
|
277
|
+
if (guardLevel & 1 /* GuardLevel.Global */) {
|
|
278
|
+
var result = beforeEach(to, from);
|
|
279
|
+
if (result === false) {
|
|
280
|
+
return false;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
if (guardLevel & 2 /* GuardLevel.Route */) {
|
|
284
|
+
var targetRoute = to.matched[to.matched.length - 1];
|
|
285
|
+
var result = targetRoute.beforeEnter(to);
|
|
286
|
+
if (result === false) {
|
|
287
|
+
return false;
|
|
288
|
+
}
|
|
289
|
+
}
|
|
290
|
+
return true;
|
|
291
|
+
}
|
|
292
|
+
catch (error) {
|
|
293
|
+
onError(error);
|
|
294
|
+
return false;
|
|
295
|
+
}
|
|
296
|
+
};
|
|
297
|
+
var executeGuards = function (to, from, guardLevel) { return __awaiter(void 0, void 0, void 0, function () {
|
|
298
|
+
var result, targetRoute, result, error_1;
|
|
299
|
+
return __generator(this, function (_a) {
|
|
300
|
+
switch (_a.label) {
|
|
301
|
+
case 0:
|
|
302
|
+
_a.trys.push([0, 3, , 4]);
|
|
303
|
+
if (guardLevel === 0 /* GuardLevel.None */) {
|
|
304
|
+
return [2 /*return*/, true];
|
|
305
|
+
}
|
|
306
|
+
if (!(guardLevel & 1 /* GuardLevel.Global */)) return [3 /*break*/, 2];
|
|
307
|
+
return [4 /*yield*/, beforeEach(to, from)];
|
|
308
|
+
case 1:
|
|
309
|
+
result = _a.sent();
|
|
310
|
+
if (result === false) {
|
|
311
|
+
return [2 /*return*/, false];
|
|
312
|
+
}
|
|
313
|
+
_a.label = 2;
|
|
314
|
+
case 2:
|
|
315
|
+
if (guardLevel & 2 /* GuardLevel.Route */) {
|
|
316
|
+
targetRoute = to.matched[to.matched.length - 1];
|
|
317
|
+
result = targetRoute.beforeEnter(to);
|
|
318
|
+
return [2 /*return*/, result !== false];
|
|
319
|
+
}
|
|
320
|
+
return [2 /*return*/, true];
|
|
321
|
+
case 3:
|
|
322
|
+
error_1 = _a.sent();
|
|
323
|
+
onError(error_1);
|
|
324
|
+
return [2 /*return*/, false];
|
|
325
|
+
case 4: return [2 /*return*/];
|
|
326
|
+
}
|
|
327
|
+
});
|
|
328
|
+
}); };
|
|
329
|
+
var navigatePrepare = function (options) {
|
|
330
|
+
var _a, _b, _c, _d, _e, _f;
|
|
331
|
+
// Resolve target route
|
|
332
|
+
var targetPath;
|
|
333
|
+
var targetRoute;
|
|
334
|
+
if (options.name) {
|
|
335
|
+
targetRoute = findByName(options.name);
|
|
336
|
+
if (!targetRoute) {
|
|
337
|
+
throws("Route not found: ".concat(options.name));
|
|
338
|
+
}
|
|
339
|
+
targetPath = targetRoute.path;
|
|
340
|
+
}
|
|
341
|
+
else if (options.path) {
|
|
342
|
+
targetPath = normalizePath(options.path);
|
|
343
|
+
targetRoute = (_a = match(targetPath)) === null || _a === void 0 ? void 0 : _a.route;
|
|
344
|
+
}
|
|
345
|
+
else {
|
|
346
|
+
throws("Either path or name must be provided");
|
|
347
|
+
}
|
|
348
|
+
// Substitute params
|
|
349
|
+
if (options.params) {
|
|
350
|
+
targetPath = emplaceParams(targetPath, options.params);
|
|
351
|
+
}
|
|
352
|
+
// Match final path
|
|
353
|
+
var matched = match(targetPath);
|
|
354
|
+
if (!matched) {
|
|
355
|
+
onNotFound(targetPath);
|
|
356
|
+
return null;
|
|
357
|
+
}
|
|
358
|
+
// Build route context
|
|
359
|
+
var queryString = options.query ? buildQuery(options.query) : '';
|
|
360
|
+
var fullPath = targetPath + queryString;
|
|
361
|
+
var to = {
|
|
362
|
+
path: targetPath,
|
|
363
|
+
name: matched.route.name,
|
|
364
|
+
params: __assign(__assign({}, matched.params), ((_b = options.params) !== null && _b !== void 0 ? _b : {})),
|
|
365
|
+
query: (_c = options.query) !== null && _c !== void 0 ? _c : {},
|
|
366
|
+
meta: (_d = matched.route.meta) !== null && _d !== void 0 ? _d : {},
|
|
367
|
+
matched: matched.result,
|
|
368
|
+
};
|
|
369
|
+
return {
|
|
370
|
+
guardLevel: (_e = options.guardLevel) !== null && _e !== void 0 ? _e : 15 /* GuardLevel.Default */,
|
|
371
|
+
replace: (_f = options.replace) !== null && _f !== void 0 ? _f : false,
|
|
372
|
+
to: to,
|
|
373
|
+
fullPath: fullPath,
|
|
374
|
+
};
|
|
375
|
+
};
|
|
376
|
+
var navigateSync = function (options) {
|
|
377
|
+
var _a;
|
|
378
|
+
try {
|
|
379
|
+
var prep = navigatePrepare(options);
|
|
380
|
+
if (!prep) {
|
|
381
|
+
return false;
|
|
382
|
+
}
|
|
383
|
+
var guardLevel = prep.guardLevel, replace = prep.replace, to = prep.to, fullPath = prep.fullPath;
|
|
384
|
+
// Execute guards
|
|
385
|
+
if (!executeGuardsSync(to, current, guardLevel)) {
|
|
386
|
+
return false;
|
|
387
|
+
}
|
|
388
|
+
// Update browser history
|
|
389
|
+
var url = fullPath;
|
|
390
|
+
if (replace) {
|
|
391
|
+
window.history.replaceState({ path: to.path }, '', url);
|
|
392
|
+
}
|
|
393
|
+
else {
|
|
394
|
+
window.history.pushState({ path: to.path }, '', url);
|
|
395
|
+
}
|
|
396
|
+
// Update current route
|
|
397
|
+
current = to;
|
|
398
|
+
history.push(to);
|
|
399
|
+
// Execute after hooks
|
|
400
|
+
executeAfterHooksSync(to, (_a = history[history.length - 2]) !== null && _a !== void 0 ? _a : null);
|
|
401
|
+
return true;
|
|
402
|
+
}
|
|
403
|
+
catch (error) {
|
|
404
|
+
onError(error);
|
|
405
|
+
return false;
|
|
406
|
+
}
|
|
407
|
+
};
|
|
408
|
+
var navigateAsync = function (options) { return __awaiter(void 0, void 0, void 0, function () {
|
|
409
|
+
var prep, guardLevel, replace, to, fullPath, passed, url, error_2;
|
|
410
|
+
var _a;
|
|
411
|
+
return __generator(this, function (_b) {
|
|
412
|
+
switch (_b.label) {
|
|
413
|
+
case 0:
|
|
414
|
+
_b.trys.push([0, 2, , 3]);
|
|
415
|
+
prep = navigatePrepare(options);
|
|
416
|
+
if (!prep) {
|
|
417
|
+
return [2 /*return*/, false];
|
|
418
|
+
}
|
|
419
|
+
guardLevel = prep.guardLevel, replace = prep.replace, to = prep.to, fullPath = prep.fullPath;
|
|
420
|
+
return [4 /*yield*/, executeGuards(to, current, guardLevel)];
|
|
421
|
+
case 1:
|
|
422
|
+
passed = _b.sent();
|
|
423
|
+
if (!passed) {
|
|
424
|
+
return [2 /*return*/, false];
|
|
425
|
+
}
|
|
426
|
+
url = fullPath;
|
|
427
|
+
if (replace) {
|
|
428
|
+
window.history.replaceState({ path: to.path }, '', url);
|
|
429
|
+
}
|
|
430
|
+
else {
|
|
431
|
+
window.history.pushState({ path: to.path }, '', url);
|
|
432
|
+
}
|
|
433
|
+
executeAfterHooks(to, (_a = history[history.length - 2]) !== null && _a !== void 0 ? _a : null);
|
|
434
|
+
return [2 /*return*/, true];
|
|
435
|
+
case 2:
|
|
436
|
+
error_2 = _b.sent();
|
|
437
|
+
onError(error_2);
|
|
438
|
+
return [2 /*return*/, false];
|
|
439
|
+
case 3: return [2 /*return*/];
|
|
440
|
+
}
|
|
441
|
+
});
|
|
442
|
+
}); };
|
|
443
|
+
var navigate = asyncGuards ? navigateSync : navigateAsync;
|
|
444
|
+
var executeAfterHooksSync = function (to, from) {
|
|
445
|
+
var targetRoute = to.matched[to.matched.length - 1];
|
|
446
|
+
targetRoute.after(to);
|
|
447
|
+
afterEach(to, from);
|
|
448
|
+
};
|
|
449
|
+
var executeAfterHooks = function (to, from) { return __awaiter(void 0, void 0, void 0, function () {
|
|
450
|
+
var targetRoute;
|
|
451
|
+
return __generator(this, function (_a) {
|
|
452
|
+
switch (_a.label) {
|
|
453
|
+
case 0:
|
|
454
|
+
targetRoute = to.matched[to.matched.length - 1];
|
|
455
|
+
return [4 /*yield*/, targetRoute.after(to)];
|
|
456
|
+
case 1:
|
|
457
|
+
_a.sent();
|
|
458
|
+
return [4 /*yield*/, afterEach(to, from)];
|
|
459
|
+
case 2:
|
|
460
|
+
_a.sent();
|
|
461
|
+
return [2 /*return*/];
|
|
462
|
+
}
|
|
463
|
+
});
|
|
464
|
+
}); };
|
|
465
|
+
/**
|
|
466
|
+
* Normalize navigation argument
|
|
467
|
+
*/
|
|
468
|
+
var normalizeLocation = function (loc) {
|
|
469
|
+
if (typeof loc === 'string') {
|
|
470
|
+
// Parse path and query
|
|
471
|
+
var _a = loc.split('?'), path = _a[0], queryString = _a[1];
|
|
472
|
+
return {
|
|
473
|
+
path: path,
|
|
474
|
+
query: queryString ? parseQuery(queryString) : undefined,
|
|
475
|
+
};
|
|
476
|
+
}
|
|
477
|
+
return loc;
|
|
478
|
+
};
|
|
479
|
+
// # register events
|
|
480
|
+
// Listen to browser back/forward
|
|
481
|
+
window.addEventListener('popstate', function (event) {
|
|
482
|
+
var _a;
|
|
483
|
+
if ((_a = event.state) === null || _a === void 0 ? void 0 : _a.path) {
|
|
484
|
+
navigate({ path: event.state.path, guardLevel: 1 /* GuardLevel.Global */, replace: true });
|
|
485
|
+
}
|
|
486
|
+
});
|
|
487
|
+
// Router instance
|
|
488
|
+
return {
|
|
489
|
+
get current() {
|
|
490
|
+
return current;
|
|
491
|
+
},
|
|
492
|
+
get history() {
|
|
493
|
+
return history.concat();
|
|
494
|
+
},
|
|
495
|
+
push: function (location) {
|
|
496
|
+
var options = normalizeLocation(location);
|
|
497
|
+
return navigate(options);
|
|
498
|
+
},
|
|
499
|
+
silentPush: function (location) {
|
|
500
|
+
var options = normalizeLocation(location);
|
|
501
|
+
return navigate(__assign(__assign({}, options), { guardLevel: 2 /* GuardLevel.Route */ }));
|
|
502
|
+
},
|
|
503
|
+
replace: function (location) {
|
|
504
|
+
var options = normalizeLocation(location);
|
|
505
|
+
return navigate(__assign(__assign({}, options), { replace: true }));
|
|
506
|
+
},
|
|
507
|
+
back: function () {
|
|
508
|
+
window.history.back();
|
|
509
|
+
},
|
|
510
|
+
forward: function () {
|
|
511
|
+
window.history.forward();
|
|
512
|
+
},
|
|
513
|
+
};
|
|
514
|
+
};
|
|
515
|
+
|
|
516
|
+
exports.createRouter = createRouter;
|
|
517
|
+
|
|
518
|
+
return exports;
|
|
519
|
+
|
|
520
|
+
})({});
|
package/dist/index.mjs
CHANGED
|
@@ -1 +1,399 @@
|
|
|
1
|
-
|
|
1
|
+
/**
|
|
2
|
+
* Default guard that always returns true
|
|
3
|
+
*/
|
|
4
|
+
const defaultHook = () => true;
|
|
5
|
+
const throws = (m) => {
|
|
6
|
+
throw new Error(`@ktjs/router: ${m}`);
|
|
7
|
+
};
|
|
8
|
+
const normalizePath = (...paths) => {
|
|
9
|
+
const p = paths
|
|
10
|
+
.map((p) => p.split('/'))
|
|
11
|
+
.flat()
|
|
12
|
+
.filter(Boolean);
|
|
13
|
+
return '/' + p.join('/');
|
|
14
|
+
};
|
|
15
|
+
/**
|
|
16
|
+
* Parse query string into object
|
|
17
|
+
*/
|
|
18
|
+
const parseQuery = (queryString) => {
|
|
19
|
+
const query = {};
|
|
20
|
+
if (!queryString || queryString === '?') {
|
|
21
|
+
return query;
|
|
22
|
+
}
|
|
23
|
+
const params = queryString.replace(/^\?/, '').split('&');
|
|
24
|
+
for (const param of params) {
|
|
25
|
+
const [key, value] = param.split('=');
|
|
26
|
+
if (key) {
|
|
27
|
+
query[decodeURIComponent(key)] = value ? decodeURIComponent(value) : '';
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
return query;
|
|
31
|
+
};
|
|
32
|
+
/**
|
|
33
|
+
* Build query string from object
|
|
34
|
+
*/
|
|
35
|
+
const buildQuery = (query) => {
|
|
36
|
+
const keys = Object.keys(query);
|
|
37
|
+
if (keys.length === 0)
|
|
38
|
+
return '';
|
|
39
|
+
const params = keys.map((key) => `${encodeURIComponent(key)}=${encodeURIComponent(query[key])}`).join('&');
|
|
40
|
+
return `?${params}`;
|
|
41
|
+
};
|
|
42
|
+
/**
|
|
43
|
+
* Substitute params into path pattern
|
|
44
|
+
* @example '/user/:id' + {id: '123'} => '/user/123'
|
|
45
|
+
*/
|
|
46
|
+
const emplaceParams = (path, params) => {
|
|
47
|
+
let result = path;
|
|
48
|
+
for (const key in params) {
|
|
49
|
+
result = result.replace(`:${key}`, params[key]);
|
|
50
|
+
}
|
|
51
|
+
return result;
|
|
52
|
+
};
|
|
53
|
+
/**
|
|
54
|
+
* Extract dynamic params from path using pattern
|
|
55
|
+
* @example pattern: '/user/:id', path: '/user/123' => {id: '123'}
|
|
56
|
+
*/
|
|
57
|
+
const extractParams = (pattern, path) => {
|
|
58
|
+
const params = {};
|
|
59
|
+
const patternParts = pattern.split('/');
|
|
60
|
+
const pathParts = path.split('/');
|
|
61
|
+
if (patternParts.length !== pathParts.length) {
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
64
|
+
for (let i = 0; i < patternParts.length; i++) {
|
|
65
|
+
const patternPart = patternParts[i];
|
|
66
|
+
const pathPart = pathParts[i];
|
|
67
|
+
if (patternPart.startsWith(':')) {
|
|
68
|
+
const paramName = patternPart.slice(1);
|
|
69
|
+
params[paramName] = pathPart;
|
|
70
|
+
}
|
|
71
|
+
else if (patternPart !== pathPart) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
}
|
|
75
|
+
return params;
|
|
76
|
+
};
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Route matcher for finding matching routes and extracting params
|
|
80
|
+
*/
|
|
81
|
+
const createMatcher = (routes) => {
|
|
82
|
+
const nameMap = {};
|
|
83
|
+
for (let i = 0; i < routes.length; i++) {
|
|
84
|
+
const route = routes[i];
|
|
85
|
+
if (route.name !== undefined) {
|
|
86
|
+
if (route.name in nameMap) {
|
|
87
|
+
throws(`Duplicate route name detected: '${route.name}'`);
|
|
88
|
+
}
|
|
89
|
+
nameMap[route.name] = route;
|
|
90
|
+
}
|
|
91
|
+
}
|
|
92
|
+
/**
|
|
93
|
+
* Find route by name
|
|
94
|
+
*/
|
|
95
|
+
const findByName = (name) => {
|
|
96
|
+
return nameMap[name] ?? null;
|
|
97
|
+
};
|
|
98
|
+
/**
|
|
99
|
+
* Match path against all routes
|
|
100
|
+
*/
|
|
101
|
+
const match = (path) => {
|
|
102
|
+
const normalizedPath = normalizePath(path);
|
|
103
|
+
// Try exact match first
|
|
104
|
+
for (const route of routes) {
|
|
105
|
+
if (route.path === normalizedPath) {
|
|
106
|
+
return {
|
|
107
|
+
route,
|
|
108
|
+
params: {},
|
|
109
|
+
result: getMatchedChain(route),
|
|
110
|
+
};
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
// Try dynamic routes
|
|
114
|
+
for (const route of routes) {
|
|
115
|
+
if (route.path.includes(':')) {
|
|
116
|
+
const params = extractParams(route.path, normalizedPath);
|
|
117
|
+
if (params) {
|
|
118
|
+
return {
|
|
119
|
+
route,
|
|
120
|
+
params,
|
|
121
|
+
result: getMatchedChain(route),
|
|
122
|
+
};
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
return null;
|
|
127
|
+
};
|
|
128
|
+
/**
|
|
129
|
+
* Get chain of matched routes (for nested routes)
|
|
130
|
+
* - parent roots ahead
|
|
131
|
+
*/
|
|
132
|
+
const getMatchedChain = (route) => {
|
|
133
|
+
const matched = [route];
|
|
134
|
+
const path = route.path;
|
|
135
|
+
// Find parent routes by path prefix matching
|
|
136
|
+
for (let i = 0; i < routes.length; i++) {
|
|
137
|
+
const r = routes[i];
|
|
138
|
+
if (r !== route && path.startsWith(r.path) && path !== r.path) {
|
|
139
|
+
matched.push(r);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
return matched.reverse();
|
|
143
|
+
};
|
|
144
|
+
return {
|
|
145
|
+
findByName,
|
|
146
|
+
match,
|
|
147
|
+
};
|
|
148
|
+
};
|
|
149
|
+
|
|
150
|
+
/**
|
|
151
|
+
* Create a new router instance
|
|
152
|
+
*/
|
|
153
|
+
const createRouter = (config) => {
|
|
154
|
+
// # default configs
|
|
155
|
+
const beforeEach = config.beforeEach ?? defaultHook;
|
|
156
|
+
const afterEach = config.afterEach ?? defaultHook;
|
|
157
|
+
const onNotFound = config.onNotFound ?? defaultHook;
|
|
158
|
+
const onError = config.onError ?? defaultHook;
|
|
159
|
+
const asyncGuards = config.asyncGuards ?? true;
|
|
160
|
+
// # private values
|
|
161
|
+
const routes = [];
|
|
162
|
+
/**
|
|
163
|
+
* Normalize routes by adding default guards
|
|
164
|
+
*/
|
|
165
|
+
const normalize = (rawRoutes, parentPath) => rawRoutes.map((route) => {
|
|
166
|
+
const path = normalizePath(parentPath, route.path);
|
|
167
|
+
const normalized = {
|
|
168
|
+
path,
|
|
169
|
+
name: route.name ?? '',
|
|
170
|
+
meta: route.meta ?? {},
|
|
171
|
+
beforeEnter: route.beforeEnter ?? defaultHook,
|
|
172
|
+
after: route.after ?? defaultHook,
|
|
173
|
+
children: route.children ? normalize(route.children, path) : [],
|
|
174
|
+
};
|
|
175
|
+
// directly push the normalized route to the list
|
|
176
|
+
// avoid flatten them again
|
|
177
|
+
routes.push(normalized);
|
|
178
|
+
return normalized;
|
|
179
|
+
});
|
|
180
|
+
// Normalize routes with default guards
|
|
181
|
+
normalize(config.routes, '/');
|
|
182
|
+
const { findByName, match } = createMatcher(routes);
|
|
183
|
+
let current = null;
|
|
184
|
+
const history = [];
|
|
185
|
+
// # methods
|
|
186
|
+
const executeGuardsSync = (to, from, guardLevel) => {
|
|
187
|
+
try {
|
|
188
|
+
if (guardLevel === 0 /* GuardLevel.None */) {
|
|
189
|
+
return true;
|
|
190
|
+
}
|
|
191
|
+
if (guardLevel & 1 /* GuardLevel.Global */) {
|
|
192
|
+
const result = beforeEach(to, from);
|
|
193
|
+
if (result === false) {
|
|
194
|
+
return false;
|
|
195
|
+
}
|
|
196
|
+
}
|
|
197
|
+
if (guardLevel & 2 /* GuardLevel.Route */) {
|
|
198
|
+
const targetRoute = to.matched[to.matched.length - 1];
|
|
199
|
+
const result = targetRoute.beforeEnter(to);
|
|
200
|
+
if (result === false) {
|
|
201
|
+
return false;
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
return true;
|
|
205
|
+
}
|
|
206
|
+
catch (error) {
|
|
207
|
+
onError(error);
|
|
208
|
+
return false;
|
|
209
|
+
}
|
|
210
|
+
};
|
|
211
|
+
const executeGuards = async (to, from, guardLevel) => {
|
|
212
|
+
try {
|
|
213
|
+
if (guardLevel === 0 /* GuardLevel.None */) {
|
|
214
|
+
return true;
|
|
215
|
+
}
|
|
216
|
+
if (guardLevel & 1 /* GuardLevel.Global */) {
|
|
217
|
+
const result = await beforeEach(to, from);
|
|
218
|
+
if (result === false) {
|
|
219
|
+
return false;
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
if (guardLevel & 2 /* GuardLevel.Route */) {
|
|
223
|
+
const targetRoute = to.matched[to.matched.length - 1];
|
|
224
|
+
const result = targetRoute.beforeEnter(to);
|
|
225
|
+
return result !== false;
|
|
226
|
+
}
|
|
227
|
+
return true;
|
|
228
|
+
}
|
|
229
|
+
catch (error) {
|
|
230
|
+
onError(error);
|
|
231
|
+
return false;
|
|
232
|
+
}
|
|
233
|
+
};
|
|
234
|
+
const navigatePrepare = (options) => {
|
|
235
|
+
// Resolve target route
|
|
236
|
+
let targetPath;
|
|
237
|
+
let targetRoute;
|
|
238
|
+
if (options.name) {
|
|
239
|
+
targetRoute = findByName(options.name);
|
|
240
|
+
if (!targetRoute) {
|
|
241
|
+
throws(`Route not found: ${options.name}`);
|
|
242
|
+
}
|
|
243
|
+
targetPath = targetRoute.path;
|
|
244
|
+
}
|
|
245
|
+
else if (options.path) {
|
|
246
|
+
targetPath = normalizePath(options.path);
|
|
247
|
+
targetRoute = match(targetPath)?.route;
|
|
248
|
+
}
|
|
249
|
+
else {
|
|
250
|
+
throws(`Either path or name must be provided`);
|
|
251
|
+
}
|
|
252
|
+
// Substitute params
|
|
253
|
+
if (options.params) {
|
|
254
|
+
targetPath = emplaceParams(targetPath, options.params);
|
|
255
|
+
}
|
|
256
|
+
// Match final path
|
|
257
|
+
const matched = match(targetPath);
|
|
258
|
+
if (!matched) {
|
|
259
|
+
onNotFound(targetPath);
|
|
260
|
+
return null;
|
|
261
|
+
}
|
|
262
|
+
// Build route context
|
|
263
|
+
const queryString = options.query ? buildQuery(options.query) : '';
|
|
264
|
+
const fullPath = targetPath + queryString;
|
|
265
|
+
const to = {
|
|
266
|
+
path: targetPath,
|
|
267
|
+
name: matched.route.name,
|
|
268
|
+
params: { ...matched.params, ...(options.params ?? {}) },
|
|
269
|
+
query: options.query ?? {},
|
|
270
|
+
meta: matched.route.meta ?? {},
|
|
271
|
+
matched: matched.result,
|
|
272
|
+
};
|
|
273
|
+
return {
|
|
274
|
+
guardLevel: options.guardLevel ?? 15 /* GuardLevel.Default */,
|
|
275
|
+
replace: options.replace ?? false,
|
|
276
|
+
to,
|
|
277
|
+
fullPath,
|
|
278
|
+
};
|
|
279
|
+
};
|
|
280
|
+
const navigateSync = (options) => {
|
|
281
|
+
try {
|
|
282
|
+
const prep = navigatePrepare(options);
|
|
283
|
+
if (!prep) {
|
|
284
|
+
return false;
|
|
285
|
+
}
|
|
286
|
+
const { guardLevel, replace, to, fullPath } = prep;
|
|
287
|
+
// Execute guards
|
|
288
|
+
if (!executeGuardsSync(to, current, guardLevel)) {
|
|
289
|
+
return false;
|
|
290
|
+
}
|
|
291
|
+
// Update browser history
|
|
292
|
+
const url = fullPath;
|
|
293
|
+
if (replace) {
|
|
294
|
+
window.history.replaceState({ path: to.path }, '', url);
|
|
295
|
+
}
|
|
296
|
+
else {
|
|
297
|
+
window.history.pushState({ path: to.path }, '', url);
|
|
298
|
+
}
|
|
299
|
+
// Update current route
|
|
300
|
+
current = to;
|
|
301
|
+
history.push(to);
|
|
302
|
+
// Execute after hooks
|
|
303
|
+
executeAfterHooksSync(to, history[history.length - 2] ?? null);
|
|
304
|
+
return true;
|
|
305
|
+
}
|
|
306
|
+
catch (error) {
|
|
307
|
+
onError(error);
|
|
308
|
+
return false;
|
|
309
|
+
}
|
|
310
|
+
};
|
|
311
|
+
const navigateAsync = async (options) => {
|
|
312
|
+
try {
|
|
313
|
+
const prep = navigatePrepare(options);
|
|
314
|
+
if (!prep) {
|
|
315
|
+
return false;
|
|
316
|
+
}
|
|
317
|
+
const { guardLevel, replace, to, fullPath } = prep;
|
|
318
|
+
const passed = await executeGuards(to, current, guardLevel);
|
|
319
|
+
if (!passed) {
|
|
320
|
+
return false;
|
|
321
|
+
}
|
|
322
|
+
// ---- Guards passed ----
|
|
323
|
+
const url = fullPath;
|
|
324
|
+
if (replace) {
|
|
325
|
+
window.history.replaceState({ path: to.path }, '', url);
|
|
326
|
+
}
|
|
327
|
+
else {
|
|
328
|
+
window.history.pushState({ path: to.path }, '', url);
|
|
329
|
+
}
|
|
330
|
+
executeAfterHooks(to, history[history.length - 2] ?? null);
|
|
331
|
+
return true;
|
|
332
|
+
}
|
|
333
|
+
catch (error) {
|
|
334
|
+
onError(error);
|
|
335
|
+
return false;
|
|
336
|
+
}
|
|
337
|
+
};
|
|
338
|
+
const navigate = asyncGuards ? navigateSync : navigateAsync;
|
|
339
|
+
const executeAfterHooksSync = (to, from) => {
|
|
340
|
+
const targetRoute = to.matched[to.matched.length - 1];
|
|
341
|
+
targetRoute.after(to);
|
|
342
|
+
afterEach(to, from);
|
|
343
|
+
};
|
|
344
|
+
const executeAfterHooks = async (to, from) => {
|
|
345
|
+
const targetRoute = to.matched[to.matched.length - 1];
|
|
346
|
+
await targetRoute.after(to);
|
|
347
|
+
await afterEach(to, from);
|
|
348
|
+
};
|
|
349
|
+
/**
|
|
350
|
+
* Normalize navigation argument
|
|
351
|
+
*/
|
|
352
|
+
const normalizeLocation = (loc) => {
|
|
353
|
+
if (typeof loc === 'string') {
|
|
354
|
+
// Parse path and query
|
|
355
|
+
const [path, queryString] = loc.split('?');
|
|
356
|
+
return {
|
|
357
|
+
path,
|
|
358
|
+
query: queryString ? parseQuery(queryString) : undefined,
|
|
359
|
+
};
|
|
360
|
+
}
|
|
361
|
+
return loc;
|
|
362
|
+
};
|
|
363
|
+
// # register events
|
|
364
|
+
// Listen to browser back/forward
|
|
365
|
+
window.addEventListener('popstate', (event) => {
|
|
366
|
+
if (event.state?.path) {
|
|
367
|
+
navigate({ path: event.state.path, guardLevel: 1 /* GuardLevel.Global */, replace: true });
|
|
368
|
+
}
|
|
369
|
+
});
|
|
370
|
+
// Router instance
|
|
371
|
+
return {
|
|
372
|
+
get current() {
|
|
373
|
+
return current;
|
|
374
|
+
},
|
|
375
|
+
get history() {
|
|
376
|
+
return history.concat();
|
|
377
|
+
},
|
|
378
|
+
push(location) {
|
|
379
|
+
const options = normalizeLocation(location);
|
|
380
|
+
return navigate(options);
|
|
381
|
+
},
|
|
382
|
+
silentPush(location) {
|
|
383
|
+
const options = normalizeLocation(location);
|
|
384
|
+
return navigate({ ...options, guardLevel: 2 /* GuardLevel.Route */ });
|
|
385
|
+
},
|
|
386
|
+
replace(location) {
|
|
387
|
+
const options = normalizeLocation(location);
|
|
388
|
+
return navigate({ ...options, replace: true });
|
|
389
|
+
},
|
|
390
|
+
back() {
|
|
391
|
+
window.history.back();
|
|
392
|
+
},
|
|
393
|
+
forward() {
|
|
394
|
+
window.history.forward();
|
|
395
|
+
},
|
|
396
|
+
};
|
|
397
|
+
};
|
|
398
|
+
|
|
399
|
+
export { createRouter };
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@ktjs/router",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.13.0",
|
|
4
4
|
"description": "Router for kt.js - client-side routing with navigation guards",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"module": "./dist/index.mjs",
|
|
@@ -31,7 +31,7 @@
|
|
|
31
31
|
"directory": "packages/router"
|
|
32
32
|
},
|
|
33
33
|
"dependencies": {
|
|
34
|
-
"@ktjs/core": "0.
|
|
34
|
+
"@ktjs/core": "0.13.0"
|
|
35
35
|
},
|
|
36
36
|
"scripts": {
|
|
37
37
|
"build": "rollup -c rollup.config.mjs",
|