@zenithbuild/router 0.6.6 → 0.6.9

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.
@@ -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.js — Zenith Router V0
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
- _subscribers.add(callback);
22
-
12
+ subscribers.add(callback);
23
13
  return () => {
24
- _subscribers.delete(callback);
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 cb of _subscribers) {
35
- cb(detail);
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
- _subscribers.clear();
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 _subscribers.size;
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 ? 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 listeners = ensureRouteProtectionState().listeners;
118
- if (listeners[eventName] instanceof Set) {
119
- listeners[eventName].add(handler);
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 listeners = ensureRouteProtectionState().listeners;
130
- if (listeners[eventName] instanceof Set) {
131
- listeners[eventName].delete(handler);
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 listeners = ensureRouteProtectionState().listeners[eventName];
137
- if (!(listeners instanceof Set)) {
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
- } catch (e) {
145
- console.error(`[Zenith Router] Error in ${eventName} listener:`, e);
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) return;
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
  *
@@ -0,0 +1,4 @@
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';
@@ -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.js — Zenith Router V0
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 = _splitPath(routePath);
33
- const pathSegments = _splitPath(pathname);
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] = _normalizeCatchAll(rest);
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
- // Dynamic param — extract value as string
68
- const paramName = routeSeg.slice(1);
69
- params[paramName] = pathSeg;
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) => _compareRouteSpecificity(a.path, b.path));
96
- for (let i = 0; i < ordered.length; i++) {
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
- * @param {string} a
131
- * @param {string} b
132
- * @returns {number}
133
- */
134
- function _compareRouteSpecificity(a, b) {
135
- if (a === '/' && b !== '/') return -1;
136
- if (b === '/' && a !== '/') return 1;
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
- for (let i = 0; i < max; i++) {
149
- const aWeight = _segmentWeight(aSegs[i]);
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
- } else if (segment.startsWith(':')) {
114
+ }
115
+ else if (segment.startsWith(':')) {
174
116
  hasParam = true;
175
117
  }
176
118
  }
177
- if (!hasParam && !hasCatchAll) return 3;
178
- if (hasCatchAll) return 1;
119
+ if (!hasParam && !hasCatchAll)
120
+ return 3;
121
+ if (hasCatchAll)
122
+ return 1;
179
123
  return 2;
180
124
  }
