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