@alepha/react 0.9.5 → 0.10.0

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,4 +1,4 @@
1
- import { $env, $hook, $inject, $module, Alepha, AlephaError, Descriptor, KIND, TypeGuard, createDescriptor, t } from "@alepha/core";
1
+ import { $env, $hook, $inject, $module, Alepha, AlephaError, Descriptor, KIND, createDescriptor, t } from "@alepha/core";
2
2
  import { AlephaServer, HttpClient, ServerProvider, ServerRouterProvider, ServerTimingProvider } from "@alepha/server";
3
3
  import { AlephaServerCache } from "@alepha/server-cache";
4
4
  import { AlephaServerLinks, LinkProvider, ServerLinksProvider } from "@alepha/server-links";
@@ -15,6 +15,91 @@ import { RouterProvider } from "@alepha/router";
15
15
  //#region src/descriptors/$page.ts
16
16
  /**
17
17
  * Main descriptor for defining a React route in the application.
18
+ *
19
+ * The $page descriptor is the core building block for creating type-safe, SSR-enabled React routes.
20
+ * It provides a declarative way to define pages with powerful features:
21
+ *
22
+ * **Routing & Navigation**
23
+ * - URL pattern matching with parameters (e.g., `/users/:id`)
24
+ * - Nested routing with parent-child relationships
25
+ * - Type-safe URL parameter and query string validation
26
+ *
27
+ * **Data Loading**
28
+ * - Server-side data fetching with the `resolve` function
29
+ * - Automatic serialization and hydration for SSR
30
+ * - Access to request context, URL params, and parent data
31
+ *
32
+ * **Component Loading**
33
+ * - Direct component rendering or lazy loading for code splitting
34
+ * - Client-only rendering when browser APIs are needed
35
+ * - Automatic fallback handling during hydration
36
+ *
37
+ * **Performance Optimization**
38
+ * - Static generation for pre-rendered pages at build time
39
+ * - Server-side caching with configurable TTL and providers
40
+ * - Code splitting through lazy component loading
41
+ *
42
+ * **Error Handling**
43
+ * - Custom error handlers with support for redirects
44
+ * - Hierarchical error handling (child → parent)
45
+ * - HTTP status code handling (404, 401, etc.)
46
+ *
47
+ * **Page Animations**
48
+ * - CSS-based enter/exit animations
49
+ * - Dynamic animations based on page state
50
+ * - Custom timing and easing functions
51
+ *
52
+ * **Lifecycle Management**
53
+ * - Server response hooks for headers and status codes
54
+ * - Page leave handlers for cleanup (browser only)
55
+ * - Permission-based access control
56
+ *
57
+ * @example Simple page with data fetching
58
+ * ```typescript
59
+ * const userProfile = $page({
60
+ * path: "/users/:id",
61
+ * schema: {
62
+ * params: t.object({ id: t.int() }),
63
+ * query: t.object({ tab: t.optional(t.string()) })
64
+ * },
65
+ * resolve: async ({ params }) => {
66
+ * const user = await userApi.getUser(params.id);
67
+ * return { user };
68
+ * },
69
+ * lazy: () => import("./UserProfile.tsx")
70
+ * });
71
+ * ```
72
+ *
73
+ * @example Nested routing with error handling
74
+ * ```typescript
75
+ * const projectSection = $page({
76
+ * path: "/projects/:id",
77
+ * children: () => [projectBoard, projectSettings],
78
+ * resolve: async ({ params }) => {
79
+ * const project = await projectApi.get(params.id);
80
+ * return { project };
81
+ * },
82
+ * errorHandler: (error) => {
83
+ * if (HttpError.is(error, 404)) {
84
+ * return <ProjectNotFound />;
85
+ * }
86
+ * }
87
+ * });
88
+ * ```
89
+ *
90
+ * @example Static generation with caching
91
+ * ```typescript
92
+ * const blogPost = $page({
93
+ * path: "/blog/:slug",
94
+ * static: {
95
+ * entries: posts.map(p => ({ params: { slug: p.slug } }))
96
+ * },
97
+ * resolve: async ({ params }) => {
98
+ * const post = await loadPost(params.slug);
99
+ * return { post };
100
+ * }
101
+ * });
102
+ * ```
18
103
  */
