@ripetchor/r-router 1.0.0 → 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
@@ -1 +1,168 @@
1
- # r-router
1
+ # R-Router
2
+
3
+ HistoryAPI router.
4
+
5
+ **WARNING**: This router is primarily intended for personal use. It may contain bugs or unexpected behavior. Use at your own risk.
6
+
7
+ ## Usage
8
+
9
+ ```typescript
10
+ import { createRouter, RouterOutlet, RouteContext } from '@ripetchor/r-router';
11
+
12
+ const routes = [
13
+ {
14
+ path: '/',
15
+ component: () => {
16
+ const div = document.createElement('div');
17
+ div.textContent = 'Home';
18
+ return div;
19
+ },
20
+ },
21
+ {
22
+ path: '/user/:id',
23
+ component: (ctx: RouteContext) => {
24
+ const div = document.createElement('div');
25
+ div.textContent = `User ID: ${ctx.params.id}`;
26
+ return div;
27
+ },
28
+ },
29
+ {
30
+ path: '/about',
31
+ lazy: async () => {
32
+ const { default: About } = await import('./About');
33
+ return About();
34
+ },
35
+ },
36
+ {
37
+ path: '/old-path',
38
+ redirectTo: '/new-path',
39
+ },
40
+ {
41
+ path: '*',
42
+ component: () => {
43
+ const div = document.createElement('div');
44
+ div.textContent = '404 Not Found';
45
+ return div;
46
+ },
47
+ },
48
+ ];
49
+
50
+ const router = createRouter(routes, { defaultTitle: 'My App' });
51
+
52
+ async function init() {
53
+ const outlet: RouterOutlet = await router.initialize();
54
+
55
+ const header = document.createElement('header');
56
+ const nav = document.createElement('nav');
57
+ const links = [
58
+ { text: 'Home', path: '/' },
59
+ { text: 'About', path: '/about' },
60
+ { text: 'User', path: '/user/1' },
61
+ ];
62
+
63
+ links.forEach((link) => {
64
+ const a = document.createElement('a');
65
+ a.href = link.path;
66
+ a.textContent = link.text;
67
+ a.addEventListener('click', (e) => {
68
+ e.preventDefault();
69
+ router.push(link.path);
70
+ });
71
+ nav.appendChild(a);
72
+ });
73
+
74
+ header.appendChild(nav);
75
+
76
+ const footer = document.createElement('footer');
77
+
78
+ document.body.appendChild(header);
79
+ document.body.appendChild(outlet);
80
+ document.body.appendChild(footer);
81
+ }
82
+
83
+ init();
84
+ ```
85
+
86
+ ## API
87
+
88
+ ### `createRouter(routes: Route[], options?: RouterOptions): Router`
89
+
90
+ Creates a router instance.
91
+
92
+ - `routes`: Array of route definitions.
93
+ - `options`:
94
+ - `defaultTitle`: Fallback document title.
95
+ - `globalErrorComponent`: Custom error component.
96
+ - `sortRoutesBySpecificity`: Sort routes by specificity (default: `true`).
97
+ - `ignoreSameLocation`: Skip same-path navigation (default: `true`).
98
+
99
+ #### `initialize(): Promise<RouterOutlet>`
100
+
101
+ Sets up popstate listener, navigates to current path, returns root `RouterOutlet`(web-component). Throws an error if it has been initialized.
102
+
103
+ #### `push(path: string, state?: unknown): Promise<void>`
104
+
105
+ Navigates, adding to history.
106
+
107
+ #### `replace(path: string, state?: unknown): Promise<void>`
108
+
109
+ Navigates, replacing history entry.
110
+
111
+ ### Route Types
112
+
113
+ - `SyncRoute`: Returns `HTMLElement` via `component(ctx: RouteContext)`.
114
+ - `AsyncRoute`: Returns `Promise<HTMLElement>` via `lazy(ctx: RouteContext)`.
115
+ - `RedirectRoute`: Redirects via `redirectTo`.
116
+
117
+ #### `Route.beforeEnter`
118
+
119
+ Runs before navigation. Return `false` to cancel, string to redirect, or `true` to proceed.
120
+
121
+ ```typescript
122
+ {
123
+ path: '/protected',
124
+ beforeEnter: [(ctx: RouteContext) => isAuthenticated() ? true : '/login'],
125
+ }
126
+ ```
127
+
128
+ #### `Route.afterEnter`
129
+
130
+ Runs after navigation.
131
+
132
+ ```typescript
133
+ {
134
+ path: '/dashboard',
135
+ afterEnter: [(ctx: RouteContext) => console.log('Entered dashboard')],
136
+ }
137
+ ```
138
+
139
+ ### Error Handling
140
+
141
+ Use `errorComponent` in routes or `globalErrorComponent` in options.
142
+
143
+ ```typescript
144
+ const routes = [
145
+ {
146
+ path: '*',
147
+ errorComponent: (error, ctx) => {
148
+ const div = document.createElement('div');
149
+ div.textContent = `Error: ${error.message}`;
150
+ return div;
151
+ },
152
+ },
153
+ ];
154
+
155
+ // or
156
+
157
+ const router = createRouter(routes, {
158
+ globalErrorComponent: (error) => {
159
+ const div = document.createElement('div');
160
+ div.textContent = 'Global Error';
161
+ return div;
162
+ },
163
+ });
164
+ ```
165
+
166
+ ## License
167
+
168
+ MIT
package/dist/index.d.ts CHANGED
@@ -1,58 +1,123 @@
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);
12
6
  beforeEnter?: BeforeEnterHook[];
