@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 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
- > 📦 Part of [KT.js](https://raw.githubusercontent.com/baendlorel/kt.js/dev/README.md) - A simple and easy-to-use web framework that never re-renders.
5
+ [![npm version](https://img.shields.io/npm/v/@ktjs/router.svg)](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.
@@ -1 +1,406 @@
1
- var __ktjs_router__=function(t){"use strict";const r=()=>!0,e=t=>{throw new Error(`@ktjs/router: ${t}`)},n=(...t)=>"/"+t.map(t=>t.split("/")).flat().filter(Boolean).join("/"),o=t=>{const r={};if(!t||"?"===t)return r;const e=t.replace(/^\?/,"").split("&");for(const t of e){const[e,n]=t.split("=");e&&(r[decodeURIComponent(e)]=n?decodeURIComponent(n):"")}return r},u=(t,r)=>{const e={},n=t.split("/"),o=r.split("/");if(n.length!==o.length)return null;for(let t=0;t<n.length;t++){const r=n[t],u=o[t];if(r.startsWith(":")){e[r.slice(1)]=u}else if(r!==u)return null}return e};return t.createRouter=t=>{const c=t.beforeEach??r,a=t.afterEach??r,s=t.onNotFound??r,i=t.onError??r,l=t.asyncGuards??!0,f=[],d=(t,e)=>t.map(t=>{const o=n(e,t.path),u={path:o,name:t.name??"",meta:t.meta??{},beforeEnter:t.beforeEnter??r,after:t.after??r,children:t.children?d(t.children,o):[]};return f.push(u),u});d(t.routes,"/");const{findByName:p,match:h}=(t=>{const r={};for(let n=0;n<t.length;n++){const o=t[n];void 0!==o.name&&(o.name in r&&e(`Duplicate route name detected: '${o.name}'`),r[o.name]=o)}const o=r=>{const e=[r],n=r.path;for(let o=0;o<t.length;o++){const u=t[o];u!==r&&n.startsWith(u.path)&&n!==u.path&&e.push(u)}return e.reverse()};return{findByName:t=>r[t]??null,match:r=>{const e=n(r);for(const r of t)if(r.path===e)return{route:r,params:{},result:o(r)};for(const r of t)if(r.path.includes(":")){const t=u(r.path,e);if(t)return{route:r,params:t,result:o(r)}}return null}}})(f);let w=null;const m=[],y=t=>{let r,o;t.name?(o=p(t.name),o||e(`Route not found: ${t.name}`),r=o.path):t.path?(r=n(t.path),o=h(r)?.route):e("Either path or name must be provided"),t.params&&(r=((t,r)=>{let e=t;for(const t in r)e=e.replace(`:${t}`,r[t]);return e})(r,t.params));const u=h(r);if(!u)return s(r),null;const c=r+(t.query?(t=>{const r=Object.keys(t);return 0===r.length?"":`?${r.map(r=>`${encodeURIComponent(r)}=${encodeURIComponent(t[r])}`).join("&")}`})(t.query):""),a={path:r,name:u.route.name,params:{...u.params,...t.params||{}},query:t.query||{},meta:u.route.meta||{},matched:u.result};return{guardLevel:t.guardLevel??15,replace:t.replace??!1,to:a,fullPath:c}},v=l?t=>{try{const r=y(t);if(!r)return!1;const{guardLevel:e,replace:n,to:o,fullPath:u}=r;if(!((t,r,e)=>{try{if(0===e)return!0;if(1&e&&!1===c(t,r))return!1;if(2&e){if(!1===t.matched[t.matched.length-1].beforeEnter(t))return!1}return!0}catch(t){return i(t),!1}})(o,w,e))return!1;const a=u;return n?window.history.replaceState({path:o.path},"",a):window.history.pushState({path:o.path},"",a),w=o,m.push(o),g(o,m[m.length-2]||null),!0}catch(t){return i(t),!1}}:async t=>{try{const r=y(t);if(!r)return!1;const{guardLevel:e,replace:n,to:o,fullPath:u}=r,a=await(async(t,r,e)=>{try{if(0===e)return!0;if(1&e&&!1===await c(t,r))return!1;if(2&e){return!1!==t.matched[t.matched.length-1].beforeEnter(t)}return!0}catch(t){return i(t),!1}})(o,w,e);if(!a)return!1;const s=u;return n?window.history.replaceState({path:o.path},"",s):window.history.pushState({path:o.path},"",s),$(o,m[m.length-2]||null),!0}catch(t){return i(t),!1}},g=(t,r)=>{t.matched[t.matched.length-1].after(t),a(t,r)},$=async(t,r)=>{const e=t.matched[t.matched.length-1];await e.after(t),await a(t,r)},L=t=>{if("string"==typeof t){const[r,e]=t.split("?");return{path:r,query:e?o(e):void 0}}return t};return window.addEventListener("popstate",t=>{t.state?.path&&v({path:t.state.path,guardLevel:1,replace:!0})}),{get current(){return w},get history(){return m.concat()},push(t){const r=L(t);return v(r)},silentPush(t){const r=L(t);return v({...r,guardLevel:2})},replace(t){const r=L(t);return v({...r,replace:!0})},back(){window.history.back()},forward(){window.history.forward()}}},t}({});
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
+ })({});
@@ -1 +1,520 @@
1
- var __ktjs_router__=function(r){"use strict";var n=function(){return n=Object.assign||function(r){for(var n,t=1,e=arguments.length;t<e;t++)for(var u in n=arguments[t])Object.prototype.hasOwnProperty.call(n,u)&&(r[u]=n[u]);return r},n.apply(this,arguments)};function t(r,n,t,e){return new(t||(t=Promise))(function(u,o){function i(r){try{c(e.next(r))}catch(r){o(r)}}function a(r){try{c(e.throw(r))}catch(r){o(r)}}function c(r){var n;r.done?u(r.value):(n=r.value,n instanceof t?n:new t(function(r){r(n)})).then(i,a)}c((e=e.apply(r,n||[])).next())})}function e(r,n){var t,e,u,o={label:0,sent:function(){if(1&u[0])throw u[1];return u[1]},trys:[],ops:[]},i=Object.create(("function"==typeof Iterator?Iterator:Object).prototype);return i.next=a(0),i.throw=a(1),i.return=a(2),"function"==typeof Symbol&&(i[Symbol.iterator]=function(){return this}),i;function a(a){return function(c){return function(a){if(t)throw new TypeError("Generator is already executing.");for(;i&&(i=0,a[0]&&(o=0)),o;)try{if(t=1,e&&(u=2&a[0]?e.return:a[0]?e.throw||((u=e.return)&&u.call(e),0):e.next)&&!(u=u.call(e,a[1])).done)return u;switch(e=0,u&&(a=[2&a[0],u.value]),a[0]){case 0:case 1:u=a;break;case 4:return o.label++,{value:a[1],done:!1};case 5:o.label++,e=a[1],a=[0];continue;case 7:a=o.ops.pop(),o.trys.pop();continue;default:if(!(u=o.trys,(u=u.length>0&&u[u.length-1])||6!==a[0]&&2!==a[0])){o=0;continue}if(3===a[0]&&(!u||a[1]>u[0]&&a[1]<u[3])){o.label=a[1];break}if(6===a[0]&&o.label<u[1]){o.label=u[1],u=a;break}if(u&&o.label<u[2]){o.label=u[2],o.ops.push(a);break}u[2]&&o.ops.pop(),o.trys.pop();continue}a=n.call(r,o)}catch(r){a=[6,r],e=0}finally{t=u=0}if(5&a[0])throw a[1];return{value:a[0]?a[1]:void 0,done:!0}}([a,c])}}}"function"==typeof SuppressedError&&SuppressedError;var u=function(){return!0},o=function(r){throw new Error("@ktjs/router: ".concat(r))},i=function(){for(var r=[],n=0;n<arguments.length;n++)r[n]=arguments[n];return"/"+r.map(function(r){return r.split("/")}).flat().filter(Boolean).join("/")},a=function(r){var n={};if(!r||"?"===r)return n;for(var t=0,e=r.replace(/^\?/,"").split("&");t<e.length;t++){var u=e[t].split("="),o=u[0],i=u[1];o&&(n[decodeURIComponent(o)]=i?decodeURIComponent(i):"")}return n},c=function(r,n){var t={},e=r.split("/"),u=n.split("/");if(e.length!==u.length)return null;for(var o=0;o<e.length;o++){var i=e[o],a=u[o];if(i.startsWith(":"))t[i.slice(1)]=a;else if(i!==a)return null}return t};return r.createRouter=function(r){var f,v,l,d,s,h=null!==(f=r.beforeEach)&&void 0!==f?f:u,p=null!==(v=r.afterEach)&&void 0!==v?v:u,w=null!==(l=r.onNotFound)&&void 0!==l?l:u,m=null!==(d=r.onError)&&void 0!==d?d:u,y=null===(s=r.asyncGuards)||void 0===s||s,b=[],g=function(r,n){return r.map(function(r){var t,e,o,a,c=i(n,r.path),f={path:c,name:null!==(t=r.name)&&void 0!==t?t:"",meta:null!==(e=r.meta)&&void 0!==e?e:{},beforeEnter:null!==(o=r.beforeEnter)&&void 0!==o?o:u,after:null!==(a=r.after)&&void 0!==a?a:u,children:r.children?g(r.children,c):[]};return b.push(f),f})};g(r.routes,"/");var j=function(r){for(var n={},t=0;t<r.length;t++){var e=r[t];void 0!==e.name&&(e.name in n&&o("Duplicate route name detected: '".concat(e.name,"'")),n[e.name]=e)}var u=function(n){for(var t=[n],e=n.path,u=0;u<r.length;u++){var o=r[u];o!==n&&e.startsWith(o.path)&&e!==o.path&&t.push(o)}return t.reverse()};return{findByName:function(r){var t;return null!==(t=n[r])&&void 0!==t?t:null},match:function(n){for(var t=i(n),e=0,o=r;e<o.length;e++)if((v=o[e]).path===t)return{route:v,params:{},result:u(v)};for(var a=0,f=r;a<f.length;a++){var v;if((v=f[a]).path.includes(":")){var l=c(v.path,t);if(l)return{route:v,params:l,result:u(v)}}}return null}}}(b),k=j.findByName,E=j.match,I=null,O=[],R=function(r,n,u){return t(void 0,void 0,void 0,function(){var t,o;return e(this,function(e){switch(e.label){case 0:return e.trys.push([0,3,,4]),0===u?[2,!0]:1&u?[4,h(r,n)]:[3,2];case 1:if(!1===e.sent())return[2,!1];e.label=2;case 2:return 2&u?(t=r.matched[r.matched.length-1],[2,!1!==t.beforeEnter(r)]):[2,!0];case 3:return o=e.sent(),m(o),[2,!1];case 4:return[2]}})})},_=function(r){var t,e,u,a,c;r.name?((c=k(r.name))||o("Route not found: ".concat(r.name)),a=c.path):r.path?(a=i(r.path),c=null===(t=E(a))||void 0===t?void 0:t.route):o("Either path or name must be provided"),r.params&&(a=function(r,n){var t=r;for(var e in n)t=t.replace(":".concat(e),n[e]);return t}(a,r.params));var f=E(a);if(!f)return w(a),null;var v=a+(r.query?function(r){var n=Object.keys(r);if(0===n.length)return"";var t=n.map(function(n){return"".concat(encodeURIComponent(n),"=").concat(encodeURIComponent(r[n]))}).join("&");return"?".concat(t)}(r.query):""),l={path:a,name:f.route.name,params:n(n({},f.params),r.params||{}),query:r.query||{},meta:f.route.meta||{},matched:f.result};return{guardLevel:null!==(e=r.guardLevel)&&void 0!==e?e:15,replace:null!==(u=r.replace)&&void 0!==u&&u,to:l,fullPath:v}},C=y?function(r){try{var n=_(r);if(!n)return!1;var t=n.guardLevel,e=n.replace,u=n.to,o=n.fullPath;if(!function(r,n,t){try{return 0===t||!(1&t&&!1===h(r,n))&&!(2&t&&!1===r.matched[r.matched.length-1].beforeEnter(r))}catch(r){return m(r),!1}}(u,I,t))return!1;var i=o;return e?window.history.replaceState({path:u.path},"",i):window.history.pushState({path:u.path},"",i),I=u,O.push(u),S(u,O[O.length-2]||null),!0}catch(r){return m(r),!1}}:function(r){return t(void 0,void 0,void 0,function(){var n,t,u,o,i,a,c;return e(this,function(e){switch(e.label){case 0:return e.trys.push([0,2,,3]),(n=_(r))?(t=n.guardLevel,u=n.replace,o=n.to,i=n.fullPath,[4,R(o,I,t)]):[2,!1];case 1:return e.sent()?(a=i,u?window.history.replaceState({path:o.path},"",a):window.history.pushState({path:o.path},"",a),U(o,O[O.length-2]||null),[2,!0]):[2,!1];case 2:return c=e.sent(),m(c),[2,!1];case 3:return[2]}})})},S=function(r,n){r.matched[r.matched.length-1].after(r),p(r,n)},U=function(r,n){return t(void 0,void 0,void 0,function(){return e(this,function(t){switch(t.label){case 0:return[4,r.matched[r.matched.length-1].after(r)];case 1:return t.sent(),[4,p(r,n)];case 2:return t.sent(),[2]}})})},L=function(r){if("string"==typeof r){var n=r.split("?"),t=n[0],e=n[1];return{path:t,query:e?a(e):void 0}}return r};return window.addEventListener("popstate",function(r){var n;(null===(n=r.state)||void 0===n?void 0:n.path)&&C({path:r.state.path,guardLevel:1,replace:!0})}),{get current(){return I},get history(){return O.concat()},push:function(r){var n=L(r);return C(n)},silentPush:function(r){var t=L(r);return C(n(n({},t),{guardLevel:2}))},replace:function(r){var t=L(r);return C(n(n({},t),{replace:!0}))},back:function(){window.history.back()},forward:function(){window.history.forward()}}},r}({});
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
- const t=()=>!0,r=t=>{throw new Error(`@ktjs/router: ${t}`)},e=(...t)=>"/"+t.map(t=>t.split("/")).flat().filter(Boolean).join("/"),n=t=>{const r={};if(!t||"?"===t)return r;const e=t.replace(/^\?/,"").split("&");for(const t of e){const[e,n]=t.split("=");e&&(r[decodeURIComponent(e)]=n?decodeURIComponent(n):"")}return r},o=(t,r)=>{const e={},n=t.split("/"),o=r.split("/");if(n.length!==o.length)return null;for(let t=0;t<n.length;t++){const r=n[t],u=o[t];if(r.startsWith(":")){e[r.slice(1)]=u}else if(r!==u)return null}return e},u=u=>{const c=u.beforeEach??t,a=u.afterEach??t,s=u.onNotFound??t,l=u.onError??t,i=u.asyncGuards??!0,f=[],d=(r,n)=>r.map(r=>{const o=e(n,r.path),u={path:o,name:r.name??"",meta:r.meta??{},beforeEnter:r.beforeEnter??t,after:r.after??t,children:r.children?d(r.children,o):[]};return f.push(u),u});d(u.routes,"/");const{findByName:p,match:h}=(t=>{const n={};for(let e=0;e<t.length;e++){const o=t[e];void 0!==o.name&&(o.name in n&&r(`Duplicate route name detected: '${o.name}'`),n[o.name]=o)}const u=r=>{const e=[r],n=r.path;for(let o=0;o<t.length;o++){const u=t[o];u!==r&&n.startsWith(u.path)&&n!==u.path&&e.push(u)}return e.reverse()};return{findByName:t=>n[t]??null,match:r=>{const n=e(r);for(const r of t)if(r.path===n)return{route:r,params:{},result:u(r)};for(const r of t)if(r.path.includes(":")){const t=o(r.path,n);if(t)return{route:r,params:t,result:u(r)}}return null}}})(f);let w=null;const m=[],y=t=>{let n,o;t.name?(o=p(t.name),o||r(`Route not found: ${t.name}`),n=o.path):t.path?(n=e(t.path),o=h(n)?.route):r("Either path or name must be provided"),t.params&&(n=((t,r)=>{let e=t;for(const t in r)e=e.replace(`:${t}`,r[t]);return e})(n,t.params));const u=h(n);if(!u)return s(n),null;const c=n+(t.query?(t=>{const r=Object.keys(t);return 0===r.length?"":`?${r.map(r=>`${encodeURIComponent(r)}=${encodeURIComponent(t[r])}`).join("&")}`})(t.query):""),a={path:n,name:u.route.name,params:{...u.params,...t.params||{}},query:t.query||{},meta:u.route.meta||{},matched:u.result};return{guardLevel:t.guardLevel??15,replace:t.replace??!1,to:a,fullPath:c}},g=i?t=>{try{const r=y(t);if(!r)return!1;const{guardLevel:e,replace:n,to:o,fullPath:u}=r;if(!((t,r,e)=>{try{if(0===e)return!0;if(1&e&&!1===c(t,r))return!1;if(2&e){if(!1===t.matched[t.matched.length-1].beforeEnter(t))return!1}return!0}catch(t){return l(t),!1}})(o,w,e))return!1;const a=u;return n?window.history.replaceState({path:o.path},"",a):window.history.pushState({path:o.path},"",a),w=o,m.push(o),v(o,m[m.length-2]||null),!0}catch(t){return l(t),!1}}:async t=>{try{const r=y(t);if(!r)return!1;const{guardLevel:e,replace:n,to:o,fullPath:u}=r,a=await(async(t,r,e)=>{try{if(0===e)return!0;if(1&e&&!1===await c(t,r))return!1;if(2&e){return!1!==t.matched[t.matched.length-1].beforeEnter(t)}return!0}catch(t){return l(t),!1}})(o,w,e);if(!a)return!1;const s=u;return n?window.history.replaceState({path:o.path},"",s):window.history.pushState({path:o.path},"",s),$(o,m[m.length-2]||null),!0}catch(t){return l(t),!1}},v=(t,r)=>{t.matched[t.matched.length-1].after(t),a(t,r)},$=async(t,r)=>{const e=t.matched[t.matched.length-1];await e.after(t),await a(t,r)},L=t=>{if("string"==typeof t){const[r,e]=t.split("?");return{path:r,query:e?n(e):void 0}}return t};return window.addEventListener("popstate",t=>{t.state?.path&&g({path:t.state.path,guardLevel:1,replace:!0})}),{get current(){return w},get history(){return m.concat()},push(t){const r=L(t);return g(r)},silentPush(t){const r=L(t);return g({...r,guardLevel:2})},replace(t){const r=L(t);return g({...r,replace:!0})},back(){window.history.back()},forward(){window.history.forward()}}};export{u as createRouter};
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.6.6",
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.6.6"
34
+ "@ktjs/core": "0.13.0"
35
35
  },
36
36
  "scripts": {
37
37
  "build": "rollup -c rollup.config.mjs",