@ripetchor/r-router 1.0.1 → 1.0.2

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -28,7 +28,7 @@ const routes = [
28
28
  },
29
29
  {
30
30
  path: '/about',
31
- loadComponent: async () => {
31
+ lazy: async () => {
32
32
  const { default: About } = await import('./About');
33
33
  return About();
34
34
  },
@@ -111,7 +111,7 @@ Navigates, replacing history entry.
111
111
  ### Route Types
112
112
 
113
113
  - `SyncRoute`: Returns `HTMLElement` via `component(ctx: RouteContext)`.
114
- - `AsyncRoute`: Returns `Promise<HTMLElement>` via `loadComponent(ctx: RouteContext)`.
114
+ - `AsyncRoute`: Returns `Promise<HTMLElement>` via `lazy(ctx: RouteContext)`.
115
115
  - `RedirectRoute`: Redirects via `redirectTo`.
116
116
 
117
117
  #### `Route.beforeEnter`
package/dist/index.d.ts CHANGED
@@ -1,11 +1,5 @@
1
1
  export declare type AfterEnterHook = (ctx: RouteContext) => void | Promise<void>;
2
2
 
3
- declare interface AsyncRoute extends BaseRoute {
4
- redirectTo?: never;
5
- component?(): never;
6
- loadComponent(ctx: RouteContext): Promise<HTMLElement>;
7
- }
8
-
9
3
  declare interface BaseRoute {
10
4
  path: string;
11
5
  title?: string | ((ctx: RouteContext) => string);
@@ -18,36 +12,107 @@ export declare type BeforeEnterHook = (ctx: RouteContext) => boolean | string |
18
12
 
19
13
  export declare function createRouter(routes: Route[], options?: RouterOptions): Router;
20
14
 
15
+ declare interface LazyRoute extends BaseRoute {
16
+ redirectTo?: never;
17
+ component?(): never;
18
+ lazy(ctx: RouteContext): Promise<HTMLElement>;
19
+ }
20
+
21
21
  declare interface RedirectRoute extends BaseRoute {
22
22
  redirectTo: string;
23
23
  component?(): never;
24
- loadComponent?(): never;
24
+ lazy?(): never;
25
25
  }
26
26
 
27
- export declare type Route = SyncRoute | AsyncRoute | RedirectRoute;
27
+ export declare type Route = SyncRoute | LazyRoute | RedirectRoute;
28
28
 
29
29
  export declare interface RouteContext {
30
30
  from: string | null;
31
31
  to: string;
32
- params: Record<string, string>;
32
+ params: Record<string, string | undefined>;
33
33
  query: URLSearchParams;
34
34
  signal: AbortSignal;
35
35
  }
36
36
 
37
37
  export declare class Router {
38
+ static instance: Router | null;
39
+ private readonly dispatcher;
38
40
  private readonly routerController;
39
- private initialized;
40
41
  private navigationController;
42
+ private initialized;
41
43
  private readonly routeMatchers;
42
44
  private readonly options;
43
45
  private readonly outlet;
44
46
  private currentPath;
47
+ private pendingPath;
45
48
  constructor(routes: Route[], options?: RouterOptions);
49
+ on<K extends keyof RouterEventMap>(event: K, handler: RouterDispatcherHandler<RouterEventMap[K]>): () => void;
50
+ private emit;
46
51
  initialize(): Promise<Readonly<RouterOutlet>>;
52
+ private handlePopState;
47
53
  destroy(): void;
48
54
  private navigate;
55
+ private abortNavigation;
56
+ private cancelCurrentNavigation;
57
+ private finalizeNavigation;
49
58
  push(path: string, state?: unknown): Promise<void>;
50
59
  replace(path: string, state?: unknown): Promise<void>;
60
+ getCurrentPath(): string | null;
61
+ getPendingPath(): string | null;
62
+ }
63
+
64
+ declare type RouterDispatcherHandler<T> = (payload: T) => void;
65
+
66
+ declare type RouterEventMap = {
67
+ navigationstart: {
68
+ from: string | null;
69
+ to: string;
70
+ mode: "push" | "replace" | "none";
71
+ };
72
+ navigating: {
73
+ from: string | null;
74
+ to: string;
75
+ depth: number;
76
+ };
77
+ navigationend: {
78
+ from: string | null;
79
+ to: string;
80
+ successful: boolean;
81
+ };
82
+ pathchanged: {
83
+ from: string | null;
84
+ to: string;
85
+ };
86
+ querychanged: {
87
+ from: URLSearchParams;
88
+ to: URLSearchParams;
89
+ };
90
+ navigationabort: {
91
+ from: string | null;
92
+ to: string;
93
+ reason: string;
94
+ };
95
+ };
96
+
97
+ export declare class RouterLink extends HTMLAnchorElement {
98
+ private isActive;
99
+ private isPending;
100
+ private unsubscribeFns;
101
+ private props;
102
+ constructor(props: RouterLinkProps);
103
+ connectedCallback(): void;
104
+ disconnectedCallback(): void;
105
+ private applyClass;
106
+ }
107
+
108
+ declare interface RouterLinkProps {
109
+ to: string;
110
+ className?: string | ((params: {
111
+ isActive: boolean;
112
+ isPending: boolean;
113
+ }) => string);
114
+ textContent: string;
115
+ replace?: boolean;
51
116
  }
52
117
 
53
118
  declare interface RouterOptions {
@@ -68,7 +133,7 @@ declare type RouterOutletProps = {
68
133
  declare interface SyncRoute extends BaseRoute {
69
134
  redirectTo?: never;
70
135
  component(ctx: RouteContext): HTMLElement;
71
- loadComponent?(): Promise<HTMLElement>;
136
+ lazy?(): never;
72
137
  }
73
138
 
74
139
  export declare function withAbortSignal<T>(fn: () => Promise<T>, signal: AbortSignal): Promise<T>;
package/dist/index.js CHANGED
@@ -1,6 +1,6 @@
1
- function P(t, e) {
2
- const o = document.createElement("div");
3
- o.style.cssText = `
1
+ function z(e, t) {
2
+ const n = document.createElement("div");
3
+ n.style.cssText = `
4
4
  position: fixed;
5
5
  inset: 0;
6
6
  background-color: rgba(0, 0, 0, 0.5);
@@ -11,8 +11,8 @@ function P(t, e) {
11
11
  backdrop-filter: blur(4px);
12
12
  animation: fadeIn 0.25s ease;
13
13
  `;
14
- const n = document.createElement("div");
15
- n.style.cssText = `
14
+ const o = document.createElement("div");
15
+ o.style.cssText = `
16
16
  position: relative;
17
17
  max-width: 80dvh;
18
18
  width: 100%;
@@ -33,15 +33,15 @@ function P(t, e) {
33
33
  color: var(--accent);
34
34
  `;
35
35
  const i = document.createElement("h2");
36
- i.textContent = t?.message || "An unexpected error occurred.", i.style.cssText = `
36
+ i.textContent = e?.message || "An unexpected error occurred.", i.style.cssText = `
37
37
  font-size: 1.1rem;
38
38
  margin-bottom: 16px;
39
39
  opacity: 0.9;
40
40
  `;
41
- const a = document.createElement("div"), s = document.createElement("p"), c = document.createElement("p");
42
- s.textContent = e?.from ?? "", c.textContent = e?.to ?? "", a.append(s, c);
43
- const d = document.createElement("pre");
44
- d.textContent = t?.stack || "", d.style.cssText = `
41
+ const a = document.createElement("div"), f = document.createElement("p"), l = document.createElement("p");
42
+ f.textContent = t?.from ?? "", l.textContent = t?.to ?? "", a.append(f, l);
43
+ const c = document.createElement("pre");
44
+ c.textContent = e?.stack || "", c.style.cssText = `
45
45
  font-size: 0.9rem;
46
46
  max-height: 30vh;
47
47
  overflow: auto;
@@ -52,8 +52,8 @@ function P(t, e) {
52
52
  white-space: pre-wrap;
53
53
  margin-bottom: 24px;
54
54
  `;
55
- const f = document.createElement("button");
56
- f.textContent = "Reload Page", f.style.cssText = `
55
+ const s = document.createElement("button");
56
+ s.textContent = "Reload Page", s.style.cssText = `
57
57
  cursor: pointer;
58
58
  padding: 10px 20px;
59
59
  border: none;
@@ -64,8 +64,8 @@ function P(t, e) {
64
64
  color: var(--button-text);
65
65
  transition: background-color 0.25s;
66
66
  `;
67
- const l = document.createElement("button");
68
- l.innerHTML = "&#10005;", l.setAttribute("aria-label", "Close error dialog"), l.style.cssText = `
67
+ const u = document.createElement("button");
68
+ u.innerHTML = "&#10005;", u.setAttribute("aria-label", "Close error dialog"), u.style.cssText = `
69
69
  position: absolute;
70
70
  top: 12px;
71
71
  right: 12px;
@@ -76,391 +76,444 @@ function P(t, e) {
76
76
  cursor: pointer;
77
77
  opacity: 0.7;
78
78
  transition: opacity 0.25s;
79
- `, l.onmouseenter = () => {
80
- l.style.opacity = "1";
81
- }, l.onmouseleave = () => {
82
- l.style.opacity = "0.7";
83
- }, l.onclick = () => {
84
- o.remove();
85
- }, f.onmouseenter = () => {
86
- f.style.backgroundColor = "var(--button-bg-hover)";
87
- }, f.onmouseleave = () => {
88
- f.style.backgroundColor = "var(--button-bg)";
89
- }, f.onclick = () => {
79
+ `, u.onmouseenter = () => {
80
+ u.style.opacity = "1";
81
+ }, u.onmouseleave = () => {
82
+ u.style.opacity = "0.7";
83
+ }, u.onclick = () => {
84
+ n.remove();
85
+ }, s.onmouseenter = () => {
86
+ s.style.backgroundColor = "var(--button-bg-hover)";
87
+ }, s.onmouseleave = () => {
88
+ s.style.backgroundColor = "var(--button-bg)";
89
+ }, s.onclick = () => {
90
90
  location.reload();
91
- }, o.appendChild(n), n.append(l, r, i, a), t.stack && n.append(d), n.append(f);
92
- const g = (u) => {
93
- u.key === "Escape" && o.remove();
91
+ }, n.appendChild(o), o.append(u, r, i, a), e.stack && o.append(c), o.append(s);
92
+ const w = (h) => {
93
+ h.key === "Escape" && n.remove();
94
94
  };
95
- window.addEventListener("keydown", g);
96
- const w = new MutationObserver(() => {
97
- document.body.contains(o) || (window.removeEventListener("keydown", g), w.disconnect());
95
+ window.addEventListener("keydown", w);
96
+ const b = new MutationObserver(() => {
97
+ document.body.contains(n) || (window.removeEventListener("keydown", w), b.disconnect());
98
98
  });
99
- w.observe(document.body, { childList: !0 });
100
- const y = () => {
101
- const u = window.matchMedia("(prefers-color-scheme: dark)").matches;
102
- n.style.setProperty("--bg", u ? "#2b2b2b" : "#fff"), n.style.setProperty("--text", u ? "#eee" : "#222"), n.style.setProperty("--accent", u ? "#ff6b6b" : "#e74c3c"), n.style.setProperty(
99
+ b.observe(document.body, { childList: !0 });
100
+ const v = () => {
101
+ const h = window.matchMedia("(prefers-color-scheme: dark)").matches;
102
+ o.style.setProperty("--bg", h ? "#2b2b2b" : "#fff"), o.style.setProperty("--text", h ? "#eee" : "#222"), o.style.setProperty("--accent", h ? "#ff6b6b" : "#e74c3c"), o.style.setProperty(
103
103
  "--code-bg",
104
- u ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.05)"
105
- ), n.style.setProperty("--button-bg", u ? "#3498db55" : "#3498db"), n.style.setProperty(
104
+ h ? "rgba(255,255,255,0.05)" : "rgba(0,0,0,0.05)"
105
+ ), o.style.setProperty("--button-bg", h ? "#3498db55" : "#3498db"), o.style.setProperty(
106
106
  "--button-bg-hover",
107
- u ? "#3498db88" : "#2980b9"
108
- ), n.style.setProperty("--button-text", "#fff");
107
+ h ? "#3498db88" : "#2980b9"
108
+ ), o.style.setProperty("--button-text", "#fff");
109
109
  };
110
- window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", y), y();
111
- const b = document.createElement("style");
112
- return b.textContent = `
110
+ window.matchMedia("(prefers-color-scheme: dark)").addEventListener("change", v), v();
111
+ const E = document.createElement("style");
112
+ return E.textContent = `
113
113
  @keyframes fadeIn { from { opacity: 0; } to { opacity: 1; } }
114
114
  @keyframes scaleIn { from { transform: scale(0.95); opacity: 0; } to { transform: scale(1); opacity: 1; } }
115
- `, document.head.appendChild(b), o;
115
+ `, document.head.appendChild(E), n;
116
116
  }
117
- async function p(t, e) {
118
- if (e.aborted)
119
- throw new DOMException("Aborted", "AbortError");
120
- return new Promise((o, n) => {
121
- const r = () => n(new DOMException("Aborted", "AbortError"));
122
- e.addEventListener("abort", r), t().then(o).catch(n).finally(() => {
123
- e.removeEventListener("abort", r);
124
- });
125
- });
117
+ function g(e, t) {
118
+ const { promise: n, resolve: o, reject: r } = Promise.withResolvers(), i = new DOMException("Aborted", "AbortError");
119
+ if (t.aborted)
120
+ return r(i), n;
121
+ function a() {
122
+ r(i);
123
+ }
124
+ return t.addEventListener("abort", a, { once: !0 }), e().then(o, r), n;
126
125
  }
127
- function k(t) {
128
- const e = typeof t == "string" ? t : "Unknown error";
129
- return t instanceof Error ? t : new Error(e);
126
+ function R(e) {
127
+ const t = typeof e == "string" ? e : "Unknown error";
128
+ return e instanceof Error ? e : new Error(t);
130
129
  }
131
- function T(t, e, o) {
132
- t.title ? document.title = typeof t.title == "function" ? t.title(e) : t.title : document.title = o ?? "";
130
+ function S(e, t, n) {
131
+ e.title ? document.title = typeof e.title == "function" ? e.title(t) : e.title : document.title = n ?? "";
133
132
  }
134
- function m(t, e, o) {
135
- const n = e ?? null;
136
- switch (o) {
133
+ function y(e, t, n) {
134
+ const o = t ?? null;
135
+ switch (n) {
137
136
  case "push":
138
- window.history.pushState(n, "", t);
137
+ window.history.pushState(o, "", e);
139
138
  break;
140
139
  case "replace":
141
- window.history.replaceState(n, "", t);
140
+ window.history.replaceState(o, "", e);
142
141
  break;
143
142
  }
144
143
  }
145
- function L(t, e) {
146
- t.replaceChildren(e);
144
+ function T(e, t) {
145
+ e.replaceChildren(t);
147
146
  }
148
- function h(t, e, o, n, r) {
149
- const i = k(e), a = o?.errorComponent?.(i, n) ?? r?.(i) ?? P(i, n);
150
- t.replaceChildren(a);
147
+ function m(e, t, n, o, r) {
148
+ const i = R(t), a = n?.errorComponent?.(i, o) ?? r?.(i) ?? z(i, o);
149
+ e.replaceChildren(a);
151
150
  }
152
- const A = "*";
153
- function z({ path: t }) {
154
- return t.includes(":");
151
+ function L(e) {
152
+ const [, t] = e.split("?");
153
+ return new URLSearchParams(t || "");
155
154
  }
156
- function x(t) {
157
- const [, e] = t.split("?");
158
- return new URLSearchParams(e || "");
159
- }
160
- function S({ path: t }) {
161
- const e = t.replace(
162
- /:([^/]+)/g,
163
- (o, n) => `(?<${n}>[^/]+)`
164
- );
165
- return new RegExp(`^${e}$`);
166
- }
167
- function $(t) {
168
- const e = S(t);
169
- return {
170
- route: t,
171
- redirectTo: t.redirectTo || null,
172
- match(o) {
173
- const n = o.split("?")[0];
174
- return e.test(n);
175
- },
176
- getParams(o) {
177
- return e.exec(o)?.groups ?? {};
178
- },
179
- getQuery: x
180
- };
181
- }
182
- function M({ path: t }) {
183
- return t === A ? new RegExp("^.*$") : new RegExp(`^${t}$`);
184
- }
185
- function W(t) {
186
- const e = M(t);
155
+ function A(e) {
156
+ const t = new URLPattern({ pathname: e.path });
187
157
  return {
188
- route: t,
189
- redirectTo: t.redirectTo || null,
190
- match(o) {
191
- const n = o.split("?")[0];
192
- return e.test(n);
158
+ route: e,
159
+ redirectTo: e.redirectTo || null,
160
+ match(n) {
161
+ const o = n.split("?")[0];
162
+ return t.test({ pathname: o });
193
163
  },
194
- getParams() {
195
- return {};
164
+ getParams(n) {
165
+ const o = n.split("?")[0];
166
+ return t.exec({ pathname: o })?.pathname.groups ?? {};
196
167
  },
197
- getQuery: x
168
+ getQuery: L
198
169
  };
199
170
  }
200
- function U(t) {
201
- return z(t) ? $(t) : W(t);
171
+ class d {
172
+ static instance;
173
+ static getInstance() {
174
+ return d.instance || (d.instance = new d()), d.instance;
175
+ }
176
+ listeners = /* @__PURE__ */ new Map();
177
+ on(t, n) {
178
+ let o = this.listeners.get(t);
179
+ return o || (o = /* @__PURE__ */ new Set(), this.listeners.set(t, o)), o.add(n), () => o.delete(n);
180
+ }
181
+ emit(t, n) {
182
+ this.listeners.get(t)?.forEach((o) => o(n));
183
+ }
184
+ clear() {
185
+ this.listeners.clear();
186
+ }
202
187
  }
203
- function B(t, e) {
204
- for (let o = 0; o < e.length; o++)
205
- if (e[o].match(t))
206
- return e[o];
188
+ function N(e, t) {
189
+ for (let n = 0; n < t.length; n++)
190
+ if (t[n].match(e))
191
+ return t[n];
207
192
  return null;
208
193
  }
209
- async function H(t, e) {
210
- return typeof t.component == "function" ? t.component(e) : typeof t.loadComponent == "function" ? await t.loadComponent(e) : new Error("Failed to get component for route: " + t.path);
194
+ async function M(e, t) {
195
+ return typeof e.component == "function" ? e.component(t) : e.lazy && typeof e.lazy == "function" ? await e.lazy(t) : new Error("Failed to get component for route: " + e.path);
211
196
  }
212
- async function I(t, e) {
213
- const o = t.afterEnter;
214
- if (!(!o || o.length === 0))
215
- for (const n of o)
216
- await n(e);
197
+ async function $(e, t) {
198
+ const n = e.afterEnter;
199
+ if (!(!n || n.length === 0))
200
+ for (const o of n)
201
+ await o(t);
217
202
  }
218
- async function O(t, e) {
219
- const o = t.beforeEnter;
220
- if (!o || o.length === 0)
203
+ async function U(e, t) {
204
+ const n = e.beforeEnter;
205
+ if (!n || n.length === 0)
221
206
  return null;
222
- for (const n of o) {
223
- const r = await n(e);
207
+ for (const o of n) {
208
+ if (t.signal.aborted)
209
+ return !1;
210
+ const r = await o(t);
224
211
  if (r === !1 || typeof r == "string")
225
212
  return r;
226
213
  }
227
214
  return null;
228
215
  }
229
- function E(t) {
230
- return t instanceof DOMException && t.name === "AbortError";
216
+ function C(e) {
217
+ return e instanceof DOMException && e.name === "AbortError";
231
218
  }
232
- function v(t) {
233
- const e = t.split("/").filter(Boolean);
234
- if (e.length === 0)
219
+ function x(e) {
220
+ const t = e.split("/").filter(Boolean);
221
+ if (t.length === 0)
235
222
  return 0;
236
- let o = 0;
237
- for (let n = 0; n < e.length; n++) {
238
- const r = e[n];
239
- r === "*" ? o -= 100 : r.startsWith(":") ? o += 5 : o += 10;
223
+ let n = 0;
224
+ for (let o = 0; o < t.length; o++) {
225
+ const r = t[o];
226
+ r === "*" ? n -= 100 : r.startsWith(":") ? n += 5 : n += 10;
240
227
  }
241
- return o += e.length * 0.1, o;
228
+ return n += t.length * 0.1, n;
242
229
  }
243
- function D(t) {
244
- return t.toSorted((e, o) => v(o.path) - v(e.path));
230
+ function B(e) {
231
+ return e.toSorted((t, n) => x(n.path) - x(t.path));
245
232
  }
246
- function Q(t, e) {
247
- if (t === null)
233
+ function I(e, t) {
234
+ if (e === null)
248
235
  return !1;
249
- const o = new URL(t, location.origin), n = new URL(e, location.origin);
250
- return !(o.pathname !== n.pathname || o.search !== n.search);
236
+ const n = new URL(e, location.origin), o = new URL(t, location.origin);
237
+ return !(n.pathname !== o.pathname || n.search !== o.search);
251
238
  }
252
- class C extends HTMLElement {
253
- constructor(e) {
254
- super(), this.setAttribute("outlet-id", e.outletId);
239
+ class P extends HTMLElement {
240
+ constructor(t) {
241
+ super(), this.setAttribute("outlet-id", t.outletId);
255
242
  }
256
243
  }
257
- customElements.define("router-outlet", C);
258
- function R(t) {
259
- if (typeof t != "string")
260
- throw new Error(`Path must be a string, got ${typeof t}`);
261
- if (t.trim() === "")
244
+ customElements.define("router-outlet", P);
245
+ function k(e) {
246
+ if (typeof e != "string")
247
+ throw new Error(`Path must be a string, got ${typeof e}`);
248
+ if (e.trim() === "")
262
249
  throw new Error("Path cannot be an empty string");
263
- if (t !== "*" && !t.startsWith("/"))
264
- throw new Error(`Path must start with '/': "${t}"`);
265
- if (t.length > 1 && t.endsWith("/"))
266
- throw new Error(`Path must not end with '/': "${t}"`);
267
- if (t.includes("//"))
268
- throw new Error(
269
- `Path must not contain multiple consecutive slashes: "${t}"`
270
- );
271
- if (t === "*") return;
272
- const e = t.match(/[^A-Za-z0-9\-._~:/@]/g);
273
- if (e)
274
- throw new Error(
275
- `Path contains invalid characters: "${e.join("")}" in "${t}"`
276
- );
250
+ if (e !== "*" && !e.startsWith("/"))
251
+ throw new Error(`Path must start with '/': "${e}"`);
252
+ if (e.length > 1 && e.endsWith("/"))
253
+ throw new Error(`Path must not end with '/': "${e}"`);
254
+ if (e.includes("//"))
255
+ throw new Error(`Path must not contain multiple consecutive slashes: "${e}"`);
256
+ if (e === "*")
257
+ return;
258
+ const t = e.match(/[^A-Za-z0-9\-._~:/]/g);
259
+ if (t)
260
+ throw new Error(`Path contains invalid characters: "${t.join("")}" in "${e}"`);
277
261
  }
278
- function _(t) {
279
- const e = /* @__PURE__ */ new Set();
280
- for (let o = 0; o < t.length; o++) {
281
- const n = t[o].path;
282
- if (e.has(n))
283
- throw new Error(`Duplicate root route path: "${n}"`);
284
- e.add(n);
262
+ function q(e) {
263
+ const t = /* @__PURE__ */ new Set();
264
+ for (let n = 0; n < e.length; n++) {
265
+ const o = e[n].path;
266
+ if (t.has(o))
267
+ throw new Error(`Duplicate root route path: "${o}"`);
268
+ t.add(o);
285
269
  }
286
270
  }
287
- function q(t) {
288
- if (!(t.redirectTo === null || t.redirectTo == null) && typeof t.redirectTo == "string") {
289
- const e = t.redirectTo;
290
- if (R(e), e === t.path)
291
- throw new Error(`Route "${t.path}" cannot redirect to itself`);
292
- if (t.path === "*")
271
+ function W(e, t) {
272
+ const n = e.split("/"), o = t.split("/");
273
+ return n.length !== o.length ? !1 : n.every((r, i) => {
274
+ const a = o[i];
275
+ return r === a || r.startsWith(":") || a.startsWith(":");
276
+ });
277
+ }
278
+ function F(e) {
279
+ for (let t = 0; t < e.length; t++)
280
+ for (let n = t + 1; n < e.length; n++)
281
+ if (W(e[t].path, e[n].path))
282
+ throw new Error(`Conflicting routes: "${e[t].path}" and "${e[n].path}"`);
283
+ }
284
+ function H(e) {
285
+ if (!(e.redirectTo === null || e.redirectTo == null) && typeof e.redirectTo == "string") {
286
+ const t = e.redirectTo;
287
+ if (k(t), t === e.path)
288
+ throw new Error(`Route "${e.path}" cannot redirect to itself`);
289
+ if (e.path === "*")
293
290
  throw new Error('Catch-all route "*" cannot have a redirect');
294
291
  }
295
292
  }
296
- function F(t) {
297
- const e = typeof t.redirectTo == "string", o = typeof t.component == "function", n = typeof t.loadComponent == "function";
298
- if (e && (o || n))
299
- throw new Error(
300
- `Redirect route "${t.path}" must not define components`
301
- );
302
- if (o && n)
303
- throw new Error(
304
- `Route "${t.path}" cannot have both component and loadComponent`
305
- );
306
- if (!e && !o && !n)
307
- throw new Error(
308
- `Route "${t.path}" must define either "component" or "loadComponent"`
309
- );
293
+ function Q(e) {
294
+ const t = typeof e.redirectTo == "string", n = typeof e.component == "function", o = typeof e.lazy == "function";
295
+ if (t && (n || o))
296
+ throw new Error(`Redirect route "${e.path}" must not define components`);
297
+ if (n && o)
298
+ throw new Error(`Route "${e.path}" cannot have both "component" and "lazy"`);
299
+ if (!t && !n && !o)
300
+ throw new Error(`Route "${e.path}" must define either "component" or "lazy"`);
310
301
  }
311
- function j(t) {
312
- R(t.path), q(t), F(t);
302
+ function D(e) {
303
+ if (e.path === "*") return;
304
+ const t = e.path.match(/:([^/]+)/g);
305
+ if (!t) return;
306
+ const n = t.map((r) => r.slice(1));
307
+ if (new Set(n).size !== n.length)
308
+ throw new Error(`Duplicate parameter names in path "${e.path}"`);
313
309
  }
314
- function K(t) {
315
- _(t);
316
- for (let e = 0; e < t.length; e++) {
317
- const o = t[e];
318
- j(o);
319
- }
310
+ function O(e) {
311
+ k(e.path), H(e), Q(e), D(e);
320
312
  }
321
- class Z {
313
+ function j(e) {
314
+ q(e), F(e);
315
+ for (let t = 0; t < e.length; t++)
316
+ O(e[t]);
317
+ }
318
+ class p {
319
+ static instance = null;
320
+ dispatcher = d.getInstance();
322
321
  routerController = new AbortController();
323
- initialized = !1;
324
322
  navigationController = null;
325
- routeMatchers = [];
323
+ initialized = !1;
324
+ routeMatchers;
326
325
  options;
327
- outlet = new C({ outletId: "root" });
326
+ outlet = new P({ outletId: "root" });
328
327
  currentPath = null;
329
- constructor(e, o) {
330
- K(e), this.options = {
328
+ pendingPath = null;
329
+ constructor(t, n) {
330
+ j(t), this.options = {
331
331
  defaultTitle: document.title,
332
332
  sortRoutesBySpecificity: !0,
333
333
  ignoreSameLocation: !0,
334
- ...o
334
+ ...n
335
335
  };
336
- const n = this.options.sortRoutesBySpecificity ? D(e) : e;
337
- this.routeMatchers = n.map(U);
336
+ const o = this.options.sortRoutesBySpecificity ? B(t) : t;
337
+ this.routeMatchers = o.map(A);
338
+ }
339
+ on(t, n) {
340
+ return this.dispatcher.on(t, n);
341
+ }
342
+ emit(t, n) {
343
+ this.dispatcher.emit(t, n);
338
344
  }
339
345
  async initialize() {
340
346
  if (this.initialized)
341
347
  throw new Error("Router already initialized");
342
- const e = async (n) => {
343
- const r = window.location.pathname + window.location.search;
344
- await this.navigate(r, n.state, "none");
345
- };
346
- window.addEventListener(
348
+ p.instance = this, window.addEventListener(
347
349
  "popstate",
348
350
  (n) => {
349
- e(n).catch(console.error);
351
+ this.handlePopState(n).catch(console.error);
350
352
  },
351
- { signal: this.routerController.signal }
353
+ {
354
+ signal: this.routerController.signal
355
+ }
352
356
  );
353
- const o = window.location.pathname + window.location.search;
354
- return await this.navigate(o, null, "none"), this.initialized = !0, this.outlet;
357
+ const t = window.location.pathname + window.location.search;
358
+ return await this.navigate(t, null, "none"), this.initialized = !0, this.outlet;
359
+ }
360
+ async handlePopState(t) {
361
+ const n = window.location.pathname + window.location.search;
362
+ await this.navigate(n, t.state, "none");
355
363
  }
356
364
  destroy() {
357
365
  if (!this.initialized)
358
366
  throw new Error("Router not initialized");
359
- this.routerController.abort(), this.navigationController && (this.navigationController.abort(), this.navigationController = null), this.initialized = !1;
367
+ this.routerController.abort(), this.navigationController?.abort(), this.navigationController = null, this.dispatcher.clear(), this.initialized = !1, p.instance = null;
360
368
  }
361
- async navigate(e, o, n, r = 0) {
362
- if (r > 5) {
363
- h(
364
- this.outlet,
365
- new Error("Too many redirects"),
366
- null,
367
- null,
368
- this.options.globalErrorComponent
369
- );
370
- return;
371
- }
372
- if (this.options.ignoreSameLocation && Q(this.currentPath, e))
373
- return;
374
- this.navigationController && (this.navigationController.abort("navigation canceled"), this.navigationController = null), this.navigationController = new AbortController();
375
- const i = this.navigationController.signal, a = B(e, this.routeMatchers), s = {
376
- from: this.currentPath,
377
- to: e,
378
- params: a?.getParams(e) ?? {},
379
- query: a?.getQuery(e) ?? new URLSearchParams(),
369
+ async navigate(t, n, o, r = 0) {
370
+ if (r === 0 && (this.emit("navigationstart", { from: this.currentPath, to: t, mode: o }), this.pendingPath = t), this.emit("navigating", { from: this.currentPath, to: t, depth: r }), r > 5)
371
+ return this.abortNavigation(t, "too many redirects", new Error("Too many redirects"));
372
+ if (this.options.ignoreSameLocation && I(this.currentPath, t))
373
+ return this.pendingPath = null, this.abortNavigation(t, "same location");
374
+ this.cancelCurrentNavigation(), this.navigationController = new AbortController();
375
+ const i = this.navigationController.signal;
376
+ i.addEventListener(
377
+ "abort",
378
+ () => this.emit("navigationabort", {
379
+ from: this.currentPath,
380
+ to: t,
381
+ reason: String(i.reason ?? "aborted")
382
+ })
383
+ );
384
+ const a = N(t, this.routeMatchers), f = this.currentPath, l = {
385
+ from: f,
386
+ to: t,
387
+ params: a?.getParams(t) ?? {},
388
+ query: a?.getQuery(t) ?? new URLSearchParams(),
380
389
  signal: i
381
390
  };
382
391
  try {
383
- if (!a) {
384
- m(e, o, n), h(
392
+ if (!a)
393
+ return y(t, n, o), m(
385
394
  this.outlet,
386
395
  new Error('Route not found. Add "*" catch-all route'),
387
396
  null,
388
- s,
397
+ l,
389
398
  this.options.globalErrorComponent
390
- ), this.currentPath = e;
391
- return;
392
- }
393
- if (a.redirectTo) {
394
- await this.navigate(a.redirectTo, o, "replace", r + 1);
395
- return;
396
- }
399
+ ), this.finalizeNavigation(t, f, !1);
400
+ if (a.redirectTo)
401
+ return this.navigate(a.redirectTo, n, "replace", r + 1);
397
402
  if (i.aborted)
398
403
  return;
399
- const c = await p(
400
- () => O(a.route, s),
404
+ const c = await g(
405
+ () => U(a.route, l),
401
406
  i
402
407
  );
403
408
  if (c === !1)
404
409
  return;
405
- if (typeof c == "string") {
406
- await this.navigate(c, o, "replace", r + 1);
407
- return;
408
- }
410
+ if (typeof c == "string")
411
+ return this.navigate(c, n, "replace", r + 1);
409
412
  if (i.aborted)
410
413
  return;
411
- const d = await p(
412
- () => H(a.route, s),
414
+ const s = await g(
415
+ () => M(a.route, l),
413
416
  i
414
417
  );
415
- if (d instanceof Error) {
416
- if (E(d))
417
- return;
418
- m(e, o, n), h(
418
+ if (s instanceof Error)
419
+ return C(s) ? void 0 : (y(t, n, o), m(
419
420
  this.outlet,
420
- d,
421
- a.route,
422
421
  s,
422
+ a.route,
423
+ l,
423
424
  this.options.globalErrorComponent
424
- ), this.currentPath = e;
425
- return;
426
- }
425
+ ), this.finalizeNavigation(t, f, !1));
427
426
  if (i.aborted)
428
427
  return;
429
- L(this.outlet, d), m(e, o, n), T(a.route, s, this.options.defaultTitle), this.currentPath = e, await p(() => I(a.route, s), i);
428
+ T(this.outlet, s), y(t, n, o), S(a.route, l, this.options.defaultTitle), this.finalizeNavigation(t, f, !0), await g(() => $(a.route, l), i);
430
429
  } catch (c) {
431
- if (E(c))
430
+ if (C(c))
432
431
  return;
433
- h(
432
+ m(
434
433
  this.outlet,
435
434
  c instanceof Error ? c : new Error("Unknown error"),
436
435
  a?.route ?? null,
437
- s,
436
+ l,
438
437
  this.options.globalErrorComponent
439
- );
438
+ ), this.emit("navigationend", { from: this.currentPath, to: t, successful: !1 });
440
439
  } finally {
441
440
  this.navigationController?.signal === i && (this.navigationController = null);
442
441
  }
443
442
  }
444
- async push(e, o) {
445
- try {
446
- await this.navigate(e, o, "push");
447
- } catch (n) {
448
- console.error("[Router.push] Error:", n);
449
- }
443
+ abortNavigation(t, n, o) {
444
+ this.pendingPath = null, this.emit("navigationabort", { from: this.currentPath, to: t, reason: n }), o && m(this.outlet, o, null, null, this.options.globalErrorComponent);
450
445
  }
451
- async replace(e, o) {
452
- try {
453
- await this.navigate(e, o, "replace");
454
- } catch (n) {
455
- console.error("[Router.replace] Error:", n);
456
- }
446
+ cancelCurrentNavigation() {
447
+ this.navigationController && (this.navigationController.abort("navigation canceled"), this.navigationController = null);
448
+ }
449
+ finalizeNavigation(t, n, o) {
450
+ const r = n ? new URLSearchParams(n.split("?")[1] ?? "") : null, i = new URLSearchParams(t.split("?")[1] ?? "");
451
+ this.currentPath = t, this.pendingPath = null, this.emit("pathchanged", { from: n, to: t }), (!r || r?.toString() !== i.toString()) && this.emit("querychanged", { from: r ?? new URLSearchParams(), to: i }), this.emit("navigationend", { from: n, to: t, successful: o });
457
452
  }
453
+ async push(t, n) {
454
+ await this.navigate(t, n, "push");
455
+ }
456
+ async replace(t, n) {
457
+ await this.navigate(t, n, "replace");
458
+ }
459
+ getCurrentPath() {
460
+ return this.currentPath;
461
+ }
462
+ getPendingPath() {
463
+ return this.pendingPath;
464
+ }
465
+ }
466
+ function G(e, t) {
467
+ return new p(e, t);
458
468
  }
459
- function J(t, e) {
460
- return new Z(t, e);
469
+ class K extends HTMLAnchorElement {
470
+ isActive = !1;
471
+ isPending = !1;
472
+ unsubscribeFns = [];
473
+ props;
474
+ constructor(t) {
475
+ super(), this.props = t, this.href = t.to, this.textContent = t.textContent, this.addEventListener("click", (n) => {
476
+ n.preventDefault();
477
+ const o = p.instance;
478
+ t.replace ? o?.replace(t.to) : o?.push(t.to);
479
+ });
480
+ }
481
+ connectedCallback() {
482
+ const t = p.instance;
483
+ if (!t) return;
484
+ const n = () => {
485
+ const o = t.getCurrentPath() || "", r = this.props.to;
486
+ this.isActive = o.split("?")[0] === r.split("?")[0], this.isPending = t.getPendingPath() === r, this.applyClass();
487
+ };
488
+ n(), this.unsubscribeFns.push(
489
+ t.on("navigationstart", n),
490
+ t.on("navigating", n),
491
+ t.on("navigationend", n),
492
+ t.on("navigationabort", n),
493
+ t.on("pathchanged", n),
494
+ t.on("querychanged", n)
495
+ );
496
+ }
497
+ disconnectedCallback() {
498
+ this.unsubscribeFns.forEach((t) => t()), this.unsubscribeFns = [];
499
+ }
500
+ applyClass() {
501
+ if (this.props.className) {
502
+ if (typeof this.props.className == "string") {
503
+ this.className = this.props.className;
504
+ return;
505
+ }
506
+ this.className = this.props.className({
507
+ isActive: this.isActive,
508
+ isPending: this.isPending
509
+ });
510
+ }
511
+ }
461
512
  }
513
+ customElements.define("router-link", K, { extends: "a" });
462
514
  export {
463
- Z as Router,
464
- J as createRouter,
465
- p as withAbortSignal
515
+ p as Router,
516
+ K as RouterLink,
517
+ G as createRouter,
518
+ g as withAbortSignal
466
519
  };
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@ripetchor/r-router",
3
- "version": "1.0.1",
3
+ "version": "1.0.2",
4
4
  "type": "module",
5
5
  "license": "MIT",
6
6
  "files": [
@@ -24,6 +24,7 @@
24
24
  },
25
25
  "devDependencies": {
26
26
  "@eslint/js": "^9.37.0",
27
+ "@types/node": "^25.0.3",
27
28
  "eslint": "^9.37.0",
28
29
  "typescript": "~5.9.3",
29
30
  "typescript-eslint": "^8.45.0",