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