@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/dist/index.js CHANGED
@@ -1,551 +1,279 @@
1
- import { $logger, $inject, Alepha, t, $hook, autoInject } from '@alepha/core';
2
- import { FastifyCookieProvider, $cookie, $route, BadRequestError, ServerProvider, ServerModule, ServerLinksProvider } from '@alepha/server';
3
- import { $ as $auth, R as Router, a as $page, A as Auth, P as PageDescriptorProvider } from './useAuth-i7wbKVrt.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-i7wbKVrt.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';
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 'path-to-regexp';
13
+ import '@alepha/router';
14
14
 
15
- class ReactAuthProvider {
16
- log = $logger();
17
- alepha = $inject(Alepha);
18
- fastifyCookieProvider = $inject(FastifyCookieProvider);
19
- authProviders = [];
20
- authorizationCode = $cookie({
21
- name: "authorizationCode",
22
- ttl: { minutes: 15 },
23
- httpOnly: true,
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
- * @param accessToken
219
- * @protected
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
- logout = $route({
247
- security: false,
248
- internal: true,
249
- url: "/_oauth/logout",
250
- group: "auth",
251
- method: "GET",
252
- schema: {
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
- * @param name
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
- const authProvider = this.authProviders.find(
287
- (provider) => provider.name === name
51
+ result = result.replace(
52
+ /<head([^>]*)>(.*?)<\/head>/is,
53
+ (_, existingAttrs, existingHead) => `<head${existingAttrs}>${existingHead}${headContent}</head>`
288
54
  );
289
- if (!authProvider) {
290
- throw new BadRequestError(`Client ${name} not found`);
291
- }
292
- return authProvider;
55
+ return result.trim();
293
56
  }
294
- /**
295
- *
296
- * @param file
297
- * @protected
298
- */
299
- isViteFile(file) {
300
- const [pathname] = file.split("?");
301
- if (pathname.startsWith("/docs")) {
302
- return false;
303
- }
304
- if (pathname.match(/\.\w{2,5}$/)) {
305
- return true;
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 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;");
311
74
  }
312
75
  }
313
76
 
314
- const envSchema$1 = t.object({
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
- REACT_SSR_OUTLET: t.string({ default: "<!--ssr-outlet-->" })
81
+ REACT_ROOT_ID: t.string({ default: "root" })
319
82
  });
320
83
  class ReactServerProvider {
321
84
  log = $logger();
322
85
  alepha = $inject(Alepha);
323
- router = $inject(Router);
324
- server = $inject(ServerProvider);
325
- 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
+ );
326
95
  configure = $hook({
327
96
  name: "configure",
328
97
  handler: async () => {
329
- await this.configureRoutes();
330
- }
331
- });
332
- async configureRoutes() {
333
- if (this.alepha.isTest()) {
334
- this.processDescriptors();
335
- }
336
- if (this.router.empty()) {
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 (!root) {
366
- this.log.warn("Missing static files, SSR will be disabled");
108
+ if (this.alepha.isServerless() === "vite") {
109
+ await this.configureVite();
367
110
  return;
368
111
  }
369
- await this.server.serve(this.createStaticHandler(root));
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
- * Check if the template contains the outlet.
383
- *
384
- * @param template
385
- * @protected
386
- */
387
- checkTemplate(template) {
388
- if (!template.includes(this.env.REACT_SSR_OUTLET)) {
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 template;
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
- prefix: this.env.REACT_SERVER_PREFIX,
410
- logLevel: "warn",
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
- * @param templateLoader
421
- * @protected
422
- */
423
- createHandler(templateLoader) {
424
- return {
425
- method: "GET",
426
- url: "/",
427
- handler: async (ctx) => {
428
- const template = await templateLoader();
429
- if (!template) {
430
- return new Response("Not found", { status: 404 });
431
- }
432
- const response = this.notFoundHandler(ctx.url);
433
- if (response) {
434
- return response;
435
- }
436
- return await this.ssr(ctx.url, template, ctx);
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
- processDescriptors() {
441
- const pages = this.alepha.getDescriptorValues($page);
442
- for (const { key, instance, value } of pages) {
443
- instance[key].render = async (options = {}) => {
444
- const name = value.options.name ?? key;
445
- const page = this.router.page(name);
446
- const layers = await this.router.createLayers(
447
- "",
448
- page,
449
- options.params ?? {},
450
- options.query ?? {},
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
- if (redirect) {
488
- return new Response("", {
489
- status: 302,
490
- headers: {
491
- Location: redirect
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
- const appHtml = renderToString(element);
496
- const script = `<script>window.__ssr=${JSON.stringify({
497
- layers: layers.map((it) => ({
498
- ...it,
499
- index: void 0,
500
- path: void 0,
501
- element: void 0
502
- }))
503
- })}<\/script>`;
504
- const index = template.indexOf("</body>");
505
- if (index !== -1) {
506
- template = template.slice(0, index) + script + template.slice(index);
507
- }
508
- if (context.helmet) {
509
- template = this.renderHelmetContext(template, context.helmet);
510
- }
511
- template = template.replace(this.env.REACT_SSR_OUTLET, appHtml);
512
- return new Response(template, {
513
- headers: { "Content-Type": "text/html" }
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
- renderHelmetContext(template, helmetContext) {
517
- if (helmetContext.title) {
518
- if (template.includes("<title>")) {
519
- template = template.replace(
520
- /<title>.*<\/title>/,
521
- `<title>${helmetContext.title}</title>`
522
- );
523
- } else {
524
- template = template.replace(
525
- "</head>",
526
- `<title>${helmetContext.title}</title></head>`
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
- return template;
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
- autoInject($page, ReactModule);
549
- autoInject($auth, ReactAuthProvider, Auth);
277
+ __bind($page, ReactModule);
550
278
 
551
- export { $auth, $page, Auth, PageDescriptorProvider, ReactAuthProvider, ReactModule, ReactServerProvider, Router, envSchema$1 as envSchema };
279
+ export { $page, PageDescriptorProvider, ReactModule, ReactServerProvider, envSchema };