@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.cjs CHANGED
@@ -38,6 +38,91 @@ const __alepha_router = __toESM(require("@alepha/router"));
38
38
  //#region src/descriptors/$page.ts
39
39
  /**
40
40
  * Main descriptor for defining a React route in the application.
41
+ *
42
+ * The $page descriptor is the core building block for creating type-safe, SSR-enabled React routes.
43
+ * It provides a declarative way to define pages with powerful features:
44
+ *
45
+ * **Routing & Navigation**
46
+ * - URL pattern matching with parameters (e.g., `/users/:id`)
47
+ * - Nested routing with parent-child relationships
48
+ * - Type-safe URL parameter and query string validation
49
+ *
50
+ * **Data Loading**
51
+ * - Server-side data fetching with the `resolve` function
52
+ * - Automatic serialization and hydration for SSR
53
+ * - Access to request context, URL params, and parent data
54
+ *
55
+ * **Component Loading**
56
+ * - Direct component rendering or lazy loading for code splitting
57
+ * - Client-only rendering when browser APIs are needed
58
+ * - Automatic fallback handling during hydration
59
+ *
60
+ * **Performance Optimization**
61
+ * - Static generation for pre-rendered pages at build time
62
+ * - Server-side caching with configurable TTL and providers
63
+ * - Code splitting through lazy component loading
64
+ *
65
+ * **Error Handling**
66
+ * - Custom error handlers with support for redirects
67
+ * - Hierarchical error handling (child → parent)
68
+ * - HTTP status code handling (404, 401, etc.)
69
+ *
70
+ * **Page Animations**
71
+ * - CSS-based enter/exit animations
72
+ * - Dynamic animations based on page state
73
+ * - Custom timing and easing functions
74
+ *
75
+ * **Lifecycle Management**
76
+ * - Server response hooks for headers and status codes
77
+ * - Page leave handlers for cleanup (browser only)
78
+ * - Permission-based access control
79
+ *
80
+ * @example Simple page with data fetching
81
+ * ```typescript
82
+ * const userProfile = $page({
83
+ * path: "/users/:id",
84
+ * schema: {
85
+ * params: t.object({ id: t.int() }),
86
+ * query: t.object({ tab: t.optional(t.string()) })
87
+ * },
88
+ * resolve: async ({ params }) => {
89
+ * const user = await userApi.getUser(params.id);
90
+ * return { user };
91
+ * },
92
+ * lazy: () => import("./UserProfile.tsx")
93
+ * });
94
+ * ```
95
+ *
96
+ * @example Nested routing with error handling
97
+ * ```typescript
98
+ * const projectSection = $page({
99
+ * path: "/projects/:id",
100
+ * children: () => [projectBoard, projectSettings],
101
+ * resolve: async ({ params }) => {
102
+ * const project = await projectApi.get(params.id);
103
+ * return { project };
104
+ * },
105
+ * errorHandler: (error) => {
106
+ * if (HttpError.is(error, 404)) {
107
+ * return <ProjectNotFound />;
108
+ * }
109
+ * }
110
+ * });
111
+ * ```
112
+ *
113
+ * @example Static generation with caching
114
+ * ```typescript
115
+ * const blogPost = $page({
116
+ * path: "/blog/:slug",
117
+ * static: {
118
+ * entries: posts.map(p => ({ params: { slug: p.slug } }))
119
+ * },
120
+ * resolve: async ({ params }) => {
121
+ * const post = await loadPost(params.slug);
122
+ * return { post };
123
+ * }
124
+ * });
125
+ * ```
41
126
  */