13
7
  afterEnter?: AfterEnterHook[];
14
- errorComponent?(error: unknown, ctx: RouteContext | null): HTMLElement;
8
+ errorComponent?(error: Error, ctx: RouteContext | null): HTMLElement;
15
9
  }
16
10
 
17
11
  export declare type BeforeEnterHook = (ctx: RouteContext) => boolean | string | Promise<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
- declare interface RouteContext {
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;
41
- private readonly matchers;
42
+ private initialized;
43
+ private readonly routeMatchers;
42
44
  private readonly options;
43
- private readonly rootOutletInternal;
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 {
54
119
  defaultTitle?: string;
55
- globalErrorComponent?: (error: unknown) => HTMLElement;
120
+ globalErrorComponent?: (error: Error) => HTMLElement;
56
121
  sortRoutesBySpecificity?: boolean;
57
122
  ignoreSameLocation?: boolean;
58
123
  }
@@ -68,7 +133,9 @@ 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
 
139
+ export declare function withAbortSignal<T>(fn: () => Promise<T>, signal: AbortSignal): Promise<T>;
140
+
74
141
  export { }
package/dist/index.js CHANGED
@@ -1,4 +1,4 @@
1
- function P(t, e) {
1
+ function z(e, t) {
2
2
  const n = document.createElement("div");
3
3
  n.style.cssText = `
4
4
  position: fixed;
@@ -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,385 +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 = () => {
79
+ `, u.onmouseenter = () => {
80
+ u.style.opacity = "1";
81
+ }, u.onmouseleave = () => {
82
+ u.style.opacity = "0.7";
83
+ }, u.onclick = () => {
84
84
  n.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 = () => {
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
- }, n.appendChild(o), o.append(l, r, i, a), t.stack && o.append(d), o.append(f);
92
- const g = (u) => {
93
- u.key === "Escape" && n.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(n) || (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
- o.style.setProperty("--bg", u ? "#2b2b2b" : "#fff"), o.style.setProperty("--text", u ? "#eee" : "#222"), o.style.setProperty("--accent", u ? "#ff6b6b" : "#e74c3c"), o.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
- ), o.style.setProperty("--button-bg", u ? "#3498db55" : "#3498db"), o.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"
107
+ h ? "#3498db88" : "#2980b9"
108
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), n;
115
+ `, document.head.appendChild(E), n;
116
116
  }
117
- function k(t, e, n) {
118
- t.title ? document.title = typeof t.title == "function" ? t.title(e) : t.title : document.title = n ?? "";
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;
125
+ }
126
+ function R(e) {
127
+ const t = typeof e == "string" ? e : "Unknown error";
128
+ return e instanceof Error ? e : new Error(t);
129
+ }
130
+ function S(e, t, n) {
131
+ e.title ? document.title = typeof e.title == "function" ? e.title(t) : e.title : document.title = n ?? "";
119
132
  }
120
- function p(t, e, n) {
121
- const o = e ?? null;
133
+ function y(e, t, n) {
134
+ const o = t ?? null;
122
135
  switch (n) {
123
136
  case "push":
124
- window.history.pushState(o, "", t);
137
+ window.history.pushState(o, "", e);
125
138
  break;
126
139
  case "replace":
127
- window.history.replaceState(o, "", t);
140
+ window.history.replaceState(o, "", e);
128
141
  break;
129
142
  }
130
143
  }
131
- function T(t, e) {
132
- t.replaceChildren(e);
133
- }
134
- function h(t, e, n, o, r) {
135
- const i = n?.errorComponent?.(e, o) ?? r?.(e) ?? P(e, o);
136
- t.replaceChildren(i);
137
- }
138
- const A = "*";
139
- function L({ path: t }) {
140
- return t.includes(":");
144
+ function T(e, t) {
145
+ e.replaceChildren(t);
141
146
  }
142
- function x(t) {
143
- const [, e] = t.split("?");
144
- return new URLSearchParams(e || "");
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);
145
150
  }
146
- function $({ path: t }) {
147
- const e = t.replace(
148
- /:([^/]+)/g,
149
- (n, o) => `(?<${o}>[^/]+)`
150
- );
151
- return new RegExp(`^${e}$`);
151
+ function L(e) {
152
+ const [, t] = e.split("?");
153
+ return new URLSearchParams(t || "");
152
154
  }
153
- function S(t) {
154
- const e = $(t);
155
+ function A(e) {
156
+ const t = new URLPattern({ pathname: e.path });
155
157
  return {
156
- route: t,
157
- redirectTo: t.redirectTo || null,
158
+ route: e,
159
+ redirectTo: e.redirectTo || null,
158
160
  match(n) {
159
161
  const o = n.split("?")[0];
160
- return e.test(o);
162
+ return t.test({ pathname: o });
161
163
  },
162
164
  getParams(n) {
163
- return e.exec(n)?.groups ?? {};
164
- },
165
- getQuery: x
166
- };
167
- }
168
- function z({ path: t }) {
169
- return t === A ? new RegExp("^.*$") : new RegExp(`^${t}$`);
170
- }
171
- function I(t) {
172
- const e = z(t);
173
- return {
174
- route: t,
175
- redirectTo: t.redirectTo || null,
176
- match(n) {
177
165
  const o = n.split("?")[0];
178
- return e.test(o);
179
- },
180
- getParams() {
181
- return {};
166
+ return t.exec({ pathname: o })?.pathname.groups ?? {};
182
167
  },
183
- getQuery: x
168
+ getQuery: L
184
169
  };
185
170
  }
186
- function O(t) {
187
- return L(t) ? S(t) : I(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
+ }
188
187
  }
189
- function M(t, e) {
190
- for (let n = 0; n < e.length; n++)
191
- if (e[n].match(t))
192
- return e[n];
188
+ function N(e, t) {
189
+ for (let n = 0; n < t.length; n++)
190
+ if (t[n].match(e))
191
+ return t[n];
193
192
  return null;
194
193
  }
195
- async function W(t, e) {
196
- return typeof t.component == "function" ? t.component(e) : typeof t.loadComponent == "function" ? await t.loadComponent(e) : new Error("Failed to get component for route");
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);
197
196
  }
198
- async function B(t, e) {
199
- const n = t.afterEnter;
197
+ async function $(e, t) {
198
+ const n = e.afterEnter;
200
199
  if (!(!n || n.length === 0))
201
200
  for (const o of n)
202
- await o(e);
201
+ await o(t);
203
202
  }
204
- async function H(t, e) {
205
- const n = t.beforeEnter;
203
+ async function U(e, t) {
204
+ const n = e.beforeEnter;
206
205
  if (!n || n.length === 0)
207
206
  return null;
208
207
  for (const o of n) {
209
- const r = await o(e);
208
+ if (t.signal.aborted)
209
+ return !1;
210
+ const r = await o(t);
210
211
  if (r === !1 || typeof r == "string")
211
212
  return r;
212
213
  }
213
214
  return null;
214
215
  }
215
- function E(t) {
216
- return t instanceof DOMException && t.name === "AbortError";
217
- }
218
- class C extends HTMLElement {
219
- constructor(e) {
220
- super(), this.setAttribute("outlet-id", e.outletId);
221
- }
222
- }
223
- customElements.define("router-outlet", C);
224
- async function m(t, e) {
225
- if (e.aborted)
226
- throw new DOMException("Aborted", "AbortError");
227
- return new Promise((n, o) => {
228
- const r = () => o(new DOMException("Aborted", "AbortError"));
229
- e.addEventListener("abort", r), t().then(n).catch(o).finally(() => {
230
- e.removeEventListener("abort", r);
231
- });
232
- });
216
+ function C(e) {
217
+ return e instanceof DOMException && e.name === "AbortError";
233
218
  }
234
- function v(t) {
235
- const e = t.split("/").filter(Boolean);
236
- if (e.length === 0)
219
+ function x(e) {
220
+ const t = e.split("/").filter(Boolean);
221
+ if (t.length === 0)
237
222
  return 0;
238
223
  let n = 0;
239
- for (let o = 0; o < e.length; o++) {
240
- const r = e[o];
224
+ for (let o = 0; o < t.length; o++) {
225
+ const r = t[o];
241
226
  r === "*" ? n -= 100 : r.startsWith(":") ? n += 5 : n += 10;
242
227
  }
243
- return n += e.length * 0.1, n;
228
+ return n += t.length * 0.1, n;
244
229
  }
245
- function U(t) {
246
- return t.toSorted((e, n) => v(n.path) - v(e.path));
230
+ function B(e) {
231
+ return e.toSorted((t, n) => x(n.path) - x(t.path));
247
232
  }
248
- function D(t, e) {
249
- if (t === null)
233
+ function I(e, t) {
234
+ if (e === null)
250
235
  return !1;
251
- const n = new URL(t, location.origin), o = new URL(e, location.origin);
236
+ const n = new URL(e, location.origin), o = new URL(t, location.origin);
252
237
  return !(n.pathname !== o.pathname || n.search !== o.search);
253
238
  }
254
- function R(t) {
255
- if (typeof t != "string")
256
- throw new Error(`Path must be a string, got ${typeof t}`);
257
- if (t.trim() === "")
239
+ class P extends HTMLElement {
240
+ constructor(t) {
241
+ super(), this.setAttribute("outlet-id", t.outletId);
242
+ }
243
+ }
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() === "")
258
249
  throw new Error("Path cannot be an empty string");
259
- if (t !== "*" && !t.startsWith("/"))
260
- throw new Error(`Path must start with '/': "${t}"`);
261
- if (t.length > 1 && t.endsWith("/"))
262
- throw new Error(`Path must not end with '/': "${t}"`);
263
- if (t.includes("//"))
264
- throw new Error(
265
- `Path must not contain multiple consecutive slashes: "${t}"`
266
- );
267
- if (t === "*") return;
268
- const e = t.match(/[^A-Za-z0-9\-._~:/@]/g);
269
- if (e)
270
- throw new Error(
271
- `Path contains invalid characters: "${e.join("")}" in "${t}"`
272
- );
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}"`);
273
261
  }
274
- function Q(t) {
275
- const e = /* @__PURE__ */ new Set();
276
- for (let n = 0; n < t.length; n++) {
277
- const o = t[n].path;
278
- if (e.has(o))
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))
279
267
  throw new Error(`Duplicate root route path: "${o}"`);
280
- e.add(o);
268
+ t.add(o);
281
269
  }
282
270
  }
