@alepha/react 0.8.1 → 0.9.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +0 -121
- package/dist/index.browser.js +63 -50
- package/dist/index.browser.js.map +1 -1
- package/dist/index.cjs +273 -383
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +48 -183
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +48 -183
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +279 -390
- package/dist/index.js.map +1 -1
- package/package.json +8 -8
- package/src/components/ErrorViewer.tsx +4 -3
- package/src/components/Link.tsx +3 -5
- package/src/descriptors/$page.ts +30 -49
- package/src/hooks/useInject.ts +1 -1
- package/src/hooks/useRouter.ts +2 -2
- package/src/index.browser.ts +13 -8
- package/src/index.ts +12 -130
- package/src/providers/PageDescriptorProvider.ts +34 -26
- package/src/providers/ReactBrowserProvider.ts +1 -1
- package/src/providers/ReactBrowserRenderer.ts +2 -2
- package/src/providers/ReactServerProvider.ts +15 -13
package/dist/index.js
CHANGED
|
@@ -1,31 +1,35 @@
|
|
|
1
|
-
import { $hook, $inject, $logger,
|
|
1
|
+
import { $env, $hook, $inject, $logger, $module, Alepha, Descriptor, KIND, NotImplementedError, createDescriptor, t } from "@alepha/core";
|
|
2
2
|
import { AlephaServer, ServerRouterProvider, ServerTimingProvider, apiLinksResponseSchema } from "@alepha/server";
|
|
3
3
|
import { AlephaServerCache } from "@alepha/server-cache";
|
|
4
4
|
import { AlephaServerLinks, LinkProvider, ServerLinksProvider } from "@alepha/server-links";
|
|
5
5
|
import React, { StrictMode, createContext, createElement, useContext, useEffect, useMemo, useState } from "react";
|
|
6
6
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
7
|
+
import { RouterProvider } from "@alepha/router";
|
|
7
8
|
import { existsSync } from "node:fs";
|
|
8
9
|
import { join } from "node:path";
|
|
9
10
|
import { ServerStaticProvider } from "@alepha/server-static";
|
|
10
11
|
import { renderToString } from "react-dom/server";
|
|
11
|
-
import { RouterProvider } from "@alepha/router";
|
|
12
12
|
|
|
13
13
|
//#region src/descriptors/$page.ts
|
|
14
|
-
const KEY = "PAGE";
|
|
15
14
|
/**
|
|
16
15
|
* Main descriptor for defining a React route in the application.
|
|
17
16
|
*/
|
|
18
17
|
const $page = (options) => {
|
|
19
|
-
|
|
20
|
-
return {
|
|
21
|
-
[KIND]: KEY,
|
|
22
|
-
[OPTIONS]: options,
|
|
23
|
-
render: () => {
|
|
24
|
-
throw new NotImplementedError(KEY);
|
|
25
|
-
}
|
|
26
|
-
};
|
|
18
|
+
return createDescriptor(PageDescriptor, options);
|
|
27
19
|
};
|
|
28
|
-
|
|
20
|
+
var PageDescriptor = class extends Descriptor {
|
|
21
|
+
get name() {
|
|
22
|
+
return this.options.name ?? this.config.propertyKey;
|
|
23
|
+
}
|
|
24
|
+
/**
|
|
25
|
+
* For testing or build purposes, this will render the page (with or without the HTML layout) and return the HTML and context.
|
|
26
|
+
* Only valid for server-side rendering, it will throw an error if called on the client-side.
|
|
27
|
+
*/
|
|
28
|
+
async render(options) {
|
|
29
|
+
throw new NotImplementedError("");
|
|
30
|
+
}
|
|
31
|
+
};
|
|
32
|
+
$page[KIND] = PageDescriptor;
|
|
29
33
|
|
|
30
34
|
//#endregion
|
|
31
35
|
//#region src/components/ClientOnly.tsx
|
|
@@ -47,23 +51,11 @@ const ClientOnly = (props) => {
|
|
|
47
51
|
};
|
|
48
52
|
var ClientOnly_default = ClientOnly;
|
|
49
53
|
|
|
50
|
-
//#endregion
|
|
51
|
-
//#region src/contexts/RouterContext.ts
|
|
52
|
-
const RouterContext = createContext(void 0);
|
|
53
|
-
|
|
54
|
-
//#endregion
|
|
55
|
-
//#region src/hooks/useAlepha.ts
|
|
56
|
-
const useAlepha = () => {
|
|
57
|
-
const routerContext = useContext(RouterContext);
|
|
58
|
-
if (!routerContext) throw new Error("useAlepha must be used within a RouterProvider");
|
|
59
|
-
return routerContext.alepha;
|
|
60
|
-
};
|
|
61
|
-
|
|
62
54
|
//#endregion
|
|
63
55
|
//#region src/components/ErrorViewer.tsx
|
|
64
|
-
const ErrorViewer = ({ error }) => {
|
|
56
|
+
const ErrorViewer = ({ error, alepha }) => {
|
|
65
57
|
const [expanded, setExpanded] = useState(false);
|
|
66
|
-
const isProduction =
|
|
58
|
+
const isProduction = alepha.isProduction();
|
|
67
59
|
if (isProduction) return /* @__PURE__ */ jsx(ErrorViewerProduction, {});
|
|
68
60
|
const stackLines = error.stack?.split("\n") ?? [];
|
|
69
61
|
const previewLines = stackLines.slice(0, 5);
|
|
@@ -207,6 +199,10 @@ const ErrorViewerProduction = () => {
|
|
|
207
199
|
});
|
|
208
200
|
};
|
|
209
201
|
|
|
202
|
+
//#endregion
|
|
203
|
+
//#region src/contexts/RouterContext.ts
|
|
204
|
+
const RouterContext = createContext(void 0);
|
|
205
|
+
|
|
210
206
|
//#endregion
|
|
211
207
|
//#region src/contexts/RouterLayerContext.ts
|
|
212
208
|
const RouterLayerContext = createContext(void 0);
|
|
@@ -341,7 +337,7 @@ var RedirectionError = class extends Error {
|
|
|
341
337
|
const envSchema$1 = t.object({ REACT_STRICT_MODE: t.boolean({ default: true }) });
|
|
342
338
|
var PageDescriptorProvider = class {
|
|
343
339
|
log = $logger();
|
|
344
|
-
env = $
|
|
340
|
+
env = $env(envSchema$1);
|
|
345
341
|
alepha = $inject(Alepha);
|
|
346
342
|
pages = [];
|
|
347
343
|
getPages() {
|
|
@@ -514,7 +510,10 @@ var PageDescriptorProvider = class {
|
|
|
514
510
|
return void 0;
|
|
515
511
|
}
|
|
516
512
|
renderError(error) {
|
|
517
|
-
return createElement(ErrorViewer_default, {
|
|
513
|
+
return createElement(ErrorViewer_default, {
|
|
514
|
+
error,
|
|
515
|
+
alepha: this.alepha
|
|
516
|
+
});
|
|
518
517
|
}
|
|
519
518
|
renderEmptyView() {
|
|
520
519
|
return createElement(NestedView_default, {});
|
|
@@ -547,18 +546,17 @@ var PageDescriptorProvider = class {
|
|
|
547
546
|
on: "configure",
|
|
548
547
|
handler: () => {
|
|
549
548
|
let hasNotFoundHandler = false;
|
|
550
|
-
const pages = this.alepha.
|
|
549
|
+
const pages = this.alepha.descriptors($page);
|
|
551
550
|
const hasParent = (it) => {
|
|
552
551
|
for (const page of pages) {
|
|
553
|
-
const children = page.
|
|
552
|
+
const children = page.options.children ? Array.isArray(page.options.children) ? page.options.children : page.options.children() : [];
|
|
554
553
|
if (children.includes(it)) return true;
|
|
555
554
|
}
|
|
556
555
|
};
|
|
557
|
-
for (const
|
|
558
|
-
|
|
559
|
-
if (
|
|
560
|
-
|
|
561
|
-
this.add(this.map(pages, value));
|
|
556
|
+
for (const page of pages) {
|
|
557
|
+
if (page.options.path === "/*") hasNotFoundHandler = true;
|
|
558
|
+
if (hasParent(page)) continue;
|
|
559
|
+
this.add(this.map(pages, page));
|
|
562
560
|
}
|
|
563
561
|
if (!hasNotFoundHandler && pages.length > 0) this.add({
|
|
564
562
|
path: "/*",
|
|
@@ -572,9 +570,10 @@ var PageDescriptorProvider = class {
|
|
|
572
570
|
}
|
|
573
571
|
});
|
|
574
572
|
map(pages, target) {
|
|
575
|
-
const children = target
|
|
573
|
+
const children = target.options.children ? Array.isArray(target.options.children) ? target.options.children : target.options.children() : [];
|
|
576
574
|
return {
|
|
577
|
-
...target
|
|
575
|
+
...target.options,
|
|
576
|
+
name: target.name,
|
|
578
577
|
parent: void 0,
|
|
579
578
|
children: children.map((it) => this.map(pages, it))
|
|
580
579
|
};
|
|
@@ -611,13 +610,222 @@ const isPageRoute = (it) => {
|
|
|
611
610
|
return it && typeof it === "object" && typeof it.path === "string" && typeof it.page === "object";
|
|
612
611
|
};
|
|
613
612
|
|
|
613
|
+
//#endregion
|
|
614
|
+
//#region src/providers/BrowserRouterProvider.ts
|
|
615
|
+
var BrowserRouterProvider = class extends RouterProvider {
|
|
616
|
+
log = $logger();
|
|
617
|
+
alepha = $inject(Alepha);
|
|
618
|
+
pageDescriptorProvider = $inject(PageDescriptorProvider);
|
|
619
|
+
add(entry) {
|
|
620
|
+
this.pageDescriptorProvider.add(entry);
|
|
621
|
+
}
|
|
622
|
+
configure = $hook({
|
|
623
|
+
on: "configure",
|
|
624
|
+
handler: async () => {
|
|
625
|
+
for (const page of this.pageDescriptorProvider.getPages()) if (page.component || page.lazy) this.push({
|
|
626
|
+
path: page.match,
|
|
627
|
+
page
|
|
628
|
+
});
|
|
629
|
+
}
|
|
630
|
+
});
|
|
631
|
+
async transition(url, options = {}) {
|
|
632
|
+
const { pathname, search } = url;
|
|
633
|
+
const state = {
|
|
634
|
+
pathname,
|
|
635
|
+
search,
|
|
636
|
+
layers: []
|
|
637
|
+
};
|
|
638
|
+
const context = {
|
|
639
|
+
url,
|
|
640
|
+
query: {},
|
|
641
|
+
params: {},
|
|
642
|
+
onError: () => null,
|
|
643
|
+
...options.context ?? {}
|
|
644
|
+
};
|
|
645
|
+
await this.alepha.emit("react:transition:begin", {
|
|
646
|
+
state,
|
|
647
|
+
context
|
|
648
|
+
});
|
|
649
|
+
try {
|
|
650
|
+
const previous = options.previous;
|
|
651
|
+
const { route, params } = this.match(pathname);
|
|
652
|
+
const query = {};
|
|
653
|
+
if (search) for (const [key, value] of new URLSearchParams(search).entries()) query[key] = String(value);
|
|
654
|
+
context.query = query;
|
|
655
|
+
context.params = params ?? {};
|
|
656
|
+
context.previous = previous;
|
|
657
|
+
if (isPageRoute(route)) {
|
|
658
|
+
const result = await this.pageDescriptorProvider.createLayers(route.page, context);
|
|
659
|
+
if (result.redirect) return {
|
|
660
|
+
redirect: result.redirect,
|
|
661
|
+
state,
|
|
662
|
+
context
|
|
663
|
+
};
|
|
664
|
+
state.layers = result.layers;
|
|
665
|
+
}
|
|
666
|
+
if (state.layers.length === 0) state.layers.push({
|
|
667
|
+
name: "not-found",
|
|
668
|
+
element: createElement(NotFoundPage),
|
|
669
|
+
index: 0,
|
|
670
|
+
path: "/"
|
|
671
|
+
});
|
|
672
|
+
await this.alepha.emit("react:transition:success", {
|
|
673
|
+
state,
|
|
674
|
+
context
|
|
675
|
+
});
|
|
676
|
+
} catch (e) {
|
|
677
|
+
this.log.error(e);
|
|
678
|
+
state.layers = [{
|
|
679
|
+
name: "error",
|
|
680
|
+
element: this.pageDescriptorProvider.renderError(e),
|
|
681
|
+
index: 0,
|
|
682
|
+
path: "/"
|
|
683
|
+
}];
|
|
684
|
+
await this.alepha.emit("react:transition:error", {
|
|
685
|
+
error: e,
|
|
686
|
+
state,
|
|
687
|
+
context
|
|
688
|
+
});
|
|
689
|
+
}
|
|
690
|
+
if (options.state) {
|
|
691
|
+
options.state.layers = state.layers;
|
|
692
|
+
options.state.pathname = state.pathname;
|
|
693
|
+
options.state.search = state.search;
|
|
694
|
+
}
|
|
695
|
+
await this.alepha.emit("react:transition:end", {
|
|
696
|
+
state: options.state,
|
|
697
|
+
context
|
|
698
|
+
});
|
|
699
|
+
return {
|
|
700
|
+
context,
|
|
701
|
+
state
|
|
702
|
+
};
|
|
703
|
+
}
|
|
704
|
+
root(state, context) {
|
|
705
|
+
return this.pageDescriptorProvider.root(state, context);
|
|
706
|
+
}
|
|
707
|
+
};
|
|
708
|
+
|
|
709
|
+
//#endregion
|
|
710
|
+
//#region src/providers/ReactBrowserProvider.ts
|
|
711
|
+
var ReactBrowserProvider = class {
|
|
712
|
+
log = $logger();
|
|
713
|
+
client = $inject(LinkProvider);
|
|
714
|
+
alepha = $inject(Alepha);
|
|
715
|
+
router = $inject(BrowserRouterProvider);
|
|
716
|
+
root;
|
|
717
|
+
transitioning;
|
|
718
|
+
state = {
|
|
719
|
+
layers: [],
|
|
720
|
+
pathname: "",
|
|
721
|
+
search: ""
|
|
722
|
+
};
|
|
723
|
+
get document() {
|
|
724
|
+
return window.document;
|
|
725
|
+
}
|
|
726
|
+
get history() {
|
|
727
|
+
return window.history;
|
|
728
|
+
}
|
|
729
|
+
get location() {
|
|
730
|
+
return window.location;
|
|
731
|
+
}
|
|
732
|
+
get url() {
|
|
733
|
+
let url = this.location.pathname + this.location.search;
|
|
734
|
+
if (import.meta?.env?.BASE_URL) {
|
|
735
|
+
url = url.replace(import.meta.env?.BASE_URL, "");
|
|
736
|
+
if (!url.startsWith("/")) url = `/${url}`;
|
|
737
|
+
}
|
|
738
|
+
return url;
|
|
739
|
+
}
|
|
740
|
+
pushState(url, replace) {
|
|
741
|
+
let path = url;
|
|
742
|
+
if (import.meta?.env?.BASE_URL) path = (import.meta.env?.BASE_URL + path).replaceAll("//", "/");
|
|
743
|
+
if (replace) this.history.replaceState({}, "", path);
|
|
744
|
+
else this.history.pushState({}, "", path);
|
|
745
|
+
}
|
|
746
|
+
async invalidate(props) {
|
|
747
|
+
const previous = [];
|
|
748
|
+
if (props) {
|
|
749
|
+
const [key] = Object.keys(props);
|
|
750
|
+
const value = props[key];
|
|
751
|
+
for (const layer of this.state.layers) {
|
|
752
|
+
if (layer.props?.[key]) {
|
|
753
|
+
previous.push({
|
|
754
|
+
...layer,
|
|
755
|
+
props: {
|
|
756
|
+
...layer.props,
|
|
757
|
+
[key]: value
|
|
758
|
+
}
|
|
759
|
+
});
|
|
760
|
+
break;
|
|
761
|
+
}
|
|
762
|
+
previous.push(layer);
|
|
763
|
+
}
|
|
764
|
+
}
|
|
765
|
+
await this.render({ previous });
|
|
766
|
+
}
|
|
767
|
+
async go(url, options = {}) {
|
|
768
|
+
const result = await this.render({ url });
|
|
769
|
+
if (result.context.url.pathname !== url) {
|
|
770
|
+
this.pushState(result.context.url.pathname);
|
|
771
|
+
return;
|
|
772
|
+
}
|
|
773
|
+
if (options.replace) {
|
|
774
|
+
this.pushState(url);
|
|
775
|
+
return;
|
|
776
|
+
}
|
|
777
|
+
this.pushState(url);
|
|
778
|
+
}
|
|
779
|
+
async render(options = {}) {
|
|
780
|
+
const previous = options.previous ?? this.state.layers;
|
|
781
|
+
const url = options.url ?? this.url;
|
|
782
|
+
this.transitioning = { to: url };
|
|
783
|
+
const result = await this.router.transition(new URL(`http://localhost${url}`), {
|
|
784
|
+
previous,
|
|
785
|
+
state: this.state
|
|
786
|
+
});
|
|
787
|
+
if (result.redirect) return await this.render({ url: result.redirect });
|
|
788
|
+
this.transitioning = void 0;
|
|
789
|
+
return result;
|
|
790
|
+
}
|
|
791
|
+
/**
|
|
792
|
+
* Get embedded layers from the server.
|
|
793
|
+
*/
|
|
794
|
+
getHydrationState() {
|
|
795
|
+
try {
|
|
796
|
+
if ("__ssr" in window && typeof window.__ssr === "object") return window.__ssr;
|
|
797
|
+
} catch (error) {
|
|
798
|
+
console.error(error);
|
|
799
|
+
}
|
|
800
|
+
}
|
|
801
|
+
ready = $hook({
|
|
802
|
+
on: "ready",
|
|
803
|
+
handler: async () => {
|
|
804
|
+
const hydration = this.getHydrationState();
|
|
805
|
+
const previous = hydration?.layers ?? [];
|
|
806
|
+
if (hydration?.links) for (const link of hydration.links.links) this.client.pushLink(link);
|
|
807
|
+
const { context } = await this.render({ previous });
|
|
808
|
+
await this.alepha.emit("react:browser:render", {
|
|
809
|
+
state: this.state,
|
|
810
|
+
context,
|
|
811
|
+
hydration
|
|
812
|
+
});
|
|
813
|
+
window.addEventListener("popstate", () => {
|
|
814
|
+
if (this.state.pathname === this.url) return;
|
|
815
|
+
this.render();
|
|
816
|
+
});
|
|
817
|
+
}
|
|
818
|
+
});
|
|
819
|
+
};
|
|
820
|
+
|
|
614
821
|
//#endregion
|
|
615
822
|
//#region src/providers/ReactServerProvider.ts
|
|
616
823
|
const envSchema = t.object({
|
|
617
824
|
REACT_SERVER_DIST: t.string({ default: "public" }),
|
|
618
825
|
REACT_SERVER_PREFIX: t.string({ default: "" }),
|
|
619
826
|
REACT_SSR_ENABLED: t.optional(t.boolean()),
|
|
620
|
-
REACT_ROOT_ID: t.string({ default: "root" })
|
|
827
|
+
REACT_ROOT_ID: t.string({ default: "root" }),
|
|
828
|
+
REACT_SERVER_TEMPLATE: t.optional(t.string({ size: "rich" }))
|
|
621
829
|
});
|
|
622
830
|
var ReactServerProvider = class {
|
|
623
831
|
log = $logger();
|
|
@@ -626,18 +834,15 @@ var ReactServerProvider = class {
|
|
|
626
834
|
serverStaticProvider = $inject(ServerStaticProvider);
|
|
627
835
|
serverRouterProvider = $inject(ServerRouterProvider);
|
|
628
836
|
serverTimingProvider = $inject(ServerTimingProvider);
|
|
629
|
-
env = $
|
|
837
|
+
env = $env(envSchema);
|
|
630
838
|
ROOT_DIV_REGEX = new RegExp(`<div([^>]*)\\s+id=["']${this.env.REACT_ROOT_ID}["']([^>]*)>(.*?)<\\/div>`, "is");
|
|
631
839
|
onConfigure = $hook({
|
|
632
840
|
on: "configure",
|
|
633
841
|
handler: async () => {
|
|
634
|
-
const pages = this.alepha.
|
|
842
|
+
const pages = this.alepha.descriptors($page);
|
|
635
843
|
const ssrEnabled = pages.length > 0 && this.env.REACT_SSR_ENABLED !== false;
|
|
636
844
|
this.alepha.state("react.server.ssr", ssrEnabled);
|
|
637
|
-
for (const
|
|
638
|
-
const name = value[OPTIONS].name ?? key;
|
|
639
|
-
instance[key].render = this.createRenderFunction(name);
|
|
640
|
-
}
|
|
845
|
+
for (const page of pages) page.render = this.createRenderFunction(page.name);
|
|
641
846
|
if (this.alepha.isServerless() === "vite") {
|
|
642
847
|
await this.configureVite(ssrEnabled);
|
|
643
848
|
return;
|
|
@@ -657,7 +862,7 @@ var ReactServerProvider = class {
|
|
|
657
862
|
return;
|
|
658
863
|
}
|
|
659
864
|
this.log.info("SSR is disabled, use History API fallback");
|
|
660
|
-
|
|
865
|
+
this.serverRouterProvider.createRoute({
|
|
661
866
|
path: "*",
|
|
662
867
|
handler: async ({ url, reply }) => {
|
|
663
868
|
if (url.pathname.includes(".")) {
|
|
@@ -673,13 +878,13 @@ var ReactServerProvider = class {
|
|
|
673
878
|
}
|
|
674
879
|
});
|
|
675
880
|
get template() {
|
|
676
|
-
return this.alepha.
|
|
881
|
+
return this.alepha.env.REACT_SERVER_TEMPLATE ?? "<!DOCTYPE html><html lang='en'><head></head><body></body></html>";
|
|
677
882
|
}
|
|
678
883
|
async registerPages(templateLoader) {
|
|
679
884
|
for (const page of this.pageDescriptorProvider.getPages()) {
|
|
680
885
|
if (page.children?.length) continue;
|
|
681
886
|
this.log.debug(`+ ${page.match} -> ${page.name}`);
|
|
682
|
-
|
|
887
|
+
this.serverRouterProvider.createRoute({
|
|
683
888
|
...page,
|
|
684
889
|
schema: void 0,
|
|
685
890
|
method: "GET",
|
|
@@ -694,7 +899,7 @@ var ReactServerProvider = class {
|
|
|
694
899
|
return "";
|
|
695
900
|
}
|
|
696
901
|
async configureStaticServer(root) {
|
|
697
|
-
await this.serverStaticProvider.
|
|
902
|
+
await this.serverStaticProvider.createStaticServer({
|
|
698
903
|
root,
|
|
699
904
|
path: this.env.REACT_SERVER_PREFIX
|
|
700
905
|
});
|
|
@@ -748,7 +953,7 @@ var ReactServerProvider = class {
|
|
|
748
953
|
onError: () => null
|
|
749
954
|
};
|
|
750
955
|
if (this.alepha.has(ServerLinksProvider)) {
|
|
751
|
-
const srv = this.alepha.
|
|
956
|
+
const srv = this.alepha.inject(ServerLinksProvider);
|
|
752
957
|
const schema = apiLinksResponseSchema;
|
|
753
958
|
context.links = this.alepha.parse(schema, await srv.getLinks({
|
|
754
959
|
user: serverRequest.user,
|
|
@@ -838,214 +1043,6 @@ var ReactServerProvider = class {
|
|
|
838
1043
|
}
|
|
839
1044
|
};
|
|
840
1045
|
|
|
841
|
-
//#endregion
|
|
842
|
-
//#region src/providers/BrowserRouterProvider.ts
|
|
843
|
-
var BrowserRouterProvider = class extends RouterProvider {
|
|
844
|
-
log = $logger();
|
|
845
|
-
alepha = $inject(Alepha);
|
|
846
|
-
pageDescriptorProvider = $inject(PageDescriptorProvider);
|
|
847
|
-
add(entry) {
|
|
848
|
-
this.pageDescriptorProvider.add(entry);
|
|
849
|
-
}
|
|
850
|
-
configure = $hook({
|
|
851
|
-
on: "configure",
|
|
852
|
-
handler: async () => {
|
|
853
|
-
for (const page of this.pageDescriptorProvider.getPages()) if (page.component || page.lazy) this.push({
|
|
854
|
-
path: page.match,
|
|
855
|
-
page
|
|
856
|
-
});
|
|
857
|
-
}
|
|
858
|
-
});
|
|
859
|
-
async transition(url, options = {}) {
|
|
860
|
-
const { pathname, search } = url;
|
|
861
|
-
const state = {
|
|
862
|
-
pathname,
|
|
863
|
-
search,
|
|
864
|
-
layers: []
|
|
865
|
-
};
|
|
866
|
-
const context = {
|
|
867
|
-
url,
|
|
868
|
-
query: {},
|
|
869
|
-
params: {},
|
|
870
|
-
onError: () => null,
|
|
871
|
-
...options.context ?? {}
|
|
872
|
-
};
|
|
873
|
-
await this.alepha.emit("react:transition:begin", {
|
|
874
|
-
state,
|
|
875
|
-
context
|
|
876
|
-
});
|
|
877
|
-
try {
|
|
878
|
-
const previous = options.previous;
|
|
879
|
-
const { route, params } = this.match(pathname);
|
|
880
|
-
const query = {};
|
|
881
|
-
if (search) for (const [key, value] of new URLSearchParams(search).entries()) query[key] = String(value);
|
|
882
|
-
context.query = query;
|
|
883
|
-
context.params = params ?? {};
|
|
884
|
-
context.previous = previous;
|
|
885
|
-
if (isPageRoute(route)) {
|
|
886
|
-
const result = await this.pageDescriptorProvider.createLayers(route.page, context);
|
|
887
|
-
if (result.redirect) return {
|
|
888
|
-
redirect: result.redirect,
|
|
889
|
-
state,
|
|
890
|
-
context
|
|
891
|
-
};
|
|
892
|
-
state.layers = result.layers;
|
|
893
|
-
}
|
|
894
|
-
if (state.layers.length === 0) state.layers.push({
|
|
895
|
-
name: "not-found",
|
|
896
|
-
element: createElement(NotFoundPage),
|
|
897
|
-
index: 0,
|
|
898
|
-
path: "/"
|
|
899
|
-
});
|
|
900
|
-
await this.alepha.emit("react:transition:success", {
|
|
901
|
-
state,
|
|
902
|
-
context
|
|
903
|
-
});
|
|
904
|
-
} catch (e) {
|
|
905
|
-
this.log.error(e);
|
|
906
|
-
state.layers = [{
|
|
907
|
-
name: "error",
|
|
908
|
-
element: this.pageDescriptorProvider.renderError(e),
|
|
909
|
-
index: 0,
|
|
910
|
-
path: "/"
|
|
911
|
-
}];
|
|
912
|
-
await this.alepha.emit("react:transition:error", {
|
|
913
|
-
error: e,
|
|
914
|
-
state,
|
|
915
|
-
context
|
|
916
|
-
});
|
|
917
|
-
}
|
|
918
|
-
if (options.state) {
|
|
919
|
-
options.state.layers = state.layers;
|
|
920
|
-
options.state.pathname = state.pathname;
|
|
921
|
-
options.state.search = state.search;
|
|
922
|
-
}
|
|
923
|
-
await this.alepha.emit("react:transition:end", {
|
|
924
|
-
state: options.state,
|
|
925
|
-
context
|
|
926
|
-
});
|
|
927
|
-
return {
|
|
928
|
-
context,
|
|
929
|
-
state
|
|
930
|
-
};
|
|
931
|
-
}
|
|
932
|
-
root(state, context) {
|
|
933
|
-
return this.pageDescriptorProvider.root(state, context);
|
|
934
|
-
}
|
|
935
|
-
};
|
|
936
|
-
|
|
937
|
-
//#endregion
|
|
938
|
-
//#region src/providers/ReactBrowserProvider.ts
|
|
939
|
-
var ReactBrowserProvider = class {
|
|
940
|
-
log = $logger();
|
|
941
|
-
client = $inject(LinkProvider);
|
|
942
|
-
alepha = $inject(Alepha);
|
|
943
|
-
router = $inject(BrowserRouterProvider);
|
|
944
|
-
root;
|
|
945
|
-
transitioning;
|
|
946
|
-
state = {
|
|
947
|
-
layers: [],
|
|
948
|
-
pathname: "",
|
|
949
|
-
search: ""
|
|
950
|
-
};
|
|
951
|
-
get document() {
|
|
952
|
-
return window.document;
|
|
953
|
-
}
|
|
954
|
-
get history() {
|
|
955
|
-
return window.history;
|
|
956
|
-
}
|
|
957
|
-
get location() {
|
|
958
|
-
return window.location;
|
|
959
|
-
}
|
|
960
|
-
get url() {
|
|
961
|
-
let url = this.location.pathname + this.location.search;
|
|
962
|
-
if (import.meta?.env?.BASE_URL) {
|
|
963
|
-
url = url.replace(import.meta.env?.BASE_URL, "");
|
|
964
|
-
if (!url.startsWith("/")) url = `/${url}`;
|
|
965
|
-
}
|
|
966
|
-
return url;
|
|
967
|
-
}
|
|
968
|
-
pushState(url, replace) {
|
|
969
|
-
let path = url;
|
|
970
|
-
if (import.meta?.env?.BASE_URL) path = (import.meta.env?.BASE_URL + path).replaceAll("//", "/");
|
|
971
|
-
if (replace) this.history.replaceState({}, "", path);
|
|
972
|
-
else this.history.pushState({}, "", path);
|
|
973
|
-
}
|
|
974
|
-
async invalidate(props) {
|
|
975
|
-
const previous = [];
|
|
976
|
-
if (props) {
|
|
977
|
-
const [key] = Object.keys(props);
|
|
978
|
-
const value = props[key];
|
|
979
|
-
for (const layer of this.state.layers) {
|
|
980
|
-
if (layer.props?.[key]) {
|
|
981
|
-
previous.push({
|
|
982
|
-
...layer,
|
|
983
|
-
props: {
|
|
984
|
-
...layer.props,
|
|
985
|
-
[key]: value
|
|
986
|
-
}
|
|
987
|
-
});
|
|
988
|
-
break;
|
|
989
|
-
}
|
|
990
|
-
previous.push(layer);
|
|
991
|
-
}
|
|
992
|
-
}
|
|
993
|
-
await this.render({ previous });
|
|
994
|
-
}
|
|
995
|
-
async go(url, options = {}) {
|
|
996
|
-
const result = await this.render({ url });
|
|
997
|
-
if (result.context.url.pathname !== url) {
|
|
998
|
-
this.pushState(result.context.url.pathname);
|
|
999
|
-
return;
|
|
1000
|
-
}
|
|
1001
|
-
if (options.replace) {
|
|
1002
|
-
this.pushState(url);
|
|
1003
|
-
return;
|
|
1004
|
-
}
|
|
1005
|
-
this.pushState(url);
|
|
1006
|
-
}
|
|
1007
|
-
async render(options = {}) {
|
|
1008
|
-
const previous = options.previous ?? this.state.layers;
|
|
1009
|
-
const url = options.url ?? this.url;
|
|
1010
|
-
this.transitioning = { to: url };
|
|
1011
|
-
const result = await this.router.transition(new URL(`http://localhost${url}`), {
|
|
1012
|
-
previous,
|
|
1013
|
-
state: this.state
|
|
1014
|
-
});
|
|
1015
|
-
if (result.redirect) return await this.render({ url: result.redirect });
|
|
1016
|
-
this.transitioning = void 0;
|
|
1017
|
-
return result;
|
|
1018
|
-
}
|
|
1019
|
-
/**
|
|
1020
|
-
* Get embedded layers from the server.
|
|
1021
|
-
*/
|
|
1022
|
-
getHydrationState() {
|
|
1023
|
-
try {
|
|
1024
|
-
if ("__ssr" in window && typeof window.__ssr === "object") return window.__ssr;
|
|
1025
|
-
} catch (error) {
|
|
1026
|
-
console.error(error);
|
|
1027
|
-
}
|
|
1028
|
-
}
|
|
1029
|
-
ready = $hook({
|
|
1030
|
-
on: "ready",
|
|
1031
|
-
handler: async () => {
|
|
1032
|
-
const hydration = this.getHydrationState();
|
|
1033
|
-
const previous = hydration?.layers ?? [];
|
|
1034
|
-
if (hydration?.links) for (const link of hydration.links.links) this.client.pushLink(link);
|
|
1035
|
-
const { context } = await this.render({ previous });
|
|
1036
|
-
await this.alepha.emit("react:browser:render", {
|
|
1037
|
-
state: this.state,
|
|
1038
|
-
context,
|
|
1039
|
-
hydration
|
|
1040
|
-
});
|
|
1041
|
-
window.addEventListener("popstate", () => {
|
|
1042
|
-
if (this.state.pathname === location.pathname) return;
|
|
1043
|
-
this.render();
|
|
1044
|
-
});
|
|
1045
|
-
}
|
|
1046
|
-
});
|
|
1047
|
-
};
|
|
1048
|
-
|
|
1049
1046
|
//#endregion
|
|
1050
1047
|
//#region src/hooks/RouterHookApi.ts
|
|
1051
1048
|
var RouterHookApi = class {
|
|
@@ -1139,9 +1136,9 @@ const useRouter = () => {
|
|
|
1139
1136
|
const layer = useContext(RouterLayerContext);
|
|
1140
1137
|
if (!ctx || !layer) throw new Error("useRouter must be used within a RouterProvider");
|
|
1141
1138
|
const pages = useMemo(() => {
|
|
1142
|
-
return ctx.alepha.
|
|
1139
|
+
return ctx.alepha.inject(PageDescriptorProvider).getPages();
|
|
1143
1140
|
}, []);
|
|
1144
|
-
return useMemo(() => new RouterHookApi(pages, ctx.context, ctx.state, layer, ctx.alepha.isBrowser() ? ctx.alepha.
|
|
1141
|
+
return useMemo(() => new RouterHookApi(pages, ctx.context, ctx.state, layer, ctx.alepha.isBrowser() ? ctx.alepha.inject(ReactBrowserProvider) : void 0), [layer]);
|
|
1145
1142
|
};
|
|
1146
1143
|
|
|
1147
1144
|
//#endregion
|
|
@@ -1149,11 +1146,11 @@ const useRouter = () => {
|
|
|
1149
1146
|
const Link = (props) => {
|
|
1150
1147
|
React.useContext(RouterContext);
|
|
1151
1148
|
const router = useRouter();
|
|
1152
|
-
const to = typeof props.to === "string" ? props.to : props.to
|
|
1149
|
+
const to = typeof props.to === "string" ? props.to : props.to.options.path;
|
|
1153
1150
|
if (!to) return null;
|
|
1154
|
-
const can = typeof props.to === "string" ? void 0 : props.to
|
|
1151
|
+
const can = typeof props.to === "string" ? void 0 : props.to.options.can;
|
|
1155
1152
|
if (can && !can()) return null;
|
|
1156
|
-
const name = typeof props.to === "string" ? void 0 : props.to
|
|
1153
|
+
const name = typeof props.to === "string" ? void 0 : props.to.options.name;
|
|
1157
1154
|
const anchorProps = {
|
|
1158
1155
|
...props,
|
|
1159
1156
|
to: void 0
|
|
@@ -1200,12 +1197,20 @@ const useActive = (path) => {
|
|
|
1200
1197
|
};
|
|
1201
1198
|
};
|
|
1202
1199
|
|
|
1200
|
+
//#endregion
|
|
1201
|
+
//#region src/hooks/useAlepha.ts
|
|
1202
|
+
const useAlepha = () => {
|
|
1203
|
+
const routerContext = useContext(RouterContext);
|
|
1204
|
+
if (!routerContext) throw new Error("useAlepha must be used within a RouterProvider");
|
|
1205
|
+
return routerContext.alepha;
|
|
1206
|
+
};
|
|
1207
|
+
|
|
1203
1208
|
//#endregion
|
|
1204
1209
|
//#region src/hooks/useInject.ts
|
|
1205
1210
|
const useInject = (clazz) => {
|
|
1206
1211
|
const ctx = useContext(RouterContext);
|
|
1207
1212
|
if (!ctx) throw new Error("useRouter must be used within a <RouterProvider>");
|
|
1208
|
-
return useMemo(() => ctx.alepha.
|
|
1213
|
+
return useMemo(() => ctx.alepha.inject(clazz), []);
|
|
1209
1214
|
};
|
|
1210
1215
|
|
|
1211
1216
|
//#endregion
|
|
@@ -1267,136 +1272,20 @@ const useRouterState = () => {
|
|
|
1267
1272
|
* It delivers seamless server-side rendering, automatic code splitting, and client-side navigation with full
|
|
1268
1273
|
* type safety and schema validation for route parameters and data.
|
|
1269
1274
|
*
|
|
1270
|
-
* **Key Features:**
|
|
1271
|
-
* - Declarative page definition with `$page` descriptor
|
|
1272
|
-
* - Server-side rendering (SSR) with automatic hydration
|
|
1273
|
-
* - Type-safe routing with parameter validation
|
|
1274
|
-
* - Schema-based data resolution and validation
|
|
1275
|
-
* - SEO-friendly meta tag management
|
|
1276
|
-
* - Automatic code splitting and lazy loading
|
|
1277
|
-
* - Client-side navigation with browser history
|
|
1278
|
-
*
|
|
1279
|
-
* **Basic Usage:**
|
|
1280
|
-
* ```ts
|
|
1281
|
-
* import { Alepha, run, t } from "alepha";
|
|
1282
|
-
* import { AlephaReact, $page } from "alepha/react";
|
|
1283
|
-
*
|
|
1284
|
-
* class AppRoutes {
|
|
1285
|
-
* // Home page
|
|
1286
|
-
* home = $page({
|
|
1287
|
-
* path: "/",
|
|
1288
|
-
* component: () => (
|
|
1289
|
-
* <div>
|
|
1290
|
-
* <h1>Welcome to Alepha</h1>
|
|
1291
|
-
* <p>Build amazing React applications!</p>
|
|
1292
|
-
* </div>
|
|
1293
|
-
* ),
|
|
1294
|
-
* });
|
|
1295
|
-
*
|
|
1296
|
-
* // About page with meta tags
|
|
1297
|
-
* about = $page({
|
|
1298
|
-
* path: "/about",
|
|
1299
|
-
* head: {
|
|
1300
|
-
* title: "About Us",
|
|
1301
|
-
* description: "Learn more about our mission",
|
|
1302
|
-
* },
|
|
1303
|
-
* component: () => (
|
|
1304
|
-
* <div>
|
|
1305
|
-
* <h1>About Us</h1>
|
|
1306
|
-
* <p>Learn more about our mission.</p>
|
|
1307
|
-
* </div>
|
|
1308
|
-
* ),
|
|
1309
|
-
* });
|
|
1310
|
-
* }
|
|
1311
|
-
*
|
|
1312
|
-
* const alepha = Alepha.create()
|
|
1313
|
-
* .with(AlephaReact)
|
|
1314
|
-
* .with(AppRoutes);
|
|
1315
|
-
*
|
|
1316
|
-
* run(alepha);
|
|
1317
|
-
* ```
|
|
1318
|
-
*
|
|
1319
|
-
* **Dynamic Routes with Parameters:**
|
|
1320
|
-
* ```tsx
|
|
1321
|
-
* class UserRoutes {
|
|
1322
|
-
* userProfile = $page({
|
|
1323
|
-
* path: "/users/:id",
|
|
1324
|
-
* schema: {
|
|
1325
|
-
* params: t.object({
|
|
1326
|
-
* id: t.string(),
|
|
1327
|
-
* }),
|
|
1328
|
-
* },
|
|
1329
|
-
* resolve: async ({ params }) => {
|
|
1330
|
-
* // Fetch user data server-side
|
|
1331
|
-
* const user = await getUserById(params.id);
|
|
1332
|
-
* return { user };
|
|
1333
|
-
* },
|
|
1334
|
-
* head: ({ user }) => ({
|
|
1335
|
-
* title: `${user.name} - Profile`,
|
|
1336
|
-
* description: `View ${user.name}'s profile`,
|
|
1337
|
-
* }),
|
|
1338
|
-
* component: ({ user }) => (
|
|
1339
|
-
* <div>
|
|
1340
|
-
* <h1>{user.name}</h1>
|
|
1341
|
-
* <p>Email: {user.email}</p>
|
|
1342
|
-
* </div>
|
|
1343
|
-
* ),
|
|
1344
|
-
* });
|
|
1345
|
-
*
|
|
1346
|
-
* userSettings = $page({
|
|
1347
|
-
* path: "/users/:id/settings",
|
|
1348
|
-
* schema: {
|
|
1349
|
-
* params: t.object({
|
|
1350
|
-
* id: t.string(),
|
|
1351
|
-
* }),
|
|
1352
|
-
* },
|
|
1353
|
-
* component: ({ params }) => (
|
|
1354
|
-
* <UserSettings userId={params.id} />
|
|
1355
|
-
* ),
|
|
1356
|
-
* });
|
|
1357
|
-
* }
|
|
1358
|
-
* ```
|
|
1359
|
-
*
|
|
1360
|
-
* **Static Generation:**
|
|
1361
|
-
* ```tsx
|
|
1362
|
-
* class BlogRoutes {
|
|
1363
|
-
* blogPost = $page({
|
|
1364
|
-
* path: "/blog/:slug",
|
|
1365
|
-
* schema: {
|
|
1366
|
-
* params: t.object({
|
|
1367
|
-
* slug: t.string(),
|
|
1368
|
-
* }),
|
|
1369
|
-
* },
|
|
1370
|
-
* static: {
|
|
1371
|
-
* entries: [
|
|
1372
|
-
* { params: { slug: "getting-started" } },
|
|
1373
|
-
* { params: { slug: "advanced-features" } },
|
|
1374
|
-
* { params: { slug: "deployment" } },
|
|
1375
|
-
* ],
|
|
1376
|
-
* },
|
|
1377
|
-
* resolve: ({ params }) => {
|
|
1378
|
-
* const post = getBlogPost(params.slug);
|
|
1379
|
-
* return { post };
|
|
1380
|
-
* },
|
|
1381
|
-
* component: ({ post }) => (
|
|
1382
|
-
* <article>
|
|
1383
|
-
* <h1>{post.title}</h1>
|
|
1384
|
-
* <div>{post.content}</div>
|
|
1385
|
-
* </article>
|
|
1386
|
-
* ),
|
|
1387
|
-
* });
|
|
1388
|
-
* }
|
|
1389
|
-
* ```
|
|
1390
|
-
*
|
|
1391
1275
|
* @see {@link $page}
|
|
1392
1276
|
* @module alepha.react
|
|
1393
1277
|
*/
|
|
1394
|
-
|
|
1395
|
-
name
|
|
1396
|
-
$
|
|
1397
|
-
|
|
1398
|
-
|
|
1278
|
+
const AlephaReact = $module({
|
|
1279
|
+
name: "alepha.react",
|
|
1280
|
+
descriptors: [$page],
|
|
1281
|
+
services: [
|
|
1282
|
+
ReactServerProvider,
|
|
1283
|
+
PageDescriptorProvider,
|
|
1284
|
+
ReactBrowserProvider
|
|
1285
|
+
],
|
|
1286
|
+
register: (alepha) => alepha.with(AlephaServer).with(AlephaServerCache).with(AlephaServerLinks).with(ReactServerProvider).with(PageDescriptorProvider)
|
|
1287
|
+
});
|
|
1399
1288
|
|
|
1400
1289
|
//#endregion
|
|
1401
|
-
export { $page, AlephaReact, ClientOnly_default as ClientOnly, ErrorBoundary_default as ErrorBoundary, Link_default as Link, NestedView_default as NestedView, NotFoundPage as NotFound, PageDescriptorProvider, ReactBrowserProvider, ReactServerProvider, RedirectionError, RouterContext, RouterHookApi, RouterLayerContext, isPageRoute, useActive, useAlepha, useClient, useInject, useQueryParams, useRouter, useRouterEvents, useRouterState };
|
|
1290
|
+
export { $page, AlephaReact, ClientOnly_default as ClientOnly, ErrorBoundary_default as ErrorBoundary, Link_default as Link, NestedView_default as NestedView, NotFoundPage as NotFound, PageDescriptor, PageDescriptorProvider, ReactBrowserProvider, ReactServerProvider, RedirectionError, RouterContext, RouterHookApi, RouterLayerContext, isPageRoute, useActive, useAlepha, useClient, useInject, useQueryParams, useRouter, useRouterEvents, useRouterState };
|
|
1402
1291
|
//# sourceMappingURL=index.js.map
|