@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.
Files changed (38) hide show
  1. package/README.md +1 -28
  2. package/dist/index.browser.cjs +20 -23
  3. package/dist/index.browser.cjs.map +1 -0
  4. package/dist/index.browser.js +8 -7
  5. package/dist/index.browser.js.map +1 -0
  6. package/dist/index.cjs +234 -542
  7. package/dist/index.cjs.map +1 -0
  8. package/dist/index.d.ts +218 -682
  9. package/dist/index.js +219 -522
  10. package/dist/index.js.map +1 -0
  11. package/dist/{useAuth-B9ypF48n.cjs → useActive-BGtt_RNQ.cjs} +310 -475
  12. package/dist/useActive-BGtt_RNQ.cjs.map +1 -0
  13. package/dist/{useAuth-Ps01oe8e.js → useActive-QkvcaSmu.js} +309 -471
  14. package/dist/useActive-QkvcaSmu.js.map +1 -0
  15. package/package.json +12 -10
  16. package/src/components/Link.tsx +35 -0
  17. package/src/components/NestedView.tsx +36 -0
  18. package/src/contexts/RouterContext.ts +18 -0
  19. package/src/contexts/RouterLayerContext.ts +10 -0
  20. package/src/descriptors/$page.ts +143 -0
  21. package/src/errors/RedirectionError.ts +7 -0
  22. package/src/hooks/RouterHookApi.ts +156 -0
  23. package/src/hooks/useActive.ts +57 -0
  24. package/src/hooks/useClient.ts +6 -0
  25. package/src/hooks/useInject.ts +14 -0
  26. package/src/hooks/useQueryParams.ts +59 -0
  27. package/src/hooks/useRouter.ts +25 -0
  28. package/src/hooks/useRouterEvents.ts +43 -0
  29. package/src/hooks/useRouterState.ts +23 -0
  30. package/src/index.browser.ts +21 -0
  31. package/src/index.shared.ts +15 -0
  32. package/src/index.ts +48 -0
  33. package/src/providers/BrowserHeadProvider.ts +43 -0
  34. package/src/providers/BrowserRouterProvider.ts +146 -0
  35. package/src/providers/PageDescriptorProvider.ts +534 -0
  36. package/src/providers/ReactBrowserProvider.ts +223 -0
  37. package/src/providers/ReactServerProvider.ts +278 -0
  38. 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 useAuth = require('./useAuth-B9ypF48n.cjs');
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 cheerio = require('cheerio');
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('path-to-regexp');
14
+ require('@alepha/router');
16
15
 
