@alepha/react 0.5.2 → 0.6.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/dist/index.browser.cjs +23 -19
- package/dist/index.browser.js +5 -4
- package/dist/index.cjs +416 -339
- package/dist/index.d.ts +591 -420
- package/dist/index.js +395 -320
- package/dist/{useRouterState-BlKHWZwk.cjs → useAuth-DOVx2kqa.cjs} +243 -102
- package/dist/{useRouterState-CvFCmaq7.js → useAuth-i7wbKVrt.js} +214 -77
- package/package.json +20 -18
- package/src/components/NestedView.tsx +0 -36
- package/src/contexts/RouterContext.ts +0 -15
- package/src/contexts/RouterLayerContext.ts +0 -10
- package/src/descriptors/$page.ts +0 -90
- package/src/hooks/RouterHookApi.ts +0 -154
- package/src/hooks/useActive.ts +0 -57
- package/src/hooks/useClient.ts +0 -6
- package/src/hooks/useInject.ts +0 -12
- package/src/hooks/useQueryParams.ts +0 -59
- package/src/hooks/useRouter.ts +0 -28
- package/src/hooks/useRouterEvents.ts +0 -43
- package/src/hooks/useRouterState.ts +0 -23
- package/src/index.browser.ts +0 -19
- package/src/index.shared.ts +0 -17
- package/src/index.ts +0 -29
- package/src/providers/PageDescriptorProvider.ts +0 -52
- package/src/providers/ReactBrowserProvider.ts +0 -228
- package/src/providers/ReactServerProvider.ts +0 -244
- package/src/providers/ReactSessionProvider.ts +0 -363
- package/src/services/Router.ts +0 -742
|
@@ -1,228 +0,0 @@
|
|
|
1
|
-
import { $hook, $inject, $logger } from "@alepha/core";
|
|
2
|
-
import { HttpClient } from "@alepha/server";
|
|
3
|
-
import type { Root } from "react-dom/client";
|
|
4
|
-
import { createRoot, hydrateRoot } from "react-dom/client";
|
|
5
|
-
import type {
|
|
6
|
-
PreviousLayerData,
|
|
7
|
-
RouterMatchOptions,
|
|
8
|
-
RouterState,
|
|
9
|
-
} from "../services/Router";
|
|
10
|
-
import { Router } from "../services/Router";
|
|
11
|
-
import type { ReactSessionProvider, Session } from "./ReactSessionProvider";
|
|
12
|
-
|
|
13
|
-
export class ReactBrowserProvider {
|
|
14
|
-
protected readonly log = $logger();
|
|
15
|
-
protected readonly client = $inject(HttpClient);
|
|
16
|
-
protected readonly router = $inject(Router);
|
|
17
|
-
protected root!: Root;
|
|
18
|
-
|
|
19
|
-
public transitioning?: {
|
|
20
|
-
to: string;
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
public state: RouterState = { layers: [], pathname: "", search: "" };
|
|
24
|
-
|
|
25
|
-
/**
|
|
26
|
-
*
|
|
27
|
-
*/
|
|
28
|
-
public get document() {
|
|
29
|
-
return window.document;
|
|
30
|
-
}
|
|
31
|
-
|
|
32
|
-
/**
|
|
33
|
-
*
|
|
34
|
-
*/
|
|
35
|
-
public get history() {
|
|
36
|
-
return window.history;
|
|
37
|
-
}
|
|
38
|
-
|
|
39
|
-
/**
|
|
40
|
-
*
|
|
41
|
-
*/
|
|
42
|
-
public get url(): string {
|
|
43
|
-
return window.location.pathname + window.location.search;
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
/**
|
|
47
|
-
*
|
|
48
|
-
* @param props
|
|
49
|
-
*/
|
|
50
|
-
public async invalidate(props?: Record<string, any>) {
|
|
51
|
-
const previous: PreviousLayerData[] = [];
|
|
52
|
-
|
|
53
|
-
if (props) {
|
|
54
|
-
const [key] = Object.keys(props);
|
|
55
|
-
const value = props[key];
|
|
56
|
-
|
|
57
|
-
for (const layer of this.state.layers) {
|
|
58
|
-
if (layer.props?.[key]) {
|
|
59
|
-
previous.push({
|
|
60
|
-
...layer,
|
|
61
|
-
props: {
|
|
62
|
-
...layer.props,
|
|
63
|
-
[key]: value,
|
|
64
|
-
},
|
|
65
|
-
});
|
|
66
|
-
break;
|
|
67
|
-
}
|
|
68
|
-
previous.push(layer);
|
|
69
|
-
}
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
await this.render({ previous });
|
|
73
|
-
}
|
|
74
|
-
|
|
75
|
-
/**
|
|
76
|
-
*
|
|
77
|
-
* @param url
|
|
78
|
-
* @param options
|
|
79
|
-
*/
|
|
80
|
-
public async go(url: string, options: RouterGoOptions = {}): Promise<void> {
|
|
81
|
-
const result = await this.render({
|
|
82
|
-
url,
|
|
83
|
-
});
|
|
84
|
-
|
|
85
|
-
if (result.url !== url) {
|
|
86
|
-
this.history.replaceState({}, "", result.url);
|
|
87
|
-
return;
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
if (options.replace) {
|
|
91
|
-
this.history.replaceState({}, "", url);
|
|
92
|
-
return;
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
this.history.pushState({}, "", url);
|
|
96
|
-
}
|
|
97
|
-
|
|
98
|
-
/**
|
|
99
|
-
*
|
|
100
|
-
* @param options
|
|
101
|
-
* @protected
|
|
102
|
-
*/
|
|
103
|
-
protected async render(
|
|
104
|
-
options: {
|
|
105
|
-
url?: string;
|
|
106
|
-
previous?: PreviousLayerData[];
|
|
107
|
-
} = {},
|
|
108
|
-
): Promise<{ url: string }> {
|
|
109
|
-
const previous = options.previous ?? this.state.layers;
|
|
110
|
-
const url = options.url ?? this.url;
|
|
111
|
-
|
|
112
|
-
this.transitioning = { to: url };
|
|
113
|
-
|
|
114
|
-
const result = await this.router.render(url, {
|
|
115
|
-
previous,
|
|
116
|
-
state: this.state,
|
|
117
|
-
});
|
|
118
|
-
|
|
119
|
-
if (result.redirect) {
|
|
120
|
-
return await this.render({ url: result.redirect });
|
|
121
|
-
}
|
|
122
|
-
|
|
123
|
-
this.transitioning = undefined;
|
|
124
|
-
|
|
125
|
-
return { url };
|
|
126
|
-
}
|
|
127
|
-
|
|
128
|
-
/**
|
|
129
|
-
* Get embedded layers from the server.
|
|
130
|
-
*
|
|
131
|
-
* @protected
|
|
132
|
-
*/
|
|
133
|
-
protected getEmbeddedCache():
|
|
134
|
-
| {
|
|
135
|
-
session?: Session;
|
|
136
|
-
layers?: PreviousLayerData[];
|
|
137
|
-
}
|
|
138
|
-
| undefined {
|
|
139
|
-
try {
|
|
140
|
-
if ("__ssr" in window && typeof window.__ssr === "object") {
|
|
141
|
-
return window.__ssr as {
|
|
142
|
-
session?: Session;
|
|
143
|
-
layers?: PreviousLayerData[];
|
|
144
|
-
};
|
|
145
|
-
}
|
|
146
|
-
} catch (error) {
|
|
147
|
-
console.error(error);
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
/**
|
|
152
|
-
*
|
|
153
|
-
* @protected
|
|
154
|
-
*/
|
|
155
|
-
protected getRootElement() {
|
|
156
|
-
const root = this.document.getElementById("root");
|
|
157
|
-
if (root) {
|
|
158
|
-
return root;
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
const div = this.document.createElement("div");
|
|
162
|
-
div.id = "root";
|
|
163
|
-
|
|
164
|
-
this.document.body.appendChild(div);
|
|
165
|
-
|
|
166
|
-
return div;
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
// -------------------------------------------------------------------------------------------------------------------
|
|
170
|
-
|
|
171
|
-
/**
|
|
172
|
-
*
|
|
173
|
-
* @protected
|
|
174
|
-
*/
|
|
175
|
-
protected ready = $hook({
|
|
176
|
-
name: "ready",
|
|
177
|
-
handler: async () => {
|
|
178
|
-
const cache = this.getEmbeddedCache();
|
|
179
|
-
const previous = cache?.layers ?? [];
|
|
180
|
-
|
|
181
|
-
// if session
|
|
182
|
-
const session =
|
|
183
|
-
cache?.session ??
|
|
184
|
-
(await this.client.of<ReactSessionProvider>().session());
|
|
185
|
-
|
|
186
|
-
await this.render({ previous });
|
|
187
|
-
|
|
188
|
-
const element = this.router.root(this.state, session);
|
|
189
|
-
|
|
190
|
-
if (previous.length > 0) {
|
|
191
|
-
this.root = hydrateRoot(this.getRootElement(), element);
|
|
192
|
-
this.log.info("Hydrated root element");
|
|
193
|
-
} else {
|
|
194
|
-
this.root = createRoot(this.getRootElement());
|
|
195
|
-
this.root.render(element);
|
|
196
|
-
this.log.info("Created root element");
|
|
197
|
-
}
|
|
198
|
-
|
|
199
|
-
window.addEventListener("popstate", () => {
|
|
200
|
-
this.render();
|
|
201
|
-
});
|
|
202
|
-
},
|
|
203
|
-
});
|
|
204
|
-
|
|
205
|
-
/**
|
|
206
|
-
*
|
|
207
|
-
* @protected
|
|
208
|
-
*/
|
|
209
|
-
protected stop = $hook({
|
|
210
|
-
name: "stop",
|
|
211
|
-
handler: async () => {
|
|
212
|
-
if (this.root) {
|
|
213
|
-
this.root.unmount();
|
|
214
|
-
this.log.info("Unmounted root element");
|
|
215
|
-
}
|
|
216
|
-
},
|
|
217
|
-
});
|
|
218
|
-
}
|
|
219
|
-
|
|
220
|
-
// ---------------------------------------------------------------------------------------------------------------------
|
|
221
|
-
|
|
222
|
-
/**
|
|
223
|
-
*
|
|
224
|
-
*/
|
|
225
|
-
export interface RouterGoOptions {
|
|
226
|
-
replace?: boolean;
|
|
227
|
-
match?: RouterMatchOptions;
|
|
228
|
-
}
|
|
@@ -1,244 +0,0 @@
|
|
|
1
|
-
import { existsSync } from "node:fs";
|
|
2
|
-
import { readFile } from "node:fs/promises";
|
|
3
|
-
import { join } from "node:path";
|
|
4
|
-
import { $logger, Alepha, type Static } from "@alepha/core";
|
|
5
|
-
import { $hook, $inject, t } from "@alepha/core";
|
|
6
|
-
import type { UserAccountInfo } from "@alepha/security";
|
|
7
|
-
import {
|
|
8
|
-
type RouteObject,
|
|
9
|
-
type ServeDescriptorOptions,
|
|
10
|
-
ServerProvider,
|
|
11
|
-
} from "@alepha/server";
|
|
12
|
-
import { renderToString } from "react-dom/server";
|
|
13
|
-
import { $page } from "../descriptors/$page";
|
|
14
|
-
import { Router } from "../services/Router";
|
|
15
|
-
|
|
16
|
-
export const envSchema = t.object({
|
|
17
|
-
REACT_SERVER_DIST: t.string({ default: "client" }),
|
|
18
|
-
REACT_SERVER_PREFIX: t.string({ default: "" }),
|
|
19
|
-
REACT_SSR_ENABLED: t.boolean({ default: false }),
|
|
20
|
-
REACT_SSR_OUTLET: t.string({ default: "<!--ssr-outlet-->" }),
|
|
21
|
-
});
|
|
22
|
-
|
|
23
|
-
declare module "@alepha/core" {
|
|
24
|
-
interface Env extends Partial<Static<typeof envSchema>> {}
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
export class ReactServerProvider {
|
|
28
|
-
protected readonly log = $logger();
|
|
29
|
-
protected readonly alepha = $inject(Alepha);
|
|
30
|
-
protected readonly router = $inject(Router);
|
|
31
|
-
protected readonly server = $inject(ServerProvider);
|
|
32
|
-
protected readonly env = $inject(envSchema);
|
|
33
|
-
|
|
34
|
-
protected readonly configure = $hook({
|
|
35
|
-
name: "configure",
|
|
36
|
-
handler: async () => {
|
|
37
|
-
await this.configureRoutes();
|
|
38
|
-
},
|
|
39
|
-
});
|
|
40
|
-
|
|
41
|
-
protected async configureRoutes() {
|
|
42
|
-
if (this.alepha.isTest()) {
|
|
43
|
-
this.processDescriptors();
|
|
44
|
-
}
|
|
45
|
-
|
|
46
|
-
if (this.router.empty()) {
|
|
47
|
-
return;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
if (process.env.VITE_ALEPHA_DEV === "true") {
|
|
51
|
-
this.log.info("SSR starting in development mode");
|
|
52
|
-
const templateUrl = `${this.server.hostname}/index.html`;
|
|
53
|
-
this.log.debug(`Fetch template from ${templateUrl}`);
|
|
54
|
-
|
|
55
|
-
const route = this.createHandler(() =>
|
|
56
|
-
fetch(templateUrl)
|
|
57
|
-
.then((it) => it.text())
|
|
58
|
-
.catch(() => undefined),
|
|
59
|
-
);
|
|
60
|
-
|
|
61
|
-
await this.server.route(route);
|
|
62
|
-
|
|
63
|
-
// fallback for static files
|
|
64
|
-
await this.server.route({
|
|
65
|
-
url: "*",
|
|
66
|
-
handler: route.handler,
|
|
67
|
-
});
|
|
68
|
-
|
|
69
|
-
return;
|
|
70
|
-
}
|
|
71
|
-
|
|
72
|
-
const maybe = [
|
|
73
|
-
join(process.cwd(), this.env.REACT_SERVER_DIST),
|
|
74
|
-
join(process.cwd(), "..", this.env.REACT_SERVER_DIST),
|
|
75
|
-
join(process.cwd(), "dist", this.env.REACT_SERVER_DIST),
|
|
76
|
-
];
|
|
77
|
-
|
|
78
|
-
let root = "";
|
|
79
|
-
for (const it of maybe) {
|
|
80
|
-
if (existsSync(it)) {
|
|
81
|
-
root = it;
|
|
82
|
-
break;
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
|
|
86
|
-
if (!root) {
|
|
87
|
-
this.log.warn("Missing static files, SSR will be disabled");
|
|
88
|
-
return;
|
|
89
|
-
}
|
|
90
|
-
|
|
91
|
-
await this.server.serve(this.createStaticHandler(root));
|
|
92
|
-
|
|
93
|
-
const template = await readFile(join(root, "index.html"), "utf-8");
|
|
94
|
-
|
|
95
|
-
const route = this.createHandler(async () => template);
|
|
96
|
-
|
|
97
|
-
await this.server.route(route); // we must take control of "/", or it will be handled by the static handler
|
|
98
|
-
|
|
99
|
-
// fallback for static files
|
|
100
|
-
await this.server.route({
|
|
101
|
-
url: "/*", // alias for "not found handler"
|
|
102
|
-
handler: route.handler,
|
|
103
|
-
});
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
/**
|
|
107
|
-
*
|
|
108
|
-
* @param root
|
|
109
|
-
* @protected
|
|
110
|
-
*/
|
|
111
|
-
protected createStaticHandler(root: string): ServeDescriptorOptions {
|
|
112
|
-
return {
|
|
113
|
-
root,
|
|
114
|
-
prefix: this.env.REACT_SERVER_PREFIX,
|
|
115
|
-
logLevel: "warn",
|
|
116
|
-
cacheControl: true,
|
|
117
|
-
immutable: true,
|
|
118
|
-
preCompressed: true,
|
|
119
|
-
maxAge: "30d",
|
|
120
|
-
index: false,
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
|
|
124
|
-
/**
|
|
125
|
-
*
|
|
126
|
-
* @param templateLoader
|
|
127
|
-
* @protected
|
|
128
|
-
*/
|
|
129
|
-
protected createHandler(
|
|
130
|
-
templateLoader: () => Promise<string | undefined>,
|
|
131
|
-
): RouteObject {
|
|
132
|
-
return {
|
|
133
|
-
url: "/",
|
|
134
|
-
handler: async ({ url, user }) => {
|
|
135
|
-
const template = await templateLoader();
|
|
136
|
-
if (!template) {
|
|
137
|
-
return new Response("Not found", { status: 404 });
|
|
138
|
-
}
|
|
139
|
-
|
|
140
|
-
const response = this.notFoundHandler(url);
|
|
141
|
-
if (response) {
|
|
142
|
-
// not found handler for static files (favicon, css, js, etc)
|
|
143
|
-
return response;
|
|
144
|
-
}
|
|
145
|
-
|
|
146
|
-
return await this.ssr(url, template, user);
|
|
147
|
-
},
|
|
148
|
-
};
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
protected processDescriptors() {
|
|
152
|
-
const pages = this.alepha.getDescriptorValues($page);
|
|
153
|
-
for (const { key, instance, value } of pages) {
|
|
154
|
-
instance[key].render = async (
|
|
155
|
-
options: {
|
|
156
|
-
params?: Record<string, string>;
|
|
157
|
-
query?: Record<string, string>;
|
|
158
|
-
} = {},
|
|
159
|
-
) => {
|
|
160
|
-
const name = value.options.name ?? key;
|
|
161
|
-
const page = this.router.page(name);
|
|
162
|
-
const layers = await this.router.createLayers(
|
|
163
|
-
"",
|
|
164
|
-
page,
|
|
165
|
-
options.params ?? {},
|
|
166
|
-
options.query ?? {},
|
|
167
|
-
[],
|
|
168
|
-
);
|
|
169
|
-
|
|
170
|
-
return renderToString(
|
|
171
|
-
this.router.root({
|
|
172
|
-
layers,
|
|
173
|
-
pathname: "",
|
|
174
|
-
search: "",
|
|
175
|
-
}),
|
|
176
|
-
);
|
|
177
|
-
};
|
|
178
|
-
}
|
|
179
|
-
}
|
|
180
|
-
|
|
181
|
-
/**
|
|
182
|
-
*
|
|
183
|
-
* @param url
|
|
184
|
-
* @protected
|
|
185
|
-
*/
|
|
186
|
-
protected notFoundHandler(url: string) {
|
|
187
|
-
if (url.match(/\.\w+$/)) {
|
|
188
|
-
return new Response("Not found", { status: 404 });
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
|
|
192
|
-
/**
|
|
193
|
-
*
|
|
194
|
-
* @param url
|
|
195
|
-
* @param template
|
|
196
|
-
* @param user
|
|
197
|
-
*/
|
|
198
|
-
public async ssr(
|
|
199
|
-
url: string,
|
|
200
|
-
template: string = this.env.REACT_SSR_OUTLET,
|
|
201
|
-
user?: UserAccountInfo,
|
|
202
|
-
): Promise<Response> {
|
|
203
|
-
const { element, layers, redirect } = await this.router.render(url, {
|
|
204
|
-
user,
|
|
205
|
-
});
|
|
206
|
-
|
|
207
|
-
if (redirect) {
|
|
208
|
-
return new Response("", {
|
|
209
|
-
status: 302,
|
|
210
|
-
headers: {
|
|
211
|
-
Location: redirect,
|
|
212
|
-
},
|
|
213
|
-
});
|
|
214
|
-
}
|
|
215
|
-
|
|
216
|
-
const appHtml = renderToString(element);
|
|
217
|
-
|
|
218
|
-
const script = `<script>window.__ssr=${JSON.stringify({
|
|
219
|
-
layers: layers.map((it) => ({
|
|
220
|
-
...it,
|
|
221
|
-
index: undefined,
|
|
222
|
-
path: undefined,
|
|
223
|
-
element: undefined,
|
|
224
|
-
})),
|
|
225
|
-
session: {
|
|
226
|
-
user: user
|
|
227
|
-
? {
|
|
228
|
-
id: user.id,
|
|
229
|
-
name: user.name,
|
|
230
|
-
}
|
|
231
|
-
: undefined,
|
|
232
|
-
},
|
|
233
|
-
})}</script>`;
|
|
234
|
-
|
|
235
|
-
const index = template.indexOf("</body>");
|
|
236
|
-
if (index !== -1) {
|
|
237
|
-
template = template.slice(0, index) + script + template.slice(index);
|
|
238
|
-
}
|
|
239
|
-
|
|
240
|
-
return new Response(template.replace(this.env.REACT_SSR_OUTLET, appHtml), {
|
|
241
|
-
headers: { "Content-Type": "text/html" },
|
|
242
|
-
});
|
|
243
|
-
}
|
|
244
|
-
}
|