@alepha/react 0.6.2 → 0.6.3
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 +1 -28
- package/dist/index.browser.cjs +19 -23
- package/dist/index.browser.js +7 -7
- package/dist/index.cjs +233 -542
- package/dist/index.d.ts +218 -682
- package/dist/index.js +218 -522
- package/dist/{useAuth-B9ypF48n.cjs → useActive-BVqdq757.cjs} +303 -469
- package/dist/{useAuth-Ps01oe8e.js → useActive-dAmCT31a.js} +302 -465
- package/package.json +7 -9
package/dist/index.js
CHANGED
|
@@ -1,325 +1,80 @@
|
|
|
1
|
-
import { $logger, $inject, Alepha,
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
export { L as Link, N as NestedView,
|
|
5
|
-
import { discovery, allowInsecureRequests, refreshTokenGrant, randomPKCECodeVerifier, calculatePKCECodeChallenge, buildAuthorizationUrl, authorizationCodeGrant, buildEndSessionUrl } from 'openid-client';
|
|
1
|
+
import { t, $logger, $inject, Alepha, $hook, __bind } from '@alepha/core';
|
|
2
|
+
import { ServerRouterProvider, ServerLinksProvider, ServerModule } from '@alepha/server';
|
|
3
|
+
import { P as PageDescriptorProvider, $ as $page } from './useActive-dAmCT31a.js';
|
|
4
|
+
export { L as Link, N as NestedView, k as ReactBrowserProvider, i as RedirectionError, R as RouterContext, b as RouterHookApi, a as RouterLayerContext, j as isPageRoute, h as useActive, c as useClient, u as useInject, d as useQueryParams, e as useRouter, f as useRouterEvents, g as useRouterState } from './useActive-dAmCT31a.js';
|
|
6
5
|
import { existsSync } from 'node:fs';
|
|
7
6
|
import { readFile } from 'node:fs/promises';
|
|
8
7
|
import { join } from 'node:path';
|
|
9
|
-
import {
|
|
8
|
+
import { ServerStaticProvider } from '@alepha/server-static';
|
|
10
9
|
import { renderToString } from 'react-dom/server';
|
|
11
10
|
import 'react/jsx-runtime';
|
|
12
11
|
import 'react';
|
|
13
12
|
import 'react-dom/client';
|
|
14
|
-
import '
|
|
13
|
+
import '@alepha/router';
|
|
15
14
|
|
|
16
|
-
class
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
schema: t.object({
|
|
26
|
-
codeVerifier: t.optional(t.string({ size: "long" })),
|
|
27
|
-
redirectUri: t.optional(t.string({ size: "long" }))
|
|
28
|
-
})
|
|
29
|
-
});
|
|
30
|
-
tokens = $cookie({
|
|
31
|
-
name: "tokens",
|
|
32
|
-
ttl: { days: 1 },
|
|
33
|
-
httpOnly: true,
|
|
34
|
-
compress: true,
|
|
35
|
-
schema: t.object({
|
|
36
|
-
access_token: t.optional(t.string({ size: "rich" })),
|
|
37
|
-
expires_in: t.optional(t.number()),
|
|
38
|
-
refresh_token: t.optional(t.string({ size: "rich" })),
|
|
39
|
-
id_token: t.optional(t.string({ size: "rich" })),
|
|
40
|
-
scope: t.optional(t.string()),
|
|
41
|
-
issued_at: t.optional(t.number())
|
|
42
|
-
})
|
|
43
|
-
});
|
|
44
|
-
user = $cookie({
|
|
45
|
-
name: "user",
|
|
46
|
-
ttl: { days: 1 },
|
|
47
|
-
schema: t.object({
|
|
48
|
-
id: t.string(),
|
|
49
|
-
name: t.optional(t.string()),
|
|
50
|
-
email: t.optional(t.string())
|
|
51
|
-
})
|
|
52
|
-
});
|
|
53
|
-
configure = $hook({
|
|
54
|
-
name: "configure",
|
|
55
|
-
handler: async () => {
|
|
56
|
-
const auths = this.alepha.getDescriptorValues($auth);
|
|
57
|
-
for (const { value, key, instance } of auths) {
|
|
58
|
-
const options = value.options;
|
|
59
|
-
if (options.oidc) {
|
|
60
|
-
this.log.debug(
|
|
61
|
-
`Discover OIDC auth provider -> ${options.oidc.issuer}`
|
|
62
|
-
);
|
|
63
|
-
const client = await discovery(
|
|
64
|
-
new URL(options.oidc.issuer),
|
|
65
|
-
options.oidc.clientId,
|
|
66
|
-
{
|
|
67
|
-
client_secret: options.oidc.clientSecret
|
|
68
|
-
},
|
|
69
|
-
void 0,
|
|
70
|
-
{
|
|
71
|
-
execute: [allowInsecureRequests]
|
|
72
|
-
}
|
|
73
|
-
);
|
|
74
|
-
instance[key].jwks = () => {
|
|
75
|
-
return client.serverMetadata().jwks_uri;
|
|
76
|
-
};
|
|
77
|
-
this.authProviders.push({
|
|
78
|
-
name: options.name ?? key,
|
|
79
|
-
redirectUri: options.oidc.redirectUri ?? "/api/_oauth/callback",
|
|
80
|
-
client
|
|
81
|
-
});
|
|
82
|
-
}
|
|
83
|
-
}
|
|
84
|
-
}
|
|
85
|
-
});
|
|
86
|
-
/**
|
|
87
|
-
* Configure Fastify to forward Session Access Token to Header Authorization.
|
|
88
|
-
*/
|
|
89
|
-
onRequest = $hook({
|
|
90
|
-
name: "server:onRequest",
|
|
91
|
-
after: this.serverCookieProvider,
|
|
92
|
-
handler: async ({ request }) => {
|
|
93
|
-
if (request.cookies && !this.isViteFile(request.url.pathname) && !!this.authProviders.length) {
|
|
94
|
-
const tokens = await this.refresh(request.cookies);
|
|
95
|
-
if (tokens) {
|
|
96
|
-
request.headers.rep("authorization", `Bearer ${tokens.access_token}`);
|
|
97
|
-
}
|
|
98
|
-
if (this.user.get(request.cookies) && !this.tokens.get(request.cookies)) {
|
|
99
|
-
this.user.del(request.cookies);
|
|
100
|
-
}
|
|
101
|
-
}
|
|
102
|
-
}
|
|
103
|
-
});
|
|
104
|
-
/**
|
|
105
|
-
*
|
|
106
|
-
* @param cookies
|
|
107
|
-
* @protected
|
|
108
|
-
*/
|
|
109
|
-
async refresh(cookies) {
|
|
110
|
-
const now = Date.now();
|
|
111
|
-
const tokens = this.tokens.get(cookies);
|
|
112
|
-
if (!tokens) {
|
|
113
|
-
return;
|
|
114
|
-
}
|
|
115
|
-
if (tokens.expires_in && tokens.issued_at) {
|
|
116
|
-
const expiresAt = tokens.issued_at + (tokens.expires_in - 10) * 1e3;
|
|
117
|
-
if (expiresAt < now) {
|
|
118
|
-
if (tokens.refresh_token) {
|
|
119
|
-
try {
|
|
120
|
-
const newTokens = await refreshTokenGrant(
|
|
121
|
-
this.authProviders[0].client,
|
|
122
|
-
tokens.refresh_token
|
|
123
|
-
);
|
|
124
|
-
this.tokens.set(cookies, {
|
|
125
|
-
...newTokens,
|
|
126
|
-
issued_at: Date.now()
|
|
127
|
-
});
|
|
128
|
-
return newTokens;
|
|
129
|
-
} catch (e) {
|
|
130
|
-
if (e instanceof Error) {
|
|
131
|
-
this.log.warn("Failed to refresh token", e.message);
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
}
|
|
135
|
-
this.tokens.del(cookies);
|
|
136
|
-
this.user.del(cookies);
|
|
137
|
-
return;
|
|
138
|
-
}
|
|
139
|
-
}
|
|
140
|
-
if (!tokens.issued_at && tokens.access_token) {
|
|
141
|
-
this.tokens.del(cookies);
|
|
142
|
-
this.user.del(cookies);
|
|
143
|
-
return;
|
|
144
|
-
}
|
|
145
|
-
return tokens;
|
|
146
|
-
}
|
|
147
|
-
/**
|
|
148
|
-
*
|
|
149
|
-
*/
|
|
150
|
-
login = $route({
|
|
151
|
-
security: false,
|
|
152
|
-
internal: true,
|
|
153
|
-
url: "/_oauth/login",
|
|
154
|
-
group: "auth",
|
|
155
|
-
method: "GET",
|
|
156
|
-
schema: {
|
|
157
|
-
query: t.object({
|
|
158
|
-
redirect: t.optional(t.string()),
|
|
159
|
-
provider: t.optional(t.string())
|
|
160
|
-
})
|
|
161
|
-
},
|
|
162
|
-
handler: async ({ query, cookies, url }) => {
|
|
163
|
-
const { client, redirectUri } = this.provider(query.provider);
|
|
164
|
-
const codeVerifier = randomPKCECodeVerifier();
|
|
165
|
-
const codeChallenge = await calculatePKCECodeChallenge(codeVerifier);
|
|
166
|
-
const scope = "openid profile email";
|
|
167
|
-
let redirect_uri = redirectUri;
|
|
168
|
-
if (redirect_uri.startsWith("/")) {
|
|
169
|
-
redirect_uri = `${url.protocol}//${url.host}${redirect_uri}`;
|
|
170
|
-
}
|
|
171
|
-
const parameters = {
|
|
172
|
-
redirect_uri,
|
|
173
|
-
scope,
|
|
174
|
-
code_challenge: codeChallenge,
|
|
175
|
-
code_challenge_method: "S256"
|
|
176
|
-
};
|
|
177
|
-
this.authorizationCode.set(cookies, {
|
|
178
|
-
codeVerifier,
|
|
179
|
-
redirectUri: query.redirect ?? "/"
|
|
180
|
-
});
|
|
181
|
-
return new Response("", {
|
|
182
|
-
status: 302,
|
|
183
|
-
headers: {
|
|
184
|
-
Location: buildAuthorizationUrl(client, parameters).toString()
|
|
185
|
-
}
|
|
186
|
-
});
|
|
187
|
-
}
|
|
188
|
-
});
|
|
189
|
-
/**
|
|
190
|
-
*
|
|
191
|
-
*/
|
|
192
|
-
callback = $route({
|
|
193
|
-
security: false,
|
|
194
|
-
internal: true,
|
|
195
|
-
url: "/_oauth/callback",
|
|
196
|
-
group: "auth",
|
|
197
|
-
method: "GET",
|
|
198
|
-
schema: {
|
|
199
|
-
query: t.object({
|
|
200
|
-
provider: t.optional(t.string())
|
|
201
|
-
})
|
|
202
|
-
},
|
|
203
|
-
handler: async ({ url, cookies, query }) => {
|
|
204
|
-
const { client } = this.provider(query.provider);
|
|
205
|
-
const authorizationCode = this.authorizationCode.get(cookies);
|
|
206
|
-
if (!authorizationCode) {
|
|
207
|
-
throw new BadRequestError("Missing code verifier");
|
|
208
|
-
}
|
|
209
|
-
const tokens = await authorizationCodeGrant(client, url, {
|
|
210
|
-
pkceCodeVerifier: authorizationCode.codeVerifier
|
|
211
|
-
});
|
|
212
|
-
this.authorizationCode.del(cookies);
|
|
213
|
-
this.tokens.set(cookies, {
|
|
214
|
-
...tokens,
|
|
215
|
-
issued_at: Date.now()
|
|
216
|
-
});
|
|
217
|
-
const user = this.userFromAccessToken(tokens.access_token);
|
|
218
|
-
if (user) {
|
|
219
|
-
this.user.set(cookies, user);
|
|
220
|
-
}
|
|
221
|
-
return Response.redirect(authorizationCode.redirectUri ?? "/");
|
|
15
|
+
class ServerHeadProvider {
|
|
16
|
+
renderHead(template, head) {
|
|
17
|
+
let result = template;
|
|
18
|
+
const htmlAttributes = head.htmlAttributes;
|
|
19
|
+
if (htmlAttributes) {
|
|
20
|
+
result = result.replace(
|
|
21
|
+
/<html([^>]*)>/i,
|
|
22
|
+
(_, existingAttrs) => `<html${this.mergeAttributes(existingAttrs, htmlAttributes)}>`
|
|
23
|
+
);
|
|
222
24
|
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
userFromAccessToken(accessToken) {
|
|
230
|
-
try {
|
|
231
|
-
const parts = accessToken.split(".");
|
|
232
|
-
if (parts.length !== 3) {
|
|
233
|
-
return;
|
|
234
|
-
}
|
|
235
|
-
const payload = parts[1];
|
|
236
|
-
const decoded = JSON.parse(atob(payload));
|
|
237
|
-
if (!decoded.sub) {
|
|
238
|
-
return;
|
|
239
|
-
}
|
|
240
|
-
return {
|
|
241
|
-
id: decoded.sub,
|
|
242
|
-
name: decoded.name,
|
|
243
|
-
email: decoded.email
|
|
244
|
-
// organization
|
|
245
|
-
// ...
|
|
246
|
-
};
|
|
247
|
-
} catch (e) {
|
|
248
|
-
this.log.warn(e, "Failed to decode access token");
|
|
25
|
+
const bodyAttributes = head.bodyAttributes;
|
|
26
|
+
if (bodyAttributes) {
|
|
27
|
+
result = result.replace(
|
|
28
|
+
/<body([^>]*)>/i,
|
|
29
|
+
(_, existingAttrs) => `<body${this.mergeAttributes(existingAttrs, bodyAttributes)}>`
|
|
30
|
+
);
|
|
249
31
|
}
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
query: t.object({
|
|
262
|
-
redirect: t.optional(t.string()),
|
|
263
|
-
provider: t.optional(t.string())
|
|
264
|
-
})
|
|
265
|
-
},
|
|
266
|
-
handler: async ({ query, cookies }) => {
|
|
267
|
-
const { client } = this.provider(query.provider);
|
|
268
|
-
const tokens = this.tokens.get(cookies);
|
|
269
|
-
const idToken = tokens?.id_token;
|
|
270
|
-
const redirect = query.redirect ?? "/";
|
|
271
|
-
const params = new URLSearchParams();
|
|
272
|
-
params.set("post_logout_redirect_uri", redirect);
|
|
273
|
-
if (idToken) {
|
|
274
|
-
params.set("id_token_hint", idToken);
|
|
32
|
+
let headContent = "";
|
|
33
|
+
const title = head.title;
|
|
34
|
+
if (title) {
|
|
35
|
+
if (template.includes("<title>")) {
|
|
36
|
+
result = result.replace(
|
|
37
|
+
/<title>(.*?)<\/title>/i,
|
|
38
|
+
() => `<title>${this.escapeHtml(title)}</title>`
|
|
39
|
+
);
|
|
40
|
+
} else {
|
|
41
|
+
headContent += `<title>${this.escapeHtml(title)}</title>
|
|
42
|
+
`;
|
|
275
43
|
}
|
|
276
|
-
this.tokens.del(cookies);
|
|
277
|
-
this.user.del(cookies);
|
|
278
|
-
return Response.redirect(buildEndSessionUrl(client, params).toString());
|
|
279
44
|
}
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
283
|
-
|
|
284
|
-
* @protected
|
|
285
|
-
*/
|
|
286
|
-
provider(name) {
|
|
287
|
-
if (!name) {
|
|
288
|
-
const client = this.authProviders[0];
|
|
289
|
-
if (!client) {
|
|
290
|
-
throw new BadRequestError("Client name is required");
|
|
45
|
+
if (head.meta) {
|
|
46
|
+
for (const meta of head.meta) {
|
|
47
|
+
headContent += `<meta name="${this.escapeHtml(meta.name)}" content="${this.escapeHtml(meta.content)}">
|
|
48
|
+
`;
|
|
291
49
|
}
|
|
292
|
-
return client;
|
|
293
50
|
}
|
|
294
|
-
|
|
295
|
-
(
|
|
51
|
+
result = result.replace(
|
|
52
|
+
/<head([^>]*)>(.*?)<\/head>/is,
|
|
53
|
+
(_, existingAttrs, existingHead) => `<head${existingAttrs}>${existingHead}${headContent}</head>`
|
|
296
54
|
);
|
|
297
|
-
|
|
298
|
-
throw new BadRequestError(`Client ${name} not found`);
|
|
299
|
-
}
|
|
300
|
-
return authProvider;
|
|
55
|
+
return result.trim();
|
|
301
56
|
}
|
|
302
|
-
|
|
303
|
-
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
const
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
}
|
|
315
|
-
if (pathname.startsWith("/@")) {
|
|
316
|
-
return true;
|
|
57
|
+
mergeAttributes(existing, attrs) {
|
|
58
|
+
const existingAttrs = this.parseAttributes(existing);
|
|
59
|
+
const merged = { ...existingAttrs, ...attrs };
|
|
60
|
+
return Object.entries(merged).map(([k, v]) => ` ${k}="${this.escapeHtml(v)}"`).join("");
|
|
61
|
+
}
|
|
62
|
+
parseAttributes(attrStr) {
|
|
63
|
+
const attrs = {};
|
|
64
|
+
const attrRegex = /([^\s=]+)(?:="([^"]*)")?/g;
|
|
65
|
+
let match = attrRegex.exec(attrStr);
|
|
66
|
+
while (match) {
|
|
67
|
+
attrs[match[1]] = match[2] ?? "";
|
|
68
|
+
match = attrRegex.exec(attrStr);
|
|
317
69
|
}
|
|
318
|
-
return
|
|
70
|
+
return attrs;
|
|
71
|
+
}
|
|
72
|
+
escapeHtml(str) {
|
|
73
|
+
return str.replace(/&/g, "&").replace(/</g, "<").replace(/>/g, ">").replace(/"/g, """).replace(/'/g, "'");
|
|
319
74
|
}
|
|
320
75
|
}
|
|
321
76
|
|
|
322
|
-
const envSchema
|
|
77
|
+
const envSchema = t.object({
|
|
323
78
|
REACT_SERVER_DIST: t.string({ default: "client" }),
|
|
324
79
|
REACT_SERVER_PREFIX: t.string({ default: "" }),
|
|
325
80
|
REACT_SSR_ENABLED: t.boolean({ default: false }),
|
|
@@ -328,256 +83,197 @@ const envSchema$1 = t.object({
|
|
|
328
83
|
class ReactServerProvider {
|
|
329
84
|
log = $logger();
|
|
330
85
|
alepha = $inject(Alepha);
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
86
|
+
pageDescriptorProvider = $inject(PageDescriptorProvider);
|
|
87
|
+
serverStaticProvider = $inject(ServerStaticProvider);
|
|
88
|
+
serverRouterProvider = $inject(ServerRouterProvider);
|
|
89
|
+
headProvider = $inject(ServerHeadProvider);
|
|
90
|
+
env = $inject(envSchema);
|
|
91
|
+
ROOT_DIV_REGEX = new RegExp(
|
|
92
|
+
`<div([^>]*)\\s+id=["']${this.env.REACT_ROOT_ID}["']([^>]*)>(.*?)<\\/div>`,
|
|
93
|
+
"is"
|
|
94
|
+
);
|
|
334
95
|
configure = $hook({
|
|
335
96
|
name: "configure",
|
|
336
97
|
handler: async () => {
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
}
|
|
346
|
-
if (this.router.empty()) {
|
|
347
|
-
return;
|
|
348
|
-
}
|
|
349
|
-
if (process.env.VITE_ALEPHA_DEV === "true") {
|
|
350
|
-
const url = `http://${process.env.SERVER_HOST}:${process.env.SERVER_PORT}`;
|
|
351
|
-
this.log.info("SSR (vite) OK");
|
|
352
|
-
this.alepha.state("ReactServerProvider.ssr", true);
|
|
353
|
-
const templateUrl = `${url}/index.html`;
|
|
354
|
-
const route2 = this.createHandler(
|
|
355
|
-
() => fetch(templateUrl).then((it) => it.text()).catch(() => void 0)
|
|
356
|
-
);
|
|
357
|
-
await this.serverProvider.route(route2);
|
|
358
|
-
await this.serverProvider.route({
|
|
359
|
-
...route2,
|
|
360
|
-
url: "*"
|
|
361
|
-
});
|
|
362
|
-
return;
|
|
363
|
-
}
|
|
364
|
-
let root = "";
|
|
365
|
-
if (!this.alepha.isServerless()) {
|
|
366
|
-
const maybe = [
|
|
367
|
-
join(process.cwd(), this.env.REACT_SERVER_DIST),
|
|
368
|
-
join(process.cwd(), "..", this.env.REACT_SERVER_DIST)
|
|
369
|
-
];
|
|
370
|
-
for (const it of maybe) {
|
|
371
|
-
if (existsSync(it)) {
|
|
372
|
-
root = it;
|
|
373
|
-
break;
|
|
98
|
+
const pages = this.alepha.getDescriptorValues($page);
|
|
99
|
+
if (pages.length === 0) {
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
for (const { key, instance, value } of pages) {
|
|
103
|
+
const name = value.options.name ?? key;
|
|
104
|
+
if (this.alepha.isTest()) {
|
|
105
|
+
instance[key].render = this.createRenderFunction(name);
|
|
374
106
|
}
|
|
375
107
|
}
|
|
376
|
-
if (
|
|
377
|
-
this.
|
|
108
|
+
if (this.alepha.isServerless() === "vite") {
|
|
109
|
+
await this.configureVite();
|
|
378
110
|
return;
|
|
379
111
|
}
|
|
380
|
-
|
|
381
|
-
|
|
382
|
-
|
|
383
|
-
|
|
384
|
-
|
|
385
|
-
|
|
386
|
-
...route,
|
|
387
|
-
url: "*"
|
|
388
|
-
});
|
|
389
|
-
this.alepha.state("ReactServerProvider.ssr", true);
|
|
390
|
-
}
|
|
391
|
-
/**
|
|
392
|
-
*
|
|
393
|
-
* @param root
|
|
394
|
-
* @protected
|
|
395
|
-
*/
|
|
396
|
-
createStaticHandler(root) {
|
|
397
|
-
return {
|
|
398
|
-
root,
|
|
399
|
-
prefix: this.env.REACT_SERVER_PREFIX,
|
|
400
|
-
logLevel: "warn",
|
|
401
|
-
cacheControl: true,
|
|
402
|
-
immutable: true,
|
|
403
|
-
preCompressed: true,
|
|
404
|
-
maxAge: "30d",
|
|
405
|
-
index: false
|
|
406
|
-
};
|
|
407
|
-
}
|
|
408
|
-
/**
|
|
409
|
-
*
|
|
410
|
-
* @param templateLoader
|
|
411
|
-
* @protected
|
|
412
|
-
*/
|
|
413
|
-
createHandler(templateLoader) {
|
|
414
|
-
return {
|
|
415
|
-
method: "GET",
|
|
416
|
-
url: "/",
|
|
417
|
-
handler: async (ctx) => {
|
|
418
|
-
const template = await templateLoader();
|
|
419
|
-
if (!template) {
|
|
420
|
-
return new Response("Not found", { status: 404 });
|
|
421
|
-
}
|
|
422
|
-
const response = this.notFoundHandler(ctx.url);
|
|
423
|
-
if (response) {
|
|
424
|
-
return response;
|
|
112
|
+
let root = "";
|
|
113
|
+
if (!this.alepha.isServerless()) {
|
|
114
|
+
root = this.getPublicDirectory();
|
|
115
|
+
if (!root) {
|
|
116
|
+
this.log.warn("Missing static files, SSR will be disabled");
|
|
117
|
+
return;
|
|
425
118
|
}
|
|
426
|
-
|
|
119
|
+
await this.configureStaticServer(root);
|
|
427
120
|
}
|
|
428
|
-
|
|
429
|
-
|
|
430
|
-
|
|
431
|
-
*
|
|
432
|
-
* @protected
|
|
433
|
-
*/
|
|
434
|
-
processDescriptors() {
|
|
435
|
-
const pages = this.alepha.getDescriptorValues($page);
|
|
436
|
-
for (const { key, instance, value } of pages) {
|
|
437
|
-
instance[key].render = async (options = {}) => {
|
|
438
|
-
const name = value.options.name ?? key;
|
|
439
|
-
const page = this.router.page(name);
|
|
440
|
-
const layers = await this.router.createLayers(
|
|
441
|
-
"",
|
|
442
|
-
page,
|
|
443
|
-
options.params ?? {},
|
|
444
|
-
options.query ?? {},
|
|
445
|
-
[]
|
|
446
|
-
);
|
|
447
|
-
return renderToString(
|
|
448
|
-
this.router.root({
|
|
449
|
-
layers,
|
|
450
|
-
pathname: "",
|
|
451
|
-
search: "",
|
|
452
|
-
context: {}
|
|
453
|
-
})
|
|
454
|
-
);
|
|
455
|
-
};
|
|
121
|
+
const template = this.alepha.state("ReactServerProvider.template") ?? await readFile(join(root, "index.html"), "utf-8");
|
|
122
|
+
await this.registerPages(async () => template);
|
|
123
|
+
this.alepha.state("ReactServerProvider.ssr", true);
|
|
456
124
|
}
|
|
457
|
-
}
|
|
458
|
-
|
|
459
|
-
|
|
460
|
-
|
|
461
|
-
|
|
462
|
-
|
|
463
|
-
|
|
464
|
-
|
|
465
|
-
|
|
125
|
+
});
|
|
126
|
+
async registerPages(templateLoader) {
|
|
127
|
+
for (const page of this.pageDescriptorProvider.getPages()) {
|
|
128
|
+
this.log.debug(`+ ${page.match} -> ${page.name}`);
|
|
129
|
+
await this.serverRouterProvider.route({
|
|
130
|
+
method: "GET",
|
|
131
|
+
path: page.match,
|
|
132
|
+
handler: this.createHandler(page, templateLoader)
|
|
133
|
+
});
|
|
466
134
|
}
|
|
467
135
|
}
|
|
468
|
-
|
|
469
|
-
|
|
470
|
-
|
|
471
|
-
|
|
472
|
-
|
|
473
|
-
|
|
474
|
-
|
|
475
|
-
|
|
476
|
-
if (!args.user && args.cookies && hasAuth) {
|
|
477
|
-
const auth = this.alepha.get(ReactAuthProvider);
|
|
478
|
-
args.user = auth.user.get(args.cookies);
|
|
479
|
-
if (args.user) {
|
|
480
|
-
args.user.roles = [];
|
|
136
|
+
getPublicDirectory() {
|
|
137
|
+
const maybe = [
|
|
138
|
+
join(process.cwd(), this.env.REACT_SERVER_DIST),
|
|
139
|
+
join(process.cwd(), "..", this.env.REACT_SERVER_DIST)
|
|
140
|
+
];
|
|
141
|
+
for (const it of maybe) {
|
|
142
|
+
if (existsSync(it)) {
|
|
143
|
+
return it;
|
|
481
144
|
}
|
|
482
145
|
}
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
|
|
489
|
-
|
|
490
|
-
|
|
491
|
-
|
|
492
|
-
|
|
146
|
+
return "";
|
|
147
|
+
}
|
|
148
|
+
async configureStaticServer(root) {
|
|
149
|
+
await this.serverStaticProvider.serve({
|
|
150
|
+
root,
|
|
151
|
+
path: this.env.REACT_SERVER_PREFIX
|
|
152
|
+
});
|
|
153
|
+
}
|
|
154
|
+
async configureVite() {
|
|
155
|
+
const url = `http://${process.env.SERVER_HOST}:${process.env.SERVER_PORT}`;
|
|
156
|
+
this.log.info("SSR (vite) OK");
|
|
157
|
+
this.alepha.state("ReactServerProvider.ssr", true);
|
|
158
|
+
const templateUrl = `${url}/index.html`;
|
|
159
|
+
await this.registerPages(
|
|
160
|
+
() => fetch(templateUrl).then((it) => it.text()).catch(() => void 0)
|
|
493
161
|
);
|
|
494
|
-
|
|
495
|
-
|
|
496
|
-
|
|
497
|
-
|
|
498
|
-
|
|
499
|
-
|
|
162
|
+
}
|
|
163
|
+
createRenderFunction(name) {
|
|
164
|
+
return async (options = {}) => {
|
|
165
|
+
const page = this.pageDescriptorProvider.page(name);
|
|
166
|
+
const state = await this.pageDescriptorProvider.createLayers(page, {
|
|
167
|
+
url: new URL("http://localhost"),
|
|
168
|
+
params: options.params ?? {},
|
|
169
|
+
query: options.query ?? {},
|
|
170
|
+
head: {}
|
|
500
171
|
});
|
|
501
|
-
|
|
502
|
-
|
|
503
|
-
const $ = load(template);
|
|
504
|
-
const script = `<script>window.__ssr=${JSON.stringify({
|
|
505
|
-
links: args.links,
|
|
506
|
-
layers: layers.map((it) => ({
|
|
507
|
-
...it,
|
|
508
|
-
error: it.error ? {
|
|
509
|
-
...it.error,
|
|
510
|
-
name: it.error.name,
|
|
511
|
-
message: it.error.message,
|
|
512
|
-
stack: it.error.stack
|
|
513
|
-
// TODO: Hide stack in production ?
|
|
514
|
-
} : void 0,
|
|
515
|
-
index: void 0,
|
|
516
|
-
path: void 0,
|
|
517
|
-
element: void 0
|
|
518
|
-
}))
|
|
519
|
-
})}<\/script>`;
|
|
520
|
-
const body = $("body");
|
|
521
|
-
const root = body.find(`#${this.env.REACT_ROOT_ID}`);
|
|
522
|
-
if (root.length) {
|
|
523
|
-
root.html(html);
|
|
524
|
-
} else {
|
|
525
|
-
body.prepend(`<div id="${this.env.REACT_ROOT_ID}">${html}</div>`);
|
|
526
|
-
}
|
|
527
|
-
body.append(script);
|
|
528
|
-
if (context.head) {
|
|
529
|
-
this.renderHeadContext($, context.head);
|
|
530
|
-
}
|
|
531
|
-
return new Response($.html(), {
|
|
532
|
-
headers: { "Content-Type": "text/html" }
|
|
533
|
-
});
|
|
172
|
+
return renderToString(this.pageDescriptorProvider.root(state));
|
|
173
|
+
};
|
|
534
174
|
}
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
175
|
+
createHandler(page, templateLoader) {
|
|
176
|
+
return async (serverRequest) => {
|
|
177
|
+
const { url, reply, query, params } = serverRequest;
|
|
178
|
+
const template = await templateLoader();
|
|
179
|
+
if (!template) {
|
|
180
|
+
throw new Error("Template not found");
|
|
541
181
|
}
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
182
|
+
const request = {
|
|
183
|
+
url,
|
|
184
|
+
params,
|
|
185
|
+
query,
|
|
186
|
+
head: {}
|
|
187
|
+
};
|
|
188
|
+
if (this.alepha.has(ServerLinksProvider)) {
|
|
189
|
+
const srv = this.alepha.get(ServerLinksProvider);
|
|
190
|
+
request.links = await srv.links();
|
|
191
|
+
this.alepha.als.set("links", request.links);
|
|
192
|
+
}
|
|
193
|
+
await this.alepha.run(
|
|
194
|
+
"react:server:render",
|
|
195
|
+
{
|
|
196
|
+
request: serverRequest,
|
|
197
|
+
pageRequest: request
|
|
198
|
+
},
|
|
199
|
+
{
|
|
200
|
+
log: false
|
|
550
201
|
}
|
|
202
|
+
);
|
|
203
|
+
const state = await this.pageDescriptorProvider.createLayers(
|
|
204
|
+
page,
|
|
205
|
+
request
|
|
206
|
+
);
|
|
207
|
+
if (state.redirect) {
|
|
208
|
+
return reply.redirect(state.redirect);
|
|
551
209
|
}
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
210
|
+
const element = this.pageDescriptorProvider.root(state, request);
|
|
211
|
+
const app = renderToString(element);
|
|
212
|
+
const script = `<script>window.__ssr=${JSON.stringify({
|
|
213
|
+
links: request.links,
|
|
214
|
+
layers: state.layers.map((it) => ({
|
|
215
|
+
...it,
|
|
216
|
+
error: it.error ? {
|
|
217
|
+
...it.error,
|
|
218
|
+
name: it.error.name,
|
|
219
|
+
message: it.error.message,
|
|
220
|
+
stack: it.error.stack
|
|
221
|
+
// TODO: Hide stack in production ?
|
|
222
|
+
} : void 0,
|
|
223
|
+
index: void 0,
|
|
224
|
+
path: void 0,
|
|
225
|
+
element: void 0
|
|
226
|
+
}))
|
|
227
|
+
})}<\/script>`;
|
|
228
|
+
const response = {
|
|
229
|
+
html: template
|
|
230
|
+
};
|
|
231
|
+
reply.status = 200;
|
|
232
|
+
reply.headers["content-type"] = "text/html";
|
|
233
|
+
reply.headers["cache-control"] = "no-store, no-cache, must-revalidate, proxy-revalidate";
|
|
234
|
+
reply.headers.pragma = "no-cache";
|
|
235
|
+
reply.headers.expires = "0";
|
|
236
|
+
this.fillTemplate(response, app, script);
|
|
237
|
+
if (state.head) {
|
|
238
|
+
response.html = this.headProvider.renderHead(response.html, state.head);
|
|
556
239
|
}
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
240
|
+
return response.html;
|
|
241
|
+
};
|
|
242
|
+
}
|
|
243
|
+
fillTemplate(response, app, script) {
|
|
244
|
+
if (this.ROOT_DIV_REGEX.test(response.html)) {
|
|
245
|
+
response.html = response.html.replace(
|
|
246
|
+
this.ROOT_DIV_REGEX,
|
|
247
|
+
(_match, beforeId, afterId) => {
|
|
248
|
+
return `<div${beforeId} id="${this.env.REACT_ROOT_ID}"${afterId}>${app}</div>`;
|
|
249
|
+
}
|
|
250
|
+
);
|
|
251
|
+
} else {
|
|
252
|
+
const bodyOpenTag = /<body([^>]*)>/i;
|
|
253
|
+
if (bodyOpenTag.test(response.html)) {
|
|
254
|
+
response.html = response.html.replace(bodyOpenTag, (match) => {
|
|
255
|
+
return `${match}
|
|
256
|
+
<div id="${this.env.REACT_ROOT_ID}">${app}</div>`;
|
|
257
|
+
});
|
|
561
258
|
}
|
|
562
259
|
}
|
|
260
|
+
const bodyCloseTagRegex = /<\/body>/i;
|
|
261
|
+
if (bodyCloseTagRegex.test(response.html)) {
|
|
262
|
+
response.html = response.html.replace(
|
|
263
|
+
bodyCloseTagRegex,
|
|
264
|
+
`${script}
|
|
265
|
+
</body>`
|
|
266
|
+
);
|
|
267
|
+
}
|
|
563
268
|
}
|
|
564
269
|
}
|
|
565
270
|
|
|
566
|
-
const envSchema = t.object({
|
|
567
|
-
REACT_AUTH_ENABLED: t.boolean({ default: false })
|
|
568
|
-
});
|
|
569
271
|
class ReactModule {
|
|
570
|
-
env = $inject(envSchema);
|
|
571
272
|
alepha = $inject(Alepha);
|
|
572
273
|
constructor() {
|
|
573
274
|
this.alepha.with(ServerModule).with(ServerLinksProvider).with(PageDescriptorProvider).with(ReactServerProvider);
|
|
574
|
-
if (this.env.REACT_AUTH_ENABLED) {
|
|
575
|
-
this.alepha.with(ReactAuthProvider);
|
|
576
|
-
this.alepha.with(Auth);
|
|
577
|
-
}
|
|
578
275
|
}
|
|
579
276
|
}
|
|
580
|
-
|
|
581
|
-
autoInject($auth, ReactAuthProvider, Auth);
|
|
277
|
+
__bind($page, ReactModule);
|
|
582
278
|
|
|
583
|
-
export { $
|
|
279
|
+
export { $page, PageDescriptorProvider, ReactModule, ReactServerProvider, envSchema };
|