17
- class ReactAuthProvider {
18
- log = core.$logger();
19
- alepha = core.$inject(core.Alepha);
20
- serverCookieProvider = core.$inject(server.ServerCookieProvider);
21
- authProviders = [];
22
- authorizationCode = server.$cookie({
23
- name: "authorizationCode",
24
- ttl: { minutes: 15 },
25
- httpOnly: true,
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
- * @param accessToken
228
- * @protected
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
- logout = server.$route({
256
- security: false,
257
- internal: true,
258
- url: "/_oauth/logout",
259
- group: "auth",
260
- method: "GET",
261
- schema: {
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
- * @param name
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
- const authProvider = this.authProviders.find(
296
- (provider) => provider.name === name
52
+ result = result.replace(
53
+ /<head([^>]*)>(.*?)<\/head>/is,
54
+ (_, existingAttrs, existingHead) => `<head${existingAttrs}>${existingHead}${headContent}</head>`
297
55
  );
298
- if (!authProvider) {
299
- throw new server.BadRequestError(`Client ${name} not found`);
300
- }
301
- return authProvider;
56
+ return result.trim();
302
57
  }
303
- /**
304
- *
305
- * @param file
306
- * @protected
307
- */
308
- isViteFile(file) {
309
- const [pathname] = file.split("?");
310
- if (pathname.startsWith("/docs")) {
311
- return false;
312
- }
313
- if (pathname.match(/\.\w{2,5}$/)) {
314
- return true;
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 false;
71
+ return attrs;
72
+ }
73
+ escapeHtml(str) {
74
+ return str.replace(/&/g, "&amp;").replace(/</g, "&lt;").replace(/>/g, "&gt;").replace(/"/g, "&quot;").replace(/'/g, "&#039;");
320
75
  }
321
76
  }
322
77
 
323
- const envSchema$1 = core.t.object({
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
- router = core.$inject(useAuth.Router);
333
- serverProvider = core.$inject(server.ServerProvider);
334
- env = core.$inject(envSchema$1);
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
- await this.configureRoutes();
339
- }
340
- });
341
- id = Math.random().toString(36).substring(2, 7);
342
- async configureRoutes() {
343
- this.alepha.state("ReactServerProvider.ssr", false);
344
- if (this.alepha.isTest()) {
345
- this.processDescriptors();
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 (!root) {
378
- this.log.warn("Missing static files, SSR will be disabled");
109
+ if (this.alepha.isServerless() === "vite") {
110
+ await this.configureVite();
379
111
  return;
380
112
  }
381
- await this.serverProvider.serve(this.createStaticHandler(root));
382
- }
383
- const template = this.alepha.state("ReactServerProvider.template") ?? await promises.readFile(node_path.join(root, "index.html"), "utf-8");
384
- const route = this.createHandler(async () => template);
385
- await this.serverProvider.route(route);
386
- await this.serverProvider.route({
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
- return await this.ssr(ctx.url, template, ctx);
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
- * @param url
462
- * @protected
463
- */
464
- notFoundHandler(url) {
465
- if (url.pathname.match(/\.\w+$/)) {
466
- return new Response("Not found", { status: 404 });
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
- * @param url
472
- * @param template
473
- * @param args
474
- */
475
- async ssr(url, template, args = {}) {
476
- const hasAuth = this.alepha.has(ReactAuthProvider);
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
- if (this.alepha.has(server.ServerLinksProvider) && hasAuth) {
485
- const srv = this.alepha.get(server.ServerLinksProvider);
486
- args.links = await srv.links();
487
- this.alepha.als.set("links", args.links);
488
- }
489
- const { element, layers, redirect, context } = await this.router.render(
490
- url.pathname + url.search,
491
- {
492
- args
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
- if (redirect) {
496
- return new Response("", {
497
- status: 302,
498
- headers: {
499
- Location: redirect
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
- const html = server$1.renderToString(element);
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
- renderHeadContext($, headContext) {
537
- const head = $("head");
538
- if (head) {
539
- if (headContext.title) {
540
- head.find("title").remove();
541
- head.append(`<title>${headContext.title}</title>`);
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
- if (headContext.meta) {
544
- for (const it of headContext.meta) {
545
- const meta = head.find(`meta[name="${it.name}"]`);
546
- if (meta.length) {
547
- meta.attr("content", it.content);
548
- } else {
549
- head.append(`<meta name="${it.name}" content="${it.content}" />`);
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
- if (headContext.htmlAttributes) {
555
- for (const [key, value] of Object.entries(headContext.htmlAttributes)) {
556
- $("html").attr(key, value);
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
- if (headContext.bodyAttributes) {
560
- for (const [key, value] of Object.entries(headContext.bodyAttributes)) {
561
- $("body").attr(key, value);
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(useAuth.PageDescriptorProvider).with(ReactServerProvider);
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.autoInject(useAuth.$page, ReactModule);
582
- core.autoInject(useAuth.$auth, ReactAuthProvider, useAuth.Auth);
278
+ core.__bind(useActive.$page, ReactModule);
583
279
 
584
- exports.$auth = useAuth.$auth;
585
- exports.$page = useAuth.$page;
586
- exports.Auth = useAuth.Auth;
587
- exports.Link = useAuth.Link;
588
- exports.NestedView = useAuth.NestedView;
589
- exports.PageDescriptorProvider = useAuth.PageDescriptorProvider;
590
- exports.ReactBrowserProvider = useAuth.ReactBrowserProvider;
591
- exports.RedirectionError = useAuth.RedirectionError;
592
- exports.Router = useAuth.Router;
593
- exports.RouterContext = useAuth.RouterContext;
594
- exports.RouterHookApi = useAuth.RouterHookApi;
595
- exports.RouterLayerContext = useAuth.RouterLayerContext;
596
- exports.pageDescriptorKey = useAuth.pageDescriptorKey;
597
- exports.useActive = useAuth.useActive;
598
- exports.useAuth = useAuth.useAuth;
599
- exports.useClient = useAuth.useClient;
600
- exports.useInject = useAuth.useInject;
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$1;
299
+ exports.envSchema = envSchema;
300
+ //# sourceMappingURL=index.cjs.map