@alepha/react 0.7.5 → 0.7.7

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