181
-
182
- /**
183
- * @param {string | undefined} segment
184
- * @returns {number}
185
- */
186
- function _segmentWeight(segment) {
187
- if (!segment) return 0;
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.js — Zenith Router V0
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
- import { push, current } from './history.js';
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
- _resolveNavigation = resolver;
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
- if (_resolveNavigation) {
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
  }
@@ -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 {};
package/dist/router.js CHANGED
@@ -1,5 +1,5 @@
1
1
  // ---------------------------------------------------------------------------
2
- // router.js — Zenith Router V0
2
+ // router.ts — Zenith Router V0
3
3
  // ---------------------------------------------------------------------------
4
4
  // Router assembly: wires match engine + history + runtime mount/unmount.
5
5
  //
@@ -11,101 +11,60 @@
11
11
  // - Mount/unmount delegated to @zenithbuild/runtime
12
12
  // - Deterministic first-match-wins
13
13
  // ---------------------------------------------------------------------------
14
-
15
- import { matchRoute } from './match.js';
16
- import { listen, current } from './history.js';
17
14
  import { _dispatchRouteChange } from './events.js';
15
+ import { current, listen } from './history.js';
16
+ import { matchRoute } from './match.js';
18
17
  import { _setNavigationResolver } from './navigate.js';
19
-
20
- /**
21
- * Create a router instance.
22
- *
23
- * @param {{ routes: Array<{ path: string, load: Function }>, container: HTMLElement, mount?: Function, cleanup?: Function }} config
24
- * @returns {{ start: () => Promise<void>, destroy: () => void }}
25
- */
26
18
  export function createRouter(config) {
27
19
  const { routes, container } = config;
28
-
29
- // Allow injecting mount/cleanup for testing without importing runtime
30
- const mountFn = config.mount || null;
31
- const cleanupFn = config.cleanup || null;
32
-
20
+ const mountFn = typeof config.mount === 'function' ? config.mount : null;
21
+ const cleanupFn = typeof config.cleanup === 'function' ? config.cleanup : null;
33
22
  if (!container || !(container instanceof HTMLElement)) {
34
23
  throw new Error('[Zenith Router] createRouter() requires an HTMLElement container');
35
24
  }
36
-
37
25
  if (!Array.isArray(routes) || routes.length === 0) {
38
26
  throw new Error('[Zenith Router] createRouter() requires a non-empty routes array');
39
27
  }
40
-
41
- let _unlisten = null;
42
- let _started = false;
43
- let _hasMounted = false;
44
-
45
- /**
46
- * Resolve a path: match → load → mount.
47
- *
48
- * @param {string} path
49
- */
50
- async function _resolve(path) {
28
+ let unlisten = null;
29
+ let started = false;
30
+ let hasMounted = false;
31
+ async function resolvePath(path) {
51
32
  const result = matchRoute(routes, path);
52
-
53
33
  if (!result) {
54
34
  _dispatchRouteChange({ path, matched: false });
55
35
  return;
56
36
  }
57
-
58
- // Teardown previous page (only if we've mounted before)
59
- if (_hasMounted && cleanupFn) {
37
+ if (hasMounted && cleanupFn) {
60
38
  cleanupFn();
61
39
  }
62
-
63
- // Load module
64
40
  const pageModule = await result.route.load(result.params);
65
-
66
- // Mount new page
67
41
  if (mountFn) {
68
42
  mountFn(container, pageModule);
69
- _hasMounted = true;
43
+ hasMounted = true;
70
44
  }
71
-
72
45
  _dispatchRouteChange({
73
46
  path,
74
47
  params: result.params,
75
48
  matched: true
76
49
  });
77
50
  }
78
-
79
- /**
80
- * Start the router — match initial route and begin listening.
81
- */
82
51
  async function start() {
83
- if (_started) return;
84
- _started = true;
85
-
86
- // Wire navigate() to use our resolver
87
- _setNavigationResolver(_resolve);
88
-
89
- // Listen for popstate (back/forward)
90
- _unlisten = listen((path) => {
91
- _resolve(path);
52
+ if (started)
53
+ return;
54
+ started = true;
55
+ _setNavigationResolver(resolvePath);
56
+ unlisten = listen((path) => {
57
+ void resolvePath(path);
92
58
  });
93
-
94
- // Initial route
95
- await _resolve(current());
59
+ await resolvePath(current());
96
60
  }
97
-
98
- /**
99
- * Tear down the router — remove listeners, clear state.
100
- */
101
61
  function destroy() {
102
- if (_unlisten) {
103
- _unlisten();
104
- _unlisten = null;
62
+ if (unlisten) {
63
+ unlisten();
64
+ unlisten = null;
105
65
  }
106
66
  _setNavigationResolver(null);
107
- _started = false;
67
+ started = false;
108
68
  }
109
-
110
69
  return { start, destroy };
111
70
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@zenithbuild/router",
3
- "version": "0.6.6",
3
+ "version": "0.6.9",
4
4
  "description": "File-based SPA router for Zenith framework with deterministic, compile-time route resolution",
5
5
  "license": "MIT",
6
6
  "type": "module",
@@ -40,8 +40,8 @@
40
40
  "scripts": {
41
41
  "format": "prettier --write \"**/*.ts\"",
42
42
  "format:check": "prettier --check \"**/*.ts\"",
43
- "typecheck": "tsc --noEmit",
44
- "build": "mkdir -p dist && cp -a src/* dist/ && cp src/ZenLink.zen dist/ZenLink.zen",
43
+ "typecheck": "tsc -p tsconfig.json --noEmit",
44
+ "build": "rm -rf dist && tsc -p tsconfig.build.json && cp src/ZenLink.zen dist/ZenLink.zen",
45
45
  "release": "bun run scripts/release.ts",
46
46
  "release:dry": "bun run scripts/release.ts --dry-run",
47
47
  "release:patch": "bun run scripts/release.ts --bump=patch",
@@ -62,6 +62,7 @@
62
62
  "@napi-rs/cli": "^2.18.4",
63
63
  "@types/bun": "latest",
64
64
  "@types/node": "latest",
65
- "prettier": "^3.7.4"
65
+ "prettier": "^3.7.4",
66
+ "typescript": "^5.7.3"
66
67
  }
67
68
  }