@manyducks.co/dolla 2.0.0-alpha.36 → 2.0.0-alpha.37

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.js CHANGED
@@ -1,261 +1,336 @@
1
- var zt = Object.defineProperty;
2
- var $t = (i) => {
3
- throw TypeError(i);
4
- };
5
- var Bt = (i, t, e) => t in i ? zt(i, t, { enumerable: !0, configurable: !0, writable: !0, value: e }) : i[t] = e;
6
- var w = (i, t, e) => Bt(i, typeof t != "symbol" ? t + "" : t, e), mt = (i, t, e) => t.has(i) || $t("Cannot " + e);
7
- var a = (i, t, e) => (mt(i, t, "read from private field"), e ? e.call(i) : t.get(i)), u = (i, t, e) => t.has(i) ? $t("Cannot add the same private member more than once") : t instanceof WeakSet ? t.add(i) : t.set(i, e), d = (i, t, e, s) => (mt(i, t, "write to private field"), s ? s.call(i, e) : t.set(i, e), e), f = (i, t, e) => (mt(i, t, "access private method"), e);
8
- var Rt = (i, t, e, s) => ({
9
- set _(r) {
10
- d(i, t, r, e);
11
- },
12
- get _() {
13
- return a(i, t, s);
14
- }
15
- });
16
- import { a as Ft, b as Kt, i as B, I as Ot, c as Ut, d as U, g as E, s as _t, e as Qt, f as M, h as V, P as jt, j as Jt, t as St, k as Mt, l as Xt, m as xt, S as Yt, n as Tt, o as Nt, p as Zt, V as te, q as ee, r as se, u as ne, v as re } from "./markup-ILMFXzoo.js";
17
- import { w as Ue, B as je, y as Ce, A as De, x as Ie, z as Ve } from "./markup-ILMFXzoo.js";
18
- function Ne(i) {
19
- return () => {
20
- if (arguments.length === 1)
21
- i = arguments[0];
22
- else if (arguments.length > 1)
1
+ import { a as assertArrayOf, b as assertString, i as isFunction, I as IS_ROUTER, c as atom, d as compose, g as get, s as shallowEqual, e as assertObject, f as isString, h as isObject, P as Passthrough, j as deepEqual, t as typeOf, k as cond, l as html, m as createMatcher, S as Store, n as StoreError, o as assertInstanceOf, p as createMarkup, V as View, q as groupElements, r as constructMarkup, u as noOp, v as okhash } from './markup-Px0heVon.js';
2
+ export { w as effect, B as list, y as peek, A as portal, x as set, z as strictEqual } from './markup-Px0heVon.js';
3
+
4
+ function ref(value) {
5
+ return function() {
6
+ if (arguments.length === 1) {
7
+ value = arguments[0];
8
+ } else if (arguments.length > 1) {
23
9
  throw new Error(`Too many arguments. Expected 0 or 1. Got: ${arguments.length}`);
24
- return i;
10
+ }
11
+ return value;
25
12
  };
26
13
  }
27
- function Q(i) {
28
- return Ft(i, "Expected `path` to be a string. Got type: %t, value: %v"), i.split("/").map((t) => t.trim()).filter((t) => t !== "");
14
+
15
+ function splitPath(path) {
16
+ assertString(path, "Expected `path` to be a string. Got type: %t, value: %v");
17
+ return path.split("/").map((f) => f.trim()).filter((f) => f !== "");
29
18
  }
30
- function G(i) {
31
- var e;
32
- Kt(
33
- (s) => B(s == null ? void 0 : s.toString),
34
- i,
19
+ function joinPath(parts) {
20
+ assertArrayOf(
21
+ (part) => isFunction(part?.toString),
22
+ parts,
35
23
  "Expected `parts` to be an array of objects with a .toString() method. Got type: %t, value: %v"
36
- ), i = i.filter((s) => s).flatMap(String);
37
- let t = (e = i.shift()) == null ? void 0 : e.toString();
38
- if (t) {
39
- for (const s of i.map((r) => r.toString()))
40
- s.startsWith(".") ? t = ct(t, s) : t[t.length - 1] !== "/" ? s[0] !== "/" ? t += "/" + s : t += s : s[0] === "/" ? t += s.slice(1) : t += s;
41
- t && t !== "/" && t.endsWith("/") && (t = t.slice(0, t.length - 1));
42
- }
43
- return t ?? "";
24
+ );
25
+ parts = parts.filter((x) => x).flatMap(String);
26
+ let joined = parts.shift()?.toString();
27
+ if (joined) {
28
+ for (const part of parts.map((p) => p.toString())) {
29
+ if (part.startsWith(".")) {
30
+ joined = resolvePath(joined, part);
31
+ } else if (joined[joined.length - 1] !== "/") {
32
+ if (part[0] !== "/") {
33
+ joined += "/" + part;
34
+ } else {
35
+ joined += part;
36
+ }
37
+ } else {
38
+ if (part[0] === "/") {
39
+ joined += part.slice(1);
40
+ } else {
41
+ joined += part;
42
+ }
43
+ }
44
+ }
45
+ if (joined && joined !== "/" && joined.endsWith("/")) {
46
+ joined = joined.slice(0, joined.length - 1);
47
+ }
48
+ }
49
+ return joined ?? "";
44
50
  }
45
- function ct(i, t) {
46
- if (Ft(i, "Expected `base` to be a string. Got type: %t, value: %v"), t == null && (t = i, i = ""), t.startsWith("/"))
47
- return t;
48
- let e = i;
49
- for (; ; )
50
- if (t.startsWith("..")) {
51
- for (let s = e.length; s > 0; --s)
52
- if (e[s] === "/" || s === 0) {
53
- e = e.slice(0, s), t = t.replace(/^\.\.\/?/, "");
51
+ function resolvePath(base, part) {
52
+ assertString(base, "Expected `base` to be a string. Got type: %t, value: %v");
53
+ if (part == null) {
54
+ part = base;
55
+ base = "";
56
+ }
57
+ if (part.startsWith("/")) {
58
+ return part;
59
+ }
60
+ let resolved = base;
61
+ while (true) {
62
+ if (part.startsWith("..")) {
63
+ for (let i = resolved.length; i > 0; --i) {
64
+ if (resolved[i] === "/" || i === 0) {
65
+ resolved = resolved.slice(0, i);
66
+ part = part.replace(/^\.\.\/?/, "");
54
67
  break;
55
68
  }
56
- } else if (t.startsWith("."))
57
- t = t.replace(/^\.\/?/, "");
58
- else
69
+ }
70
+ } else if (part.startsWith(".")) {
71
+ part = part.replace(/^\.\/?/, "");
72
+ } else {
59
73
  break;
60
- return G([e, t]);
74
+ }
75
+ }
76
+ return joinPath([resolved, part]);
61
77
  }
62
- function ae(i) {
63
- if (!i) return {};
64
- i.startsWith("?") && (i = i.slice(1));
65
- const t = i.split("&").filter((e) => e.trim() !== "").map((e) => {
66
- const [s, r] = e.split("=").map((n) => n.trim());
67
- return r.toLowerCase() === "true" ? [s, !0] : r.toLowerCase() === "false" ? [s, !1] : isNaN(Number(r)) ? [s, r] : [s, Number(r)];
78
+ function parseQueryParams(query) {
79
+ if (!query) return {};
80
+ if (query.startsWith("?")) {
81
+ query = query.slice(1);
82
+ }
83
+ const entries = query.split("&").filter((x) => x.trim() !== "").map((entry) => {
84
+ const [key, value] = entry.split("=").map((x) => x.trim());
85
+ if (value.toLowerCase() === "true") {
86
+ return [key, true];
87
+ }
88
+ if (value.toLowerCase() === "false") {
89
+ return [key, false];
90
+ }
91
+ if (!isNaN(Number(value))) {
92
+ return [key, Number(value)];
93
+ }
94
+ return [key, value];
68
95
  });
69
- return Object.fromEntries(t);
96
+ return Object.fromEntries(entries);
70
97
  }
71
- function Ct(i, t, e = {}) {
72
- var o;
73
- const [s, r] = t.split("?"), n = Q(s);
74
- t: for (const c of i) {
75
- const { fragments: l } = c;
76
- if (!(((o = l[l.length - 1]) == null ? void 0 : o.type) === 3) && l.length !== n.length || e.willMatch && !e.willMatch(c))
77
- continue t;
78
- const v = [];
79
- e: for (let h = 0; h < l.length; h++) {
80
- const x = n[h], $ = l[h];
81
- if (x == null && $.type !== 3)
82
- continue t;
83
- switch ($.type) {
84
- case 1:
85
- if ($.name.toLowerCase() === x.toLowerCase()) {
86
- v.push($);
98
+ function matchRoutes(routes, url, options = {}) {
99
+ const [path, query] = url.split("?");
100
+ const parts = splitPath(path);
101
+ routes: for (const route of routes) {
102
+ const { fragments } = route;
103
+ const hasWildcard = fragments[fragments.length - 1]?.type === 3 /* Wildcard */;
104
+ if (!hasWildcard && fragments.length !== parts.length) {
105
+ continue routes;
106
+ }
107
+ if (options.willMatch && !options.willMatch(route)) {
108
+ continue routes;
109
+ }
110
+ const matched = [];
111
+ fragments: for (let i = 0; i < fragments.length; i++) {
112
+ const part = parts[i];
113
+ const frag = fragments[i];
114
+ if (part == null && frag.type !== 3 /* Wildcard */) {
115
+ continue routes;
116
+ }
117
+ switch (frag.type) {
118
+ case 1 /* Literal */:
119
+ if (frag.name.toLowerCase() === part.toLowerCase()) {
120
+ matched.push(frag);
87
121
  break;
88
- } else
89
- continue t;
90
- case 2:
91
- v.push({ ...$, value: x });
92
- break;
93
- case 3:
94
- v.push({ ...$, value: n.slice(h).join("/") });
95
- break e;
96
- case 4:
97
- if (isNaN(Number(x)))
98
- continue t;
99
- v.push({ ...$, value: Number(x) });
122
+ } else {
123
+ continue routes;
124
+ }
125
+ case 2 /* Param */:
126
+ matched.push({ ...frag, value: part });
100
127
  break;
128
+ case 3 /* Wildcard */:
129
+ matched.push({ ...frag, value: parts.slice(i).join("/") });
130
+ break fragments;
131
+ case 4 /* NumericParam */:
132
+ if (!isNaN(Number(part))) {
133
+ matched.push({ ...frag, value: Number(part) });
134
+ break;
135
+ } else {
136
+ continue routes;
137
+ }
101
138
  default:
102
- throw new Error(`Unknown fragment type: ${$.type}`);
139
+ throw new Error(`Unknown fragment type: ${frag.type}`);
140
+ }
141
+ }
142
+ const params = {};
143
+ for (const frag of matched) {
144
+ if (frag.type === 2 /* Param */) {
145
+ params[frag.name] = decodeURIComponent(frag.value);
146
+ }
147
+ if (frag.type === 4 /* NumericParam */) {
148
+ params[frag.name] = frag.value;
149
+ }
150
+ if (frag.type === 3 /* Wildcard */) {
151
+ params.wildcard = "/" + decodeURIComponent(frag.value);
103
152
  }
104
153
  }
105
- const k = {};
106
- for (const h of v)
107
- h.type === 2 && (k[h.name] = decodeURIComponent(h.value)), h.type === 4 && (k[h.name] = h.value), h.type === 3 && (k.wildcard = "/" + decodeURIComponent(h.value));
108
154
  return {
109
- path: "/" + v.map((h) => h.value).join("/"),
110
- pattern: "/" + l.map((h) => h.type === 2 ? `{${h.name}}` : h.type === 4 ? `{#${h.name}}` : h.name).join("/"),
111
- params: k,
112
- query: ae(r),
113
- meta: c.meta
155
+ path: "/" + matched.map((f) => f.value).join("/"),
156
+ pattern: "/" + fragments.map((f) => {
157
+ if (f.type === 2 /* Param */) {
158
+ return `{${f.name}}`;
159
+ }
160
+ if (f.type === 4 /* NumericParam */) {
161
+ return `{#${f.name}}`;
162
+ }
163
+ return f.name;
164
+ }).join("/"),
165
+ params,
166
+ query: parseQueryParams(query),
167
+ meta: route.meta
114
168
  };
115
169
  }
116
170
  }
117
- function ie(i) {
118
- const t = [], e = [], s = [], r = [];
119
- for (const o of i) {
120
- const { fragments: c } = o;
121
- c.some(
122
- (l) => l.type === 3
123
- /* Wildcard */
124
- ) ? r.push(o) : c.some(
125
- (l) => l.type === 4
126
- /* NumericParam */
127
- ) ? e.push(o) : c.some(
128
- (l) => l.type === 2
129
- /* Param */
130
- ) ? s.push(o) : t.push(o);
131
- }
132
- const n = (o, c) => o.fragments.length > c.fragments.length ? -1 : 1;
133
- return t.sort(n), e.sort(n), s.sort(n), r.sort(n), [...t, ...e, ...s, ...r];
171
+ function sortRoutes(routes) {
172
+ const withoutParams = [];
173
+ const withNumericParams = [];
174
+ const withParams = [];
175
+ const wildcard = [];
176
+ for (const route of routes) {
177
+ const { fragments } = route;
178
+ if (fragments.some((f) => f.type === 3 /* Wildcard */)) {
179
+ wildcard.push(route);
180
+ } else if (fragments.some((f) => f.type === 4 /* NumericParam */)) {
181
+ withNumericParams.push(route);
182
+ } else if (fragments.some((f) => f.type === 2 /* Param */)) {
183
+ withParams.push(route);
184
+ } else {
185
+ withoutParams.push(route);
186
+ }
187
+ }
188
+ const bySizeDesc = (a, b) => {
189
+ if (a.fragments.length > b.fragments.length) {
190
+ return -1;
191
+ } else {
192
+ return 1;
193
+ }
194
+ };
195
+ withoutParams.sort(bySizeDesc);
196
+ withNumericParams.sort(bySizeDesc);
197
+ withParams.sort(bySizeDesc);
198
+ wildcard.sort(bySizeDesc);
199
+ return [...withoutParams, ...withNumericParams, ...withParams, ...wildcard];
134
200
  }
135
- function oe(i) {
136
- const t = Q(i), e = [];
137
- for (let s = 0; s < t.length; s++) {
138
- const r = t[s];
139
- if (r === "*") {
140
- if (s !== t.length - 1)
141
- throw new Error(`Wildcard must be at the end of a pattern. Received: ${i}`);
142
- e.push({
143
- type: 3,
201
+ function patternToFragments(pattern) {
202
+ const parts = splitPath(pattern);
203
+ const fragments = [];
204
+ for (let i = 0; i < parts.length; i++) {
205
+ const part = parts[i];
206
+ if (part === "*") {
207
+ if (i !== parts.length - 1) {
208
+ throw new Error(`Wildcard must be at the end of a pattern. Received: ${pattern}`);
209
+ }
210
+ fragments.push({
211
+ type: 3 /* Wildcard */,
144
212
  name: "*",
145
213
  value: null
146
214
  });
147
- } else r.at(0) === "{" && r.at(-1) === "}" ? e.push({
148
- type: r[1] === "#" ? 4 : 2,
149
- name: r[1] === "#" ? r.slice(2, -1) : r.slice(1, -1),
150
- value: null
151
- }) : e.push({
152
- type: 1,
153
- name: r,
154
- value: r
155
- });
215
+ } else if (part.at(0) === "{" && part.at(-1) === "}") {
216
+ fragments.push({
217
+ type: part[1] === "#" ? 4 /* NumericParam */ : 2 /* Param */,
218
+ name: part[1] === "#" ? part.slice(2, -1) : part.slice(1, -1),
219
+ value: null
220
+ });
221
+ } else {
222
+ fragments.push({
223
+ type: 1 /* Literal */,
224
+ name: part,
225
+ value: part
226
+ });
227
+ }
156
228
  }
157
- return e;
229
+ return fragments;
158
230
  }
159
- function Pe(i) {
160
- return new he(i);
231
+
232
+ function createRouter(options) {
233
+ return new Router(options);
161
234
  }
162
- const Dt = Symbol.for("DollaRouterMountMethod"), It = Symbol.for("DollaRouterUnmountMethod");
163
- function pt(i) {
164
- return (i == null ? void 0 : i[Ot]) === !0;
235
+ const ROUTER_MOUNT = Symbol.for("DollaRouterMountMethod");
236
+ const ROUTER_UNMOUNT = Symbol.for("DollaRouterUnmountMethod");
237
+ function _isRouter(value) {
238
+ return value?.[IS_ROUTER] === true;
165
239
  }
166
- async function le(i, t) {
167
- return i[Dt](t);
240
+ async function _mountRouter(router, dolla) {
241
+ return router[ROUTER_MOUNT](dolla);
168
242
  }
169
- async function ce(i) {
170
- return i[It]();
243
+ async function _unmountRouter(router) {
244
+ return router[ROUTER_UNMOUNT]();
171
245
  }
172
- var Lt, W, S, ht, T, A, J, j, C, N, m, wt, Vt, qt, K, Gt, gt, yt;
173
- class he {
174
- constructor(t) {
175
- u(this, m);
176
- w(this, Lt, !0);
177
- u(this, W);
178
- u(this, S);
179
- u(this, ht, 0);
180
- u(this, T, []);
181
- u(this, A, []);
182
- u(this, J, !1);
183
- /**
184
- * Use hash routing when true. Configured in router options.
185
- */
186
- u(this, j, !1);
187
- // Callbacks that need to be called on unmount.
188
- u(this, C, []);
189
- /**
190
- * The current match object.
191
- */
192
- u(this, N, Ut());
193
- /**
194
- * The currently matched route pattern, if any.
195
- */
196
- w(this, "pattern", U(() => {
197
- var t;
198
- return (t = E(a(this, N))) == null ? void 0 : t.pattern;
199
- }));
200
- /**
201
- * The current URL path.
202
- */
203
- w(this, "path", U(() => {
204
- var t;
205
- return ((t = E(a(this, N))) == null ? void 0 : t.path) ?? window.location.pathname;
206
- }));
207
- /**
208
- * The current named path params.
209
- */
210
- w(this, "params", U(() => {
211
- var t;
212
- return ((t = E(a(this, N))) == null ? void 0 : t.params) ?? {};
213
- }, { equals: _t }));
214
- /**
215
- * The current query params. Changes to this object will be reflected in the URL.
216
- */
217
- w(this, "query", U(() => {
218
- var t;
219
- return ((t = E(a(this, N))) == null ? void 0 : t.query) ?? {};
220
- }, { equals: _t }));
221
- Qt(t, "Options must be an object. Got: %t"), t.hash && d(this, j, !0), d(this, A, ie(
222
- t.routes.flatMap((e) => f(this, m, yt).call(this, e)).map((e) => ({
223
- pattern: e.pattern,
224
- meta: e.meta,
225
- fragments: oe(e.pattern)
246
+ class Router {
247
+ [IS_ROUTER] = true;
248
+ #dolla;
249
+ #logger;
250
+ #layerId = 0;
251
+ #activeLayers = [];
252
+ #routes = [];
253
+ #isMounted = false;
254
+ /**
255
+ * Use hash routing when true. Configured in router options.
256
+ */
257
+ #hash = false;
258
+ // Callbacks that need to be called on unmount.
259
+ #unsubscribers = [];
260
+ /**
261
+ * The current match object.
262
+ */
263
+ #match = atom();
264
+ /**
265
+ * The currently matched route pattern, if any.
266
+ */
267
+ pattern = compose(() => get(this.#match)?.pattern);
268
+ /**
269
+ * The current URL path.
270
+ */
271
+ path = compose(() => get(this.#match)?.path ?? window.location.pathname);
272
+ /**
273
+ * The current named path params.
274
+ */
275
+ params = compose(() => get(this.#match)?.params ?? {}, { equals: shallowEqual });
276
+ /**
277
+ * The current query params. Changes to this object will be reflected in the URL.
278
+ */
279
+ query = compose(() => get(this.#match)?.query ?? {}, { equals: shallowEqual });
280
+ constructor(options) {
281
+ assertObject(options, "Options must be an object. Got: %t");
282
+ if (options.hash) {
283
+ this.#hash = true;
284
+ }
285
+ this.#routes = sortRoutes(
286
+ options.routes.flatMap((route) => this.#prepareRoute(route)).map((route) => ({
287
+ pattern: route.pattern,
288
+ meta: route.meta,
289
+ fragments: patternToFragments(route.pattern)
226
290
  }))
227
- )), pe(a(this, A));
291
+ );
292
+ assertValidRedirects(this.#routes);
228
293
  }
229
- async [(Lt = Ot, Dt)](t) {
230
- d(this, W, t), d(this, S, t.createLogger("Dolla.router"));
231
- const e = () => {
232
- f(this, m, K).call(this);
294
+ async [ROUTER_MOUNT](dolla) {
295
+ this.#dolla = dolla;
296
+ this.#logger = dolla.createLogger("Dolla.router");
297
+ const onPopState = () => {
298
+ this.#updateRoute();
233
299
  };
234
- window.addEventListener("popstate", e), a(this, C).push(() => window.removeEventListener("popstate", e));
235
- const s = t.getRootElement();
236
- a(this, C).push(
237
- de(s, (r) => {
238
- let n = r.getAttribute("href");
239
- a(this, S).info("intercepted click on <a> tag", r), /^https?:\/\/|^\//.test(n) || (n = G([window.location.pathname, n])), f(this, m, wt).call(this, n);
300
+ window.addEventListener("popstate", onPopState);
301
+ this.#unsubscribers.push(() => window.removeEventListener("popstate", onPopState));
302
+ const rootElement = dolla.getRootElement();
303
+ this.#unsubscribers.push(
304
+ catchLinks(rootElement, (anchor) => {
305
+ let href = anchor.getAttribute("href");
306
+ this.#logger.info("intercepted click on <a> tag", anchor);
307
+ if (!/^https?:\/\/|^\//.test(href)) {
308
+ href = joinPath([window.location.pathname, href]);
309
+ }
310
+ this.#push(href);
240
311
  })
241
- ), a(this, S).info("will intercept clicks on <a> tags within root element", s), d(this, J, !0), await f(this, m, K).call(this);
312
+ );
313
+ this.#logger.info("will intercept clicks on <a> tags within root element", rootElement);
314
+ this.#isMounted = true;
315
+ await this.#updateRoute();
242
316
  }
243
- async [It]() {
244
- for (const t of a(this, C))
245
- t();
246
- d(this, C, []);
317
+ async [ROUTER_UNMOUNT]() {
318
+ for (const callback of this.#unsubscribers) {
319
+ callback();
320
+ }
321
+ this.#unsubscribers = [];
247
322
  }
248
323
  /**
249
324
  * Navigate backward. Pass a number of steps to hit the back button that many times.
250
325
  */
251
- back(t = 1) {
252
- window.history.go(-t);
326
+ back(steps = 1) {
327
+ window.history.go(-steps);
253
328
  }
254
329
  /**
255
330
  * Navigate forward. Pass a number of steps to hit the forward button that many times.
256
331
  */
257
- forward(t = 1) {
258
- window.history.go(t);
332
+ forward(steps = 1) {
333
+ window.history.go(steps);
259
334
  }
260
335
  /**
261
336
  * Navigates to another route.
@@ -264,499 +339,741 @@ class he {
264
339
  * router.go("/login"); // navigate to `/login`
265
340
  * router.go["/users", 215], { replace: true }); // replace current history entry with `/users/215`
266
341
  */
267
- go(t, e = {}) {
268
- if (a(this, W) == null)
269
- throw new Error("Routa methods won't work until you register it: Dolla.use(Routa, { /* ...options */ })");
270
- let s;
271
- Array.isArray(t) ? s = G(t) : s = t.toString(), s = ct(window.location.pathname, s), e.preserveQuery && (s += window.location.search), e.replace ? f(this, m, Vt).call(this, s) : f(this, m, wt).call(this, s);
342
+ go(path, options = {}) {
343
+ let joined;
344
+ if (Array.isArray(path)) {
345
+ joined = joinPath(path);
346
+ } else {
347
+ joined = path.toString();
348
+ }
349
+ joined = resolvePath(window.location.pathname, joined);
350
+ if (options.preserveQuery) {
351
+ joined += window.location.search;
352
+ }
353
+ if (options.replace) {
354
+ this.#replace(joined);
355
+ } else {
356
+ this.#push(joined);
357
+ }
272
358
  }
273
- }
274
- W = new WeakMap(), S = new WeakMap(), ht = new WeakMap(), T = new WeakMap(), A = new WeakMap(), J = new WeakMap(), j = new WeakMap(), C = new WeakMap(), N = new WeakMap(), m = new WeakSet(), wt = function(t, e) {
275
- var s;
276
- (s = a(this, S)) == null || s.info("(push)", t), window.history.pushState(e, "", a(this, j) ? "/#" + t : t), f(this, m, K).call(this, t);
277
- }, Vt = function(t, e) {
278
- var s;
279
- (s = a(this, S)) == null || s.info("(replace)", t), window.history.replaceState(e, "", a(this, j) ? "/#" + t : t), f(this, m, K).call(this, t);
280
- }, qt = function() {
281
- return a(this, j) ? new URL(window.location.hash.slice(1), window.location.origin) : new URL(window.location.pathname, window.location.origin);
282
- }, K = async function(t) {
283
- var c;
284
- const e = a(this, S), s = (c = a(this, W)) == null ? void 0 : c.getRootView(), r = t ? new URL(t, window.location.origin) : f(this, m, qt).call(this), { match: n, journey: o } = await f(this, m, gt).call(this, r);
285
- for (const l of o)
286
- switch (l.kind) {
287
- case "match":
288
- e == null || e.info(`📍 ${l.message}`);
289
- break;
290
- case "redirect":
291
- e == null || e.info(`↩️ ${l.message}`);
292
- break;
293
- case "miss":
294
- e == null || e.info(`💀 ${l.message}`);
295
- break;
296
- }
297
- if (n) {
298
- const l = this.pattern.value;
299
- a(this, N).value = n, s && n.pattern !== l && f(this, m, Gt).call(this, s, n);
300
- } else
301
- a(this, J) && e.crash(new we(`Failed to match route '${r.pathname}'`));
302
- return { match: n, journey: o };
303
- }, /**
304
- * Takes a matched route and mounts it.
305
- */
306
- Gt = function(t, e) {
307
- const s = e.meta.layers;
308
- for (let r = 0; r < s.length; r++) {
309
- const n = s[r], o = a(this, T)[r];
310
- if ((o == null ? void 0 : o.id) !== n.id) {
311
- d(this, T, a(this, T).slice(0, r)), o == null || o.view.unmount();
312
- const c = a(this, T).at(-1), p = ((c == null ? void 0 : c.view) ?? t).setChildView(n.view);
313
- a(this, T).push({ id: n.id, view: p });
314
- }
315
- }
316
- }, gt = async function(t, e = []) {
317
- const s = Ct(a(this, A), t.pathname);
318
- if (!s)
319
- return {
320
- match: null,
321
- journey: [...e, { kind: "miss", message: `no match for '${t.pathname}'` }]
322
- };
323
- let r = s.meta.redirect;
324
- if (s.meta.beforeMatch && await s.meta.beforeMatch({
325
- // TODO: Allow setting context variables from here? Would apply to the context of the matched view.
326
- redirect: (n) => {
327
- r = n;
328
- }
329
- }), r != null) {
330
- let n;
331
- if (M(r))
332
- n = me(r, s.params);
333
- else if (B(r)) {
334
- const o = {
335
- path: s.path,
336
- pattern: s.pattern,
337
- params: s.params,
338
- query: s.query
359
+ #push(href, state) {
360
+ this.#logger?.info("(push)", href);
361
+ window.history.pushState(state, "", this.#hash ? "/#" + href : href);
362
+ this.#updateRoute(href);
363
+ }
364
+ #replace(href, state) {
365
+ this.#logger?.info("(replace)", href);
366
+ window.history.replaceState(state, "", this.#hash ? "/#" + href : href);
367
+ this.#updateRoute(href);
368
+ }
369
+ #getCurrentURL() {
370
+ if (this.#hash) {
371
+ return new URL(window.location.hash.slice(1), window.location.origin);
372
+ } else {
373
+ return new URL(window.location.pathname, window.location.origin);
374
+ }
375
+ }
376
+ /**
377
+ * Run when the location changes. Diffs and mounts new routes and updates
378
+ * the $path, $route, $params and $query states accordingly.
379
+ */
380
+ async #updateRoute(href) {
381
+ const logger = this.#logger;
382
+ const rootView = this.#dolla?.getRootView();
383
+ const url = href ? new URL(href, window.location.origin) : this.#getCurrentURL();
384
+ const { match, journey } = await this.#resolveRoute(url);
385
+ if (match) {
386
+ const oldPattern = this.pattern.value;
387
+ this.#match.value = match;
388
+ if (rootView && match.pattern !== oldPattern) {
389
+ this.#mountRoute(rootView, match);
390
+ }
391
+ } else {
392
+ if (this.#isMounted) {
393
+ logger.crash(new NoRouteError(`Failed to match route '${url.pathname}'`));
394
+ }
395
+ }
396
+ return { match, journey };
397
+ }
398
+ /**
399
+ * Takes a matched route and mounts it.
400
+ */
401
+ #mountRoute(rootView, match) {
402
+ const layers = match.meta.layers;
403
+ for (let i = 0; i < layers.length; i++) {
404
+ const matchedLayer = layers[i];
405
+ const activeLayer = this.#activeLayers[i];
406
+ if (activeLayer?.id !== matchedLayer.id) {
407
+ this.#activeLayers = this.#activeLayers.slice(0, i);
408
+ activeLayer?.view.unmount();
409
+ const parentLayer = this.#activeLayers.at(-1);
410
+ const parent2 = parentLayer?.view ?? rootView;
411
+ const view = parent2.setChildView(matchedLayer.view);
412
+ this.#activeLayers.push({ id: matchedLayer.id, view });
413
+ }
414
+ }
415
+ }
416
+ /**
417
+ * Takes a URL and finds a match, following redirects.
418
+ */
419
+ async #resolveRoute(url, journey = []) {
420
+ const match = matchRoutes(this.#routes, url.pathname);
421
+ if (!match) {
422
+ return {
423
+ match: null,
424
+ journey: [...journey, { kind: "miss", message: `no match for '${url.pathname}'` }]
339
425
  };
340
- if (n = await r(o), !M(n))
341
- throw new Error("Redirect function must return a path to redirect to.");
342
- n.startsWith("/") || (n = ct(s.path, n));
343
- } else
344
- throw new TypeError("Redirect must either be a path string or a function.");
345
- return f(this, m, gt).call(this, new URL(n, window.location.origin), [
346
- ...e,
347
- { kind: "redirect", message: `redirecting '${s.path}' -> '${n}'` }
348
- ]);
349
- } else
350
- return { match: s, journey: [...e, { kind: "match", message: `matched route '${s.path}'` }] };
351
- }, /**
352
- * Parses a route definition object into a set of matchable routes.
353
- *
354
- * @param route - Route config object.
355
- * @param layers - Array of parent layers. Passed when this function calls itself on nested routes.
356
- */
357
- yt = function(t, e = [], s = []) {
358
- if (!V(t) || !M(t.path))
359
- throw new TypeError(`Route configs must be objects with a 'path' string property. Got: ${t}`);
360
- if (t.redirect && t.routes)
361
- throw new Error("Route cannot have both a 'redirect' and nested 'routes'.");
362
- if (t.redirect && t.view)
363
- throw new Error("Route cannot have both a 'redirect' and a 'view'.");
364
- if (!t.view && !t.routes && !t.redirect)
365
- throw new Error("Route must have a 'view', a 'redirect', or a set of nested 'routes'.");
366
- let r = [];
367
- for (const l of e)
368
- r.push(...Q(l.path));
369
- r.push(...Q(t.path)), r[r.length - 1] === "*" && r.pop();
370
- const n = [];
371
- if (t.redirect) {
372
- let l = t.redirect;
373
- return M(l) && (l = ct(G(r), l), l.startsWith("/") || (l = "/" + l)), n.push({
374
- pattern: "/" + G([...r, ...Q(t.path)]),
375
- meta: {
376
- redirect: l
426
+ }
427
+ let redirect = match.meta.redirect;
428
+ if (match.meta.beforeMatch) {
429
+ await match.meta.beforeMatch({
430
+ // TODO: Allow setting context variables from here? Would apply to the context of the matched view.
431
+ redirect: (path) => {
432
+ redirect = path;
433
+ }
434
+ });
435
+ }
436
+ if (redirect != null) {
437
+ let path;
438
+ if (isString(redirect)) {
439
+ path = replaceParams(redirect, match.params);
440
+ } else if (isFunction(redirect)) {
441
+ const redirectContext = {
442
+ path: match.path,
443
+ pattern: match.pattern,
444
+ params: match.params,
445
+ query: match.query
446
+ };
447
+ path = await redirect(redirectContext);
448
+ if (!isString(path)) {
449
+ throw new Error(`Redirect function must return a path to redirect to.`);
450
+ }
451
+ if (!path.startsWith("/")) {
452
+ path = resolvePath(match.path, path);
453
+ }
454
+ } else {
455
+ throw new TypeError(`Redirect must either be a path string or a function.`);
377
456
  }
378
- }), n;
379
- }
380
- let o = jt;
381
- if (B(t.view))
382
- o = t.view;
383
- else if (t.view)
384
- throw new TypeError(`Route '${t.path}' expected a view function or undefined. Got: ${t.view}`);
385
- const c = { id: Rt(this, ht)._++, view: o };
386
- if (t.routes)
387
- for (const l of t.routes)
388
- n.push(...f(this, m, yt).call(this, l, [...e, t], [...s, c]));
389
- else
390
- n.push({
391
- pattern: parent ? G([...e.map((l) => l.path), t.path]) : t.path,
392
- meta: {
393
- pattern: t.path,
394
- layers: [...s, c],
395
- beforeMatch: t.beforeMatch
457
+ return this.#resolveRoute(new URL(path, window.location.origin), [
458
+ ...journey,
459
+ { kind: "redirect", message: `redirecting '${match.path}' -> '${path}'` }
460
+ ]);
461
+ } else {
462
+ return { match, journey: [...journey, { kind: "match", message: `matched route '${match.path}'` }] };
463
+ }
464
+ }
465
+ /**
466
+ * Parses a route definition object into a set of matchable routes.
467
+ *
468
+ * @param route - Route config object.
469
+ * @param layers - Array of parent layers. Passed when this function calls itself on nested routes.
470
+ */
471
+ #prepareRoute(route, parents = [], layers = []) {
472
+ if (!isObject(route) || !isString(route.path)) {
473
+ throw new TypeError(`Route configs must be objects with a 'path' string property. Got: ${route}`);
474
+ }
475
+ if (route.redirect && route.routes) {
476
+ throw new Error(`Route cannot have both a 'redirect' and nested 'routes'.`);
477
+ } else if (route.redirect && route.view) {
478
+ throw new Error(`Route cannot have both a 'redirect' and a 'view'.`);
479
+ } else if (!route.view && !route.routes && !route.redirect) {
480
+ throw new Error(`Route must have a 'view', a 'redirect', or a set of nested 'routes'.`);
481
+ }
482
+ let parts = [];
483
+ for (const parent2 of parents) {
484
+ parts.push(...splitPath(parent2.path));
485
+ }
486
+ parts.push(...splitPath(route.path));
487
+ if (parts[parts.length - 1] === "*") {
488
+ parts.pop();
489
+ }
490
+ const routes = [];
491
+ if (route.redirect) {
492
+ let redirect = route.redirect;
493
+ if (isString(redirect)) {
494
+ redirect = resolvePath(joinPath(parts), redirect);
495
+ if (!redirect.startsWith("/")) {
496
+ redirect = "/" + redirect;
497
+ }
396
498
  }
397
- });
398
- return n;
399
- };
400
- const ue = /(noopener|noreferrer) (noopener|noreferrer)/, fe = /^[\w-_]+:/;
401
- function de(i, t, e = window) {
402
- function s(n) {
403
- return !n || n === i ? null : n.localName !== "a" || n.href === void 0 ? s(n.parentNode) : n;
404
- }
405
- function r(n) {
406
- if (n.button && n.button !== 0 || n.ctrlKey || n.metaKey || n.altKey || n.shiftKey || n.defaultPrevented)
499
+ routes.push({
500
+ pattern: "/" + joinPath([...parts, ...splitPath(route.path)]),
501
+ meta: {
502
+ redirect
503
+ }
504
+ });
505
+ return routes;
506
+ }
507
+ let view = Passthrough;
508
+ if (isFunction(route.view)) {
509
+ view = route.view;
510
+ } else if (route.view) {
511
+ throw new TypeError(`Route '${route.path}' expected a view function or undefined. Got: ${route.view}`);
512
+ }
513
+ const layer = { id: this.#layerId++, view };
514
+ if (route.routes) {
515
+ for (const subroute of route.routes) {
516
+ routes.push(...this.#prepareRoute(subroute, [...parents, route], [...layers, layer]));
517
+ }
518
+ } else {
519
+ routes.push({
520
+ pattern: parent ? joinPath([...parents.map((p) => p.path), route.path]) : route.path,
521
+ meta: {
522
+ pattern: route.path,
523
+ layers: [...layers, layer],
524
+ beforeMatch: route.beforeMatch
525
+ }
526
+ });
527
+ }
528
+ return routes;
529
+ }
530
+ }
531
+ const safeExternalLink = /(noopener|noreferrer) (noopener|noreferrer)/;
532
+ const protocolLink = /^[\w-_]+:/;
533
+ function catchLinks(root, callback, _window = window) {
534
+ function traverse(node) {
535
+ if (!node || node === root) {
536
+ return null;
537
+ }
538
+ if (node.localName !== "a" || node.href === void 0) {
539
+ return traverse(node.parentNode);
540
+ }
541
+ return node;
542
+ }
543
+ function handler(e) {
544
+ if (e.button && e.button !== 0 || e.ctrlKey || e.metaKey || e.altKey || e.shiftKey || e.defaultPrevented) {
545
+ return;
546
+ }
547
+ const anchor = traverse(e.target);
548
+ if (!anchor) {
407
549
  return;
408
- const o = s(n.target);
409
- o && (e.location.protocol !== o.protocol || e.location.hostname !== o.hostname || e.location.port !== o.port || o.hasAttribute("data-router-ignore") || o.hasAttribute("download") || o.getAttribute("target") === "_blank" && ue.test(o.getAttribute("rel")) || fe.test(o.getAttribute("href")) || (n.preventDefault(), t(o)));
550
+ }
551
+ if (_window.location.protocol !== anchor.protocol || _window.location.hostname !== anchor.hostname || _window.location.port !== anchor.port || anchor.hasAttribute("data-router-ignore") || anchor.hasAttribute("download") || anchor.getAttribute("target") === "_blank" && safeExternalLink.test(anchor.getAttribute("rel")) || protocolLink.test(anchor.getAttribute("href"))) {
552
+ return;
553
+ }
554
+ e.preventDefault();
555
+ callback(anchor);
410
556
  }
411
- return i.addEventListener("click", r), function() {
412
- i.removeEventListener("click", r);
557
+ root.addEventListener("click", handler);
558
+ return function cancel() {
559
+ root.removeEventListener("click", handler);
413
560
  };
414
561
  }
415
- function me(i, t) {
416
- for (const e in t) {
417
- const s = t[e].toString();
418
- i = i.replace(`{${e}}`, s).replace(`{#${e}}`, s);
562
+ function replaceParams(path, params) {
563
+ for (const key in params) {
564
+ const value = params[key].toString();
565
+ path = path.replace(`{${key}}`, value).replace(`{#${key}}`, value);
419
566
  }
420
- return i;
567
+ return path;
421
568
  }
422
- function pe(i) {
423
- for (const t of i)
424
- if (t.meta.redirect) {
425
- let e;
426
- if (!B(t.meta.redirect)) if (M(t.meta.redirect)) {
427
- if (e = t.meta.redirect, !Ct(i, e, {
569
+ function assertValidRedirects(routes) {
570
+ for (const route of routes) {
571
+ if (route.meta.redirect) {
572
+ let redirectPath;
573
+ if (isFunction(route.meta.redirect)) ; else if (isString(route.meta.redirect)) {
574
+ redirectPath = route.meta.redirect;
575
+ const match = matchRoutes(routes, redirectPath, {
428
576
  willMatch(r) {
429
- return r !== t;
577
+ return r !== route;
430
578
  }
431
- }))
432
- throw new Error(`Found a redirect to an undefined URL. From '${t.pattern}' to '${t.meta.redirect}'`);
433
- } else
434
- throw new TypeError(`Expected a string or redirect function. Got: ${t.meta.redirect}`);
579
+ });
580
+ if (!match) {
581
+ throw new Error(`Found a redirect to an undefined URL. From '${route.pattern}' to '${route.meta.redirect}'`);
582
+ }
583
+ } else {
584
+ throw new TypeError(`Expected a string or redirect function. Got: ${route.meta.redirect}`);
585
+ }
435
586
  }
587
+ }
436
588
  }
437
- class we extends Error {
589
+ class NoRouteError extends Error {
438
590
  }
439
- var D, ut, R, _;
440
- class ge {
441
- // #dolla: Dolla;
442
- // #logger: Logger;
443
- constructor() {
444
- u(this, R);
445
- u(this, D, []);
446
- u(this, ut, ye());
447
- }
591
+
592
+ class HTTP {
593
+ #middleware = [];
594
+ #fetch = getDefaultFetch();
448
595
  /**
449
596
  * Adds a new middleware that will apply to subsequent requests.
450
597
  * Returns a function to remove this middleware.
451
598
  *
452
599
  * @param middleware - A middleware function that will intercept requests.
453
600
  */
454
- use(t) {
455
- return a(this, D).push(t), () => {
456
- a(this, D).splice(a(this, D).indexOf(t), 1);
601
+ use(fn) {
602
+ this.#middleware.push(fn);
603
+ return () => {
604
+ this.#middleware.splice(this.#middleware.indexOf(fn), 1);
457
605
  };
458
606
  }
459
- async get(t, e) {
460
- return f(this, R, _).call(this, "get", t, e);
607
+ async get(uri, options) {
608
+ return this.#request("get", uri, options);
461
609
  }
462
- async put(t, e) {
463
- return f(this, R, _).call(this, "put", t, e);
610
+ async put(uri, options) {
611
+ return this.#request("put", uri, options);
464
612
  }
465
- async patch(t, e) {
466
- return f(this, R, _).call(this, "patch", t, e);
613
+ async patch(uri, options) {
614
+ return this.#request("patch", uri, options);
467
615
  }
468
- async post(t, e) {
469
- return f(this, R, _).call(this, "post", t, e);
616
+ async post(uri, options) {
617
+ return this.#request("post", uri, options);
470
618
  }
471
- async delete(t, e) {
472
- return f(this, R, _).call(this, "delete", t, e);
619
+ async delete(uri, options) {
620
+ return this.#request("delete", uri, options);
473
621
  }
474
- async head(t, e) {
475
- return f(this, R, _).call(this, "head", t, e);
622
+ async head(uri, options) {
623
+ return this.#request("head", uri, options);
476
624
  }
477
- async options(t, e) {
478
- return f(this, R, _).call(this, "options", t, e);
625
+ async options(uri, options) {
626
+ return this.#request("options", uri, options);
479
627
  }
480
- async trace(t, e) {
481
- return f(this, R, _).call(this, "trace", t, e);
628
+ async trace(uri, options) {
629
+ return this.#request("trace", uri, options);
630
+ }
631
+ async #request(method, uri, options) {
632
+ const runner = new Runner({
633
+ ...options,
634
+ method,
635
+ uri,
636
+ middleware: this.#middleware,
637
+ fetch: this.#fetch
638
+ });
639
+ return runner.fetch();
482
640
  }
483
641
  }
484
- D = new WeakMap(), ut = new WeakMap(), R = new WeakSet(), _ = async function(t, e, s) {
485
- return new Ee({
486
- ...s,
487
- method: t,
488
- uri: e,
489
- middleware: a(this, D),
490
- fetch: a(this, ut)
491
- }).fetch();
492
- };
493
- function ye() {
494
- if (typeof window < "u" && window.fetch)
642
+ function getDefaultFetch() {
643
+ if (typeof window !== "undefined" && window.fetch) {
495
644
  return window.fetch.bind(window);
496
- if (typeof global < "u" && global.fetch)
645
+ }
646
+ if (typeof global !== "undefined" && global.fetch) {
497
647
  return global.fetch.bind(global);
648
+ }
498
649
  throw new Error("Running in neither browser nor node. Please run this app in one of the supported environments.");
499
650
  }
500
- class be extends Error {
501
- constructor(e) {
502
- const { status: s, statusText: r, method: n, url: o } = e, c = `${s} ${r}: Request failed (${n.toUpperCase()} ${o.toString()})`;
503
- super(c);
504
- w(this, "response");
505
- this.response = e;
651
+ class HTTPResponseError extends Error {
652
+ response;
653
+ constructor(response) {
654
+ const { status, statusText, method, url } = response;
655
+ const message = `${status} ${statusText}: Request failed (${method.toUpperCase()} ${url.toString()})`;
656
+ super(message);
657
+ this.response = response;
506
658
  }
507
659
  }
508
- class ve {
509
- constructor(t) {
510
- w(this, "method");
511
- w(this, "url");
512
- w(this, "headers", new Headers());
513
- w(this, "body");
514
- this.method = t.method, this.body = t.body, t.uri.startsWith("http") ? this.url = new URL(t.uri) : this.url = new URL(t.uri, window.location.origin), this._applyHeaders(t.headers), this._applyQueryParams(t.query);
515
- }
660
+ class Request {
661
+ method;
662
+ url;
663
+ headers = new Headers();
664
+ body;
516
665
  get isSameOrigin() {
517
666
  return this.url.origin === window.location.origin;
518
667
  }
519
- _applyHeaders(t) {
520
- if (t != null)
521
- if (t instanceof Map || t instanceof Headers)
522
- t.forEach((e, s) => {
523
- this.headers.set(s, e);
524
- });
525
- else if (V(t))
526
- for (const e in t) {
527
- const s = t[e];
528
- s instanceof Date ? this.headers.set(e, s.toISOString()) : s != null && this.headers.set(e, String(s));
668
+ constructor(config) {
669
+ this.method = config.method;
670
+ this.body = config.body;
671
+ if (config.uri.startsWith("http")) {
672
+ this.url = new URL(config.uri);
673
+ } else {
674
+ this.url = new URL(config.uri, window.location.origin);
675
+ }
676
+ this._applyHeaders(config.headers);
677
+ this._applyQueryParams(config.query);
678
+ }
679
+ _applyHeaders(headers) {
680
+ if (headers == null) return;
681
+ if (headers instanceof Map || headers instanceof Headers) {
682
+ headers.forEach((value, key) => {
683
+ this.headers.set(key, value);
684
+ });
685
+ } else if (isObject(headers)) {
686
+ for (const name in headers) {
687
+ const value = headers[name];
688
+ if (value instanceof Date) {
689
+ this.headers.set(name, value.toISOString());
690
+ } else if (value != null) {
691
+ this.headers.set(name, String(value));
529
692
  }
530
- else
531
- throw new TypeError(`Unknown headers type. Got: ${t}`);
532
- }
533
- _applyQueryParams(t) {
534
- if (t != null)
535
- if (t instanceof Map || t instanceof URLSearchParams)
536
- t.forEach((e, s) => {
537
- this.url.searchParams.set(s, e);
538
- });
539
- else if (V(t))
540
- for (const e in t) {
541
- const s = t[e];
542
- s instanceof Date ? this.url.searchParams.set(e, s.toISOString()) : s != null && this.url.searchParams.set(e, String(s));
693
+ }
694
+ } else {
695
+ throw new TypeError(`Unknown headers type. Got: ${headers}`);
696
+ }
697
+ }
698
+ _applyQueryParams(query) {
699
+ if (query == null) return;
700
+ if (query instanceof Map || query instanceof URLSearchParams) {
701
+ query.forEach((value, key) => {
702
+ this.url.searchParams.set(key, value);
703
+ });
704
+ } else if (isObject(query)) {
705
+ for (const name in query) {
706
+ const value = query[name];
707
+ if (value instanceof Date) {
708
+ this.url.searchParams.set(name, value.toISOString());
709
+ } else if (value != null) {
710
+ this.url.searchParams.set(name, String(value));
543
711
  }
544
- else
545
- throw new TypeError(`Unknown query params type. Got: ${t}`);
712
+ }
713
+ } else {
714
+ throw new TypeError(`Unknown query params type. Got: ${query}`);
715
+ }
546
716
  }
547
717
  }
548
- class Ee {
549
- constructor(t) {
550
- w(this, "_middleware");
551
- w(this, "_fetch");
552
- w(this, "_request");
553
- w(this, "_response");
554
- this._middleware = t.middleware, this._fetch = t.fetch, this._request = new ve(t);
718
+ class Runner {
719
+ _middleware;
720
+ _fetch;
721
+ _request;
722
+ _response;
723
+ constructor(config) {
724
+ this._middleware = config.middleware;
725
+ this._fetch = config.fetch;
726
+ this._request = new Request(config);
555
727
  }
556
728
  async fetch() {
557
729
  if (this._middleware.length > 0) {
558
- const t = (e = 0) => {
559
- const s = this._middleware[e], r = this._middleware[e + 1] ? t(e + 1) : this._handler.bind(this);
560
- return async () => s(this._request, async () => (await r(), this._response));
730
+ const mount = (index = 0) => {
731
+ const current = this._middleware[index];
732
+ const next = this._middleware[index + 1] ? mount(index + 1) : this._handler.bind(this);
733
+ return async () => current(this._request, async () => {
734
+ await next();
735
+ return this._response;
736
+ });
561
737
  };
562
- await t()();
563
- } else
738
+ await mount()();
739
+ } else {
564
740
  await this._handler();
565
- if (this._response.status < 200 || this._response.status >= 400)
566
- throw new be(this._response);
741
+ }
742
+ if (this._response.status < 200 || this._response.status >= 400) {
743
+ throw new HTTPResponseError(this._response);
744
+ }
567
745
  return this._response;
568
746
  }
569
747
  // This is the function that performs the actual request after the final middleware.
570
748
  async _handler() {
571
- let t;
572
- const e = this._request;
573
- !e.headers.has("content-type") && V(e.body) ? (e.headers.set("content-type", "application/json"), t = JSON.stringify(e.body)) : t = e.body;
574
- const s = await this._fetch(e.url.toString(), {
575
- method: e.method,
576
- headers: e.headers,
577
- body: t
578
- }), r = s.headers.get("content-type");
579
- let n;
580
- r != null && r.includes("application/json") ? n = await s.json() : r != null && r.includes("application/x-www-form-urlencoded") ? n = await s.formData() : n = await s.text(), this._response = {
581
- method: e.method,
582
- url: e.url,
583
- status: s.status,
584
- statusText: s.statusText,
585
- headers: s.headers,
586
- body: n
749
+ let reqBody;
750
+ const req = this._request;
751
+ if (!req.headers.has("content-type") && isObject(req.body)) {
752
+ req.headers.set("content-type", "application/json");
753
+ reqBody = JSON.stringify(req.body);
754
+ } else {
755
+ reqBody = req.body;
756
+ }
757
+ const fetched = await this._fetch(req.url.toString(), {
758
+ method: req.method,
759
+ headers: req.headers,
760
+ body: reqBody
761
+ });
762
+ const contentType = fetched.headers.get("content-type");
763
+ let body;
764
+ if (contentType?.includes("application/json")) {
765
+ body = await fetched.json();
766
+ } else if (contentType?.includes("application/x-www-form-urlencoded")) {
767
+ body = await fetched.formData();
768
+ } else {
769
+ body = await fetched.text();
770
+ }
771
+ this._response = {
772
+ method: req.method,
773
+ url: req.url,
774
+ status: fetched.status,
775
+ statusText: fetched.statusText,
776
+ headers: fetched.headers,
777
+ body
587
778
  };
588
779
  }
589
780
  }
590
- var ft, H, q, bt, Wt;
591
- class ke {
592
- constructor(t, e) {
593
- u(this, q);
594
- w(this, "dolla");
595
- w(this, "config");
596
- u(this, ft, !1);
597
- u(this, H, /* @__PURE__ */ new Map());
598
- this.config = t, this.dolla = e;
781
+
782
+ class Translation {
783
+ dolla;
784
+ config;
785
+ #isLoaded = false;
786
+ #templates = /* @__PURE__ */ new Map();
787
+ constructor(config, dolla) {
788
+ this.config = config;
789
+ this.dolla = dolla;
599
790
  }
600
791
  async load() {
601
- let t;
602
- if (!a(this, ft)) {
603
- if (V(this.config.strings))
604
- t = this.config.strings;
605
- else if (B(this.config.fetch)) {
606
- if (t = await this.config.fetch(), !V(t))
607
- throw new Error(`Fetch function did not return an object of language strings: ${t}`);
608
- } else if (M(this.config.path)) {
609
- const e = await fetch(this.config.path);
610
- if (e.ok) {
611
- const s = await e.json();
612
- if (V(s))
613
- t = s;
614
- else
792
+ let strings;
793
+ if (!this.#isLoaded) {
794
+ if (isObject(this.config.strings)) {
795
+ strings = this.config.strings;
796
+ } else if (isFunction(this.config.fetch)) {
797
+ strings = await this.config.fetch();
798
+ if (!isObject(strings)) {
799
+ throw new Error(`Fetch function did not return an object of language strings: ${strings}`);
800
+ }
801
+ } else if (isString(this.config.path)) {
802
+ const res = await fetch(this.config.path);
803
+ if (res.ok) {
804
+ const body = await res.json();
805
+ if (isObject(body)) {
806
+ strings = body;
807
+ } else {
615
808
  throw new Error(
616
- `Language path '${this.config.path}' did not return an object of language strings: ${s}`
809
+ `Language path '${this.config.path}' did not return an object of language strings: ${body}`
617
810
  );
618
- } else
619
- throw new Error("HTTP request failed.");
811
+ }
812
+ } else {
813
+ throw new Error(`HTTP request failed.`);
814
+ }
815
+ }
816
+ }
817
+ if (strings) {
818
+ const entries = this.#compile(strings);
819
+ for (const entry of entries) {
820
+ this.#templates.set(entry[0], entry[1]);
620
821
  }
822
+ } else {
823
+ throw new Error(`Language could not be loaded.`);
621
824
  }
622
- if (t) {
623
- const e = f(this, q, bt).call(this, t);
624
- for (const s of e)
625
- a(this, H).set(s[0], s[1]);
626
- } else
627
- throw new Error("Language could not be loaded.");
628
825
  }
629
- getTemplate(t) {
630
- return a(this, H).get(t) ?? {
631
- segments: [{ type: 0, text: `[MISSING: ${t}]` }]
826
+ getTemplate(selector) {
827
+ return this.#templates.get(selector) ?? {
828
+ segments: [{ type: 0 /* Static */, text: `[MISSING: ${selector}]` }]
632
829
  };
633
830
  }
634
- hasTemplate(t) {
635
- return a(this, H).has(t);
831
+ hasTemplate(selector) {
832
+ return this.#templates.has(selector);
636
833
  }
637
- }
638
- ft = new WeakMap(), H = new WeakMap(), q = new WeakSet(), bt = function(t, e = []) {
639
- const s = [];
640
- for (const r in t)
641
- switch (St(t[r])) {
642
- case "string":
643
- s.push([[...e, r].join("."), f(this, q, Wt).call(this, t[r])]);
644
- break;
645
- case "object":
646
- s.push(...f(this, q, bt).call(this, t[r], [...e, r]));
647
- break;
648
- default:
649
- throw new Error(
650
- `Expected to find a string or object at ${[...e, r].join(".")}. Got: ${St(t[r])}`
651
- );
834
+ #compile(strings, path = []) {
835
+ const entries = [];
836
+ for (const key in strings) {
837
+ switch (typeOf(strings[key])) {
838
+ case "string":
839
+ entries.push([[...path, key].join("."), this.#parseTemplate(strings[key])]);
840
+ break;
841
+ case "object":
842
+ entries.push(...this.#compile(strings[key], [...path, key]));
843
+ break;
844
+ default:
845
+ throw new Error(
846
+ `Expected to find a string or object at ${[...path, key].join(".")}. Got: ${typeOf(strings[key])}`
847
+ );
848
+ }
652
849
  }
653
- return s;
654
- }, Wt = function(t) {
655
- let e;
656
- ((h) => {
657
- h[h.Static = 0] = "Static", h[h.ValueName = 1] = "ValueName", h[h.FormatName = 2] = "FormatName", h[h.FormatOptionName = 3] = "FormatOptionName", h[h.FormatOptionValue = 4] = "FormatOptionValue", h[h.FormatOptionEnd = 5] = "FormatOptionEnd";
658
- })(e || (e = {}));
659
- const s = {
660
- segments: []
661
- };
662
- let r = "", n = 0, o = 0, c, l, p;
663
- const v = () => {
664
- c = {
665
- type: 1,
666
- name: "",
667
- formats: []
850
+ return entries;
851
+ }
852
+ #parseTemplate(template) {
853
+ let Loc;
854
+ ((Loc2) => {
855
+ Loc2[Loc2["Static"] = 0] = "Static";
856
+ Loc2[Loc2["ValueName"] = 1] = "ValueName";
857
+ Loc2[Loc2["FormatName"] = 2] = "FormatName";
858
+ Loc2[Loc2["FormatOptionName"] = 3] = "FormatOptionName";
859
+ Loc2[Loc2["FormatOptionValue"] = 4] = "FormatOptionValue";
860
+ Loc2[Loc2["FormatOptionEnd"] = 5] = "FormatOptionEnd";
861
+ })(Loc || (Loc = {}));
862
+ const parsed = {
863
+ segments: []
668
864
  };
669
- }, k = () => {
670
- l = {
671
- name: "",
672
- options: {}
865
+ let buffer = "";
866
+ let i = 0;
867
+ let loc = 0 /* Static */;
868
+ let segment;
869
+ let format;
870
+ let formatOptionName;
871
+ const startSegment = () => {
872
+ segment = {
873
+ type: 1 /* Variable */,
874
+ name: "",
875
+ formats: []
876
+ };
673
877
  };
674
- };
675
- for (; n < t.length; ) {
676
- if (o !== 0 && t[n] === " ") {
677
- n++;
678
- continue;
679
- }
680
- switch (o) {
681
- case 0:
682
- t[n] === "{" && t[n + 1] === "{" ? (o = 1, n += 2, r.length > 0 && (s.segments.push({ type: 0, text: r }), r = ""), v()) : (r += t[n], n++);
683
- break;
684
- case 1:
685
- t[n] === "|" ? (o = 2, n += 1, c.name = r, r = "", k()) : t[n] === "}" && t[n + 1] === "}" ? (o = 0, n += 2, c.name = r, r = "", s.segments.push(c)) : (r += t[n], n++);
686
- break;
687
- case 2:
688
- t[n] === "(" ? (o = 3, n += 1, l.name = r, r = "") : t[n] === "}" && t[n + 1] === "}" ? (o = 0, n += 2, c.formats.push(l), s.segments.push(c)) : (r += t[n], n++);
689
- break;
690
- case 3:
691
- t[n] === ")" || (t[n] === ":" ? (o = 4, n += 1, p = r, r = "") : t[n] === "}" && t[n + 1] === "}" || (r += t[n], n++));
692
- break;
693
- case 4:
694
- t[n] === ")" ? (o = 5, n += 1, l.options[p] = r, r = "", c.formats.push(l)) : t[n] === "," ? (o = 3, n += 1, l.options[p] = r, r = "") : t[n] === "}" && t[n + 1] === "}" || (r += t[n], n++);
695
- break;
696
- case 5:
697
- t[n] === "|" ? (o = 2, n += 1, k()) : t[n] === "}" && t[n + 1] === "}" && (o = 0, n += 2, s.segments.push(c));
698
- break;
699
- }
700
- }
701
- return o === 0 && r.length > 0 && s.segments.push({ type: 0, text: r }), s;
702
- };
703
- var X, P, g, Y, Z, tt, b, y, At, vt, Et, kt, Ht;
704
- class $e {
705
- constructor(t) {
706
- u(this, y);
707
- u(this, X);
708
- u(this, P);
709
- u(this, g, /* @__PURE__ */ new Map());
710
- u(this, Y, []);
711
- u(this, Z, /* @__PURE__ */ new Map());
712
- u(this, tt, "auto");
713
- u(this, b, Ut(""));
714
- d(this, X, t), d(this, P, t.createLogger("Dolla.i18n")), this.addFormat("number", (e, s, r) => f(this, y, vt).call(this, Number(s), r)), this.addFormat("datetime", (e, s, r) => f(this, y, Et).call(this, s, r)), this.addFormat("list", (e, s, r) => f(this, y, kt).call(this, s, r)), t.beforeMount(async () => {
715
- a(this, g).size > 0 && await this.setLocale(a(this, tt));
716
- });
878
+ const startFormat = () => {
879
+ format = {
880
+ name: "",
881
+ options: {}
882
+ };
883
+ };
884
+ while (i < template.length) {
885
+ if (loc !== 0 /* Static */ && template[i] === " ") {
886
+ i++;
887
+ continue;
888
+ }
889
+ switch (loc) {
890
+ case 0 /* Static */:
891
+ if (template[i] === "{" && template[i + 1] === "{") {
892
+ loc = 1 /* ValueName */;
893
+ i += 2;
894
+ if (buffer.length > 0) {
895
+ parsed.segments.push({ type: 0 /* Static */, text: buffer });
896
+ buffer = "";
897
+ }
898
+ startSegment();
899
+ } else {
900
+ buffer += template[i];
901
+ i++;
902
+ }
903
+ break;
904
+ case 1 /* ValueName */:
905
+ if (template[i] === "|") {
906
+ loc = 2 /* FormatName */;
907
+ i += 1;
908
+ segment.name = buffer;
909
+ buffer = "";
910
+ startFormat();
911
+ } else if (template[i] === "}" && template[i + 1] === "}") {
912
+ loc = 0 /* Static */;
913
+ i += 2;
914
+ segment.name = buffer;
915
+ buffer = "";
916
+ parsed.segments.push(segment);
917
+ } else {
918
+ buffer += template[i];
919
+ i++;
920
+ }
921
+ break;
922
+ case 2 /* FormatName */:
923
+ if (template[i] === "(") {
924
+ loc = 3 /* FormatOptionName */;
925
+ i += 1;
926
+ format.name = buffer;
927
+ buffer = "";
928
+ } else if (template[i] === "}" && template[i + 1] === "}") {
929
+ loc = 0 /* Static */;
930
+ i += 2;
931
+ segment.formats.push(format);
932
+ parsed.segments.push(segment);
933
+ } else {
934
+ buffer += template[i];
935
+ i++;
936
+ }
937
+ break;
938
+ case 3 /* FormatOptionName */:
939
+ if (template[i] === ")") ; else if (template[i] === ":") {
940
+ loc = 4 /* FormatOptionValue */;
941
+ i += 1;
942
+ formatOptionName = buffer;
943
+ buffer = "";
944
+ } else if (template[i] === "}" && template[i + 1] === "}") ; else {
945
+ buffer += template[i];
946
+ i++;
947
+ }
948
+ break;
949
+ case 4 /* FormatOptionValue */:
950
+ if (template[i] === ")") {
951
+ loc = 5 /* FormatOptionEnd */;
952
+ i += 1;
953
+ format.options[formatOptionName] = buffer;
954
+ buffer = "";
955
+ segment.formats.push(format);
956
+ } else if (template[i] === ",") {
957
+ loc = 3 /* FormatOptionName */;
958
+ i += 1;
959
+ format.options[formatOptionName] = buffer;
960
+ buffer = "";
961
+ } else if (template[i] === "}" && template[i + 1] === "}") ; else {
962
+ buffer += template[i];
963
+ i++;
964
+ }
965
+ break;
966
+ case 5 /* FormatOptionEnd */:
967
+ if (template[i] === "|") {
968
+ loc = 2 /* FormatName */;
969
+ i += 1;
970
+ startFormat();
971
+ } else if (template[i] === "}" && template[i + 1] === "}") {
972
+ loc = 0 /* Static */;
973
+ i += 2;
974
+ parsed.segments.push(segment);
975
+ } else ;
976
+ break;
977
+ }
978
+ }
979
+ if (loc === 0 /* Static */ && buffer.length > 0) {
980
+ parsed.segments.push({ type: 0 /* Static */, text: buffer });
981
+ }
982
+ return parsed;
717
983
  }
984
+ }
985
+ class I18n {
986
+ #dolla;
987
+ #logger;
988
+ #translations = /* @__PURE__ */ new Map();
989
+ #cache = [];
990
+ #formats = /* @__PURE__ */ new Map();
991
+ #initialLocale = "auto";
992
+ #locale = atom("");
718
993
  get locale() {
719
- return a(this, b);
994
+ return this.#locale;
995
+ }
996
+ constructor(dolla) {
997
+ this.#dolla = dolla;
998
+ this.#logger = dolla.createLogger("Dolla.i18n");
999
+ this.addFormat("number", (_, value, options) => {
1000
+ return this.#formatNumber(Number(value), options);
1001
+ });
1002
+ this.addFormat("datetime", (_, value, options) => {
1003
+ return this.#formatDateTime(value, options);
1004
+ });
1005
+ this.addFormat("list", (_, value, options) => {
1006
+ return this.#formatList(value, options);
1007
+ });
1008
+ dolla.beforeMount(async () => {
1009
+ if (this.#translations.size > 0) {
1010
+ await this.setLocale(this.#initialLocale);
1011
+ }
1012
+ });
720
1013
  }
721
1014
  get locales() {
722
- return [...a(this, g).keys()];
723
- }
724
- setup(t) {
725
- if (t.translations.forEach((e) => {
726
- a(this, g).set(e.locale, new ke(e, a(this, X)));
727
- }), t.locale && t.locale !== "auto") {
728
- if (!t.translations.some((s) => s.locale === t.locale))
729
- throw new Error(`Initial locale '${t.locale}' is not registered in the locales array.`);
730
- d(this, tt, t.locale);
731
- }
732
- a(this, P).info(
733
- `${a(this, g).size} language${a(this, g).size === 1 ? "" : "s"} supported: '${[...a(this, g).keys()].join("', '")}'`
1015
+ return [...this.#translations.keys()];
1016
+ }
1017
+ setup(options) {
1018
+ options.translations.forEach((entry) => {
1019
+ this.#translations.set(entry.locale, new Translation(entry, this.#dolla));
1020
+ });
1021
+ if (options.locale && options.locale !== "auto") {
1022
+ const isRegistered = options.translations.some((entry) => entry.locale === options.locale);
1023
+ if (!isRegistered) {
1024
+ throw new Error(`Initial locale '${options.locale}' is not registered in the locales array.`);
1025
+ }
1026
+ this.#initialLocale = options.locale;
1027
+ }
1028
+ this.#logger.info(
1029
+ `${this.#translations.size} language${this.#translations.size === 1 ? "" : "s"} supported: '${[...this.#translations.keys()].join("', '")}'`
734
1030
  );
735
1031
  }
736
- async setLocale(t) {
737
- var r;
738
- let e;
739
- if (t === "auto") {
740
- let n = [];
741
- if (typeof navigator < "u") {
742
- const o = navigator;
743
- ((r = o.languages) == null ? void 0 : r.length) > 0 ? n.push(...o.languages) : o.language ? n.push(o.language) : o.browserLanguage ? n.push(o.browserLanguage) : o.userLanguage && n.push(o.userLanguage);
1032
+ async setLocale(name) {
1033
+ let realName;
1034
+ if (name === "auto") {
1035
+ let names = [];
1036
+ if (typeof navigator !== "undefined") {
1037
+ const nav = navigator;
1038
+ if (nav.languages?.length > 0) {
1039
+ names.push(...nav.languages);
1040
+ } else if (nav.language) {
1041
+ names.push(nav.language);
1042
+ } else if (nav.browserLanguage) {
1043
+ names.push(nav.browserLanguage);
1044
+ } else if (nav.userLanguage) {
1045
+ names.push(nav.userLanguage);
1046
+ }
1047
+ }
1048
+ for (const name2 of names) {
1049
+ if (this.#translations.has(name2)) {
1050
+ realName = name2;
1051
+ }
1052
+ }
1053
+ } else {
1054
+ if (this.#translations.has(name)) {
1055
+ realName = name;
1056
+ }
1057
+ }
1058
+ if (realName == null) {
1059
+ const firstLanguage = this.#translations.keys().next().value;
1060
+ if (firstLanguage) {
1061
+ realName = firstLanguage;
744
1062
  }
745
- for (const o of n)
746
- a(this, g).has(o) && (e = o);
747
- } else
748
- a(this, g).has(t) && (e = t);
749
- if (e == null) {
750
- const n = a(this, g).keys().next().value;
751
- n && (e = n);
752
- }
753
- if (!e || !a(this, g).has(e))
754
- throw new Error(`Locale '${t}' has no translation.`);
755
- const s = a(this, g).get(e);
1063
+ }
1064
+ if (!realName || !this.#translations.has(realName)) {
1065
+ throw new Error(`Locale '${name}' has no translation.`);
1066
+ }
1067
+ const translation = this.#translations.get(realName);
756
1068
  try {
757
- await s.load(), d(this, Y, []), a(this, b).value = e, a(this, P).info("set language to " + e);
758
- } catch (n) {
759
- n instanceof Error && a(this, P).crash(n);
1069
+ await translation.load();
1070
+ this.#cache = [];
1071
+ this.#locale.value = realName;
1072
+ this.#logger.info("set language to " + realName);
1073
+ } catch (error) {
1074
+ if (error instanceof Error) {
1075
+ this.#logger.crash(error);
1076
+ }
760
1077
  }
761
1078
  }
762
1079
  /**
@@ -768,18 +1085,71 @@ class $e {
768
1085
  * @example
769
1086
  * const $value = t("your.key.here", { count: 5 });
770
1087
  */
771
- t(t, e) {
772
- if (this === void 0)
1088
+ t(selector, options) {
1089
+ if (this === void 0) {
773
1090
  throw new Error(
774
1091
  `The 't' function cannot be destructured. If you need a standalone version you can import it like so: 'import { t } from "@manyducks.co/dolla"'`
775
1092
  );
776
- return U(() => {
777
- const s = {};
778
- for (const r in e)
779
- s[r] = E(e[r]);
780
- return f(this, y, At).call(this, E(a(this, b)), t, s);
1093
+ }
1094
+ return compose(() => {
1095
+ const values = {};
1096
+ for (const key in options) {
1097
+ values[key] = get(options[key]);
1098
+ }
1099
+ return this.#getValue(get(this.#locale), selector, values);
781
1100
  });
782
1101
  }
1102
+ #getValue(locale, selector, options) {
1103
+ const cached = this.#getCached(selector, options);
1104
+ if (cached) return cached;
1105
+ const translation = this.#translations.get(locale);
1106
+ if (options.context != null) {
1107
+ selector += "_" + options.context;
1108
+ }
1109
+ if (options.count != null) {
1110
+ if (options.ordinal) {
1111
+ const exact = `${selector}_ordinal_(=${options.count})`;
1112
+ if (translation.hasTemplate(exact)) {
1113
+ selector = exact;
1114
+ } else {
1115
+ selector += "_ordinal_" + new Intl.PluralRules(locale, { type: "ordinal" }).select(options.count);
1116
+ }
1117
+ } else {
1118
+ const exact = `${selector}_(=${options.count})`;
1119
+ if (translation.hasTemplate(exact)) {
1120
+ selector = exact;
1121
+ } else {
1122
+ selector += "_" + new Intl.PluralRules(locale).select(options.count);
1123
+ }
1124
+ }
1125
+ }
1126
+ const template = translation.getTemplate(selector);
1127
+ let output = "";
1128
+ for (const segment of template.segments) {
1129
+ if (segment.type === 0 /* Static */) {
1130
+ output += segment.text;
1131
+ } else if (segment.type === 1 /* Variable */) {
1132
+ let value = resolve(options, segment.name);
1133
+ const formats = options.formatOverrides?.[segment.name] ?? [...segment.formats];
1134
+ if (segment.name === "count" && formats.length === 0) {
1135
+ formats.push({ name: "number", options: {} });
1136
+ }
1137
+ for (const format of formats) {
1138
+ const fn = this.#formats.get(format.name);
1139
+ if (fn == null) {
1140
+ const error = new Error(
1141
+ `Failed to load format '${format.name}' when processing '${selector}', template: ${template}`
1142
+ );
1143
+ this.#logger.crash(error);
1144
+ throw error;
1145
+ }
1146
+ value = fn(locale, value, format.options);
1147
+ }
1148
+ output += value;
1149
+ }
1150
+ }
1151
+ return output;
1152
+ }
783
1153
  /**
784
1154
  * Add a custom format callback.
785
1155
  *
@@ -794,8 +1164,8 @@ class $e {
794
1164
  *
795
1165
  * t("greeting", {name: "world"}); // State<"Hello, WORLD!">
796
1166
  */
797
- addFormat(t, e) {
798
- a(this, Z).set(t, e);
1167
+ addFormat(name, callback) {
1168
+ this.#formats.set(name, callback);
799
1169
  }
800
1170
  /**
801
1171
  * Creates an `Intl.Collator` configured for the current locale.
@@ -803,16 +1173,22 @@ class $e {
803
1173
  *
804
1174
  * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/Collator/Collator#options
805
1175
  */
806
- collator(t) {
807
- return new Intl.Collator(a(this, b).value, t);
1176
+ collator(options) {
1177
+ return new Intl.Collator(this.#locale.value, options);
808
1178
  }
809
1179
  /**
810
1180
  * Formats a number for the current locale. Uses `Intl.NumberFormat` under the hood.
811
1181
  *
812
1182
  * @see https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Intl/NumberFormat/NumberFormat#options
813
1183
  */
814
- number(t, e) {
815
- return U(() => (E(a(this, b)), f(this, y, vt).call(this, E(t), e)));
1184
+ number(count, options) {
1185
+ return compose(() => {
1186
+ get(this.#locale);
1187
+ return this.#formatNumber(get(count), options);
1188
+ });
1189
+ }
1190
+ #formatNumber(count, options) {
1191
+ return new Intl.NumberFormat(this.#locale.value, options).format(count);
816
1192
  }
817
1193
  /**
818
1194
  * Formats a date for the current locale. Uses `Intl.DateTimeFormat` under the hood.
@@ -823,8 +1199,14 @@ class $e {
823
1199
  * const date = new Date();
824
1200
  * const $formatted = Dolla.i18n.dateTime(date, { dateFormat: "short" });
825
1201
  */
826
- dateTime(t, e) {
827
- return U(() => (E(a(this, b)), f(this, y, Et).call(this, E(t), e)));
1202
+ dateTime(date, options) {
1203
+ return compose(() => {
1204
+ get(this.#locale);
1205
+ return this.#formatDateTime(get(date), options);
1206
+ });
1207
+ }
1208
+ #formatDateTime(date, options) {
1209
+ return new Intl.DateTimeFormat(this.#locale.value, options).format(isString(date) ? new Date(date) : date);
828
1210
  }
829
1211
  /**
830
1212
  * Formats a list for the current locale. Uses `Intl.ListFormat` under the hood.
@@ -835,69 +1217,41 @@ class $e {
835
1217
  * const list = new Date();
836
1218
  * const $formatted = Dolla.i18n.list(list, { });
837
1219
  */
838
- list(t, e) {
839
- return U(() => (E(a(this, b)), f(this, y, kt).call(this, E(t), e)));
1220
+ list(list, options) {
1221
+ return compose(() => {
1222
+ get(this.#locale);
1223
+ return this.#formatList(get(list), options);
1224
+ });
1225
+ }
1226
+ #formatList(list, options) {
1227
+ return new Intl.ListFormat(this.#locale.value, options).format(list);
1228
+ }
1229
+ // relativeTime(): State<string> {
1230
+ // }
1231
+ #getCached(key, values) {
1232
+ for (const entry of this.#cache) {
1233
+ if (entry[0] === key && deepEqual(entry[1], values)) {
1234
+ return entry[2];
1235
+ }
1236
+ }
840
1237
  }
841
1238
  }
842
- X = new WeakMap(), P = new WeakMap(), g = new WeakMap(), Y = new WeakMap(), Z = new WeakMap(), tt = new WeakMap(), b = new WeakMap(), y = new WeakSet(), At = function(t, e, s) {
843
- var l;
844
- const r = f(this, y, Ht).call(this, e, s);
845
- if (r) return r;
846
- const n = a(this, g).get(t);
847
- if (s.context != null && (e += "_" + s.context), s.count != null)
848
- if (s.ordinal) {
849
- const p = `${e}_ordinal_(=${s.count})`;
850
- n.hasTemplate(p) ? e = p : e += "_ordinal_" + new Intl.PluralRules(t, { type: "ordinal" }).select(s.count);
1239
+ function resolve(object, key) {
1240
+ const parsed = String(key).split(/[\.\[\]]/).filter((part) => part.trim() !== "");
1241
+ let value = object;
1242
+ while (parsed.length > 0) {
1243
+ const part = parsed.shift();
1244
+ if (value != null) {
1245
+ value = value[part];
851
1246
  } else {
852
- const p = `${e}_(=${s.count})`;
853
- n.hasTemplate(p) ? e = p : e += "_" + new Intl.PluralRules(t).select(s.count);
854
- }
855
- const o = n.getTemplate(e);
856
- let c = "";
857
- for (const p of o.segments)
858
- if (p.type === 0)
859
- c += p.text;
860
- else if (p.type === 1) {
861
- let v = Re(s, p.name);
862
- const k = ((l = s.formatOverrides) == null ? void 0 : l[p.name]) ?? [...p.formats];
863
- p.name === "count" && k.length === 0 && k.push({ name: "number", options: {} });
864
- for (const h of k) {
865
- const x = a(this, Z).get(h.name);
866
- if (x == null) {
867
- const $ = new Error(
868
- `Failed to load format '${h.name}' when processing '${e}', template: ${o}`
869
- );
870
- throw a(this, P).crash($), $;
871
- }
872
- v = x(t, v, h.options);
873
- }
874
- c += v;
875
- }
876
- return c;
877
- }, vt = function(t, e) {
878
- return new Intl.NumberFormat(a(this, b).value, e).format(t);
879
- }, Et = function(t, e) {
880
- return new Intl.DateTimeFormat(a(this, b).value, e).format(M(t) ? new Date(t) : t);
881
- }, kt = function(t, e) {
882
- return new Intl.ListFormat(a(this, b).value, e).format(t);
883
- }, // relativeTime(): State<string> {
884
- // }
885
- Ht = function(t, e) {
886
- for (const s of a(this, Y))
887
- if (s[0] === t && Jt(s[1], e))
888
- return s[2];
889
- };
890
- function Re(i, t) {
891
- const e = String(t).split(/[\.\[\]]/).filter((r) => r.trim() !== "");
892
- let s = i;
893
- for (; e.length > 0; ) {
894
- const r = e.shift();
895
- s != null ? s = s[r] : s = void 0;
896
- }
897
- return s;
1247
+ value = void 0;
1248
+ }
1249
+ }
1250
+ return value;
898
1251
  }
899
- function _e(i) {
900
- return Mt`
1252
+
1253
+ function DefaultCrashView(props) {
1254
+ return html`
901
1255
  <div
902
1256
  style=${{
903
1257
  backgroundColor: "#880000",
@@ -910,8 +1264,8 @@ function _e(i) {
910
1264
  >
911
1265
  <h1 style=${{ marginBottom: "0.5rem" }}>The app has crashed</h1>
912
1266
  <p style=${{ marginBottom: "0.25rem" }}>
913
- <span style=${{ fontFamily: "monospace" }}>${i.loggerName}</span>
914
- ${Xt(i.uid, Mt`<span style=${{ fontFamily: "monospace", opacity: 0.5 }}> [uid: ${i.uid}]</span>`)}
1267
+ <span style=${{ fontFamily: "monospace" }}>${props.loggerName}</span>
1268
+ ${cond(props.uid, html`<span style=${{ fontFamily: "monospace", opacity: 0.5 }}> [uid: ${props.uid}]</span>`)}
915
1269
  ${" "}says:
916
1270
  </p>
917
1271
  <blockquote
@@ -934,250 +1288,276 @@ function _e(i) {
934
1288
  fontWeight: "bold"
935
1289
  }}
936
1290
  >
937
- ${i.error.name}
1291
+ ${props.error.name}
938
1292
  </span>
939
- ${i.error.message}
1293
+ ${props.error.message}
940
1294
  </blockquote>
941
1295
 
942
1296
  <p>Please see the browser console for details.</p>
943
1297
  </div>
944
1298
  `;
945
1299
  }
946
- var L, et, F, I, st, z, nt, rt, at, it, O, ot, lt, dt;
947
- class Se {
1300
+
1301
+ class Dolla {
1302
+ // TODO: Take these off the global Dolla object.
1303
+ http;
1304
+ i18n;
1305
+ #isMounted = false;
1306
+ #env = "production";
1307
+ #rootElement;
1308
+ #rootView;
1309
+ #crashView = DefaultCrashView;
1310
+ #router;
1311
+ #beforeMountCallbacks = [];
1312
+ #onMountCallbacks = [];
1313
+ #beforeUnmountCallbacks = [];
1314
+ #onUnmountCallbacks = [];
1315
+ #rootElementContext = {
1316
+ root: this,
1317
+ stores: /* @__PURE__ */ new Map(),
1318
+ viewName: "Dolla"
1319
+ };
1320
+ #loggles = {
1321
+ info: "development",
1322
+ log: "development",
1323
+ warn: "development",
1324
+ error: true
1325
+ };
1326
+ #match = createMatcher("*,-Dolla.*");
1327
+ // Registration functions for modules.
1328
+ // All modules will be registered before mount.
1329
+ #modules = [];
948
1330
  constructor() {
949
- // TODO: Take these off the global Dolla object.
950
- w(this, "http");
951
- w(this, "i18n");
952
- u(this, L, !1);
953
- u(this, et, "production");
954
- u(this, F);
955
- u(this, I);
956
- u(this, st, _e);
957
- u(this, z);
958
- u(this, nt, []);
959
- u(this, rt, []);
960
- u(this, at, []);
961
- u(this, it, []);
962
- u(this, O, {
963
- root: this,
964
- stores: /* @__PURE__ */ new Map(),
965
- viewName: "Dolla"
966
- });
967
- u(this, ot, {
968
- info: "development",
969
- log: "development",
970
- warn: "development",
971
- error: !0
972
- });
973
- u(this, lt, xt("*,-Dolla.*"));
974
- // Registration functions for modules.
975
- // All modules will be registered before mount.
976
- u(this, dt, []);
977
- this.http = new ge(), this.i18n = new $e(this);
1331
+ this.http = new HTTP();
1332
+ this.i18n = new I18n(this);
978
1333
  }
979
1334
  /**
980
1335
  * True when the app is connected to a DOM node and displayed to the user.
981
1336
  */
982
1337
  get isMounted() {
983
- return a(this, L);
1338
+ return this.#isMounted;
984
1339
  }
985
1340
  /**
986
1341
  * Get the current environment that this app is running in.
987
1342
  * Environment affects which log messages will print and how much debugging info is included in the DOM.
988
1343
  */
989
1344
  getEnv() {
990
- return a(this, et);
1345
+ return this.#env;
991
1346
  }
992
1347
  /**
993
1348
  * Sets the environment that this app is running in.
994
1349
  * Environment affects which log messages will print and how much debugging info is included in the DOM.
995
1350
  */
996
- setEnv(t) {
997
- d(this, et, t);
1351
+ setEnv(value) {
1352
+ this.#env = value;
998
1353
  }
999
1354
  /**
1000
1355
  * Sets the view that will be shown when the `crash` method is called on any logger.
1001
1356
  * When a crash is reported the app will be unmounted and replaced with this crash page.
1002
1357
  */
1003
- setCrashView(t) {
1004
- d(this, st, t);
1358
+ setCrashView(view) {
1359
+ this.#crashView = view;
1005
1360
  }
1006
1361
  /**
1007
1362
  * Returns the HTMLElement Dolla is mounted to. This will return undefined until Dolla.mount() is called.
1008
1363
  */
1009
1364
  getRootElement() {
1010
- return a(this, F);
1365
+ return this.#rootElement;
1011
1366
  }
1012
1367
  /**
1013
1368
  * Returns the top level view Dolla is rendering inside the root element. This will return undefined until Dolla.mount() is called.
1014
1369
  */
1015
1370
  getRootView() {
1016
- return a(this, I);
1017
- }
1018
- provide(t, e) {
1019
- const s = new Yt(t, e);
1020
- if (s.attach(a(this, O)))
1021
- return s.value;
1022
- {
1023
- let n = t.name ? `'${t.name}'` : "this store";
1024
- return console.warn(`An instance of ${n} was already attached to this context.`), this.get(t);
1371
+ return this.#rootView;
1372
+ }
1373
+ provide(store, options) {
1374
+ const instance = new Store(store, options);
1375
+ const attached = instance.attach(this.#rootElementContext);
1376
+ if (!attached) {
1377
+ let name = store.name ? `'${store.name}'` : "this store";
1378
+ console.warn(`An instance of ${name} was already attached to this context.`);
1379
+ return this.get(store);
1380
+ } else {
1381
+ return instance.value;
1025
1382
  }
1026
1383
  }
1027
1384
  /**
1028
1385
  * Gets the nearest instance of a store. Throws an error if the store isn't provided higher in the tree.
1029
1386
  */
1030
- get(t) {
1031
- if (B(t)) {
1032
- const e = a(this, O).stores.get(t);
1033
- if (e == null)
1034
- throw new Tt("Store not found on this context.");
1035
- return e.value;
1036
- } else
1037
- throw new Tt("Invalid store.");
1038
- }
1039
- async mount(t, e) {
1040
- if (a(this, L))
1041
- throw new Error("Dolla is already mounted.");
1042
- if (M(t)) {
1043
- const n = document.querySelector(t);
1044
- Nt(HTMLElement, n, `Selector '${t}' did not match any element.`), d(this, F, n);
1045
- } else
1046
- Nt(HTMLElement, t, "Expected an HTML element or a selector string. Got type: %t, value: %v"), d(this, F, t);
1047
- pt(e) && d(this, z, e);
1048
- const s = pt(e) ? jt : e, r = Zt(s);
1049
- d(this, I, this.constructView(r.type, r.props)), await Promise.all(a(this, dt).map((n) => n())), pt(e) && await le(e, this), await Promise.all(a(this, nt).map((n) => n())), a(this, I).mount(a(this, F)), d(this, L, !0);
1050
- for (const n of a(this, O).stores.values())
1051
- n.handleMount();
1052
- for (const n of a(this, rt))
1053
- n();
1387
+ get(store) {
1388
+ if (isFunction(store)) {
1389
+ const instance = this.#rootElementContext.stores.get(store);
1390
+ if (instance == null) {
1391
+ throw new StoreError(`Store not found on this context.`);
1392
+ } else {
1393
+ return instance.value;
1394
+ }
1395
+ } else {
1396
+ throw new StoreError(`Invalid store.`);
1397
+ }
1398
+ }
1399
+ async mount(target, root) {
1400
+ if (this.#isMounted) {
1401
+ throw new Error(`Dolla is already mounted.`);
1402
+ }
1403
+ if (isString(target)) {
1404
+ const match = document.querySelector(target);
1405
+ assertInstanceOf(HTMLElement, match, `Selector '${target}' did not match any element.`);
1406
+ this.#rootElement = match;
1407
+ } else {
1408
+ assertInstanceOf(HTMLElement, target, "Expected an HTML element or a selector string. Got type: %t, value: %v");
1409
+ this.#rootElement = target;
1410
+ }
1411
+ if (_isRouter(root)) {
1412
+ this.#router = root;
1413
+ }
1414
+ const view = _isRouter(root) ? Passthrough : root;
1415
+ const rootViewMarkup = createMarkup(view);
1416
+ this.#rootView = this.constructView(rootViewMarkup.type, rootViewMarkup.props);
1417
+ await Promise.all(this.#modules.map((register) => register()));
1418
+ if (_isRouter(root)) {
1419
+ await _mountRouter(root, this);
1420
+ }
1421
+ await Promise.all(this.#beforeMountCallbacks.map((callback) => callback()));
1422
+ this.#rootView.mount(this.#rootElement);
1423
+ this.#isMounted = true;
1424
+ for (const store of this.#rootElementContext.stores.values()) {
1425
+ store.handleMount();
1426
+ }
1427
+ for (const callback of this.#onMountCallbacks) {
1428
+ callback();
1429
+ }
1054
1430
  }
1055
1431
  async unmount() {
1056
- var t;
1057
- if (a(this, L)) {
1058
- await Promise.all(a(this, at).map((e) => e())), (t = a(this, I)) == null || t.unmount(!1), a(this, z) && await ce(a(this, z)), d(this, L, !1);
1059
- for (const e of a(this, it))
1060
- e();
1432
+ if (!this.#isMounted) return;
1433
+ await Promise.all(this.#beforeUnmountCallbacks.map((callback) => callback()));
1434
+ this.#rootView?.unmount(false);
1435
+ if (this.#router) {
1436
+ await _unmountRouter(this.#router);
1437
+ }
1438
+ this.#isMounted = false;
1439
+ for (const callback of this.#onUnmountCallbacks) {
1440
+ callback();
1061
1441
  }
1062
1442
  }
1063
1443
  /**
1064
1444
  * Registers a `callback` to run after `Dolla.mount` is called, before the app is mounted. If `callback` returns a Promise,
1065
1445
  * it will be awaited before mounting finishes. Use this to perform initial setup before the app is displayed to the user.
1066
1446
  */
1067
- beforeMount(t) {
1068
- a(this, nt).push(t);
1447
+ beforeMount(callback) {
1448
+ this.#beforeMountCallbacks.push(callback);
1069
1449
  }
1070
1450
  /**
1071
1451
  * Registers a `callback` to run after the app is mounted.
1072
1452
  */
1073
- onMount(t) {
1074
- a(this, rt).push(t);
1453
+ onMount(callback) {
1454
+ this.#onMountCallbacks.push(callback);
1075
1455
  }
1076
1456
  /**
1077
1457
  * Registers a `callback` to run after `Dolla.unmount` is called, before the app is unmounted. If `callback` returns a Promise,
1078
1458
  * it will be awaited before unmounting finishes. Use this to perform cleanup.
1079
1459
  */
1080
- beforeUnmount(t) {
1081
- a(this, at).push(t);
1460
+ beforeUnmount(callback) {
1461
+ this.#beforeUnmountCallbacks.push(callback);
1082
1462
  }
1083
1463
  /**
1084
1464
  * Registers a `callback` to run after the app is unmounted.
1085
1465
  */
1086
- onUnmount(t) {
1087
- a(this, it).push(t);
1466
+ onUnmount(callback) {
1467
+ this.#onUnmountCallbacks.push(callback);
1088
1468
  }
1089
1469
  /**
1090
1470
  * Update log type toggles. Values that are not passed will remain unchanged.
1091
1471
  */
1092
- setLoggles(t) {
1093
- for (const e in t) {
1094
- const s = t[e];
1095
- s && (a(this, ot)[e] = s);
1096
- }
1097
- }
1098
- setLogFilter(t) {
1099
- d(this, lt, xt(t));
1100
- }
1101
- createLogger(t, e) {
1102
- const s = (e == null ? void 0 : e.console) ?? Me(), r = this, n = a(this, ot), o = (c) => {
1103
- if (n[c] === !1 || M(n[c]) && n[c] !== r.getEnv() || !a(this, lt).call(this, t))
1104
- return ne;
1105
- {
1106
- let l = `%c${t}`;
1107
- return e != null && e.uid ? l += ` %c[uid: %c${e.uid}%c]` : l += "%c%c%c", s[c].bind(
1108
- s,
1109
- l,
1110
- `color:${re(l)};font-weight:bold`,
1111
- "color:#777",
1112
- "color:#aaa",
1113
- "color:#777"
1472
+ setLoggles(options) {
1473
+ for (const key in options) {
1474
+ const value = options[key];
1475
+ if (value) {
1476
+ this.#loggles[key] = value;
1477
+ }
1478
+ }
1479
+ }
1480
+ setLogFilter(filter) {
1481
+ this.#match = createMatcher(filter);
1482
+ }
1483
+ createLogger(name, options) {
1484
+ const _console = options?.console ?? getDefaultConsole();
1485
+ const self = this;
1486
+ const loggles = this.#loggles;
1487
+ const bind = (method) => {
1488
+ if (loggles[method] === false || isString(loggles[method]) && loggles[method] !== self.getEnv() || !this.#match(name)) {
1489
+ return noOp;
1490
+ } else {
1491
+ let label = `%c${name}`;
1492
+ if (options?.uid) {
1493
+ label += ` %c[uid: %c${options.uid}%c]`;
1494
+ } else {
1495
+ label += `%c%c%c`;
1496
+ }
1497
+ return _console[method].bind(
1498
+ _console,
1499
+ label,
1500
+ `color:${okhash(label)};font-weight:bold`,
1501
+ `color:#777`,
1502
+ `color:#aaa`,
1503
+ `color:#777`
1114
1504
  );
1115
1505
  }
1116
1506
  };
1117
1507
  return {
1118
- setName(c) {
1119
- return t = c, this;
1508
+ setName(newName) {
1509
+ name = newName;
1510
+ return this;
1120
1511
  },
1121
1512
  get info() {
1122
- return o("info");
1513
+ return bind("info");
1123
1514
  },
1124
1515
  get log() {
1125
- return o("log");
1516
+ return bind("log");
1126
1517
  },
1127
1518
  get warn() {
1128
- return o("warn");
1519
+ return bind("warn");
1129
1520
  },
1130
1521
  get error() {
1131
- return o("error");
1522
+ return bind("error");
1132
1523
  },
1133
- crash(c) {
1134
- r.isMounted && (r.unmount(), r.constructView(a(r, st), {
1135
- error: c,
1136
- loggerName: t,
1137
- uid: e == null ? void 0 : e.uid
1138
- }).mount(a(r, F)));
1524
+ crash(error) {
1525
+ if (self.isMounted) {
1526
+ self.unmount();
1527
+ const crashPage = self.constructView(self.#crashView, {
1528
+ error,
1529
+ loggerName: name,
1530
+ uid: options?.uid
1531
+ });
1532
+ crashPage.mount(self.#rootElement);
1533
+ }
1139
1534
  }
1140
1535
  };
1141
1536
  }
1142
1537
  /**
1143
1538
  *
1144
1539
  */
1145
- constructView(t, e, s = []) {
1146
- return new te(a(this, O), t, e, s);
1540
+ constructView(view, props, children = []) {
1541
+ return new View(this.#rootElementContext, view, props, children);
1147
1542
  }
1148
1543
  /**
1149
1544
  *
1150
1545
  */
1151
- constructMarkup(t) {
1152
- return ee(se(a(this, O), t));
1546
+ constructMarkup(markup) {
1547
+ return groupElements(constructMarkup(this.#rootElementContext, markup));
1153
1548
  }
1154
1549
  }
1155
- L = new WeakMap(), et = new WeakMap(), F = new WeakMap(), I = new WeakMap(), st = new WeakMap(), z = new WeakMap(), nt = new WeakMap(), rt = new WeakMap(), at = new WeakMap(), it = new WeakMap(), O = new WeakMap(), ot = new WeakMap(), lt = new WeakMap(), dt = new WeakMap();
1156
- function Me() {
1157
- if (typeof window < "u" && window.console)
1550
+ function getDefaultConsole() {
1551
+ if (typeof window !== "undefined" && window.console) {
1158
1552
  return window.console;
1159
- if (typeof global < "u" && global.console)
1553
+ }
1554
+ if (typeof global !== "undefined" && global.console) {
1160
1555
  return global.console;
1556
+ }
1161
1557
  }
1162
- const Pt = new Se(), Le = Pt.i18n.t.bind(Pt.i18n);
1163
- export {
1164
- Ut as atom,
1165
- U as compose,
1166
- Xt as cond,
1167
- Zt as createMarkup,
1168
- Pe as createRouter,
1169
- Jt as deepEqual,
1170
- Pt as default,
1171
- Ue as effect,
1172
- E as get,
1173
- Mt as html,
1174
- je as list,
1175
- Ce as peek,
1176
- De as portal,
1177
- Ne as ref,
1178
- Ie as set,
1179
- _t as shallowEqual,
1180
- Ve as strictEqual,
1181
- Le as t
1182
- };
1558
+
1559
+ const dolla = new Dolla();
1560
+ const t = dolla.i18n.t.bind(dolla.i18n);
1561
+
1562
+ export { atom, compose, cond, createMarkup, createRouter, deepEqual, dolla as default, get, html, ref, shallowEqual, t };
1183
1563
  //# sourceMappingURL=index.js.map