@alepha/react 0.7.5 → 0.7.6

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.
@@ -1,1161 +0,0 @@
1
- import { jsx, jsxs } from 'react/jsx-runtime';
2
- import { __descriptor, OPTIONS, NotImplementedError, KIND, t, $logger, $inject, Alepha, $hook } from '@alepha/core';
3
- import React, { useState, useEffect, createContext, useContext, createElement, StrictMode, useMemo } from 'react';
4
- import { HttpClient } from '@alepha/server';
5
- import { RouterProvider } from '@alepha/router';
6
-
7
- const KEY = "PAGE";
8
- const $page = (options) => {
9
- __descriptor(KEY);
10
- if (options.children) {
11
- for (const child of options.children) {
12
- child[OPTIONS].parent = {
13
- [OPTIONS]: options
14
- };
15
- }
16
- }
17
- if (options.parent) {
18
- options.parent[OPTIONS].children ??= [];
19
- options.parent[OPTIONS].children.push({
20
- [OPTIONS]: options
21
- });
22
- }
23
- return {
24
- [KIND]: KEY,
25
- [OPTIONS]: options,
26
- render: () => {
27
- throw new NotImplementedError(KEY);
28
- }
29
- };
30
- };
31
- $page[KIND] = KEY;
32
-
33
- const ClientOnly = (props) => {
34
- const [mounted, setMounted] = useState(false);
35
- useEffect(() => setMounted(true), []);
36
- if (props.disabled) {
37
- return props.children;
38
- }
39
- return mounted ? props.children : props.fallback;
40
- };
41
-
42
- const RouterContext = createContext(
43
- void 0
44
- );
45
-
46
- const useAlepha = () => {
47
- const routerContext = useContext(RouterContext);
48
- if (!routerContext) {
49
- throw new Error("useAlepha must be used within a RouterProvider");
50
- }
51
- return routerContext.alepha;
52
- };
53
-
54
- const ErrorViewer = ({ error }) => {
55
- const [expanded, setExpanded] = useState(false);
56
- const isProduction = useAlepha().isProduction();
57
- if (isProduction) {
58
- return /* @__PURE__ */ jsx(ErrorViewerProduction, {});
59
- }
60
- const stackLines = error.stack?.split("\n") ?? [];
61
- const previewLines = stackLines.slice(0, 5);
62
- const hiddenLineCount = stackLines.length - previewLines.length;
63
- const copyToClipboard = (text) => {
64
- navigator.clipboard.writeText(text).catch((err) => {
65
- console.error("Clipboard error:", err);
66
- });
67
- };
68
- const styles = {
69
- container: {
70
- padding: "24px",
71
- backgroundColor: "#FEF2F2",
72
- color: "#7F1D1D",
73
- border: "1px solid #FECACA",
74
- borderRadius: "16px",
75
- boxShadow: "0 8px 24px rgba(0,0,0,0.05)",
76
- fontFamily: "monospace",
77
- maxWidth: "768px",
78
- margin: "40px auto"
79
- },
80
- heading: {
81
- fontSize: "20px",
82
- fontWeight: "bold",
83
- marginBottom: "4px"
84
- },
85
- name: {
86
- fontSize: "16px",
87
- fontWeight: 600
88
- },
89
- message: {
90
- fontSize: "14px",
91
- marginBottom: "16px"
92
- },
93
- sectionHeader: {
94
- display: "flex",
95
- justifyContent: "space-between",
96
- alignItems: "center",
97
- fontSize: "12px",
98
- marginBottom: "4px",
99
- color: "#991B1B"
100
- },
101
- copyButton: {
102
- fontSize: "12px",
103
- color: "#DC2626",
104
- background: "none",
105
- border: "none",
106
- cursor: "pointer",
107
- textDecoration: "underline"
108
- },
109
- stackContainer: {
110
- backgroundColor: "#FEE2E2",
111
- padding: "12px",
112
- borderRadius: "8px",
113
- fontSize: "13px",
114
- lineHeight: "1.4",
115
- overflowX: "auto",
116
- whiteSpace: "pre-wrap"
117
- },
118
- expandLine: {
119
- color: "#F87171",
120
- cursor: "pointer",
121
- marginTop: "8px"
122
- }
123
- };
124
- return /* @__PURE__ */ jsxs("div", { style: styles.container, children: [
125
- /* @__PURE__ */ jsxs("div", { children: [
126
- /* @__PURE__ */ jsx("div", { style: styles.heading, children: "\u{1F525} Error" }),
127
- /* @__PURE__ */ jsx("div", { style: styles.name, children: error.name }),
128
- /* @__PURE__ */ jsx("div", { style: styles.message, children: error.message })
129
- ] }),
130
- stackLines.length > 0 && /* @__PURE__ */ jsxs("div", { children: [
131
- /* @__PURE__ */ jsxs("div", { style: styles.sectionHeader, children: [
132
- /* @__PURE__ */ jsx("span", { children: "Stack trace" }),
133
- /* @__PURE__ */ jsx(
134
- "button",
135
- {
136
- onClick: () => copyToClipboard(error.stack),
137
- style: styles.copyButton,
138
- children: "Copy all"
139
- }
140
- )
141
- ] }),
142
- /* @__PURE__ */ jsxs("pre", { style: styles.stackContainer, children: [
143
- (expanded ? stackLines : previewLines).map((line, i) => /* @__PURE__ */ jsx("div", { children: line }, i)),
144
- !expanded && hiddenLineCount > 0 && /* @__PURE__ */ jsxs("div", { style: styles.expandLine, onClick: () => setExpanded(true), children: [
145
- "+ ",
146
- hiddenLineCount,
147
- " more lines..."
148
- ] })
149
- ] })
150
- ] })
151
- ] });
152
- };
153
- const ErrorViewerProduction = () => {
154
- const styles = {
155
- container: {
156
- padding: "24px",
157
- backgroundColor: "#FEF2F2",
158
- color: "#7F1D1D",
159
- border: "1px solid #FECACA",
160
- borderRadius: "16px",
161
- boxShadow: "0 8px 24px rgba(0,0,0,0.05)",
162
- fontFamily: "monospace",
163
- maxWidth: "768px",
164
- margin: "40px auto",
165
- textAlign: "center"
166
- },
167
- heading: {
168
- fontSize: "20px",
169
- fontWeight: "bold",
170
- marginBottom: "8px"
171
- },
172
- message: {
173
- fontSize: "14px",
174
- opacity: 0.85
175
- }
176
- };
177
- return /* @__PURE__ */ jsxs("div", { style: styles.container, children: [
178
- /* @__PURE__ */ jsx("div", { style: styles.heading, children: "\u{1F6A8} An error occurred" }),
179
- /* @__PURE__ */ jsx("div", { style: styles.message, children: "Something went wrong. Please try again later." })
180
- ] });
181
- };
182
-
183
- const RouterLayerContext = createContext(void 0);
184
-
185
- const useRouterEvents = (opts = {}, deps = []) => {
186
- const ctx = useContext(RouterContext);
187
- if (!ctx) {
188
- throw new Error("useRouter must be used within a RouterProvider");
189
- }
190
- useEffect(() => {
191
- if (!ctx.alepha.isBrowser()) {
192
- return;
193
- }
194
- const subs = [];
195
- const onBegin = opts.onBegin;
196
- const onEnd = opts.onEnd;
197
- const onError = opts.onError;
198
- if (onBegin) {
199
- subs.push(
200
- ctx.alepha.on("react:transition:begin", {
201
- callback: onBegin
202
- })
203
- );
204
- }
205
- if (onEnd) {
206
- subs.push(
207
- ctx.alepha.on("react:transition:end", {
208
- callback: onEnd
209
- })
210
- );
211
- }
212
- if (onError) {
213
- subs.push(
214
- ctx.alepha.on("react:transition:error", {
215
- callback: onError
216
- })
217
- );
218
- }
219
- return () => {
220
- for (const sub of subs) {
221
- sub();
222
- }
223
- };
224
- }, deps);
225
- };
226
-
227
- class ErrorBoundary extends React.Component {
228
- constructor(props) {
229
- super(props);
230
- this.state = {};
231
- }
232
- /**
233
- * Update state so the next render shows the fallback UI.
234
- */
235
- static getDerivedStateFromError(error) {
236
- return {
237
- error
238
- };
239
- }
240
- /**
241
- * Lifecycle method called when an error is caught.
242
- * You can log the error or perform side effects here.
243
- */
244
- componentDidCatch(error, info) {
245
- if (this.props.onError) {
246
- this.props.onError(error, info);
247
- }
248
- }
249
- render() {
250
- if (this.state.error) {
251
- return this.props.fallback(this.state.error);
252
- }
253
- return this.props.children;
254
- }
255
- }
256
-
257
- const NestedView = (props) => {
258
- const app = useContext(RouterContext);
259
- const layer = useContext(RouterLayerContext);
260
- const index = layer?.index ?? 0;
261
- const [view, setView] = useState(
262
- app?.state.layers[index]?.element
263
- );
264
- useRouterEvents(
265
- {
266
- onEnd: ({ state }) => {
267
- setView(state.layers[index]?.element);
268
- }
269
- },
270
- [app]
271
- );
272
- if (!app) {
273
- throw new Error("NestedView must be used within a RouterContext.");
274
- }
275
- const element = view ?? props.children ?? null;
276
- return /* @__PURE__ */ jsx(ErrorBoundary, { fallback: app.context.onError, children: element });
277
- };
278
-
279
- function NotFoundPage() {
280
- return /* @__PURE__ */ jsx(
281
- "div",
282
- {
283
- style: {
284
- height: "100vh",
285
- display: "flex",
286
- flexDirection: "column",
287
- justifyContent: "center",
288
- alignItems: "center",
289
- textAlign: "center",
290
- fontFamily: "sans-serif",
291
- padding: "1rem"
292
- },
293
- children: /* @__PURE__ */ jsx("h1", { style: { fontSize: "1rem", marginBottom: "0.5rem" }, children: "This page does not exist" })
294
- }
295
- );
296
- }
297
-
298
- class RedirectionError extends Error {
299
- page;
300
- constructor(page) {
301
- super("Redirection");
302
- this.page = page;
303
- }
304
- }
305
-
306
- const envSchema = t.object({
307
- REACT_STRICT_MODE: t.boolean({ default: true })
308
- });
309
- class PageDescriptorProvider {
310
- log = $logger();
311
- env = $inject(envSchema);
312
- alepha = $inject(Alepha);
313
- pages = [];
314
- getPages() {
315
- return this.pages;
316
- }
317
- page(name) {
318
- for (const page of this.pages) {
319
- if (page.name === name) {
320
- return page;
321
- }
322
- }
323
- throw new Error(`Page ${name} not found`);
324
- }
325
- url(name, options = {}) {
326
- const page = this.page(name);
327
- if (!page) {
328
- throw new Error(`Page ${name} not found`);
329
- }
330
- let url = page.path ?? "";
331
- let parent = page.parent;
332
- while (parent) {
333
- url = `${parent.path ?? ""}/${url}`;
334
- parent = parent.parent;
335
- }
336
- url = this.compile(url, options.params ?? {});
337
- return new URL(
338
- url.replace(/\/\/+/g, "/") || "/",
339
- options.base ?? `http://localhost`
340
- );
341
- }
342
- root(state, context) {
343
- const root = createElement(
344
- RouterContext.Provider,
345
- {
346
- value: {
347
- alepha: this.alepha,
348
- state,
349
- context
350
- }
351
- },
352
- createElement(NestedView, {}, state.layers[0]?.element)
353
- );
354
- if (this.env.REACT_STRICT_MODE) {
355
- return createElement(StrictMode, {}, root);
356
- }
357
- return root;
358
- }
359
- async createLayers(route, request) {
360
- const { pathname, search } = request.url;
361
- const layers = [];
362
- let context = {};
363
- const stack = [{ route }];
364
- request.onError = (error) => this.renderError(error);
365
- let parent = route.parent;
366
- while (parent) {
367
- stack.unshift({ route: parent });
368
- parent = parent.parent;
369
- }
370
- let forceRefresh = false;
371
- for (let i = 0; i < stack.length; i++) {
372
- const it = stack[i];
373
- const route2 = it.route;
374
- const config = {};
375
- try {
376
- config.query = route2.schema?.query ? this.alepha.parse(route2.schema.query, request.query) : request.query;
377
- } catch (e) {
378
- it.error = e;
379
- break;
380
- }
381
- try {
382
- config.params = route2.schema?.params ? this.alepha.parse(route2.schema.params, request.params) : request.params;
383
- } catch (e) {
384
- it.error = e;
385
- break;
386
- }
387
- it.config = {
388
- ...config
389
- };
390
- if (!route2.resolve) {
391
- continue;
392
- }
393
- const previous = request.previous;
394
- if (previous?.[i] && !forceRefresh && previous[i].name === route2.name) {
395
- const url = (str) => str ? str.replace(/\/\/+/g, "/") : "/";
396
- const prev = JSON.stringify({
397
- part: url(previous[i].part),
398
- params: previous[i].config?.params ?? {}
399
- });
400
- const curr = JSON.stringify({
401
- part: url(route2.path),
402
- params: config.params ?? {}
403
- });
404
- if (prev === curr) {
405
- it.props = previous[i].props;
406
- it.error = previous[i].error;
407
- context = {
408
- ...context,
409
- ...it.props
410
- };
411
- continue;
412
- }
413
- forceRefresh = true;
414
- }
415
- try {
416
- const props = await route2.resolve?.({
417
- ...request,
418
- // request
419
- ...config,
420
- // params, query
421
- ...context
422
- // previous props
423
- }) ?? {};
424
- it.props = {
425
- ...props
426
- };
427
- context = {
428
- ...context,
429
- ...props
430
- };
431
- } catch (e) {
432
- if (e instanceof RedirectionError) {
433
- return {
434
- layers: [],
435
- redirect: typeof e.page === "string" ? e.page : this.href(e.page),
436
- pathname,
437
- search
438
- };
439
- }
440
- this.log.error(e);
441
- it.error = e;
442
- break;
443
- }
444
- }
445
- let acc = "";
446
- for (let i = 0; i < stack.length; i++) {
447
- const it = stack[i];
448
- const props = it.props ?? {};
449
- const params = { ...it.config?.params };
450
- for (const key of Object.keys(params)) {
451
- params[key] = String(params[key]);
452
- }
453
- if (it.route.head && !it.error) {
454
- this.fillHead(it.route, request, {
455
- ...props,
456
- ...context
457
- });
458
- }
459
- acc += "/";
460
- acc += it.route.path ? this.compile(it.route.path, params) : "";
461
- const path = acc.replace(/\/+/, "/");
462
- const localErrorHandler = this.getErrorHandler(it.route);
463
- if (localErrorHandler) {
464
- request.onError = localErrorHandler;
465
- }
466
- if (it.error) {
467
- let element2 = await request.onError(it.error);
468
- if (element2 === null) {
469
- element2 = this.renderError(it.error);
470
- }
471
- layers.push({
472
- props,
473
- error: it.error,
474
- name: it.route.name,
475
- part: it.route.path,
476
- config: it.config,
477
- element: this.renderView(i + 1, path, element2, it.route),
478
- index: i + 1,
479
- path
480
- });
481
- break;
482
- }
483
- const element = await this.createElement(it.route, {
484
- ...props,
485
- ...context
486
- });
487
- layers.push({
488
- name: it.route.name,
489
- props,
490
- part: it.route.path,
491
- config: it.config,
492
- element: this.renderView(i + 1, path, element, it.route),
493
- index: i + 1,
494
- path
495
- });
496
- }
497
- return { layers, pathname, search };
498
- }
499
- getErrorHandler(route) {
500
- if (route.errorHandler) return route.errorHandler;
501
- let parent = route.parent;
502
- while (parent) {
503
- if (parent.errorHandler) return parent.errorHandler;
504
- parent = parent.parent;
505
- }
506
- }
507
- async createElement(page, props) {
508
- if (page.lazy) {
509
- const component = await page.lazy();
510
- return createElement(component.default, props);
511
- }
512
- if (page.component) {
513
- return createElement(page.component, props);
514
- }
515
- return void 0;
516
- }
517
- fillHead(page, ctx, props) {
518
- if (!page.head) {
519
- return;
520
- }
521
- ctx.head ??= {};
522
- const head = typeof page.head === "function" ? page.head(props, ctx.head) : page.head;
523
- if (head.title) {
524
- ctx.head ??= {};
525
- if (ctx.head.titleSeparator) {
526
- ctx.head.title = `${head.title}${ctx.head.titleSeparator}${ctx.head.title}`;
527
- } else {
528
- ctx.head.title = head.title;
529
- }
530
- ctx.head.titleSeparator = head.titleSeparator;
531
- }
532
- if (head.htmlAttributes) {
533
- ctx.head.htmlAttributes = {
534
- ...ctx.head.htmlAttributes,
535
- ...head.htmlAttributes
536
- };
537
- }
538
- if (head.bodyAttributes) {
539
- ctx.head.bodyAttributes = {
540
- ...ctx.head.bodyAttributes,
541
- ...head.bodyAttributes
542
- };
543
- }
544
- if (head.meta) {
545
- ctx.head.meta = [...ctx.head.meta ?? [], ...head.meta ?? []];
546
- }
547
- }
548
- renderError(error) {
549
- return createElement(ErrorViewer, { error });
550
- }
551
- renderEmptyView() {
552
- return createElement(NestedView, {});
553
- }
554
- href(page, params = {}) {
555
- const found = this.pages.find((it) => it.name === page.options.name);
556
- if (!found) {
557
- throw new Error(`Page ${page.options.name} not found`);
558
- }
559
- let url = found.path ?? "";
560
- let parent = found.parent;
561
- while (parent) {
562
- url = `${parent.path ?? ""}/${url}`;
563
- parent = parent.parent;
564
- }
565
- url = this.compile(url, params);
566
- return url.replace(/\/\/+/g, "/") || "/";
567
- }
568
- compile(path, params = {}) {
569
- for (const [key, value] of Object.entries(params)) {
570
- path = path.replace(`:${key}`, value);
571
- }
572
- return path;
573
- }
574
- renderView(index, path, view, page) {
575
- view ??= this.renderEmptyView();
576
- const element = page.client ? createElement(
577
- ClientOnly,
578
- typeof page.client === "object" ? page.client : {},
579
- view
580
- ) : view;
581
- return createElement(
582
- RouterLayerContext.Provider,
583
- {
584
- value: {
585
- index,
586
- path
587
- }
588
- },
589
- element
590
- );
591
- }
592
- configure = $hook({
593
- name: "configure",
594
- handler: () => {
595
- let hasNotFoundHandler = false;
596
- const pages = this.alepha.getDescriptorValues($page);
597
- for (const { value, key } of pages) {
598
- value[OPTIONS].name ??= key;
599
- }
600
- for (const { value } of pages) {
601
- if (value[OPTIONS].parent) {
602
- continue;
603
- }
604
- if (value[OPTIONS].path === "/*") {
605
- hasNotFoundHandler = true;
606
- }
607
- this.add(this.map(pages, value));
608
- }
609
- if (!hasNotFoundHandler && pages.length > 0) {
610
- this.add({
611
- path: "/*",
612
- name: "notFound",
613
- cache: true,
614
- component: NotFoundPage,
615
- afterHandler: ({ reply }) => {
616
- reply.status = 404;
617
- }
618
- });
619
- }
620
- }
621
- });
622
- map(pages, target) {
623
- const children = target[OPTIONS].children ?? [];
624
- return {
625
- ...target[OPTIONS],
626
- parent: void 0,
627
- children: children.map((it) => this.map(pages, it))
628
- };
629
- }
630
- add(entry) {
631
- if (this.alepha.isReady()) {
632
- throw new Error("Router is already initialized");
633
- }
634
- entry.name ??= this.nextId();
635
- const page = entry;
636
- page.match = this.createMatch(page);
637
- this.pages.push(page);
638
- if (page.children) {
639
- for (const child of page.children) {
640
- child.parent = page;
641
- this.add(child);
642
- }
643
- }
644
- }
645
- createMatch(page) {
646
- let url = page.path ?? "/";
647
- let target = page.parent;
648
- while (target) {
649
- url = `${target.path ?? ""}/${url}`;
650
- target = target.parent;
651
- }
652
- let path = url.replace(/\/\/+/g, "/");
653
- if (path.endsWith("/") && path !== "/") {
654
- path = path.slice(0, -1);
655
- }
656
- return path;
657
- }
658
- _next = 0;
659
- nextId() {
660
- this._next += 1;
661
- return `P${this._next}`;
662
- }
663
- }
664
- const isPageRoute = (it) => {
665
- return it && typeof it === "object" && typeof it.path === "string" && typeof it.page === "object";
666
- };
667
-
668
- class BrowserHeadProvider {
669
- renderHead(document, head) {
670
- if (head.title) {
671
- document.title = head.title;
672
- }
673
- if (head.bodyAttributes) {
674
- for (const [key, value] of Object.entries(head.bodyAttributes)) {
675
- if (value) {
676
- document.body.setAttribute(key, value);
677
- } else {
678
- document.body.removeAttribute(key);
679
- }
680
- }
681
- }
682
- if (head.htmlAttributes) {
683
- for (const [key, value] of Object.entries(head.htmlAttributes)) {
684
- if (value) {
685
- document.documentElement.setAttribute(key, value);
686
- } else {
687
- document.documentElement.removeAttribute(key);
688
- }
689
- }
690
- }
691
- if (head.meta) {
692
- for (const [key, value] of Object.entries(head.meta)) {
693
- const meta = document.querySelector(`meta[name="${key}"]`);
694
- if (meta) {
695
- meta.setAttribute("content", value.content);
696
- } else {
697
- const newMeta = document.createElement("meta");
698
- newMeta.setAttribute("name", key);
699
- newMeta.setAttribute("content", value.content);
700
- document.head.appendChild(newMeta);
701
- }
702
- }
703
- }
704
- }
705
- }
706
-
707
- class BrowserRouterProvider extends RouterProvider {
708
- log = $logger();
709
- alepha = $inject(Alepha);
710
- pageDescriptorProvider = $inject(PageDescriptorProvider);
711
- add(entry) {
712
- this.pageDescriptorProvider.add(entry);
713
- }
714
- configure = $hook({
715
- name: "configure",
716
- handler: async () => {
717
- for (const page of this.pageDescriptorProvider.getPages()) {
718
- if (page.component || page.lazy) {
719
- this.push({
720
- path: page.match,
721
- page
722
- });
723
- }
724
- }
725
- }
726
- });
727
- async transition(url, options = {}) {
728
- const { pathname, search } = url;
729
- const state = {
730
- pathname,
731
- search,
732
- layers: []
733
- };
734
- const context = {
735
- url,
736
- query: {},
737
- params: {},
738
- head: {},
739
- onError: () => null,
740
- ...options.context ?? {}
741
- };
742
- await this.alepha.emit("react:transition:begin", { state, context });
743
- try {
744
- const previous = options.previous;
745
- const { route, params } = this.match(pathname);
746
- const query = {};
747
- if (search) {
748
- for (const [key, value] of new URLSearchParams(search).entries()) {
749
- query[key] = String(value);
750
- }
751
- }
752
- context.query = query;
753
- context.params = params ?? {};
754
- context.previous = previous;
755
- if (isPageRoute(route)) {
756
- const result = await this.pageDescriptorProvider.createLayers(
757
- route.page,
758
- context
759
- );
760
- if (result.redirect) {
761
- return {
762
- redirect: result.redirect,
763
- state,
764
- context
765
- };
766
- }
767
- state.layers = result.layers;
768
- }
769
- if (state.layers.length === 0) {
770
- state.layers.push({
771
- name: "not-found",
772
- element: createElement(NotFoundPage),
773
- index: 0,
774
- path: "/"
775
- });
776
- }
777
- await this.alepha.emit("react:transition:success", { state });
778
- } catch (e) {
779
- this.log.error(e);
780
- state.layers = [
781
- {
782
- name: "error",
783
- element: this.pageDescriptorProvider.renderError(e),
784
- index: 0,
785
- path: "/"
786
- }
787
- ];
788
- await this.alepha.emit("react:transition:error", {
789
- error: e,
790
- state,
791
- context
792
- });
793
- }
794
- if (options.state) {
795
- options.state.layers = state.layers;
796
- options.state.pathname = state.pathname;
797
- options.state.search = state.search;
798
- }
799
- await this.alepha.emit("react:transition:end", {
800
- state: options.state,
801
- context
802
- });
803
- return {
804
- context,
805
- state
806
- };
807
- }
808
- root(state, context) {
809
- return this.pageDescriptorProvider.root(state, context);
810
- }
811
- }
812
-
813
- class ReactBrowserProvider {
814
- log = $logger();
815
- client = $inject(HttpClient);
816
- alepha = $inject(Alepha);
817
- router = $inject(BrowserRouterProvider);
818
- headProvider = $inject(BrowserHeadProvider);
819
- root;
820
- transitioning;
821
- state = {
822
- layers: [],
823
- pathname: "",
824
- search: ""
825
- };
826
- get document() {
827
- return window.document;
828
- }
829
- get history() {
830
- return window.history;
831
- }
832
- get url() {
833
- return window.location.pathname + window.location.search;
834
- }
835
- async invalidate(props) {
836
- const previous = [];
837
- if (props) {
838
- const [key] = Object.keys(props);
839
- const value = props[key];
840
- for (const layer of this.state.layers) {
841
- if (layer.props?.[key]) {
842
- previous.push({
843
- ...layer,
844
- props: {
845
- ...layer.props,
846
- [key]: value
847
- }
848
- });
849
- break;
850
- }
851
- previous.push(layer);
852
- }
853
- }
854
- await this.render({ previous });
855
- }
856
- async go(url, options = {}) {
857
- const result = await this.render({
858
- url
859
- });
860
- if (result.context.url.pathname !== url) {
861
- this.history.replaceState({}, "", result.context.url.pathname);
862
- return;
863
- }
864
- if (options.replace) {
865
- this.history.replaceState({}, "", url);
866
- return;
867
- }
868
- this.history.pushState({}, "", url);
869
- }
870
- async render(options = {}) {
871
- const previous = options.previous ?? this.state.layers;
872
- const url = options.url ?? this.url;
873
- this.transitioning = { to: url };
874
- const result = await this.router.transition(
875
- new URL(`http://localhost${url}`),
876
- {
877
- previous,
878
- state: this.state
879
- }
880
- );
881
- if (result.redirect) {
882
- return await this.render({ url: result.redirect });
883
- }
884
- this.transitioning = void 0;
885
- return result;
886
- }
887
- /**
888
- * Get embedded layers from the server.
889
- */
890
- getHydrationState() {
891
- try {
892
- if ("__ssr" in window && typeof window.__ssr === "object") {
893
- return window.__ssr;
894
- }
895
- } catch (error) {
896
- console.error(error);
897
- }
898
- }
899
- // -------------------------------------------------------------------------------------------------------------------
900
- ready = $hook({
901
- name: "ready",
902
- handler: async () => {
903
- const hydration = this.getHydrationState();
904
- const previous = hydration?.layers ?? [];
905
- if (hydration?.links) {
906
- for (const link of hydration.links.links) {
907
- this.client.pushLink(link);
908
- }
909
- }
910
- const { context } = await this.render({ previous });
911
- if (context.head) {
912
- this.headProvider.renderHead(this.document, context.head);
913
- }
914
- await this.alepha.emit("react:browser:render", {
915
- state: this.state,
916
- context,
917
- hydration
918
- });
919
- window.addEventListener("popstate", () => {
920
- this.render();
921
- });
922
- }
923
- });
924
- onTransitionEnd = $hook({
925
- name: "react:transition:end",
926
- handler: async ({ context }) => {
927
- this.headProvider.renderHead(this.document, context.head);
928
- }
929
- });
930
- }
931
-
932
- class RouterHookApi {
933
- constructor(pages, state, layer, browser) {
934
- this.pages = pages;
935
- this.state = state;
936
- this.layer = layer;
937
- this.browser = browser;
938
- }
939
- get current() {
940
- return this.state;
941
- }
942
- get pathname() {
943
- return this.state.pathname;
944
- }
945
- get query() {
946
- const query = {};
947
- for (const [key, value] of new URLSearchParams(
948
- this.state.search
949
- ).entries()) {
950
- query[key] = String(value);
951
- }
952
- return query;
953
- }
954
- async back() {
955
- this.browser?.history.back();
956
- }
957
- async forward() {
958
- this.browser?.history.forward();
959
- }
960
- async invalidate(props) {
961
- await this.browser?.invalidate(props);
962
- }
963
- /**
964
- * Create a valid href for the given pathname.
965
- *
966
- * @param pathname
967
- * @param layer
968
- */
969
- createHref(pathname, layer = this.layer, options = {}) {
970
- if (typeof pathname === "object") {
971
- pathname = pathname.options.path ?? "";
972
- }
973
- if (options.params) {
974
- for (const [key, value] of Object.entries(options.params)) {
975
- pathname = pathname.replace(`:${key}`, String(value));
976
- }
977
- }
978
- return pathname.startsWith("/") ? pathname : `${layer.path}/${pathname}`.replace(/\/\/+/g, "/");
979
- }
980
- async go(path, options) {
981
- for (const page of this.pages) {
982
- if (page.name === path) {
983
- path = page.path ?? "";
984
- break;
985
- }
986
- }
987
- await this.browser?.go(this.createHref(path, this.layer, options), options);
988
- }
989
- anchor(path, options = {}) {
990
- for (const page of this.pages) {
991
- if (page.name === path) {
992
- path = page.path ?? "";
993
- break;
994
- }
995
- }
996
- const href = this.createHref(path, this.layer, options);
997
- return {
998
- href,
999
- onClick: (ev) => {
1000
- ev.stopPropagation();
1001
- ev.preventDefault();
1002
- this.go(path, options).catch(console.error);
1003
- }
1004
- };
1005
- }
1006
- /**
1007
- * Set query params.
1008
- *
1009
- * @param record
1010
- * @param options
1011
- */
1012
- setQueryParams(record, options = {}) {
1013
- const func = typeof record === "function" ? record : () => record;
1014
- const search = new URLSearchParams(func(this.query)).toString();
1015
- const state = search ? `${this.pathname}?${search}` : this.pathname;
1016
- if (options.push) {
1017
- window.history.pushState({}, "", state);
1018
- } else {
1019
- window.history.replaceState({}, "", state);
1020
- }
1021
- }
1022
- }
1023
-
1024
- const useRouter = () => {
1025
- const ctx = useContext(RouterContext);
1026
- const layer = useContext(RouterLayerContext);
1027
- if (!ctx || !layer) {
1028
- throw new Error("useRouter must be used within a RouterProvider");
1029
- }
1030
- const pages = useMemo(() => {
1031
- return ctx.alepha.get(PageDescriptorProvider).getPages();
1032
- }, []);
1033
- return useMemo(
1034
- () => new RouterHookApi(
1035
- pages,
1036
- ctx.state,
1037
- layer,
1038
- ctx.alepha.isBrowser() ? ctx.alepha.get(ReactBrowserProvider) : void 0
1039
- ),
1040
- [layer]
1041
- );
1042
- };
1043
-
1044
- const Link = (props) => {
1045
- React.useContext(RouterContext);
1046
- const router = useRouter();
1047
- const to = typeof props.to === "string" ? props.to : props.to[OPTIONS].path;
1048
- if (!to) {
1049
- return null;
1050
- }
1051
- const can = typeof props.to === "string" ? void 0 : props.to[OPTIONS].can;
1052
- if (can && !can()) {
1053
- return null;
1054
- }
1055
- const name = typeof props.to === "string" ? void 0 : props.to[OPTIONS].name;
1056
- const anchorProps = {
1057
- ...props,
1058
- to: void 0
1059
- };
1060
- return /* @__PURE__ */ jsx("a", { ...router.anchor(to), ...anchorProps, children: props.children ?? name });
1061
- };
1062
-
1063
- const useActive = (path) => {
1064
- const router = useRouter();
1065
- const ctx = useContext(RouterContext);
1066
- const layer = useContext(RouterLayerContext);
1067
- if (!ctx || !layer) {
1068
- throw new Error("useRouter must be used within a RouterProvider");
1069
- }
1070
- let name;
1071
- if (typeof path === "object" && path.options.name) {
1072
- name = path.options.name;
1073
- }
1074
- const [current, setCurrent] = useState(ctx.state.pathname);
1075
- const href = useMemo(() => router.createHref(path, layer), [path, layer]);
1076
- const [isPending, setPending] = useState(false);
1077
- const isActive = current === href;
1078
- useRouterEvents({
1079
- onEnd: ({ state }) => setCurrent(state.pathname)
1080
- });
1081
- return {
1082
- name,
1083
- isPending,
1084
- isActive,
1085
- anchorProps: {
1086
- href,
1087
- onClick: (ev) => {
1088
- ev.stopPropagation();
1089
- ev.preventDefault();
1090
- if (isActive) return;
1091
- if (isPending) return;
1092
- setPending(true);
1093
- router.go(href).then(() => {
1094
- setPending(false);
1095
- });
1096
- }
1097
- }
1098
- };
1099
- };
1100
-
1101
- const useInject = (clazz) => {
1102
- const ctx = useContext(RouterContext);
1103
- if (!ctx) {
1104
- throw new Error("useRouter must be used within a <RouterProvider>");
1105
- }
1106
- return useMemo(() => ctx.alepha.get(clazz), []);
1107
- };
1108
-
1109
- const useClient = (_scope) => {
1110
- return useInject(HttpClient).of();
1111
- };
1112
-
1113
- const useQueryParams = (schema, options = {}) => {
1114
- const ctx = useContext(RouterContext);
1115
- if (!ctx) {
1116
- throw new Error("useQueryParams must be used within a RouterProvider");
1117
- }
1118
- const key = options.key ?? "q";
1119
- const router = useRouter();
1120
- const querystring = router.query[key];
1121
- const [queryParams, setQueryParams] = useState(
1122
- decode(ctx.alepha, schema, router.query[key])
1123
- );
1124
- useEffect(() => {
1125
- setQueryParams(decode(ctx.alepha, schema, querystring));
1126
- }, [querystring]);
1127
- return [
1128
- queryParams,
1129
- (queryParams2) => {
1130
- setQueryParams(queryParams2);
1131
- router.setQueryParams((data) => {
1132
- return { ...data, [key]: encode(ctx.alepha, schema, queryParams2) };
1133
- });
1134
- }
1135
- ];
1136
- };
1137
- const encode = (alepha, schema, data) => {
1138
- return btoa(JSON.stringify(alepha.parse(schema, data)));
1139
- };
1140
- const decode = (alepha, schema, data) => {
1141
- try {
1142
- return alepha.parse(schema, JSON.parse(atob(decodeURIComponent(data))));
1143
- } catch (_error) {
1144
- return {};
1145
- }
1146
- };
1147
-
1148
- const useRouterState = () => {
1149
- const ctx = useContext(RouterContext);
1150
- const layer = useContext(RouterLayerContext);
1151
- if (!ctx || !layer) {
1152
- throw new Error("useRouter must be used within a RouterProvider");
1153
- }
1154
- const [state, setState] = useState(ctx.state);
1155
- useRouterEvents({
1156
- onEnd: ({ state: state2 }) => setState({ ...state2 })
1157
- });
1158
- return state;
1159
- };
1160
-
1161
- export { $page as $, BrowserRouterProvider as B, ClientOnly as C, ErrorBoundary as E, Link as L, NestedView as N, PageDescriptorProvider as P, RouterContext as R, RouterLayerContext as a, RedirectionError as b, RouterHookApi as c, useAlepha as d, useClient as e, useInject as f, useQueryParams as g, useRouter as h, useRouterEvents as i, useRouterState as j, isPageRoute as k, ReactBrowserProvider as l, useActive as u };