42
127
  const $page = (options) => {
43
128
  return (0, __alepha_core.createDescriptor)(PageDescriptor, options);
@@ -271,7 +356,7 @@ const AlephaContext = (0, react.createContext)(void 0);
271
356
  *
272
357
  * - alepha.state() for state management
273
358
  * - alepha.inject() for dependency injection
274
- * - alepha.emit() for event handling
359
+ * - alepha.events.emit() for event handling
275
360
  * etc...
276
361
  */
277
362
  const useAlepha = () => {
@@ -298,10 +383,10 @@ const useRouterEvents = (opts = {}, deps = []) => {
298
383
  const onEnd = opts.onEnd;
299
384
  const onError = opts.onError;
300
385
  const onSuccess = opts.onSuccess;
301
- if (onBegin) subs.push(alepha.on("react:transition:begin", cb(onBegin)));
302
- if (onEnd) subs.push(alepha.on("react:transition:end", cb(onEnd)));
303
- if (onError) subs.push(alepha.on("react:transition:error", cb(onError)));
304
- if (onSuccess) subs.push(alepha.on("react:transition:success", cb(onSuccess)));
386
+ if (onBegin) subs.push(alepha.events.on("react:transition:begin", cb(onBegin)));
387
+ if (onEnd) subs.push(alepha.events.on("react:transition:end", cb(onEnd)));
388
+ if (onError) subs.push(alepha.events.on("react:transition:error", cb(onError)));
389
+ if (onSuccess) subs.push(alepha.events.on("react:transition:success", cb(onSuccess)));
305
390
  return () => {
306
391
  for (const sub of subs) sub();
307
392
  };
@@ -316,17 +401,17 @@ const useRouterEvents = (opts = {}, deps = []) => {
316
401
  const useStore = (key, defaultValue) => {
317
402
  const alepha = useAlepha();
318
403
  (0, react.useMemo)(() => {
319
- if (defaultValue != null && alepha.state(key) == null) alepha.state(key, defaultValue);
404
+ if (defaultValue != null && alepha.state.get(key) == null) alepha.state.set(key, defaultValue);
320
405
  }, [defaultValue]);
321
- const [state, setState] = (0, react.useState)(alepha.state(key));
406
+ const [state, setState] = (0, react.useState)(alepha.state.get(key));
322
407
  (0, react.useEffect)(() => {
323
408
  if (!alepha.isBrowser()) return;
324
- return alepha.on("state:mutate", (ev) => {
409
+ return alepha.events.on("state:mutate", (ev) => {
325
410
  if (ev.key === key) setState(ev.value);
326
411
  });
327
412
  }, []);
328
413
  return [state, (value) => {
329
- alepha.state(key, value);
414
+ alepha.state.set(key, value);
330
415
  }];
331
416
  };
332
417
 
@@ -560,8 +645,8 @@ var ReactPageProvider = class {
560
645
  return root;
561
646
  }
562
647
  convertStringObjectToObject = (schema, value) => {
563
- if (__alepha_core.TypeGuard.IsObject(schema) && typeof value === "object") {
564
- for (const key in schema.properties) if (__alepha_core.TypeGuard.IsObject(schema.properties[key]) && typeof value[key] === "string") try {
648
+ if (__alepha_core.t.schema.isObject(schema) && typeof value === "object") {
649
+ for (const key in schema.properties) if (__alepha_core.t.schema.isObject(schema.properties[key]) && typeof value[key] === "string") try {
565
650
  value[key] = this.alepha.parse(schema.properties[key], decodeURIComponent(value[key]));
566
651
  } catch (e) {}
567
652
  }
@@ -848,12 +933,13 @@ var ReactServerProvider = class {
848
933
  serverTimingProvider = (0, __alepha_core.$inject)(__alepha_server.ServerTimingProvider);
849
934
  env = (0, __alepha_core.$env)(envSchema$1);
850
935
  ROOT_DIV_REGEX = new RegExp(`<div([^>]*)\\s+id=["']${this.env.REACT_ROOT_ID}["']([^>]*)>(.*?)<\\/div>`, "is");
936
+ preprocessedTemplate = null;
851
937
  onConfigure = (0, __alepha_core.$hook)({
852
938
  on: "configure",
853
939
  handler: async () => {
854
940
  const pages = this.alepha.descriptors($page);
855
941
  const ssrEnabled = pages.length > 0 && this.env.REACT_SSR_ENABLED !== false;
856
- this.alepha.state("react.server.ssr", ssrEnabled);
942
+ this.alepha.state.set("react.server.ssr", ssrEnabled);
857
943
  for (const page of pages) {
858
944
  page.render = this.createRenderFunction(page.name);
859
945
  page.fetch = async (options) => {
@@ -909,6 +995,8 @@ var ReactServerProvider = class {
909
995
  return this.alepha.env.REACT_SERVER_TEMPLATE ?? "<!DOCTYPE html><html lang='en'><head></head><body></body></html>";
910
996
  }
911
997
  async registerPages(templateLoader) {
998
+ const template = await templateLoader();
999
+ if (template) this.preprocessedTemplate = this.preprocessTemplate(template);
912
1000
  for (const page of this.pageApi.getPages()) {
913
1001
  if (page.children?.length) continue;
914
1002
  this.log.debug(`+ ${page.match} -> ${page.name}`);
@@ -955,7 +1043,7 @@ var ReactServerProvider = class {
955
1043
  };
956
1044
  const state = entry;
957
1045
  this.log.trace("Rendering", { url });
958
- await this.alepha.emit("react:server:render:begin", { state });
1046
+ await this.alepha.events.emit("react:server:render:begin", { state });
959
1047
  const { redirect } = await this.pageApi.createLayers(page, state);
960
1048
  if (redirect) return {
961
1049
  state,
@@ -963,13 +1051,14 @@ var ReactServerProvider = class {
963
1051
  redirect
964
1052
  };
965
1053
  if (!withIndex && !options.html) {
966
- this.alepha.state("react.router.state", state);
1054
+ this.alepha.state.set("react.router.state", state);
967
1055
  return {
968
1056
  state,
969
1057
  html: (0, react_dom_server.renderToString)(this.pageApi.root(state))
970
1058
  };
971
1059
  }
972
- const html = this.renderToHtml(this.template ?? "", state, options.hydration);
1060
+ const template = this.template ?? "";
1061
+ const html = this.renderToHtml(template, state, options.hydration);
973
1062
  if (html instanceof Redirection) return {
974
1063
  state,
975
1064
  html: "",
@@ -979,7 +1068,7 @@ var ReactServerProvider = class {
979
1068
  state,
980
1069
  html
981
1070
  };
982
- await this.alepha.emit("react:server:render:end", result);
1071
+ await this.alepha.events.emit("react:server:render:end", result);
983
1072
  return result;
984
1073
  };
985
1074
  }
@@ -997,7 +1086,7 @@ var ReactServerProvider = class {
997
1086
  layers: []
998
1087
  };
999
1088
  const state = entry;
1000
- if (this.alepha.has(__alepha_server_links.ServerLinksProvider)) this.alepha.state("api", await this.alepha.inject(__alepha_server_links.ServerLinksProvider).getUserApiLinks({
1089
+ if (this.alepha.has(__alepha_server_links.ServerLinksProvider)) this.alepha.state.set("api", await this.alepha.inject(__alepha_server_links.ServerLinksProvider).getUserApiLinks({
1001
1090
  user: serverRequest.user,
1002
1091
  authorization: serverRequest.headers.authorization
1003
1092
  }));
@@ -1010,7 +1099,7 @@ var ReactServerProvider = class {
1010
1099
  }
1011
1100
  target = target.parent;
1012
1101
  }
1013
- await this.alepha.emit("react:server:render:begin", {
1102
+ await this.alepha.events.emit("react:server:render:begin", {
1014
1103
  request: serverRequest,
1015
1104
  state
1016
1105
  });
@@ -1032,7 +1121,7 @@ var ReactServerProvider = class {
1032
1121
  state,
1033
1122
  html
1034
1123
  };
1035
- await this.alepha.emit("react:server:render:end", event);
1124
+ await this.alepha.events.emit("react:server:render:end", event);
1036
1125
  route.onServerResponse?.(serverRequest);
1037
1126
  this.log.trace("Page rendered", { name: route.name });
1038
1127
  return event.html;
@@ -1040,7 +1129,7 @@ var ReactServerProvider = class {
1040
1129
  }
1041
1130
  renderToHtml(template, state, hydration = true) {
1042
1131
  const element = this.pageApi.root(state);
1043
- this.alepha.state("react.router.state", state);
1132
+ this.alepha.state.set("react.router.state", state);
1044
1133
  this.serverTimingProvider.beginTiming("renderToString");
1045
1134
  let app = "";
1046
1135
  try {
@@ -1078,18 +1167,48 @@ var ReactServerProvider = class {
1078
1167
  }
1079
1168
  return response.html;
1080
1169
  }
1081
- fillTemplate(response, app, script) {
1082
- if (this.ROOT_DIV_REGEX.test(response.html)) response.html = response.html.replace(this.ROOT_DIV_REGEX, (_match, beforeId, afterId) => {
1083
- return `<div${beforeId} id="${this.env.REACT_ROOT_ID}"${afterId}>${app}</div>`;
1084
- });
1085
- else {
1086
- const bodyOpenTag = /<body([^>]*)>/i;
1087
- if (bodyOpenTag.test(response.html)) response.html = response.html.replace(bodyOpenTag, (match) => {
1088
- return `${match}<div id="${this.env.REACT_ROOT_ID}">${app}</div>`;
1089
- });
1170
+ preprocessTemplate(template) {
1171
+ const bodyCloseMatch = template.match(/<\/body>/i);
1172
+ const bodyCloseIndex = bodyCloseMatch?.index ?? template.length;
1173
+ const beforeScript = template.substring(0, bodyCloseIndex);
1174
+ const afterScript = template.substring(bodyCloseIndex);
1175
+ const rootDivMatch = beforeScript.match(this.ROOT_DIV_REGEX);
1176
+ if (rootDivMatch) {
1177
+ const beforeDiv = beforeScript.substring(0, rootDivMatch.index);
1178
+ const afterDivStart = rootDivMatch.index + rootDivMatch[0].length;
1179
+ const afterDiv = beforeScript.substring(afterDivStart);
1180
+ const beforeApp = `${beforeDiv}<div${rootDivMatch[1]} id="${this.env.REACT_ROOT_ID}"${rootDivMatch[2]}>`;
1181
+ const afterApp = `</div>${afterDiv}`;
1182
+ return {
1183
+ beforeApp,
1184
+ afterApp,
1185
+ beforeScript: "",
1186
+ afterScript
1187
+ };
1090
1188
  }
1091
- const bodyCloseTagRegex = /<\/body>/i;
1092
- if (bodyCloseTagRegex.test(response.html)) response.html = response.html.replace(bodyCloseTagRegex, `${script}</body>`);
1189
+ const bodyMatch = beforeScript.match(/<body([^>]*)>/i);
1190
+ if (bodyMatch) {
1191
+ const beforeBody = beforeScript.substring(0, bodyMatch.index + bodyMatch[0].length);
1192
+ const afterBody = beforeScript.substring(bodyMatch.index + bodyMatch[0].length);
1193
+ const beforeApp = `${beforeBody}<div id="${this.env.REACT_ROOT_ID}">`;
1194
+ const afterApp = `</div>${afterBody}`;
1195
+ return {
1196
+ beforeApp,
1197
+ afterApp,
1198
+ beforeScript: "",
1199
+ afterScript
1200
+ };
1201
+ }
1202
+ return {
1203
+ beforeApp: `<div id="${this.env.REACT_ROOT_ID}">`,
1204
+ afterApp: `</div>`,
1205
+ beforeScript,
1206
+ afterScript
1207
+ };
1208
+ }
1209
+ fillTemplate(response, app, script) {
1210
+ if (!this.preprocessedTemplate) this.preprocessedTemplate = this.preprocessTemplate(response.html);
1211
+ response.html = this.preprocessedTemplate.beforeApp + app + this.preprocessedTemplate.afterApp + script + this.preprocessedTemplate.afterScript;
1093
1212
  }
1094
1213
  };
1095
1214
 
@@ -1122,8 +1241,8 @@ var ReactBrowserRouterProvider = class extends __alepha_router.RouterProvider {
1122
1241
  meta
1123
1242
  };
1124
1243
  const state = entry;
1125
- await this.alepha.emit("react:transition:begin", {
1126
- previous: this.alepha.state("react.router.state"),
1244
+ await this.alepha.events.emit("react:transition:begin", {
1245
+ previous: this.alepha.state.get("react.router.state"),
1127
1246
  state
1128
1247
  });
1129
1248
  try {
@@ -1142,7 +1261,7 @@ var ReactBrowserRouterProvider = class extends __alepha_router.RouterProvider {
1142
1261
  index: 0,
1143
1262
  path: "/"
1144
1263
  });
1145
- await this.alepha.emit("react:transition:success", { state });
1264
+ await this.alepha.events.emit("react:transition:success", { state });
1146
1265
  } catch (e) {
1147
1266
  this.log.error("Transition has failed", e);
1148
1267
  state.layers = [{
@@ -1151,7 +1270,7 @@ var ReactBrowserRouterProvider = class extends __alepha_router.RouterProvider {
1151
1270
  index: 0,
1152
1271
  path: "/"
1153
1272
  }];
1154
- await this.alepha.emit("react:transition:error", {
1273
+ await this.alepha.events.emit("react:transition:error", {
1155
1274
  error: e,
1156
1275
  state
1157
1276
  });
@@ -1160,8 +1279,8 @@ var ReactBrowserRouterProvider = class extends __alepha_router.RouterProvider {
1160
1279
  const layer = previous[i];
1161
1280
  if (state.layers[i]?.name !== layer.name) this.pageApi.page(layer.name)?.onLeave?.();
1162
1281
  }
1163
- this.alepha.state("react.router.state", state);
1164
- await this.alepha.emit("react:transition:end", { state });
1282
+ this.alepha.state.set("react.router.state", state);
1283
+ await this.alepha.events.emit("react:transition:end", { state });
1165
1284
  }
1166
1285
  root(state) {
1167
1286
  return this.pageApi.root(state);
@@ -1189,7 +1308,7 @@ var ReactBrowserProvider = class {
1189
1308
  }
1190
1309
  transitioning;
1191
1310
  get state() {
1192
- return this.alepha.state("react.router.state");
1311
+ return this.alepha.state.get("react.router.state");
1193
1312
  }
1194
1313
  /**
1195
1314
  * Accessor for Document DOM API.
@@ -1305,11 +1424,11 @@ var ReactBrowserProvider = class {
1305
1424
  const hydration = this.getHydrationState();
1306
1425
  const previous = hydration?.layers ?? [];
1307
1426
  if (hydration) {
1308
- for (const [key, value] of Object.entries(hydration)) if (key !== "layers") this.alepha.state(key, value);
1427
+ for (const [key, value] of Object.entries(hydration)) if (key !== "layers") this.alepha.state.set(key, value);
1309
1428
  }
1310
1429
  await this.render({ previous });
1311
1430
  const element = this.router.root(this.state);
1312
- await this.alepha.emit("react:browser:render", {
1431
+ await this.alepha.events.emit("react:browser:render", {
1313
1432
  element,
1314
1433
  root: this.getRootElement(),
1315
1434
  hydration,
@@ -1330,7 +1449,7 @@ var ReactRouter = class {
1330
1449
  alepha = (0, __alepha_core.$inject)(__alepha_core.Alepha);
1331
1450
  pageApi = (0, __alepha_core.$inject)(ReactPageProvider);
1332
1451
  get state() {
1333
- return this.alepha.state("react.router.state");
1452
+ return this.alepha.state.get("react.router.state");
1334
1453
  }
1335
1454
  get pages() {
1336
1455
  return this.pageApi.getPages();
@@ -1516,7 +1635,7 @@ const useQueryParams = (schema, options = {}) => {
1516
1635
  const key = options.key ?? "q";
1517
1636
  const router = useRouter();
1518
1637
  const querystring = router.query[key];
1519
- const [queryParams, setQueryParams] = (0, react.useState)(decode(alepha, schema, router.query[key]));
1638
+ const [queryParams = {}, setQueryParams] = (0, react.useState)(decode(alepha, schema, router.query[key]));
1520
1639
  (0, react.useEffect)(() => {
1521
1640
  setQueryParams(decode(alepha, schema, querystring));
1522
1641
  }, [querystring]);
@@ -1536,8 +1655,8 @@ const encode = (alepha, schema, data) => {
1536
1655
  const decode = (alepha, schema, data) => {
1537
1656
  try {
1538
1657
  return alepha.parse(schema, JSON.parse(atob(decodeURIComponent(data))));
1539
- } catch (_error) {
1540
- return {};
1658
+ } catch {
1659
+ return;
1541
1660
  }
1542
1661
  };
1543
1662