@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/dist/index.js CHANGED
@@ -1,325 +1,80 @@
1
- import { $logger, $inject, Alepha, t, $hook, autoInject } from '@alepha/core';
2
- import { ServerCookieProvider, $cookie, $route, BadRequestError, ServerProvider, ServerLinksProvider, ServerModule } from '@alepha/server';
3
- import { $ as $auth, R as Router, a as $page, A as Auth, P as PageDescriptorProvider } from './useAuth-Ps01oe8e.js';
4
- export { L as Link, N as NestedView, l as ReactBrowserProvider, m as RedirectionError, b as RouterContext, d as RouterHookApi, c as RouterLayerContext, p as pageDescriptorKey, j as useActive, k as useAuth, e as useClient, u as useInject, f as useQueryParams, g as useRouter, h as useRouterEvents, i as useRouterState } from './useAuth-Ps01oe8e.js';
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 { load } from 'cheerio';
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 'path-to-regexp';
13
+ import '@alepha/router';
15
14
 
16
- class ReactAuthProvider {
17
- log = $logger();
18
- alepha = $inject(Alepha);
19
- serverCookieProvider = $inject(ServerCookieProvider);
20
- authProviders = [];
21
- authorizationCode = $cookie({
22
- name: "authorizationCode",
23
- ttl: { minutes: 15 },
24
- httpOnly: true,
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
- * @param accessToken
227
- * @protected
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
- logout = $route({
255
- security: false,
256
- internal: true,
257
- url: "/_oauth/logout",
258
- group: "auth",
259
- method: "GET",
260
- schema: {
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
- * @param name
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
- const authProvider = this.authProviders.find(
295
- (provider) => provider.name === name
51
+ result = result.replace(
52
+ /<head([^>]*)>(.*?)<\/head>/is,
53
+ (_, existingAttrs, existingHead) => `<head${existingAttrs}>${existingHead}${headContent}</head>`
296
54
  );
297
- if (!authProvider) {
298
- throw new BadRequestError(`Client ${name} not found`);
299
- }
300
- return authProvider;
55
+ return result.trim();
301
56
  }
302
- /**
303
- *
304
- * @param file
305
- * @protected
306
- */
307
- isViteFile(file) {
308
- const [pathname] = file.split("?");
309
- if (pathname.startsWith("/docs")) {
310
- return false;
311
- }
312
- if (pathname.match(/\.\w{2,5}$/)) {
313
- return true;
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 false;
70
+ return attrs;
71
+ }
72
+ escapeHtml(str) {
73
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
319
74
  }
320
75
  }
321
76
 
322
- const envSchema$1 = t.object({
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
- router = $inject(Router);
332
- serverProvider = $inject(ServerProvider);
333
- env = $inject(envSchema$1);
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
- await this.configureRoutes();
338
- }
339
- });
340
- id = Math.random().toString(36).substring(2, 7);
341
- async configureRoutes() {
342
- this.alepha.state("ReactServerProvider.ssr", false);
343
- if (this.alepha.isTest()) {
344
- this.processDescriptors();
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 (!root) {
377
- this.log.warn("Missing static files, SSR will be disabled");
108
+ if (this.alepha.isServerless() === "vite") {
109
+ await this.configureVite();
378
110
  return;
379
111
  }
380
- await this.serverProvider.serve(this.createStaticHandler(root));
381
- }
382
- const template = this.alepha.state("ReactServerProvider.template") ?? await readFile(join(root, "index.html"), "utf-8");
383
- const route = this.createHandler(async () => template);
384
- await this.serverProvider.route(route);
385
- await this.serverProvider.route({
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
- return await this.ssr(ctx.url, template, ctx);
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
- * @param url
461
- * @protected
462
- */
463
- notFoundHandler(url) {
464
- if (url.pathname.match(/\.\w+$/)) {
465
- return new Response("Not found", { status: 404 });
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
- * @param url
471
- * @param template
472
- * @param args
473
- */
474
- async ssr(url, template, args = {}) {
475
- const hasAuth = this.alepha.has(ReactAuthProvider);
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
- if (this.alepha.has(ServerLinksProvider) && hasAuth) {
484
- const srv = this.alepha.get(ServerLinksProvider);
485
- args.links = await srv.links();
486
- this.alepha.als.set("links", args.links);
487
- }
488
- const { element, layers, redirect, context } = await this.router.render(
489
- url.pathname + url.search,
490
- {
491
- args
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
- if (redirect) {
495
- return new Response("", {
496
- status: 302,
497
- headers: {
498
- Location: redirect
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
- const html = renderToString(element);
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
- renderHeadContext($, headContext) {
536
- const head = $("head");
537
- if (head) {
538
- if (headContext.title) {
539
- head.find("title").remove();
540
- head.append(`<title>${headContext.title}</title>`);
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
- if (headContext.meta) {
543
- for (const it of headContext.meta) {
544
- const meta = head.find(`meta[name="${it.name}"]`);
545
- if (meta.length) {
546
- meta.attr("content", it.content);
547
- } else {
548
- head.append(`<meta name="${it.name}" content="${it.content}" />`);
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
- if (headContext.htmlAttributes) {
554
- for (const [key, value] of Object.entries(headContext.htmlAttributes)) {
555
- $("html").attr(key, value);
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
- if (headContext.bodyAttributes) {
559
- for (const [key, value] of Object.entries(headContext.bodyAttributes)) {
560
- $("body").attr(key, value);
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
- autoInject($page, ReactModule);
581
- autoInject($auth, ReactAuthProvider, Auth);
277
+ __bind($page, ReactModule);
582
278
 
583
- export { $auth, $page, Auth, PageDescriptorProvider, ReactAuthProvider, ReactModule, ReactServerProvider, Router, envSchema$1 as envSchema };
279
+ export { $page, PageDescriptorProvider, ReactModule, ReactServerProvider, envSchema };