@alepha/react 0.6.0 → 0.6.2

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.d.ts CHANGED
@@ -1,14 +1,15 @@
1
1
  import * as _alepha_core from '@alepha/core';
2
- import { EventEmitter, Alepha, TSchema as TSchema$1, Static as Static$1, Async, KIND, Class, TObject as TObject$1 } from '@alepha/core';
2
+ import { EventEmitter, Alepha, Static as Static$1, TSchema as TSchema$1, KIND, Async, Class, TObject as TObject$1 } from '@alepha/core';
3
3
  import * as react from 'react';
4
- import react__default, { ReactNode, AnchorHTMLAttributes, FC } from 'react';
4
+ import react__default, { ReactNode, FC, AnchorHTMLAttributes } from 'react';
5
5
  import * as react_jsx_runtime from 'react/jsx-runtime';
6
6
  import { UserAccountToken } from '@alepha/security';
7
7
  import * as _alepha_server from '@alepha/server';
8
- import { FastifyCookieProvider, CookieManager, HttpClient, ServerProvider, ServeDescriptorOptions, CreateRoute } from '@alepha/server';
8
+ import { HttpLink, ServerCookieProvider, CookieManager, HttpClient, ServerProvider, ServeDescriptorOptions, RouteObject } from '@alepha/server';
9
9
  import { Root } from 'react-dom/client';
10
10
  import { MatchFunction, ParamData } from 'path-to-regexp';
11
11
  import { Configuration } from 'openid-client';
12
+ import { CheerioAPI } from 'cheerio';
12
13
 
13
14
  /** Symbol key applied to readonly types */
14
15
  declare const ReadonlyKind: unique symbol;
@@ -149,13 +150,7 @@ interface NestedViewProps {
149
150
  * @param props
150
151
  * @constructor
151
152
  */
152
- declare const NestedView: (props: NestedViewProps) => string | number | boolean | react.ReactElement<any, string | react.JSXElementConstructor<any>> | Iterable<ReactNode> | null;
153
-
154
- interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
155
- to: string;
156
- children?: react__default.ReactNode;
157
- }
158
- declare const Link: (props: LinkProps) => react_jsx_runtime.JSX.Element;
153
+ declare const NestedView: (props: NestedViewProps) => string | number | bigint | boolean | react.ReactElement<unknown, string | react.JSXElementConstructor<any>> | Iterable<ReactNode> | Promise<string | number | bigint | boolean | react.ReactPortal | react.ReactElement<unknown, string | react.JSXElementConstructor<any>> | Iterable<ReactNode> | null | undefined> | null;
159
154
 
