@alepha/react 0.7.4 → 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,946 +0,0 @@
1
- import { __descriptor, OPTIONS, NotImplementedError, KIND, t, $logger, $inject, Alepha, $hook } from '@alepha/core';
2
- import { HttpClient } from '@alepha/server';
3
- import { RouterProvider } from '@alepha/router';
4
- import React, { useState, useEffect, createContext, useContext, createElement, StrictMode } from 'react';
5
- import { jsx, jsxs } from 'react/jsx-runtime';
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__ */ jsxs(
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: [
294
- /* @__PURE__ */ jsx("h1", { style: { fontSize: "1rem", marginBottom: "0.5rem" }, children: "This page does not exist" }),
295
- /* @__PURE__ */ jsx(
296
- "a",
297
- {
298
- href: "/",
299
- style: {
300
- fontSize: "0.7rem",
301
- color: "#007bff",
302
- textDecoration: "none"
303
- },
304
- children: "\u2190 Back to home"
305
- }
306
- )
307
- ]
308
- }
309
- );
310
- }
311
-
312
- class RedirectionError extends Error {
313
- page;
314
- constructor(page) {
315
- super("Redirection");
316
- this.page = page;
317
- }
318
- }
319
-
320
- const envSchema = t.object({
321
- REACT_STRICT_MODE: t.boolean({ default: true })
322
- });
323
- class PageDescriptorProvider {
324
- log = $logger();
325
- env = $inject(envSchema);
326
- alepha = $inject(Alepha);
327
- pages = [];
328
- getPages() {
329
- return this.pages;
330
- }
331
- page(name) {
332
- for (const page of this.pages) {
333
- if (page.name === name) {
334
- return page;
335
- }
336
- }
337
- throw new Error(`Page ${name} not found`);
338
- }
339
- url(name, options = {}) {
340
- const page = this.page(name);
341
- if (!page) {
342
- throw new Error(`Page ${name} not found`);
343
- }
344
- let url = page.path ?? "";
345
- let parent = page.parent;
346
- while (parent) {
347
- url = `${parent.path ?? ""}/${url}`;
348
- parent = parent.parent;
349
- }
350
- url = this.compile(url, options.params ?? {});
351
- return new URL(
352
- url.replace(/\/\/+/g, "/") || "/",
353
- options.base ?? `http://localhost`
354
- );
355
- }
356
- root(state, context) {
357
- const root = createElement(
358
- RouterContext.Provider,
359
- {
360
- value: {
361
- alepha: this.alepha,
362
- state,
363
- context
364
- }
365
- },
366
- createElement(NestedView, {}, state.layers[0]?.element)
367
- );
368
- if (this.env.REACT_STRICT_MODE) {
369
- return createElement(StrictMode, {}, root);
370
- }
371
- return root;
372
- }
373
- async createLayers(route, request) {
374
- const { pathname, search } = request.url;
375
- const layers = [];
376
- let context = {};
377
- const stack = [{ route }];
378
- request.onError = (error) => this.renderError(error);
379
- let parent = route.parent;
380
- while (parent) {
381
- stack.unshift({ route: parent });
382
- parent = parent.parent;
383
- }
384
- let forceRefresh = false;
385
- for (let i = 0; i < stack.length; i++) {
386
- const it = stack[i];
387
- const route2 = it.route;
388
- const config = {};
389
- try {
390
- config.query = route2.schema?.query ? this.alepha.parse(route2.schema.query, request.query) : request.query;
391
- } catch (e) {
392
- it.error = e;
393
- break;
394
- }
395
- try {
396
- config.params = route2.schema?.params ? this.alepha.parse(route2.schema.params, request.params) : request.params;
397
- } catch (e) {
398
- it.error = e;
399
- break;
400
- }
401
- it.config = {
402
- ...config
403
- };
404
- if (!route2.resolve) {
405
- continue;
406
- }
407
- const previous = request.previous;
408
- if (previous?.[i] && !forceRefresh && previous[i].name === route2.name) {
409
- const url = (str) => str ? str.replace(/\/\/+/g, "/") : "/";
410
- const prev = JSON.stringify({
411
- part: url(previous[i].part),
412
- params: previous[i].config?.params ?? {}
413
- });
414
- const curr = JSON.stringify({
415
- part: url(route2.path),
416
- params: config.params ?? {}
417
- });
418
- if (prev === curr) {
419
- it.props = previous[i].props;
420
- it.error = previous[i].error;
421
- context = {
422
- ...context,
423
- ...it.props
424
- };
425
- continue;
426
- }
427
- forceRefresh = true;
428
- }
429
- try {
430
- const props = await route2.resolve?.({
431
- ...request,
432
- // request
433
- ...config,
434
- // params, query
435
- ...context
436
- // previous props
437
- }) ?? {};
438
- it.props = {
439
- ...props
440
- };
441
- context = {
442
- ...context,
443
- ...props
444
- };
445
- } catch (e) {
446
- if (e instanceof RedirectionError) {
447
- return {
448
- layers: [],
449
- redirect: typeof e.page === "string" ? e.page : this.href(e.page),
450
- pathname,
451
- search
452
- };
453
- }
454
- this.log.error(e);
455
- it.error = e;
456
- break;
457
- }
458
- }
459
- let acc = "";
460
- for (let i = 0; i < stack.length; i++) {
461
- const it = stack[i];
462
- const props = it.props ?? {};
463
- const params = { ...it.config?.params };
464
- for (const key of Object.keys(params)) {
465
- params[key] = String(params[key]);
466
- }
467
- if (it.route.head && !it.error) {
468
- this.fillHead(it.route, request, {
469
- ...props,
470
- ...context
471
- });
472
- }
473
- acc += "/";
474
- acc += it.route.path ? this.compile(it.route.path, params) : "";
475
- const path = acc.replace(/\/+/, "/");
476
- const localErrorHandler = this.getErrorHandler(it.route);
477
- if (localErrorHandler) {
478
- request.onError = localErrorHandler;
479
- }
480
- if (it.error) {
481
- let element2 = await request.onError(it.error);
482
- if (element2 === null) {
483
- element2 = this.renderError(it.error);
484
- }
485
- layers.push({
486
- props,
487
- error: it.error,
488
- name: it.route.name,
489
- part: it.route.path,
490
- config: it.config,
491
- element: this.renderView(i + 1, path, element2, it.route),
492
- index: i + 1,
493
- path
494
- });
495
- break;
496
- }
497
- const element = await this.createElement(it.route, {
498
- ...props,
499
- ...context
500
- });
501
- layers.push({
502
- name: it.route.name,
503
- props,
504
- part: it.route.path,
505
- config: it.config,
506
- element: this.renderView(i + 1, path, element, it.route),
507
- index: i + 1,
508
- path
509
- });
510
- }
511
- return { layers, pathname, search };
512
- }
513
- getErrorHandler(route) {
514
- if (route.errorHandler) return route.errorHandler;
515
- let parent = route.parent;
516
- while (parent) {
517
- if (parent.errorHandler) return parent.errorHandler;
518
- parent = parent.parent;
519
- }
520
- }
521
- async createElement(page, props) {
522
- if (page.lazy) {
523
- const component = await page.lazy();
524
- return createElement(component.default, props);
525
- }
526
- if (page.component) {
527
- return createElement(page.component, props);
528
- }
529
- return void 0;
530
- }
531
- fillHead(page, ctx, props) {
532
- if (!page.head) {
533
- return;
534
- }
535
- ctx.head ??= {};
536
- const head = typeof page.head === "function" ? page.head(props, ctx.head) : page.head;
537
- if (head.title) {
538
- ctx.head ??= {};
539
- if (ctx.head.titleSeparator) {
540
- ctx.head.title = `${head.title}${ctx.head.titleSeparator}${ctx.head.title}`;
541
- } else {
542
- ctx.head.title = head.title;
543
- }
544
- ctx.head.titleSeparator = head.titleSeparator;
545
- }
546
- if (head.htmlAttributes) {
547
- ctx.head.htmlAttributes = {
548
- ...ctx.head.htmlAttributes,
549
- ...head.htmlAttributes
550
- };
551
- }
552
- if (head.bodyAttributes) {
553
- ctx.head.bodyAttributes = {
554
- ...ctx.head.bodyAttributes,
555
- ...head.bodyAttributes
556
- };
557
- }
558
- if (head.meta) {
559
- ctx.head.meta = [...ctx.head.meta ?? [], ...head.meta ?? []];
560
- }
561
- }
562
- renderError(error) {
563
- return createElement(ErrorViewer, { error });
564
- }
565
- renderEmptyView() {
566
- return createElement(NestedView, {});
567
- }
568
- href(page, params = {}) {
569
- const found = this.pages.find((it) => it.name === page.options.name);
570
- if (!found) {
571
- throw new Error(`Page ${page.options.name} not found`);
572
- }
573
- let url = found.path ?? "";
574
- let parent = found.parent;
575
- while (parent) {
576
- url = `${parent.path ?? ""}/${url}`;
577
- parent = parent.parent;
578
- }
579
- url = this.compile(url, params);
580
- return url.replace(/\/\/+/g, "/") || "/";
581
- }
582
- compile(path, params = {}) {
583
- for (const [key, value] of Object.entries(params)) {
584
- path = path.replace(`:${key}`, value);
585
- }
586
- return path;
587
- }
588
- renderView(index, path, view, page) {
589
- view ??= this.renderEmptyView();
590
- const element = page.client ? createElement(
591
- ClientOnly,
592
- typeof page.client === "object" ? page.client : {},
593
- view
594
- ) : view;
595
- return createElement(
596
- RouterLayerContext.Provider,
597
- {
598
- value: {
599
- index,
600
- path
601
- }
602
- },
603
- element
604
- );
605
- }
606
- configure = $hook({
607
- name: "configure",
608
- handler: () => {
609
- let hasNotFoundHandler = false;
610
- const pages = this.alepha.getDescriptorValues($page);
611
- for (const { value, key } of pages) {
612
- value[OPTIONS].name ??= key;
613
- }
614
- for (const { value } of pages) {
615
- if (value[OPTIONS].parent) {
616
- continue;
617
- }
618
- if (value[OPTIONS].path === "/*") {
619
- hasNotFoundHandler = true;
620
- }
621
- this.add(this.map(pages, value));
622
- }
623
- if (!hasNotFoundHandler && pages.length > 0) {
624
- this.add({
625
- path: "/*",
626
- name: "notFound",
627
- cache: true,
628
- component: NotFoundPage,
629
- afterHandler: ({ reply }) => {
630
- reply.status = 404;
631
- }
632
- });
633
- }
634
- }
635
- });
636
- map(pages, target) {
637
- const children = target[OPTIONS].children ?? [];
638
- return {
639
- ...target[OPTIONS],
640
- parent: void 0,
641
- children: children.map((it) => this.map(pages, it))
642
- };
643
- }
644
- add(entry) {
645
- if (this.alepha.isReady()) {
646
- throw new Error("Router is already initialized");
647
- }
648
- entry.name ??= this.nextId();
649
- const page = entry;
650
- page.match = this.createMatch(page);
651
- this.pages.push(page);
652
- if (page.children) {
653
- for (const child of page.children) {
654
- child.parent = page;
655
- this.add(child);
656
- }
657
- }
658
- }
659
- createMatch(page) {
660
- let url = page.path ?? "/";
661
- let target = page.parent;
662
- while (target) {
663
- url = `${target.path ?? ""}/${url}`;
664
- target = target.parent;
665
- }
666
- let path = url.replace(/\/\/+/g, "/");
667
- if (path.endsWith("/") && path !== "/") {
668
- path = path.slice(0, -1);
669
- }
670
- return path;
671
- }
672
- _next = 0;
673
- nextId() {
674
- this._next += 1;
675
- return `P${this._next}`;
676
- }
677
- }
678
- const isPageRoute = (it) => {
679
- return it && typeof it === "object" && typeof it.path === "string" && typeof it.page === "object";
680
- };
681
-
682
- class BrowserHeadProvider {
683
- renderHead(document, head) {
684
- if (head.title) {
685
- document.title = head.title;
686
- }
687
- if (head.bodyAttributes) {
688
- for (const [key, value] of Object.entries(head.bodyAttributes)) {
689
- if (value) {
690
- document.body.setAttribute(key, value);
691
- } else {
692
- document.body.removeAttribute(key);
693
- }
694
- }
695
- }
696
- if (head.htmlAttributes) {
697
- for (const [key, value] of Object.entries(head.htmlAttributes)) {
698
- if (value) {
699
- document.documentElement.setAttribute(key, value);
700
- } else {
701
- document.documentElement.removeAttribute(key);
702
- }
703
- }
704
- }
705
- if (head.meta) {
706
- for (const [key, value] of Object.entries(head.meta)) {
707
- const meta = document.querySelector(`meta[name="${key}"]`);
708
- if (meta) {
709
- meta.setAttribute("content", value.content);
710
- } else {
711
- const newMeta = document.createElement("meta");
712
- newMeta.setAttribute("name", key);
713
- newMeta.setAttribute("content", value.content);
714
- document.head.appendChild(newMeta);
715
- }
716
- }
717
- }
718
- }
719
- }
720
-
721
- class BrowserRouterProvider extends RouterProvider {
722
- log = $logger();
723
- alepha = $inject(Alepha);
724
- pageDescriptorProvider = $inject(PageDescriptorProvider);
725
- add(entry) {
726
- this.pageDescriptorProvider.add(entry);
727
- }
728
- configure = $hook({
729
- name: "configure",
730
- handler: async () => {
731
- for (const page of this.pageDescriptorProvider.getPages()) {
732
- if (page.component || page.lazy) {
733
- this.push({
734
- path: page.match,
735
- page
736
- });
737
- }
738
- }
739
- }
740
- });
741
- async transition(url, options = {}) {
742
- const { pathname, search } = url;
743
- const state = {
744
- pathname,
745
- search,
746
- layers: []
747
- };
748
- const context = {
749
- url,
750
- query: {},
751
- params: {},
752
- head: {},
753
- onError: () => null,
754
- ...options.context ?? {}
755
- };
756
- await this.alepha.emit("react:transition:begin", { state, context });
757
- try {
758
- const previous = options.previous;
759
- const { route, params } = this.match(pathname);
760
- const query = {};
761
- if (search) {
762
- for (const [key, value] of new URLSearchParams(search).entries()) {
763
- query[key] = String(value);
764
- }
765
- }
766
- context.query = query;
767
- context.params = params ?? {};
768
- context.previous = previous;
769
- if (isPageRoute(route)) {
770
- const result = await this.pageDescriptorProvider.createLayers(
771
- route.page,
772
- context
773
- );
774
- if (result.redirect) {
775
- return {
776
- redirect: result.redirect,
777
- state,
778
- context
779
- };
780
- }
781
- state.layers = result.layers;
782
- }
783
- if (state.layers.length === 0) {
784
- state.layers.push({
785
- name: "not-found",
786
- element: createElement(NotFoundPage),
787
- index: 0,
788
- path: "/"
789
- });
790
- }
791
- await this.alepha.emit("react:transition:success", { state });
792
- } catch (e) {
793
- this.log.error(e);
794
- state.layers = [
795
- {
796
- name: "error",
797
- element: this.pageDescriptorProvider.renderError(e),
798
- index: 0,
799
- path: "/"
800
- }
801
- ];
802
- await this.alepha.emit("react:transition:error", {
803
- error: e,
804
- state,
805
- context
806
- });
807
- }
808
- if (options.state) {
809
- options.state.layers = state.layers;
810
- options.state.pathname = state.pathname;
811
- options.state.search = state.search;
812
- }
813
- await this.alepha.emit("react:transition:end", {
814
- state: options.state,
815
- context
816
- });
817
- return {
818
- context,
819
- state
820
- };
821
- }
822
- root(state, context) {
823
- return this.pageDescriptorProvider.root(state, context);
824
- }
825
- }
826
-
827
- class ReactBrowserProvider {
828
- log = $logger();
829
- client = $inject(HttpClient);
830
- alepha = $inject(Alepha);
831
- router = $inject(BrowserRouterProvider);
832
- headProvider = $inject(BrowserHeadProvider);
833
- root;
834
- transitioning;
835
- state = {
836
- layers: [],
837
- pathname: "",
838
- search: ""
839
- };
840
- get document() {
841
- return window.document;
842
- }
843
- get history() {
844
- return window.history;
845
- }
846
- get url() {
847
- return window.location.pathname + window.location.search;
848
- }
849
- async invalidate(props) {
850
- const previous = [];
851
- if (props) {
852
- const [key] = Object.keys(props);
853
- const value = props[key];
854
- for (const layer of this.state.layers) {
855
- if (layer.props?.[key]) {
856
- previous.push({
857
- ...layer,
858
- props: {
859
- ...layer.props,
860
- [key]: value
861
- }
862
- });
863
- break;
864
- }
865
- previous.push(layer);
866
- }
867
- }
868
- await this.render({ previous });
869
- }
870
- async go(url, options = {}) {
871
- const result = await this.render({
872
- url
873
- });
874
- if (result.context.url.pathname !== url) {
875
- this.history.replaceState({}, "", result.context.url.pathname);
876
- return;
877
- }
878
- if (options.replace) {
879
- this.history.replaceState({}, "", url);
880
- return;
881
- }
882
- this.history.pushState({}, "", url);
883
- }
884
- async render(options = {}) {
885
- const previous = options.previous ?? this.state.layers;
886
- const url = options.url ?? this.url;
887
- this.transitioning = { to: url };
888
- const result = await this.router.transition(
889
- new URL(`http://localhost${url}`),
890
- {
891
- previous,
892
- state: this.state
893
- }
894
- );
895
- if (result.redirect) {
896
- return await this.render({ url: result.redirect });
897
- }
898
- this.transitioning = void 0;
899
- return result;
900
- }
901
- /**
902
- * Get embedded layers from the server.
903
- */
904
- getHydrationState() {
905
- try {
906
- if ("__ssr" in window && typeof window.__ssr === "object") {
907
- return window.__ssr;
908
- }
909
- } catch (error) {
910
- console.error(error);
911
- }
912
- }
913
- // -------------------------------------------------------------------------------------------------------------------
914
- ready = $hook({
915
- name: "ready",
916
- handler: async () => {
917
- const hydration = this.getHydrationState();
918
- const previous = hydration?.layers ?? [];
919
- if (hydration?.links) {
920
- for (const link of hydration.links.links) {
921
- this.client.pushLink(link);
922
- }
923
- }
924
- const { context } = await this.render({ previous });
925
- if (context.head) {
926
- this.headProvider.renderHead(this.document, context.head);
927
- }
928
- await this.alepha.emit("react:browser:render", {
929
- state: this.state,
930
- context,
931
- hydration
932
- });
933
- window.addEventListener("popstate", () => {
934
- this.render();
935
- });
936
- }
937
- });
938
- onTransitionEnd = $hook({
939
- name: "react:transition:end",
940
- handler: async ({ context }) => {
941
- this.headProvider.renderHead(this.document, context.head);
942
- }
943
- });
944
- }
945
-
946
- export { $page as $, BrowserRouterProvider as B, ClientOnly as C, ErrorBoundary as E, NestedView as N, PageDescriptorProvider as P, RouterContext as R, RouterLayerContext as a, ReactBrowserProvider as b, RedirectionError as c, useAlepha as d, isPageRoute as i, useRouterEvents as u };