@zenithbuild/router 0.6.5 → 0.6.7
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/dist/events.d.ts +21 -0
- package/dist/events.js +23 -75
- package/dist/history.d.ts +26 -0
- package/dist/history.js +2 -10
- package/dist/index.d.ts +4 -41
- package/dist/index.js +0 -1
- package/dist/match.d.ts +22 -0
- package/dist/match.js +40 -98
- package/dist/navigate.d.ts +5 -0
- package/dist/navigate.js +6 -40
- package/dist/router.d.ts +18 -0
- package/dist/router.js +22 -63
- package/package.json +11 -5
- package/dist/index.d.ts.map +0 -1
- package/dist/index.js.map +0 -1
- package/dist/manifest.d.ts +0 -32
- package/dist/manifest.d.ts.map +0 -1
- package/dist/manifest.js +0 -180
- package/dist/manifest.js.map +0 -1
- package/dist/navigation/client-router.d.ts +0 -59
- package/dist/navigation/client-router.d.ts.map +0 -1
- package/dist/navigation/client-router.js +0 -373
- package/dist/navigation/client-router.js.map +0 -1
- package/dist/navigation/index.d.ts +0 -30
- package/dist/navigation/index.d.ts.map +0 -1
- package/dist/navigation/index.js +0 -44
- package/dist/navigation/index.js.map +0 -1
- package/dist/navigation/zen-link.d.ts +0 -234
- package/dist/navigation/zen-link.d.ts.map +0 -1
- package/dist/navigation/zen-link.js +0 -437
- package/dist/navigation/zen-link.js.map +0 -1
- package/dist/runtime.d.ts +0 -33
- package/dist/runtime.d.ts.map +0 -1
- package/dist/runtime.js +0 -157
- package/dist/runtime.js.map +0 -1
- package/dist/types.d.ts +0 -50
- package/dist/types.d.ts.map +0 -1
- package/dist/types.js +0 -7
- package/dist/types.js.map +0 -1
package/dist/events.d.ts
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
type RouteParams = Record<string, string>;
|
|
2
|
+
type RouteChangeDetail = {
|
|
3
|
+
path: string;
|
|
4
|
+
params?: RouteParams;
|
|
5
|
+
matched: boolean;
|
|
6
|
+
};
|
|
7
|
+
type RouteProtectionPolicy = {
|
|
8
|
+
beforeResolve?: boolean;
|
|
9
|
+
emitRedirects?: boolean;
|
|
10
|
+
};
|
|
11
|
+
type RouteEventHandler = (payload: unknown) => void;
|
|
12
|
+
export declare function onRouteChange(callback: (detail: RouteChangeDetail) => void): () => void;
|
|
13
|
+
export declare function _dispatchRouteChange(detail: RouteChangeDetail): void;
|
|
14
|
+
export declare function _clearSubscribers(): void;
|
|
15
|
+
export declare function _getSubscriberCount(): number;
|
|
16
|
+
export declare function setRouteProtectionPolicy(policy: RouteProtectionPolicy): void;
|
|
17
|
+
export declare function _getRouteProtectionPolicy(): RouteProtectionPolicy;
|
|
18
|
+
export declare function on(eventName: string, handler: RouteEventHandler): void;
|
|
19
|
+
export declare function off(eventName: string, handler: RouteEventHandler): void;
|
|
20
|
+
export declare function _dispatchRouteEvent(eventName: string, payload: unknown): void;
|
|
21
|
+
export {};
|
package/dist/events.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
|
-
// events.
|
|
2
|
+
// events.ts — Zenith Router V0
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
4
|
// Route change event system.
|
|
5
5
|
//
|
|
@@ -7,53 +7,24 @@
|
|
|
7
7
|
// Returns unsubscribe function.
|
|
8
8
|
// No batching. No queue. Synchronous dispatch.
|
|
9
9
|
// ---------------------------------------------------------------------------
|
|
10
|
-
|
|
11
|
-
/** @type {Set<(detail: object) => void>} */
|
|
12
|
-
const _subscribers = new Set();
|
|
13
|
-
|
|
14
|
-
/**
|
|
15
|
-
* Subscribe to route change events.
|
|
16
|
-
*
|
|
17
|
-
* @param {(detail: { path: string, params?: Record<string, string>, matched: boolean }) => void} callback
|
|
18
|
-
* @returns {() => void} unsubscribe
|
|
19
|
-
*/
|
|
10
|
+
const subscribers = new Set();
|
|
20
11
|
export function onRouteChange(callback) {
|
|
21
|
-
|
|
22
|
-
|
|
12
|
+
subscribers.add(callback);
|
|
23
13
|
return () => {
|
|
24
|
-
|
|
14
|
+
subscribers.delete(callback);
|
|
25
15
|
};
|
|
26
16
|
}
|
|
27
|
-
|
|
28
|
-
/**
|
|
29
|
-
* Dispatch a route change to all subscribers.
|
|
30
|
-
*
|
|
31
|
-
* @param {{ path: string, params?: Record<string, string>, matched: boolean }} detail
|
|
32
|
-
*/
|
|
33
17
|
export function _dispatchRouteChange(detail) {
|
|
34
|
-
for (const
|
|
35
|
-
|
|
18
|
+
for (const callback of subscribers) {
|
|
19
|
+
callback(detail);
|
|
36
20
|
}
|
|
37
21
|
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
* Clear all subscribers. Used for testing and teardown.
|
|
41
|
-
*/
|
|
42
22
|
export function _clearSubscribers() {
|
|
43
|
-
|
|
23
|
+
subscribers.clear();
|
|
44
24
|
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Get the current subscriber count. Used for leak detection.
|
|
48
|
-
*
|
|
49
|
-
* @returns {number}
|
|
50
|
-
*/
|
|
51
25
|
export function _getSubscriberCount() {
|
|
52
|
-
return
|
|
26
|
+
return subscribers.size;
|
|
53
27
|
}
|
|
54
|
-
|
|
55
|
-
// --- Route Protection Events & Policy ---
|
|
56
|
-
|
|
57
28
|
const ROUTE_POLICY_KEY = '__zenith_route_protection_policy';
|
|
58
29
|
const ROUTE_EVENT_LISTENERS_KEY = '__zenith_route_event_listeners';
|
|
59
30
|
const ROUTE_EVENT_NAMES = [
|
|
@@ -65,84 +36,61 @@ const ROUTE_EVENT_NAMES = [
|
|
|
65
36
|
'route:deny',
|
|
66
37
|
'route:redirect'
|
|
67
38
|
];
|
|
68
|
-
|
|
69
39
|
function getRouteProtectionScope() {
|
|
70
|
-
return typeof globalThis === 'object' && globalThis
|
|
40
|
+
return typeof globalThis === 'object' && globalThis
|
|
41
|
+
? globalThis
|
|
42
|
+
: {};
|
|
71
43
|
}
|
|
72
|
-
|
|
73
44
|
function ensureRouteProtectionState() {
|
|
74
45
|
const scope = getRouteProtectionScope();
|
|
75
|
-
|
|
76
46
|
let policy = scope[ROUTE_POLICY_KEY];
|
|
77
47
|
if (!policy || typeof policy !== 'object') {
|
|
78
48
|
policy = {};
|
|
79
49
|
scope[ROUTE_POLICY_KEY] = policy;
|
|
80
50
|
}
|
|
81
|
-
|
|
82
51
|
let listeners = scope[ROUTE_EVENT_LISTENERS_KEY];
|
|
83
52
|
if (!listeners || typeof listeners !== 'object') {
|
|
84
53
|
listeners = Object.create(null);
|
|
85
54
|
scope[ROUTE_EVENT_LISTENERS_KEY] = listeners;
|
|
86
55
|
}
|
|
87
|
-
|
|
88
56
|
for (const eventName of ROUTE_EVENT_NAMES) {
|
|
89
57
|
if (!(listeners[eventName] instanceof Set)) {
|
|
90
58
|
listeners[eventName] = new Set();
|
|
91
59
|
}
|
|
92
60
|
}
|
|
93
|
-
|
|
94
61
|
return { policy, listeners };
|
|
95
62
|
}
|
|
96
|
-
|
|
97
|
-
/**
|
|
98
|
-
* Configure default behaviors for route protection.
|
|
99
|
-
* @param {import('../index').RouteProtectionPolicy} policy
|
|
100
|
-
*/
|
|
101
63
|
export function setRouteProtectionPolicy(policy) {
|
|
102
64
|
const state = ensureRouteProtectionState();
|
|
103
65
|
state.policy = Object.assign({}, state.policy, policy);
|
|
104
66
|
getRouteProtectionScope()[ROUTE_POLICY_KEY] = state.policy;
|
|
105
67
|
}
|
|
106
|
-
|
|
107
68
|
export function _getRouteProtectionPolicy() {
|
|
108
69
|
return ensureRouteProtectionState().policy;
|
|
109
70
|
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Listen to route protection lifecycle events.
|
|
113
|
-
* @param {string} eventName
|
|
114
|
-
* @param {Function} handler
|
|
115
|
-
*/
|
|
116
71
|
export function on(eventName, handler) {
|
|
117
|
-
const
|
|
118
|
-
if (
|
|
119
|
-
|
|
72
|
+
const eventListeners = ensureRouteProtectionState().listeners[eventName];
|
|
73
|
+
if (eventListeners instanceof Set) {
|
|
74
|
+
eventListeners.add(handler);
|
|
120
75
|
}
|
|
121
76
|
}
|
|
122
|
-
|
|
123
|
-
/**
|
|
124
|
-
* Remove a route protection lifecycle event listener.
|
|
125
|
-
* @param {string} eventName
|
|
126
|
-
* @param {Function} handler
|
|
127
|
-
*/
|
|
128
77
|
export function off(eventName, handler) {
|
|
129
|
-
const
|
|
130
|
-
if (
|
|
131
|
-
|
|
78
|
+
const eventListeners = ensureRouteProtectionState().listeners[eventName];
|
|
79
|
+
if (eventListeners instanceof Set) {
|
|
80
|
+
eventListeners.delete(handler);
|
|
132
81
|
}
|
|
133
82
|
}
|
|
134
|
-
|
|
135
83
|
export function _dispatchRouteEvent(eventName, payload) {
|
|
136
|
-
const
|
|
137
|
-
if (!(
|
|
84
|
+
const eventListeners = ensureRouteProtectionState().listeners[eventName];
|
|
85
|
+
if (!(eventListeners instanceof Set)) {
|
|
138
86
|
return;
|
|
139
87
|
}
|
|
140
|
-
|
|
141
|
-
for (const handler of listeners) {
|
|
88
|
+
for (const handler of eventListeners) {
|
|
142
89
|
try {
|
|
143
90
|
handler(payload);
|
|
144
|
-
}
|
|
145
|
-
|
|
91
|
+
}
|
|
92
|
+
catch (error) {
|
|
93
|
+
console.error(`[Zenith Router] Error in ${eventName} listener:`, error);
|
|
146
94
|
}
|
|
147
95
|
}
|
|
148
96
|
}
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Push a new path to the browser history.
|
|
3
|
+
*
|
|
4
|
+
* @param {string} path
|
|
5
|
+
*/
|
|
6
|
+
export function push(path: string): void;
|
|
7
|
+
/**
|
|
8
|
+
* Replace the current path in browser history.
|
|
9
|
+
*
|
|
10
|
+
* @param {string} path
|
|
11
|
+
*/
|
|
12
|
+
export function replace(path: string): void;
|
|
13
|
+
/**
|
|
14
|
+
* Subscribe to popstate (back/forward) events.
|
|
15
|
+
* Returns an unlisten function.
|
|
16
|
+
*
|
|
17
|
+
* @param {(path: string) => void} callback
|
|
18
|
+
* @returns {() => void} unlisten
|
|
19
|
+
*/
|
|
20
|
+
export function listen(callback: (path: string) => void): () => void;
|
|
21
|
+
/**
|
|
22
|
+
* Get the current pathname.
|
|
23
|
+
*
|
|
24
|
+
* @returns {string}
|
|
25
|
+
*/
|
|
26
|
+
export function current(): string;
|
package/dist/history.js
CHANGED
|
@@ -10,13 +10,10 @@
|
|
|
10
10
|
//
|
|
11
11
|
// No hash routing. No scroll restoration. No batching.
|
|
12
12
|
// ---------------------------------------------------------------------------
|
|
13
|
-
|
|
14
13
|
/** @type {Set<(path: string) => void>} */
|
|
15
14
|
const _listeners = new Set();
|
|
16
|
-
|
|
17
15
|
/** @type {boolean} */
|
|
18
16
|
let _listening = false;
|
|
19
|
-
|
|
20
17
|
/**
|
|
21
18
|
* Internal popstate handler — fires all registered listeners.
|
|
22
19
|
*/
|
|
@@ -26,16 +23,15 @@ function _onPopState() {
|
|
|
26
23
|
cb(path);
|
|
27
24
|
}
|
|
28
25
|
}
|
|
29
|
-
|
|
30
26
|
/**
|
|
31
27
|
* Ensure the global popstate listener is attached (once).
|
|
32
28
|
*/
|
|
33
29
|
function _ensureListening() {
|
|
34
|
-
if (_listening)
|
|
30
|
+
if (_listening)
|
|
31
|
+
return;
|
|
35
32
|
window.addEventListener('popstate', _onPopState);
|
|
36
33
|
_listening = true;
|
|
37
34
|
}
|
|
38
|
-
|
|
39
35
|
/**
|
|
40
36
|
* Push a new path to the browser history.
|
|
41
37
|
*
|
|
@@ -44,7 +40,6 @@ function _ensureListening() {
|
|
|
44
40
|
export function push(path) {
|
|
45
41
|
window.location.assign(path);
|
|
46
42
|
}
|
|
47
|
-
|
|
48
43
|
/**
|
|
49
44
|
* Replace the current path in browser history.
|
|
50
45
|
*
|
|
@@ -53,7 +48,6 @@ export function push(path) {
|
|
|
53
48
|
export function replace(path) {
|
|
54
49
|
window.location.replace(path);
|
|
55
50
|
}
|
|
56
|
-
|
|
57
51
|
/**
|
|
58
52
|
* Subscribe to popstate (back/forward) events.
|
|
59
53
|
* Returns an unlisten function.
|
|
@@ -64,7 +58,6 @@ export function replace(path) {
|
|
|
64
58
|
export function listen(callback) {
|
|
65
59
|
_ensureListening();
|
|
66
60
|
_listeners.add(callback);
|
|
67
|
-
|
|
68
61
|
return () => {
|
|
69
62
|
_listeners.delete(callback);
|
|
70
63
|
if (_listeners.size === 0) {
|
|
@@ -73,7 +66,6 @@ export function listen(callback) {
|
|
|
73
66
|
}
|
|
74
67
|
};
|
|
75
68
|
}
|
|
76
|
-
|
|
77
69
|
/**
|
|
78
70
|
* Get the current pathname.
|
|
79
71
|
*
|
package/dist/index.d.ts
CHANGED
|
@@ -1,41 +1,4 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
* Includes routing, navigation, and ZenLink components.
|
|
6
|
-
*
|
|
7
|
-
* Features:
|
|
8
|
-
* - Deterministic, compile-time route resolution
|
|
9
|
-
* - File-based routing (pages/ directory → routes)
|
|
10
|
-
* - SPA navigation with prefetching
|
|
11
|
-
* - ZenLink component for declarative links
|
|
12
|
-
* - Type-safe route parameters
|
|
13
|
-
* - Hydration-safe, no runtime hacks
|
|
14
|
-
*
|
|
15
|
-
* @example
|
|
16
|
-
* ```ts
|
|
17
|
-
* import { navigate, isActive, prefetch } from '@zenithbuild/router'
|
|
18
|
-
*
|
|
19
|
-
* // Navigate programmatically
|
|
20
|
-
* navigate('/about')
|
|
21
|
-
*
|
|
22
|
-
* // Check active state
|
|
23
|
-
* if (isActive('/blog')) {
|
|
24
|
-
* console.log('On blog section')
|
|
25
|
-
* }
|
|
26
|
-
* ```
|
|
27
|
-
*
|
|
28
|
-
* @example
|
|
29
|
-
* ```ts
|
|
30
|
-
* // Build-time manifest generation
|
|
31
|
-
* import { generateRouteManifest, discoverPages } from '@zenithbuild/router/manifest'
|
|
32
|
-
*
|
|
33
|
-
* const manifest = generateRouteManifest('./src/pages')
|
|
34
|
-
* ```
|
|
35
|
-
*/
|
|
36
|
-
export * from "./types";
|
|
37
|
-
export { generateRouteManifest, generateRouteManifestCode } from "./manifest";
|
|
38
|
-
export { initRouter, resolveRoute, navigate, getRoute, onRouteChange, isActive, prefetch, isPrefetched, generateRuntimeRouterCode, zenRoute } from "./runtime";
|
|
39
|
-
export { zenNavigate, zenBack, zenForward, zenGo, zenIsActive, zenPrefetch, zenIsPrefetched, zenGetRoute, zenGetParam, zenGetQuery, createZenLink, zenLink, back, forward, go, getParam, getQuery, isExternalUrl, shouldUseSPANavigation, normalizePath, setGlobalTransition, getGlobalTransition, createTransitionContext } from "./navigation/index";
|
|
40
|
-
export type { ZenLinkProps, TransitionContext, TransitionHandler } from "./navigation/index";
|
|
41
|
-
//# sourceMappingURL=index.d.ts.map
|
|
1
|
+
export { createRouter } from "./router.js";
|
|
2
|
+
export { matchRoute } from "./match.js";
|
|
3
|
+
export { navigate, back, forward, getCurrentPath } from "./navigate.js";
|
|
4
|
+
export { onRouteChange, on, off, setRouteProtectionPolicy, _getRouteProtectionPolicy, _dispatchRouteEvent } from "./events.js";
|
package/dist/index.js
CHANGED
|
@@ -3,7 +3,6 @@
|
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
4
|
// Structural navigation exports + route protection policy/event hooks.
|
|
5
5
|
// ---------------------------------------------------------------------------
|
|
6
|
-
|
|
7
6
|
export { createRouter } from './router.js';
|
|
8
7
|
export { navigate, back, forward, getCurrentPath } from './navigate.js';
|
|
9
8
|
export { onRouteChange, on, off, setRouteProtectionPolicy, _getRouteProtectionPolicy, _dispatchRouteEvent } from './events.js';
|
package/dist/match.d.ts
ADDED
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
type RouteParams = Record<string, string>;
|
|
2
|
+
type RouteEntry = {
|
|
3
|
+
path: string;
|
|
4
|
+
load: (params: RouteParams) => unknown;
|
|
5
|
+
};
|
|
6
|
+
type MatchResult = {
|
|
7
|
+
route: RouteEntry;
|
|
8
|
+
params: RouteParams;
|
|
9
|
+
};
|
|
10
|
+
/**
|
|
11
|
+
* Match a pathname against a single route definition.
|
|
12
|
+
*/
|
|
13
|
+
export declare function matchPath(routePath: string, pathname: string): {
|
|
14
|
+
matched: boolean;
|
|
15
|
+
params: RouteParams;
|
|
16
|
+
};
|
|
17
|
+
/**
|
|
18
|
+
* Match a pathname against an ordered array of route definitions.
|
|
19
|
+
* Returns the first match (deterministic, first-match-wins).
|
|
20
|
+
*/
|
|
21
|
+
export declare function matchRoute(routes: RouteEntry[], pathname: string): MatchResult | null;
|
|
22
|
+
export {};
|
package/dist/match.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
|
-
// match.
|
|
2
|
+
// match.ts — Zenith Router V0
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
4
|
// Deterministic path matching engine.
|
|
5
5
|
//
|
|
@@ -15,35 +15,20 @@
|
|
|
15
15
|
//
|
|
16
16
|
// No regex.
|
|
17
17
|
// ---------------------------------------------------------------------------
|
|
18
|
-
|
|
19
|
-
/**
|
|
20
|
-
* @typedef {{ path: string, load: Function }} RouteEntry
|
|
21
|
-
* @typedef {{ route: RouteEntry, params: Record<string, string> }} MatchResult
|
|
22
|
-
*/
|
|
23
|
-
|
|
24
18
|
/**
|
|
25
19
|
* Match a pathname against a single route definition.
|
|
26
|
-
*
|
|
27
|
-
* @param {string} routePath - The route pattern (e.g. '/users/:id')
|
|
28
|
-
* @param {string} pathname - The actual URL path (e.g. '/users/42')
|
|
29
|
-
* @returns {{ matched: boolean, params: Record<string, string> }}
|
|
30
20
|
*/
|
|
31
21
|
export function matchPath(routePath, pathname) {
|
|
32
|
-
const routeSegments =
|
|
33
|
-
const pathSegments =
|
|
34
|
-
|
|
22
|
+
const routeSegments = splitPath(routePath);
|
|
23
|
+
const pathSegments = splitPath(pathname);
|
|
35
24
|
const params = {};
|
|
36
25
|
let routeIndex = 0;
|
|
37
26
|
let pathIndex = 0;
|
|
38
|
-
|
|
39
27
|
while (routeIndex < routeSegments.length) {
|
|
40
|
-
const routeSeg = routeSegments[routeIndex];
|
|
28
|
+
const routeSeg = routeSegments[routeIndex] || '';
|
|
41
29
|
if (routeSeg.startsWith('*')) {
|
|
42
|
-
// Catch-all must be terminal.
|
|
43
30
|
const optionalCatchAll = routeSeg.endsWith('?');
|
|
44
|
-
const paramName = optionalCatchAll
|
|
45
|
-
? routeSeg.slice(1, -1)
|
|
46
|
-
: routeSeg.slice(1);
|
|
31
|
+
const paramName = optionalCatchAll ? routeSeg.slice(1, -1) : routeSeg.slice(1);
|
|
47
32
|
if (routeIndex !== routeSegments.length - 1) {
|
|
48
33
|
return { matched: false, params: {} };
|
|
49
34
|
}
|
|
@@ -52,140 +37,97 @@ export function matchPath(routePath, pathname) {
|
|
|
52
37
|
if (rest.length === 0 && !optionalCatchAll && !rootRequiredCatchAll) {
|
|
53
38
|
return { matched: false, params: {} };
|
|
54
39
|
}
|
|
55
|
-
params[paramName] =
|
|
40
|
+
params[paramName] = normalizeCatchAll(rest);
|
|
56
41
|
pathIndex = pathSegments.length;
|
|
57
42
|
routeIndex = routeSegments.length;
|
|
58
43
|
break;
|
|
59
44
|
}
|
|
60
|
-
|
|
61
45
|
if (pathIndex >= pathSegments.length) {
|
|
62
46
|
return { matched: false, params: {} };
|
|
63
47
|
}
|
|
64
|
-
|
|
65
|
-
const pathSeg = pathSegments[pathIndex];
|
|
48
|
+
const pathSeg = pathSegments[pathIndex] || '';
|
|
66
49
|
if (routeSeg.startsWith(':')) {
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
} else if (routeSeg !== pathSeg) {
|
|
71
|
-
// Literal mismatch
|
|
50
|
+
params[routeSeg.slice(1)] = pathSeg;
|
|
51
|
+
}
|
|
52
|
+
else if (routeSeg !== pathSeg) {
|
|
72
53
|
return { matched: false, params: {} };
|
|
73
54
|
}
|
|
74
|
-
|
|
75
55
|
routeIndex += 1;
|
|
76
56
|
pathIndex += 1;
|
|
77
57
|
}
|
|
78
|
-
|
|
79
58
|
if (routeIndex !== routeSegments.length || pathIndex !== pathSegments.length) {
|
|
80
59
|
return { matched: false, params: {} };
|
|
81
60
|
}
|
|
82
|
-
|
|
83
61
|
return { matched: true, params };
|
|
84
62
|
}
|
|
85
|
-
|
|
86
63
|
/**
|
|
87
64
|
* Match a pathname against an ordered array of route definitions.
|
|
88
65
|
* Returns the first match (deterministic, first-match-wins).
|
|
89
|
-
*
|
|
90
|
-
* @param {RouteEntry[]} routes - Ordered route manifest
|
|
91
|
-
* @param {string} pathname - The URL path to match
|
|
92
|
-
* @returns {MatchResult | null}
|
|
93
66
|
*/
|
|
94
67
|
export function matchRoute(routes, pathname) {
|
|
95
|
-
const ordered = [...routes].sort((a, b) =>
|
|
96
|
-
for (
|
|
97
|
-
const route = ordered[i];
|
|
68
|
+
const ordered = [...routes].sort((a, b) => compareRouteSpecificity(a.path, b.path));
|
|
69
|
+
for (const route of ordered) {
|
|
98
70
|
const result = matchPath(route.path, pathname);
|
|
99
|
-
|
|
100
71
|
if (result.matched) {
|
|
101
72
|
return { route, params: result.params };
|
|
102
73
|
}
|
|
103
74
|
}
|
|
104
|
-
|
|
105
75
|
return null;
|
|
106
76
|
}
|
|
107
|
-
|
|
108
|
-
/**
|
|
109
|
-
* Split a path string into non-empty segments.
|
|
110
|
-
*
|
|
111
|
-
* @param {string} path
|
|
112
|
-
* @returns {string[]}
|
|
113
|
-
*/
|
|
114
|
-
function _splitPath(path) {
|
|
77
|
+
function splitPath(path) {
|
|
115
78
|
return path.split('/').filter(Boolean);
|
|
116
79
|
}
|
|
117
|
-
|
|
118
|
-
/**
|
|
119
|
-
* Catch-all params are normalized as slash-joined, non-empty path segments.
|
|
120
|
-
* Segments keep raw URL-encoded bytes (no decodeURIComponent).
|
|
121
|
-
*
|
|
122
|
-
* @param {string[]} segments
|
|
123
|
-
* @returns {string}
|
|
124
|
-
*/
|
|
125
|
-
function _normalizeCatchAll(segments) {
|
|
80
|
+
function normalizeCatchAll(segments) {
|
|
126
81
|
return segments.filter(Boolean).join('/');
|
|
127
82
|
}
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
const aSegs = _splitPath(a);
|
|
139
|
-
const bSegs = _splitPath(b);
|
|
140
|
-
const aClass = _routeClass(aSegs);
|
|
141
|
-
const bClass = _routeClass(bSegs);
|
|
83
|
+
function compareRouteSpecificity(a, b) {
|
|
84
|
+
if (a === '/' && b !== '/')
|
|
85
|
+
return -1;
|
|
86
|
+
if (b === '/' && a !== '/')
|
|
87
|
+
return 1;
|
|
88
|
+
const aSegs = splitPath(a);
|
|
89
|
+
const bSegs = splitPath(b);
|
|
90
|
+
const aClass = routeClass(aSegs);
|
|
91
|
+
const bClass = routeClass(bSegs);
|
|
142
92
|
if (aClass !== bClass) {
|
|
143
93
|
return bClass - aClass;
|
|
144
94
|
}
|
|
145
|
-
|
|
146
95
|
const max = Math.min(aSegs.length, bSegs.length);
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
const
|
|
150
|
-
const bWeight = _segmentWeight(bSegs[i]);
|
|
96
|
+
for (let index = 0; index < max; index += 1) {
|
|
97
|
+
const aWeight = segmentWeight(aSegs[index]);
|
|
98
|
+
const bWeight = segmentWeight(bSegs[index]);
|
|
151
99
|
if (aWeight !== bWeight) {
|
|
152
100
|
return bWeight - aWeight;
|
|
153
101
|
}
|
|
154
102
|
}
|
|
155
|
-
|
|
156
103
|
if (aSegs.length !== bSegs.length) {
|
|
157
104
|
return bSegs.length - aSegs.length;
|
|
158
105
|
}
|
|
159
|
-
|
|
160
106
|
return a.localeCompare(b);
|
|
161
107
|
}
|
|
162
|
-
|
|
163
|
-
/**
|
|
164
|
-
* @param {string[]} segments
|
|
165
|
-
* @returns {number}
|
|
166
|
-
*/
|
|
167
|
-
function _routeClass(segments) {
|
|
108
|
+
function routeClass(segments) {
|
|
168
109
|
let hasParam = false;
|
|
169
110
|
let hasCatchAll = false;
|
|
170
111
|
for (const segment of segments) {
|
|
171
112
|
if (segment.startsWith('*')) {
|
|
172
113
|
hasCatchAll = true;
|
|
173
|
-
}
|
|
114
|
+
}
|
|
115
|
+
else if (segment.startsWith(':')) {
|
|
174
116
|
hasParam = true;
|
|
175
117
|
}
|
|
176
118
|
}
|
|
177
|
-
if (!hasParam && !hasCatchAll)
|
|
178
|
-
|
|
119
|
+
if (!hasParam && !hasCatchAll)
|
|
120
|
+
return 3;
|
|
121
|
+
if (hasCatchAll)
|
|
122
|
+
return 1;
|
|
179
123
|
return 2;
|
|
180
124
|
}
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
*
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
if (segment.startsWith('*')) return 1;
|
|
189
|
-
if (segment.startsWith(':')) return 2;
|
|
125
|
+
function segmentWeight(segment) {
|
|
126
|
+
if (!segment)
|
|
127
|
+
return 0;
|
|
128
|
+
if (segment.startsWith('*'))
|
|
129
|
+
return 1;
|
|
130
|
+
if (segment.startsWith(':'))
|
|
131
|
+
return 2;
|
|
190
132
|
return 3;
|
|
191
133
|
}
|
|
@@ -0,0 +1,5 @@
|
|
|
1
|
+
export declare function _setNavigationResolver(resolver: ((path: string) => Promise<void>) | null): void;
|
|
2
|
+
export declare function navigate(path: string): Promise<void>;
|
|
3
|
+
export declare function back(): void;
|
|
4
|
+
export declare function forward(): void;
|
|
5
|
+
export declare function getCurrentPath(): string;
|
package/dist/navigate.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
// ---------------------------------------------------------------------------
|
|
2
|
-
// navigate.
|
|
2
|
+
// navigate.ts — Zenith Router V0
|
|
3
3
|
// ---------------------------------------------------------------------------
|
|
4
4
|
// Navigation API.
|
|
5
5
|
//
|
|
@@ -11,57 +11,23 @@
|
|
|
11
11
|
// The navigate function accepts a resolver callback so the router
|
|
12
12
|
// can wire match → mount logic without circular dependencies.
|
|
13
13
|
// ---------------------------------------------------------------------------
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
import { _dispatchRouteChange } from './events.js';
|
|
17
|
-
|
|
18
|
-
/** @type {((path: string) => Promise<void>) | null} */
|
|
19
|
-
let _resolveNavigation = null;
|
|
20
|
-
|
|
21
|
-
/**
|
|
22
|
-
* Wire the navigation resolver.
|
|
23
|
-
* Called once by createRouter() to bind match → mount logic.
|
|
24
|
-
*
|
|
25
|
-
* @param {(path: string) => Promise<void>} resolver
|
|
26
|
-
*/
|
|
14
|
+
import { current, push } from './history.js';
|
|
15
|
+
let resolveNavigation = null;
|
|
27
16
|
export function _setNavigationResolver(resolver) {
|
|
28
|
-
|
|
17
|
+
resolveNavigation = resolver;
|
|
29
18
|
}
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Navigate to a path.
|
|
33
|
-
* Pushes history, then resolves through the router pipeline.
|
|
34
|
-
*
|
|
35
|
-
* @param {string} path
|
|
36
|
-
* @returns {Promise<void>}
|
|
37
|
-
*/
|
|
38
19
|
export async function navigate(path) {
|
|
39
20
|
push(path);
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
await _resolveNavigation(path);
|
|
21
|
+
if (resolveNavigation) {
|
|
22
|
+
await resolveNavigation(path);
|
|
43
23
|
}
|
|
44
24
|
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
* Go back in history.
|
|
48
|
-
*/
|
|
49
25
|
export function back() {
|
|
50
26
|
history.back();
|
|
51
27
|
}
|
|
52
|
-
|
|
53
|
-
/**
|
|
54
|
-
* Go forward in history.
|
|
55
|
-
*/
|
|
56
28
|
export function forward() {
|
|
57
29
|
history.forward();
|
|
58
30
|
}
|
|
59
|
-
|
|
60
|
-
/**
|
|
61
|
-
* Get the current pathname.
|
|
62
|
-
*
|
|
63
|
-
* @returns {string}
|
|
64
|
-
*/
|
|
65
31
|
export function getCurrentPath() {
|
|
66
32
|
return current();
|
|
67
33
|
}
|
package/dist/router.d.ts
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
type RouteParams = Record<string, string>;
|
|
2
|
+
type RouteLoader = (params: RouteParams) => unknown | Promise<unknown>;
|
|
3
|
+
type RouteDefinition = {
|
|
4
|
+
path: string;
|
|
5
|
+
load: RouteLoader;
|
|
6
|
+
};
|
|
7
|
+
type RouterConfig = {
|
|
8
|
+
routes: RouteDefinition[];
|
|
9
|
+
container: HTMLElement;
|
|
10
|
+
mount?: unknown;
|
|
11
|
+
cleanup?: unknown;
|
|
12
|
+
};
|
|
13
|
+
type RouterInstance = {
|
|
14
|
+
start: () => Promise<void>;
|
|
15
|
+
destroy: () => void;
|
|
16
|
+
};
|
|
17
|
+
export declare function createRouter(config: RouterConfig): RouterInstance;
|
|
18
|
+
export {};
|