160
155
  declare class Router extends EventEmitter<RouterEvents> {
161
156
  protected readonly log: _alepha_core.Logger;
@@ -348,6 +343,10 @@ interface Layer {
348
343
  *
349
344
  */
350
345
  props?: Record<string, any>;
346
+ /**
347
+ *
348
+ */
349
+ error?: Error;
351
350
  /**
352
351
  *
353
352
  */
@@ -430,7 +429,7 @@ interface RouterRenderContext {
430
429
  /**
431
430
  *
432
431
  */
433
- helmet?: RouterRenderHelmetContext;
432
+ head?: RouterRenderHeadContext;
434
433
  }
435
434
  interface RouterRenderOptions extends RouterMatchOptions {
436
435
  /**
@@ -456,7 +455,7 @@ interface RouterStackItem {
456
455
  */
457
456
  error?: Error;
458
457
  }
459
- interface RouterRenderHelmetContext {
458
+ interface RouterRenderHeadContext {
460
459
  /**
461
460
  *
462
461
  */
@@ -464,15 +463,15 @@ interface RouterRenderHelmetContext {
464
463
  /**
465
464
  *
466
465
  */
467
- html?: {
468
- attributes?: Record<string, string>;
469
- };
466
+ titleSeparator?: string;
470
467
  /**
471
- *
468
+ * Add html attributes to the <html> tag.
472
469
  */
473
- body?: {
474
- attributes?: Record<string, string>;
475
- };
470
+ htmlAttributes?: Record<string, string>;
471
+ /**
472
+ * Add html attributes to the <body> tag.
473
+ */
474
+ bodyAttributes?: Record<string, string>;
476
475
  /**
477
476
  *
478
477
  */
@@ -503,7 +502,7 @@ interface RouterRenderResult {
503
502
  declare class ReactAuthProvider {
504
503
  protected readonly log: _alepha_core.Logger;
505
504
  protected readonly alepha: Alepha;
506
- protected readonly fastifyCookieProvider: FastifyCookieProvider;
505
+ protected readonly serverCookieProvider: ServerCookieProvider;
507
506
  protected authProviders: AuthProvider[];
508
507
  protected readonly authorizationCode: _alepha_server.CookieDescriptor<TObject<{
509
508
  codeVerifier: TOptional<TString>;
@@ -517,7 +516,7 @@ declare class ReactAuthProvider {
517
516
  scope: TOptional<TString>;
518
517
  issued_at: TOptional<TNumber>;
519
518
  }>>;
520
- protected readonly user: _alepha_server.CookieDescriptor<TObject<{
519
+ readonly user: _alepha_server.CookieDescriptor<TObject<{
521
520
  id: TString;
522
521
  name: TOptional<TString>;
523
522
  email: TOptional<TString>;
@@ -526,7 +525,7 @@ declare class ReactAuthProvider {
526
525
  /**
527
526
  * Configure Fastify to forward Session Access Token to Header Authorization.
528
527
  */
529
- protected readonly configureFastify: _alepha_core.HookDescriptor<"configure:fastify">;
528
+ protected readonly onRequest: _alepha_core.HookDescriptor<"server:onRequest">;
530
529
  /**
531
530
  *
532
531
  * @param cookies
@@ -611,12 +610,23 @@ interface ReactHydrationState {
611
610
  user?: ReactUser;
612
611
  auth?: "server" | "client";
613
612
  layers?: PreviousLayerData[];
613
+ links?: HttpLink[];
614
614
  }
615
615
 
616
+ declare const envSchema$2: _alepha_core.TObject<{
617
+ REACT_ROOT_ID: TString;
618
+ }>;
619
+ declare module "@alepha/core" {
620
+ interface Env extends Partial<Static$1<typeof envSchema$2>> {
621
+ }
622
+ }
616
623
  declare class ReactBrowserProvider {
617
624
  protected readonly log: _alepha_core.Logger;
618
625
  protected readonly client: HttpClient;
619
626
  protected readonly router: Router;
627
+ protected readonly env: {
628
+ REACT_ROOT_ID: string;
629
+ };
620
630
  protected root: Root;
621
631
  transitioning?: {
622
632
  to: string;
@@ -655,8 +665,15 @@ declare class ReactBrowserProvider {
655
665
  previous?: PreviousLayerData[];
656
666
  }): Promise<{
657
667
  url: string;
668
+ context: RouterRenderContext;
658
669
  }>;
659
- protected renderHelmetContext(ctx: RouterRenderHelmetContext): void;
670
+ /**
671
+ * Render the helmet context.
672
+ *
673
+ * @param ctx
674
+ * @protected
675
+ */
676
+ protected renderHeadContext(ctx: RouterRenderHeadContext): void;
660
677
  /**
661
678
  * Get embedded layers from the server.
662
679
  *
@@ -674,11 +691,6 @@ declare class ReactBrowserProvider {
674
691
  * @protected
675
692
  */
676
693
  protected ready: _alepha_core.HookDescriptor<"ready">;
677
- /**
678
- *
679
- * @protected
680
- */
681
- protected stop: _alepha_core.HookDescriptor<"stop">;
682
694
  }
683
695
  /**
684
696
  *
@@ -815,12 +827,16 @@ interface PageDescriptorOptions<TConfig extends PageDescriptorConfigSchema = Pag
815
827
  /**
816
828
  *
817
829
  */
818
- helmet?: RouterRenderHelmetContext | ((props: TProps) => RouterRenderHelmetContext);
830
+ can?: () => boolean;
831
+ /**
832
+ *
833
+ */
834
+ head?: RouterRenderHeadContext | ((props: TProps, previous?: RouterRenderHeadContext) => RouterRenderHeadContext);
819
835
  /**
820
836
  *
821
837
  */
822
838
  notFoundHandler?: FC<{
823
- error: Error;
839
+ url: string;
824
840
  }>;
825
841
  /**
826
842
  *
@@ -833,6 +849,7 @@ interface PageDescriptorOptions<TConfig extends PageDescriptorConfigSchema = Pag
833
849
  interface PageContext {
834
850
  user?: UserAccountToken;
835
851
  cookies?: CookieManager;
852
+ links?: HttpLink[];
836
853
  }
837
854
  interface PageDescriptorConfigValue<TConfig extends PageDescriptorConfigSchema = PageDescriptorConfigSchema> {
838
855
  query: TConfig["query"] extends TSchema$1 ? Static$1<TConfig["query"]> : Record<string, string>;
@@ -857,6 +874,12 @@ declare const $page: {
857
874
  [KIND]: string;
858
875
  };
859
876
 
877
+ interface LinkProps extends AnchorHTMLAttributes<HTMLAnchorElement> {
878
+ to: string | PageDescriptor;
879
+ children?: react__default.ReactNode;
880
+ }
881
+ declare const Link: (props: LinkProps) => react_jsx_runtime.JSX.Element | null;
882
+
860
883
  interface RouterContextValue {
861
884
  router: Router;
862
885
  alepha: Alepha;
@@ -875,7 +898,10 @@ declare class Auth {
875
898
  alepha: Alepha;
876
899
  log: _alepha_core.Logger;
877
900
  client: HttpClient;
878
- api: string;
901
+ slugs: {
902
+ login: string;
903
+ logout: string;
904
+ };
879
905
  start: _alepha_core.HookDescriptor<"start">;
880
906
  login: (provider?: string) => void;
881
907
  logout: () => void;
@@ -894,6 +920,7 @@ interface AuthDescriptorOptions {
894
920
  interface AuthDescriptor {
895
921
  [KIND]: typeof KEY;
896
922
  options: AuthDescriptorOptions;
923
+ jwks: () => string;
897
924
  }
898
925
  declare const $auth: {
899
926
  (options: AuthDescriptorOptions): AuthDescriptor;
@@ -962,47 +989,46 @@ declare const envSchema$1: TObject<{
962
989
  REACT_SERVER_DIST: TString;
963
990
  REACT_SERVER_PREFIX: TString;
964
991
  REACT_SSR_ENABLED: TBoolean;
965
- REACT_SSR_OUTLET: TString;
992
+ REACT_ROOT_ID: TString;
966
993
  }>;
967
994
  declare module "@alepha/core" {
968
995
  interface Env extends Partial<Static$1<typeof envSchema$1>> {
969
996
  }
970
997
  interface State {
971
998
  "ReactServerProvider.template"?: string;
999
+ "ReactServerProvider.ssr"?: boolean;
972
1000
  }
973
1001
  }
974
1002
  declare class ReactServerProvider {
975
1003
  protected readonly log: _alepha_core.Logger;
976
1004
  protected readonly alepha: Alepha;
977
1005
  protected readonly router: Router;
978
- protected readonly server: ServerProvider;
1006
+ protected readonly serverProvider: ServerProvider;
979
1007
  protected readonly env: {
1008
+ REACT_ROOT_ID: string;
980
1009
  REACT_SERVER_DIST: string;
981
1010
  REACT_SERVER_PREFIX: string;
982
1011
  REACT_SSR_ENABLED: boolean;
983
- REACT_SSR_OUTLET: string;
984
1012
  };
985
1013
  protected readonly configure: _alepha_core.HookDescriptor<"configure">;
1014
+ id: string;
986
1015
  protected configureRoutes(): Promise<void>;
987
1016
  /**
988
- * Check if the template contains the outlet.
989
1017
  *
990
- * @param template
1018
+ * @param root
991
1019
  * @protected
992
1020
  */
993
- protected checkTemplate(template: string): string;
1021
+ protected createStaticHandler(root: string): ServeDescriptorOptions;
994
1022
  /**
995
1023
  *
996
- * @param root
1024
+ * @param templateLoader
997
1025
  * @protected
998
1026
  */
999
- protected createStaticHandler(root: string): ServeDescriptorOptions;
1027
+ protected createHandler(templateLoader: () => Promise<string | undefined>): RouteObject;
1000
1028
  /**
1001
1029
  *
1002
- * @param templateLoader
1003
1030
  * @protected
1004
1031
  */
1005
- protected createHandler(templateLoader: () => Promise<string | undefined>): CreateRoute;
1006
1032
  protected processDescriptors(): void;
1007
1033
  /**
1008
1034
  *
@@ -1014,10 +1040,10 @@ declare class ReactServerProvider {
1014
1040
  *
1015
1041
  * @param url
1016
1042
  * @param template
1017
- * @param page
1043
+ * @param args
1018
1044
  */
1019
- ssr(url: URL, template?: string, page?: PageContext): Promise<Response>;
1020
- protected renderHelmetContext(template: string, helmetContext: RouterRenderHelmetContext): string;
1045
+ ssr(url: URL, template: string, args?: PageContext): Promise<Response>;
1046
+ protected renderHeadContext($: CheerioAPI, headContext: RouterRenderHeadContext): void;
1021
1047
  }
1022
1048
 
1023
1049
  declare class RedirectionError extends Error {
@@ -1040,4 +1066,4 @@ declare class ReactModule {
1040
1066
  constructor();
1041
1067
  }
1042
1068
 
1043
- export { $auth, $page, type AnchorProps, Auth, type AuthDescriptor, type AuthDescriptorOptions, type AuthHook, type AuthProvider, type HrefLike, type Layer, Link, NestedView, type PageContext, type PageDescriptor, type PageDescriptorConfigSchema, type PageDescriptorConfigValue, type PageDescriptorOptions, PageDescriptorProvider, type PageRoute, type PageRouteEntry, type PreviousLayerData, ReactAuthProvider, ReactBrowserProvider, type ReactHydrationState, ReactModule, ReactServerProvider, type ReactUser, RedirectionError, Router, RouterContext, type RouterContextValue, type RouterEvents, type RouterGoOptions, RouterHookApi, RouterLayerContext, type RouterLayerContextValue, type RouterMatchOptions, type RouterRenderContext, type RouterRenderHelmetContext, type RouterRenderOptions, type RouterRenderResult, type RouterStackItem, type RouterState, type SessionAuthorizationCode, type SessionTokens, type TPropsDefault, type TPropsParentDefault, type UseActiveHook, type UseQueryParamsHookOptions, envSchema$1 as envSchema, pageDescriptorKey, useActive, useAuth, useClient, useInject, useQueryParams, useRouter, useRouterEvents, useRouterState };
1069
+ export { $auth, $page, type AnchorProps, Auth, type AuthDescriptor, type AuthDescriptorOptions, type AuthHook, type AuthProvider, type HrefLike, type Layer, Link, NestedView, type PageContext, type PageDescriptor, type PageDescriptorConfigSchema, type PageDescriptorConfigValue, type PageDescriptorOptions, PageDescriptorProvider, type PageRoute, type PageRouteEntry, type PreviousLayerData, ReactAuthProvider, ReactBrowserProvider, type ReactHydrationState, ReactModule, ReactServerProvider, type ReactUser, RedirectionError, Router, RouterContext, type RouterContextValue, type RouterEvents, type RouterGoOptions, RouterHookApi, RouterLayerContext, type RouterLayerContextValue, type RouterMatchOptions, type RouterRenderContext, type RouterRenderHeadContext, type RouterRenderOptions, type RouterRenderResult, type RouterStackItem, type RouterState, type SessionAuthorizationCode, type SessionTokens, type TPropsDefault, type TPropsParentDefault, type UseActiveHook, type UseQueryParamsHookOptions, envSchema$1 as envSchema, pageDescriptorKey, useActive, useAuth, useClient, useInject, useQueryParams, useRouter, useRouterEvents, useRouterState };
package/dist/index.js CHANGED
@@ -1,11 +1,12 @@
1
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';
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
5
  import { discovery, allowInsecureRequests, refreshTokenGrant, randomPKCECodeVerifier, calculatePKCECodeChallenge, buildAuthorizationUrl, authorizationCodeGrant, buildEndSessionUrl } from 'openid-client';
6
6
  import { existsSync } from 'node:fs';
7
7
  import { readFile } from 'node:fs/promises';
8
8
  import { join } from 'node:path';
9
+ import { load } from 'cheerio';
9
10
  import { renderToString } from 'react-dom/server';
10
11
  import 'react/jsx-runtime';
11
12
  import 'react';
@@ -15,7 +16,7 @@ import 'path-to-regexp';
15
16
  class ReactAuthProvider {
16
17
  log = $logger();
17
18
  alepha = $inject(Alepha);
18
- fastifyCookieProvider = $inject(FastifyCookieProvider);
19
+ serverCookieProvider = $inject(ServerCookieProvider);
19
20
  authProviders = [];
20
21
  authorizationCode = $cookie({
21
22
  name: "authorizationCode",
@@ -53,23 +54,30 @@ class ReactAuthProvider {
53
54
  name: "configure",
54
55
  handler: async () => {
55
56
  const auths = this.alepha.getDescriptorValues($auth);
56
- for (const auth of auths) {
57
- const options = auth.value.options;
57
+ for (const { value, key, instance } of auths) {
58
+ const options = value.options;
58
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
+ };
59
77
  this.authProviders.push({
60
- name: options.name ?? auth.key,
78
+ name: options.name ?? key,
61
79
  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
- )
80
+ client
73
81
  });
74
82
  }
75
83
  }
@@ -78,21 +86,19 @@ class ReactAuthProvider {
78
86
  /**
79
87
  * Configure Fastify to forward Session Access Token to Header Authorization.
80
88
  */
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
- }
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}`);
94
97
  }
95
- });
98
+ if (this.user.get(request.cookies) && !this.tokens.get(request.cookies)) {
99
+ this.user.del(request.cookies);
100
+ }
101
+ }
96
102
  }
97
103
  });
98
104
  /**
@@ -121,7 +127,9 @@ class ReactAuthProvider {
121
127
  });
122
128
  return newTokens;
123
129
  } catch (e) {
124
- this.log.warn(e, "Failed to refresh token -");
130
+ if (e instanceof Error) {
131
+ this.log.warn("Failed to refresh token", e.message);
132
+ }
125
133
  }
126
134
  }
127
135
  this.tokens.del(cookies);
@@ -141,7 +149,7 @@ class ReactAuthProvider {
141
149
  */
142
150
  login = $route({
143
151
  security: false,
144
- private: true,
152
+ internal: true,
145
153
  url: "/_oauth/login",
146
154
  group: "auth",
147
155
  method: "GET",
@@ -152,13 +160,13 @@ class ReactAuthProvider {
152
160
  })
153
161
  },
154
162
  handler: async ({ query, cookies, url }) => {
155
- const { client } = this.provider(query.provider);
163
+ const { client, redirectUri } = this.provider(query.provider);
156
164
  const codeVerifier = randomPKCECodeVerifier();
157
165
  const codeChallenge = await calculatePKCECodeChallenge(codeVerifier);
158
166
  const scope = "openid profile email";
159
- let redirect_uri = this.authProviders[0].redirectUri;
167
+ let redirect_uri = redirectUri;
160
168
  if (redirect_uri.startsWith("/")) {
161
- redirect_uri = `${url.protocol}://${url.host}${redirect_uri}`;
169
+ redirect_uri = `${url.protocol}//${url.host}${redirect_uri}`;
162
170
  }
163
171
  const parameters = {
164
172
  redirect_uri,
@@ -183,7 +191,7 @@ class ReactAuthProvider {
183
191
  */
184
192
  callback = $route({
185
193
  security: false,
186
- private: true,
194
+ internal: true,
187
195
  url: "/_oauth/callback",
188
196
  group: "auth",
189
197
  method: "GET",
@@ -245,7 +253,7 @@ class ReactAuthProvider {
245
253
  */
246
254
  logout = $route({
247
255
  security: false,
248
- private: true,
256
+ internal: true,
249
257
  url: "/_oauth/logout",
250
258
  group: "auth",
251
259
  method: "GET",
@@ -315,13 +323,13 @@ const envSchema$1 = t.object({
315
323
  REACT_SERVER_DIST: t.string({ default: "client" }),
316
324
  REACT_SERVER_PREFIX: t.string({ default: "" }),
317
325
  REACT_SSR_ENABLED: t.boolean({ default: false }),
318
- REACT_SSR_OUTLET: t.string({ default: "<!--ssr-outlet-->" })
326
+ REACT_ROOT_ID: t.string({ default: "root" })
319
327
  });
320
328
  class ReactServerProvider {
321
329
  log = $logger();
322
330
  alepha = $inject(Alepha);
323
331
  router = $inject(Router);
324
- server = $inject(ServerProvider);
332
+ serverProvider = $inject(ServerProvider);
325
333
  env = $inject(envSchema$1);
326
334
  configure = $hook({
327
335
  name: "configure",
@@ -329,7 +337,9 @@ class ReactServerProvider {
329
337
  await this.configureRoutes();
330
338
  }
331
339
  });
340
+ id = Math.random().toString(36).substring(2, 7);
332
341
  async configureRoutes() {
342
+ this.alepha.state("ReactServerProvider.ssr", false);
333
343
  if (this.alepha.isTest()) {
334
344
  this.processDescriptors();
335
345
  }
@@ -337,14 +347,15 @@ class ReactServerProvider {
337
347
  return;
338
348
  }
339
349
  if (process.env.VITE_ALEPHA_DEV === "true") {
350
+ const url = `http://${process.env.SERVER_HOST}:${process.env.SERVER_PORT}`;
340
351
  this.log.info("SSR (vite) OK");
341
- const templateUrl = "http://127.0.0.1:5173/index.html";
342
- this.log.debug(`Fetch template from ${templateUrl}`);
352
+ this.alepha.state("ReactServerProvider.ssr", true);
353
+ const templateUrl = `${url}/index.html`;
343
354
  const route2 = this.createHandler(
344
- () => fetch(templateUrl).then((it) => it.text()).catch(() => void 0).then((it) => it ? this.checkTemplate(it) : void 0)
355
+ () => fetch(templateUrl).then((it) => it.text()).catch(() => void 0)
345
356
  );
346
- await this.server.route(route2);
347
- await this.server.route({
357
+ await this.serverProvider.route(route2);
358
+ await this.serverProvider.route({
348
359
  ...route2,
349
360
  url: "*"
350
361
  });
@@ -366,37 +377,16 @@ class ReactServerProvider {
366
377
  this.log.warn("Missing static files, SSR will be disabled");
367
378
  return;
368
379
  }
369
- await this.server.serve(this.createStaticHandler(root));
380
+ await this.serverProvider.serve(this.createStaticHandler(root));
370
381
  }
371
- const template = this.checkTemplate(
372
- this.alepha.state("ReactServerProvider.template") ?? await readFile(join(root, "index.html"), "utf-8")
373
- );
382
+ const template = this.alepha.state("ReactServerProvider.template") ?? await readFile(join(root, "index.html"), "utf-8");
374
383
  const route = this.createHandler(async () => template);
375
- await this.server.route(route);
376
- await this.server.route({
384
+ await this.serverProvider.route(route);
385
+ await this.serverProvider.route({
377
386
  ...route,
378
387
  url: "*"
379
388
  });
380
- }
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
- );
393
- }
394
- return template.replace(
395
- `<div id="root"></div>`,
396
- `<div id="root">${this.env.REACT_SSR_OUTLET}</div>`
397
- );
398
- }
399
- return template;
389
+ this.alepha.state("ReactServerProvider.ssr", true);
400
390
  }
401
391
  /**
402
392
  *
@@ -433,13 +423,14 @@ class ReactServerProvider {
433
423
  if (response) {
434
424
  return response;
435
425
  }
436
- return await this.ssr(ctx.url, template, {
437
- user: ctx.user,
438
- cookies: ctx.cookies
439
- });
426
+ return await this.ssr(ctx.url, template, ctx);
440
427
  }
441
428
  };
442
429
  }
430
+ /**
431
+ *
432
+ * @protected
433
+ */
443
434
  processDescriptors() {
444
435
  const pages = this.alepha.getDescriptorValues($page);
445
436
  for (const { key, instance, value } of pages) {
@@ -478,13 +469,26 @@ class ReactServerProvider {
478
469
  *
479
470
  * @param url
480
471
  * @param template
481
- * @param page
472
+ * @param args
482
473
  */
483
- async ssr(url, template = this.env.REACT_SSR_OUTLET, page = {}) {
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 = [];
481
+ }
482
+ }
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
+ }
484
488
  const { element, layers, redirect, context } = await this.router.render(
485
489
  url.pathname + url.search,
486
490
  {
487
- args: page
491
+ args
488
492
  }
489
493
  );
490
494
  if (redirect) {
@@ -495,42 +499,67 @@ class ReactServerProvider {
495
499
  }
496
500
  });
497
501
  }
498
- const appHtml = renderToString(element);
502
+ const html = renderToString(element);
503
+ const $ = load(template);
499
504
  const script = `<script>window.__ssr=${JSON.stringify({
505
+ links: args.links,
500
506
  layers: layers.map((it) => ({
501
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,
502
515
  index: void 0,
503
516
  path: void 0,
504
517
  element: void 0
505
518
  }))
506
519
  })}<\/script>`;
507
- const index = template.indexOf("</body>");
508
- if (index !== -1) {
509
- template = template.slice(0, index) + script + template.slice(index);
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>`);
510
526
  }
511
- if (context.helmet) {
512
- template = this.renderHelmetContext(template, context.helmet);
527
+ body.append(script);
528
+ if (context.head) {
529
+ this.renderHeadContext($, context.head);
513
530
  }
514
- template = template.replace(this.env.REACT_SSR_OUTLET, appHtml);
515
- return new Response(template, {
531
+ return new Response($.html(), {
516
532
  headers: { "Content-Type": "text/html" }
517
533
  });
518
534
  }
519
- renderHelmetContext(template, helmetContext) {
520
- if (helmetContext.title) {
521
- if (template.includes("<title>")) {
522
- template = template.replace(
523
- /<title>.*<\/title>/,
524
- `<title>${helmetContext.title}</title>`
525
- );
526
- } else {
527
- template = template.replace(
528
- "</head>",
529
- `<title>${helmetContext.title}</title></head>`
530
- );
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>`);
541
+ }
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
+ }
550
+ }
551
+ }
552
+ }
553
+ if (headContext.htmlAttributes) {
554
+ for (const [key, value] of Object.entries(headContext.htmlAttributes)) {
555
+ $("html").attr(key, value);
556
+ }
557
+ }
558
+ if (headContext.bodyAttributes) {
559
+ for (const [key, value] of Object.entries(headContext.bodyAttributes)) {
560
+ $("body").attr(key, value);
531
561
  }
532
562
  }
533
- return template;
534
563
  }
535
564
  }
536
565