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