19
104
  const $page = (options) => {
20
105
  return createDescriptor(PageDescriptor, options);
@@ -248,7 +333,7 @@ const AlephaContext = createContext(void 0);
248
333
  *
249
334
  * - alepha.state() for state management
250
335
  * - alepha.inject() for dependency injection
251
- * - alepha.emit() for event handling
336
+ * - alepha.events.emit() for event handling
252
337
  * etc...
253
338
  */
254
339
  const useAlepha = () => {
@@ -275,10 +360,10 @@ const useRouterEvents = (opts = {}, deps = []) => {
275
360
  const onEnd = opts.onEnd;
276
361
  const onError = opts.onError;
277
362
  const onSuccess = opts.onSuccess;
278
- if (onBegin) subs.push(alepha.on("react:transition:begin", cb(onBegin)));
279
- if (onEnd) subs.push(alepha.on("react:transition:end", cb(onEnd)));
280
- if (onError) subs.push(alepha.on("react:transition:error", cb(onError)));
281
- if (onSuccess) subs.push(alepha.on("react:transition:success", cb(onSuccess)));
363
+ if (onBegin) subs.push(alepha.events.on("react:transition:begin", cb(onBegin)));
364
+ if (onEnd) subs.push(alepha.events.on("react:transition:end", cb(onEnd)));
365
+ if (onError) subs.push(alepha.events.on("react:transition:error", cb(onError)));
366
+ if (onSuccess) subs.push(alepha.events.on("react:transition:success", cb(onSuccess)));
282
367
  return () => {
283
368
  for (const sub of subs) sub();
284
369
  };
@@ -293,17 +378,17 @@ const useRouterEvents = (opts = {}, deps = []) => {
293
378
  const useStore = (key, defaultValue) => {
294
379
  const alepha = useAlepha();
295
380
  useMemo(() => {
296
- if (defaultValue != null && alepha.state(key) == null) alepha.state(key, defaultValue);
381
+ if (defaultValue != null && alepha.state.get(key) == null) alepha.state.set(key, defaultValue);
297
382
  }, [defaultValue]);
298
- const [state, setState] = useState(alepha.state(key));
383
+ const [state, setState] = useState(alepha.state.get(key));
299
384
  useEffect(() => {
300
385
  if (!alepha.isBrowser()) return;
301
- return alepha.on("state:mutate", (ev) => {
386
+ return alepha.events.on("state:mutate", (ev) => {
302
387
  if (ev.key === key) setState(ev.value);
303
388
  });
304
389
  }, []);
305
390
  return [state, (value) => {
306
- alepha.state(key, value);
391
+ alepha.state.set(key, value);
307
392
  }];
308
393
  };
309
394
 
@@ -537,8 +622,8 @@ var ReactPageProvider = class {
537
622
  return root;
538
623
  }
539
624
  convertStringObjectToObject = (schema, value) => {
540
- if (TypeGuard.IsObject(schema) && typeof value === "object") {
541
- for (const key in schema.properties) if (TypeGuard.IsObject(schema.properties[key]) && typeof value[key] === "string") try {
625
+ if (t.schema.isObject(schema) && typeof value === "object") {
626
+ for (const key in schema.properties) if (t.schema.isObject(schema.properties[key]) && typeof value[key] === "string") try {
542
627
  value[key] = this.alepha.parse(schema.properties[key], decodeURIComponent(value[key]));
543
628
  } catch (e) {}
544
629
  }
@@ -825,12 +910,13 @@ var ReactServerProvider = class {
825
910
  serverTimingProvider = $inject(ServerTimingProvider);
826
911
  env = $env(envSchema$1);
827
912
  ROOT_DIV_REGEX = new RegExp(`<div([^>]*)\\s+id=["']${this.env.REACT_ROOT_ID}["']([^>]*)>(.*?)<\\/div>`, "is");
913
+ preprocessedTemplate = null;
828
914
  onConfigure = $hook({
829
915
  on: "configure",
830
916
  handler: async () => {
831
917
  const pages = this.alepha.descriptors($page);
832
918
  const ssrEnabled = pages.length > 0 && this.env.REACT_SSR_ENABLED !== false;
833
- this.alepha.state("react.server.ssr", ssrEnabled);
919
+ this.alepha.state.set("react.server.ssr", ssrEnabled);
834
920
  for (const page of pages) {
835
921
  page.render = this.createRenderFunction(page.name);
836
922
  page.fetch = async (options) => {
@@ -886,6 +972,8 @@ var ReactServerProvider = class {
886
972
  return this.alepha.env.REACT_SERVER_TEMPLATE ?? "<!DOCTYPE html><html lang='en'><head></head><body></body></html>";
887
973
  }
888
974
  async registerPages(templateLoader) {
975
+ const template = await templateLoader();
976
+ if (template) this.preprocessedTemplate = this.preprocessTemplate(template);
889
977
  for (const page of this.pageApi.getPages()) {
890
978
  if (page.children?.length) continue;
891
979
  this.log.debug(`+ ${page.match} -> ${page.name}`);
@@ -932,7 +1020,7 @@ var ReactServerProvider = class {
932
1020
  };
933
1021
  const state = entry;
934
1022
  this.log.trace("Rendering", { url });
935
- await this.alepha.emit("react:server:render:begin", { state });
1023
+ await this.alepha.events.emit("react:server:render:begin", { state });
936
1024
  const { redirect } = await this.pageApi.createLayers(page, state);
937
1025
  if (redirect) return {
938
1026
  state,
@@ -940,13 +1028,14 @@ var ReactServerProvider = class {
940
1028
  redirect
941
1029
  };
942
1030
  if (!withIndex && !options.html) {
943
- this.alepha.state("react.router.state", state);
1031
+ this.alepha.state.set("react.router.state", state);
944
1032
  return {
945
1033
  state,
946
1034
  html: renderToString(this.pageApi.root(state))
947
1035
  };
948
1036
  }
949
- const html = this.renderToHtml(this.template ?? "", state, options.hydration);
1037
+ const template = this.template ?? "";
1038
+ const html = this.renderToHtml(template, state, options.hydration);
950
1039
  if (html instanceof Redirection) return {
951
1040
  state,
952
1041
  html: "",
@@ -956,7 +1045,7 @@ var ReactServerProvider = class {
956
1045
  state,
957
1046
  html
958
1047
  };
959
- await this.alepha.emit("react:server:render:end", result);
1048
+ await this.alepha.events.emit("react:server:render:end", result);
960
1049
  return result;
961
1050
  };
962
1051
  }
@@ -974,7 +1063,7 @@ var ReactServerProvider = class {
974
1063
  layers: []
975
1064
  };
976
1065
  const state = entry;
977
- if (this.alepha.has(ServerLinksProvider)) this.alepha.state("api", await this.alepha.inject(ServerLinksProvider).getUserApiLinks({
1066
+ if (this.alepha.has(ServerLinksProvider)) this.alepha.state.set("api", await this.alepha.inject(ServerLinksProvider).getUserApiLinks({
978
1067
  user: serverRequest.user,
979
1068
  authorization: serverRequest.headers.authorization
980
1069
  }));
@@ -987,7 +1076,7 @@ var ReactServerProvider = class {
987
1076
  }
988
1077
  target = target.parent;
989
1078
  }
990
- await this.alepha.emit("react:server:render:begin", {
1079
+ await this.alepha.events.emit("react:server:render:begin", {
991
1080
  request: serverRequest,
992
1081
  state
993
1082
  });
@@ -1009,7 +1098,7 @@ var ReactServerProvider = class {
1009
1098
  state,
1010
1099
  html
1011
1100
  };
1012
- await this.alepha.emit("react:server:render:end", event);
1101
+ await this.alepha.events.emit("react:server:render:end", event);
1013
1102
  route.onServerResponse?.(serverRequest);
1014
1103
  this.log.trace("Page rendered", { name: route.name });
1015
1104
  return event.html;
@@ -1017,7 +1106,7 @@ var ReactServerProvider = class {
1017
1106
  }
1018
1107
  renderToHtml(template, state, hydration = true) {
1019
1108
  const element = this.pageApi.root(state);
1020
- this.alepha.state("react.router.state", state);
1109
+ this.alepha.state.set("react.router.state", state);
1021
1110
  this.serverTimingProvider.beginTiming("renderToString");
1022
1111
  let app = "";
1023
1112
  try {
@@ -1055,18 +1144,48 @@ var ReactServerProvider = class {
1055
1144
  }
1056
1145
  return response.html;
1057
1146
  }
1058
- fillTemplate(response, app, script) {
1059
- if (this.ROOT_DIV_REGEX.test(response.html)) response.html = response.html.replace(this.ROOT_DIV_REGEX, (_match, beforeId, afterId) => {
1060
- return `<div${beforeId} id="${this.env.REACT_ROOT_ID}"${afterId}>${app}</div>`;
1061
- });
1062
- else {
1063
- const bodyOpenTag = /<body([^>]*)>/i;
1064
- if (bodyOpenTag.test(response.html)) response.html = response.html.replace(bodyOpenTag, (match) => {
1065
- return `${match}<div id="${this.env.REACT_ROOT_ID}">${app}</div>`;
1066
- });
1147
+ preprocessTemplate(template) {
1148
+ const bodyCloseMatch = template.match(/<\/body>/i);
1149
+ const bodyCloseIndex = bodyCloseMatch?.index ?? template.length;
1150
+ const beforeScript = template.substring(0, bodyCloseIndex);
1151
+ const afterScript = template.substring(bodyCloseIndex);
1152
+ const rootDivMatch = beforeScript.match(this.ROOT_DIV_REGEX);
1153
+ if (rootDivMatch) {
1154
+ const beforeDiv = beforeScript.substring(0, rootDivMatch.index);
1155
+ const afterDivStart = rootDivMatch.index + rootDivMatch[0].length;
1156
+ const afterDiv = beforeScript.substring(afterDivStart);
1157
+ const beforeApp = `${beforeDiv}<div${rootDivMatch[1]} id="${this.env.REACT_ROOT_ID}"${rootDivMatch[2]}>`;
1158
+ const afterApp = `</div>${afterDiv}`;
1159
+ return {
1160
+ beforeApp,
1161
+ afterApp,
1162
+ beforeScript: "",
1163
+ afterScript
1164
+ };
1067
1165
  }
1068
- const bodyCloseTagRegex = /<\/body>/i;
1069
- if (bodyCloseTagRegex.test(response.html)) response.html = response.html.replace(bodyCloseTagRegex, `${script}</body>`);
1166
+ const bodyMatch = beforeScript.match(/<body([^>]*)>/i);
1167
+ if (bodyMatch) {
1168
+ const beforeBody = beforeScript.substring(0, bodyMatch.index + bodyMatch[0].length);
1169
+ const afterBody = beforeScript.substring(bodyMatch.index + bodyMatch[0].length);
1170
+ const beforeApp = `${beforeBody}<div id="${this.env.REACT_ROOT_ID}">`;
1171
+ const afterApp = `</div>${afterBody}`;
1172
+ return {
1173
+ beforeApp,
1174
+ afterApp,
1175
+ beforeScript: "",
1176
+ afterScript
1177
+ };
1178
+ }
1179
+ return {
1180
+ beforeApp: `<div id="${this.env.REACT_ROOT_ID}">`,
1181
+ afterApp: `</div>`,
1182
+ beforeScript,
1183
+ afterScript
1184
+ };
1185
+ }
1186
+ fillTemplate(response, app, script) {
1187
+ if (!this.preprocessedTemplate) this.preprocessedTemplate = this.preprocessTemplate(response.html);
1188
+ response.html = this.preprocessedTemplate.beforeApp + app + this.preprocessedTemplate.afterApp + script + this.preprocessedTemplate.afterScript;
1070
1189
  }
1071
1190
  };
1072
1191
 
@@ -1099,8 +1218,8 @@ var ReactBrowserRouterProvider = class extends RouterProvider {
1099
1218
  meta
1100
1219
  };
1101
1220
  const state = entry;
1102
- await this.alepha.emit("react:transition:begin", {
1103
- previous: this.alepha.state("react.router.state"),
1221
+ await this.alepha.events.emit("react:transition:begin", {
1222
+ previous: this.alepha.state.get("react.router.state"),
1104
1223
  state
1105
1224
  });
1106
1225
  try {
@@ -1119,7 +1238,7 @@ var ReactBrowserRouterProvider = class extends RouterProvider {
1119
1238
  index: 0,
1120
1239
  path: "/"
1121
1240
  });
1122
- await this.alepha.emit("react:transition:success", { state });
1241
+ await this.alepha.events.emit("react:transition:success", { state });
1123
1242
  } catch (e) {
1124
1243
  this.log.error("Transition has failed", e);
1125
1244
  state.layers = [{
@@ -1128,7 +1247,7 @@ var ReactBrowserRouterProvider = class extends RouterProvider {
1128
1247
  index: 0,
1129
1248
  path: "/"
1130
1249
  }];
1131
- await this.alepha.emit("react:transition:error", {
1250
+ await this.alepha.events.emit("react:transition:error", {
1132
1251
  error: e,
1133
1252
  state
1134
1253
  });
@@ -1137,8 +1256,8 @@ var ReactBrowserRouterProvider = class extends RouterProvider {
1137
1256
  const layer = previous[i];
1138
1257
  if (state.layers[i]?.name !== layer.name) this.pageApi.page(layer.name)?.onLeave?.();
1139
1258
  }
1140
- this.alepha.state("react.router.state", state);
1141
- await this.alepha.emit("react:transition:end", { state });
1259
+ this.alepha.state.set("react.router.state", state);
1260
+ await this.alepha.events.emit("react:transition:end", { state });
1142
1261
  }
1143
1262
  root(state) {
1144
1263
  return this.pageApi.root(state);
@@ -1166,7 +1285,7 @@ var ReactBrowserProvider = class {
1166
1285
  }
1167
1286
  transitioning;
1168
1287
  get state() {
1169
- return this.alepha.state("react.router.state");
1288
+ return this.alepha.state.get("react.router.state");
1170
1289
  }
1171
1290
  /**
1172
1291
  * Accessor for Document DOM API.
@@ -1282,11 +1401,11 @@ var ReactBrowserProvider = class {
1282
1401
  const hydration = this.getHydrationState();
1283
1402
  const previous = hydration?.layers ?? [];
1284
1403
  if (hydration) {
1285
- for (const [key, value] of Object.entries(hydration)) if (key !== "layers") this.alepha.state(key, value);
1404
+ for (const [key, value] of Object.entries(hydration)) if (key !== "layers") this.alepha.state.set(key, value);
1286
1405
  }
1287
1406
  await this.render({ previous });
1288
1407
  const element = this.router.root(this.state);
1289
- await this.alepha.emit("react:browser:render", {
1408
+ await this.alepha.events.emit("react:browser:render", {
1290
1409
  element,
1291
1410
  root: this.getRootElement(),
1292
1411
  hydration,
@@ -1307,7 +1426,7 @@ var ReactRouter = class {
1307
1426
  alepha = $inject(Alepha);
1308
1427
  pageApi = $inject(ReactPageProvider);
1309
1428
  get state() {
1310
- return this.alepha.state("react.router.state");
1429
+ return this.alepha.state.get("react.router.state");
1311
1430
  }
1312
1431
  get pages() {
1313
1432
  return this.pageApi.getPages();
@@ -1493,7 +1612,7 @@ const useQueryParams = (schema, options = {}) => {
1493
1612
  const key = options.key ?? "q";
1494
1613
  const router = useRouter();
1495
1614
  const querystring = router.query[key];
1496
- const [queryParams, setQueryParams] = useState(decode(alepha, schema, router.query[key]));
1615
+ const [queryParams = {}, setQueryParams] = useState(decode(alepha, schema, router.query[key]));
1497
1616
  useEffect(() => {
1498
1617
  setQueryParams(decode(alepha, schema, querystring));
1499
1618
  }, [querystring]);
@@ -1513,8 +1632,8 @@ const encode = (alepha, schema, data) => {
1513
1632
  const decode = (alepha, schema, data) => {
1514
1633
  try {
1515
1634
  return alepha.parse(schema, JSON.parse(atob(decodeURIComponent(data))));
1516
- } catch (_error) {
1517
- return {};
1635
+ } catch {
1636
+ return;
1518
1637
  }
1519
1638
  };
1520
1639