283
- function _(t) {
284
- if (!(t.redirectTo === null || t.redirectTo == null) && typeof t.redirectTo == "string") {
285
- const e = t.redirectTo;
286
- if (R(e), e === t.path)
287
- throw new Error(`Route "${t.path}" cannot redirect to itself`);
288
- 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 === "*")
289
290
  throw new Error('Catch-all route "*" cannot have a redirect');
290
291
  }
291
292
  }
292
- function q(t) {
293
- const e = typeof t.redirectTo == "string", n = typeof t.component == "function", o = typeof t.loadComponent == "function";
294
- if (e && (n || o))
295
- throw new Error(
296
- `Redirect route "${t.path}" must not define components`
297
- );
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`);
298
297
  if (n && o)
299
- throw new Error(
300
- `Route "${t.path}" cannot have both component and loadComponent`
301
- );
302
- if (!e && !n && !o)
303
- throw new Error(
304
- `Route "${t.path}" must define either "component" or "loadComponent"`
305
- );
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"`);
306
301
  }
307
- function F(t) {
308
- R(t.path), _(t), q(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}"`);
309
309
  }
310
- function j(t) {
311
- Q(t);
312
- for (let e = 0; e < t.length; e++) {
313
- const n = t[e];
314
- F(n);
315
- }
310
+ function O(e) {
311
+ k(e.path), H(e), Q(e), D(e);
316
312
  }
317
- class K {
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();
318
321
  routerController = new AbortController();
319
- initialized = !1;
320
322
  navigationController = null;
321
- matchers = [];
323
+ initialized = !1;
324
+ routeMatchers;
322
325
  options;
323
- rootOutletInternal = new C({ outletId: "root" });
326
+ outlet = new P({ outletId: "root" });
324
327
  currentPath = null;
325
- constructor(e, n) {
326
- j(e), this.options = {
328
+ pendingPath = null;
329
+ constructor(t, n) {
330
+ j(t), this.options = {
327
331
  defaultTitle: document.title,
328
332
  sortRoutesBySpecificity: !0,
333
+ ignoreSameLocation: !0,
329
334
  ...n
330
335
  };
331
- const o = this.options.sortRoutesBySpecificity ? U(e) : e;
332
- this.matchers = o.map(O);
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);
333
344
  }
334
345
  async initialize() {
335
346
  if (this.initialized)
336
347
  throw new Error("Router already initialized");
337
- const e = async (o) => {
338
- const r = window.location.pathname + window.location.search;
339
- await this.navigate(r, o.state, "none");
340
- };
341
- window.addEventListener(
348
+ p.instance = this, window.addEventListener(
342
349
  "popstate",
343
- (o) => {
344
- e(o).catch(console.error);
350
+ (n) => {
351
+ this.handlePopState(n).catch(console.error);
345
352
  },
346
- { signal: this.routerController.signal }
353
+ {
354
+ signal: this.routerController.signal
355
+ }
347
356
  );
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) {
348
361
  const n = window.location.pathname + window.location.search;
349
- return await this.navigate(n, null, "none"), this.initialized = !0, this.rootOutletInternal;
362
+ await this.navigate(n, t.state, "none");
350
363
  }
351
364
  destroy() {
352
365
  if (!this.initialized)
353
366
  throw new Error("Router not initialized");
354
- 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;
355
368
  }
356
- async navigate(e, n, o, r = 0) {
357
- if (r > 5) {
358
- h(
359
- this.rootOutletInternal,
360
- new Error("Too many redirects"),
361
- null,
362
- null,
363
- this.options.globalErrorComponent
364
- );
365
- return;
366
- }
367
- if (this.options.ignoreSameLocation && D(this.currentPath, e))
368
- return;
369
- this.navigationController && (this.navigationController.abort("navigation canceled"), this.navigationController = null), this.navigationController = new AbortController();
370
- const i = this.navigationController.signal, a = M(e, this.matchers), s = {
371
- from: this.currentPath,
372
- to: e,
373
- params: a?.getParams(e) ?? {},
374
- 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(),
375
389
  signal: i
376
390
  };
377
391
  try {
378
- if (!a) {
379
- p(e, n, o), h(
380
- this.rootOutletInternal,
392
+ if (!a)
393
+ return y(t, n, o), m(
394
+ this.outlet,
381
395
  new Error('Route not found. Add "*" catch-all route'),
382
396
  null,
383
- s,
397
+ l,
384
398
  this.options.globalErrorComponent
385
- ), this.currentPath = e;
386
- return;
387
- }
388
- if (a.redirectTo) {
389
- await this.navigate(a.redirectTo, n, "replace", r + 1);
390
- return;
391
- }
399
+ ), this.finalizeNavigation(t, f, !1);
400
+ if (a.redirectTo)
401
+ return this.navigate(a.redirectTo, n, "replace", r + 1);
392
402
  if (i.aborted)
393
403
  return;
394
- const c = await m(
395
- () => H(a.route, s),
404
+ const c = await g(
405
+ () => U(a.route, l),
396
406
  i
397
407
  );
398
408
  if (c === !1)
399
409
  return;
400
- if (typeof c == "string") {
401
- await this.navigate(c, n, "replace", r + 1);
402
- return;
403
- }
410
+ if (typeof c == "string")
411
+ return this.navigate(c, n, "replace", r + 1);
404
412
  if (i.aborted)
405
413
  return;
406
- const d = await m(
407
- () => W(a.route, s),
414
+ const s = await g(
415
+ () => M(a.route, l),
408
416
  i
409
417
  );
410
- if (d instanceof Error) {
411
- if (E(d))
412
- return;
413
- p(e, n, o), h(
414
- this.rootOutletInternal,
415
- d,
416
- a.route,
418
+ if (s instanceof Error)
419
+ return C(s) ? void 0 : (y(t, n, o), m(
420
+ this.outlet,
417
421
  s,
422
+ a.route,
423
+ l,
418
424
  this.options.globalErrorComponent
419
- ), this.currentPath = e;
420
- return;
421
- }
425
+ ), this.finalizeNavigation(t, f, !1));
422
426
  if (i.aborted)
423
427
  return;
424
- T(this.rootOutletInternal, d), p(e, n, o), k(a.route, s, this.options.defaultTitle), this.currentPath = e, await m(() => B(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);
425
429
  } catch (c) {
426
- if (E(c))
430
+ if (C(c))
427
431
  return;
428
- h(
429
- this.rootOutletInternal,
432
+ m(
433
+ this.outlet,
430
434
  c instanceof Error ? c : new Error("Unknown error"),
431
435
  a?.route ?? null,
432
- s,
436
+ l,
433
437
  this.options.globalErrorComponent
434
- );
438
+ ), this.emit("navigationend", { from: this.currentPath, to: t, successful: !1 });
435
439
  } finally {
436
440
  this.navigationController?.signal === i && (this.navigationController = null);
437
441
  }
438
442
  }
439
- async push(e, n) {
440
- try {
441
- await this.navigate(e, n, "push");
442
- } catch (o) {
443
- console.error("[Router.push] Error:", o);
444
- }
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);
445
445
  }
446
- async replace(e, n) {
447
- try {
448
- await this.navigate(e, n, "replace");
449
- } catch (o) {
450
- console.error("[Router.replace] Error:", o);
451
- }
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 });
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;
452
464
  }
453
465
  }
454
- function G(t, e) {
455
- return new K(t, e);
466
+ function G(e, t) {
467
+ return new p(e, t);
468
+ }
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
+ }
456
512
  }
513
+ customElements.define("router-link", K, { extends: "a" });
457
514
  export {
458
- K as Router,
459
- G as createRouter
515
+ p as Router,
516
+ K as RouterLink,
517
+ G as createRouter,
518
+ g as withAbortSignal
460
519
  };
package/package.json CHANGED
@@ -1,7 +1,8 @@
1
1
  {
2
2
  "name": "@ripetchor/r-router",
3
- "version": "1.0.0",
3
+ "version": "1.0.2",
4
4
  "type": "module",
5
+ "license": "MIT",
5
6
  "files": [
6
7
  "dist"
7
8
  ],
@@ -23,6 +24,7 @@
23
24
  },
24
25
  "devDependencies": {
25
26
  "@eslint/js": "^9.37.0",
27
+ "@types/node": "^25.0.3",
26
28
  "eslint": "^9.37.0",
27
29
  "typescript": "~5.9.3",
28
30
  "typescript-eslint": "^8.45.0",