@alepha/react 0.9.2 → 0.9.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/dist/index.cjs CHANGED
@@ -41,6 +41,12 @@ const $page = (options) => {
41
41
  return (0, __alepha_core.createDescriptor)(PageDescriptor, options);
42
42
  };
43
43
  var PageDescriptor = class extends __alepha_core.Descriptor {
44
+ onInit() {
45
+ if (this.options.static) this.options.cache ??= {
46
+ provider: "memory",
47
+ ttl: [1, "week"]
48
+ };
49
+ }
44
50
  get name() {
45
51
  return this.options.name ?? this.config.propertyKey;
46
52
  }
@@ -49,7 +55,7 @@ var PageDescriptor = class extends __alepha_core.Descriptor {
49
55
  * Only valid for server-side rendering, it will throw an error if called on the client-side.
50
56
  */
51
57
  async render(options) {
52
- throw new __alepha_core.NotImplementedError("");
58
+ throw new Error("render method is not implemented in this environment");
53
59
  }
54
60
  };
55
61
  $page[__alepha_core.KIND] = PageDescriptor;
@@ -103,7 +109,7 @@ const ErrorViewer = ({ error, alepha }) => {
103
109
  heading: {
104
110
  fontSize: "20px",
105
111
  fontWeight: "bold",
106
- marginBottom: "4px"
112
+ marginBottom: "10px"
107
113
  },
108
114
  name: {
109
115
  fontSize: "16px",
@@ -320,13 +326,16 @@ const NestedView = (props) => {
320
326
  const layer = (0, react.useContext)(RouterLayerContext);
321
327
  const index = layer?.index ?? 0;
322
328
  const [view, setView] = (0, react.useState)(app?.state.layers[index]?.element);
323
- useRouterEvents({ onEnd: ({ state }) => {
329
+ useRouterEvents({ onEnd: ({ state, context }) => {
330
+ if (app) app.context = context;
324
331
  if (!state.layers[index]?.cache) setView(state.layers[index]?.element);
325
332
  } }, [app]);
326
333
  if (!app) throw new Error("NestedView must be used within a RouterContext.");
327
334
  const element = view ?? props.children ?? null;
328
335
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBoundary_default, {
329
- fallback: app.context.onError,
336
+ fallback: (error) => {
337
+ return app.context.onError?.(error, app.context);
338
+ },
330
339
  children: element
331
340
  });
332
341
  };
@@ -334,7 +343,7 @@ var NestedView_default = NestedView;
334
343
 
335
344
  //#endregion
336
345
  //#region src/components/NotFound.tsx
337
- function NotFoundPage() {
346
+ function NotFoundPage(props) {
338
347
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
339
348
  style: {
340
349
  height: "100vh",
@@ -344,7 +353,8 @@ function NotFoundPage() {
344
353
  alignItems: "center",
345
354
  textAlign: "center",
346
355
  fontFamily: "sans-serif",
347
- padding: "1rem"
356
+ padding: "1rem",
357
+ ...props.style
348
358
  },
349
359
  children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("h1", {
350
360
  style: {
@@ -357,8 +367,8 @@ function NotFoundPage() {
357
367
  }
358
368
 
359
369
  //#endregion
360
- //#region src/errors/RedirectionError.ts
361
- var RedirectionError = class extends Error {
370
+ //#region src/errors/Redirection.ts
371
+ var Redirection = class extends Error {
362
372
  page;
363
373
  constructor(page) {
364
374
  super("Redirection");
@@ -381,7 +391,7 @@ var PageDescriptorProvider = class {
381
391
  for (const page of this.pages) if (page.name === name) return page;
382
392
  throw new Error(`Page ${name} not found`);
383
393
  }
384
- url(name, options = {}) {
394
+ pathname(name, options = {}) {
385
395
  const page = this.page(name);
386
396
  if (!page) throw new Error(`Page ${name} not found`);
387
397
  let url = page.path ?? "";
@@ -391,7 +401,14 @@ var PageDescriptorProvider = class {
391
401
  parent = parent.parent;
392
402
  }
393
403
  url = this.compile(url, options.params ?? {});
394
- return new URL(url.replace(/\/\/+/g, "/") || "/", options.base ?? `http://localhost`);
404
+ if (options.query) {
405
+ const query = new URLSearchParams(options.query);
406
+ if (query.toString()) url += `?${query.toString()}`;
407
+ }
408
+ return url.replace(/\/\/+/g, "/") || "/";
409
+ }
410
+ url(name, options = {}) {
411
+ return new URL(this.pathname(name, options), options.base ?? `http://localhost`);
395
412
  }
396
413
  root(state, context) {
397
414
  const root = (0, react.createElement)(AlephaContext.Provider, { value: this.alepha }, (0, react.createElement)(RouterContext.Provider, { value: {
@@ -466,12 +483,10 @@ var PageDescriptorProvider = class {
466
483
  ...props
467
484
  };
468
485
  } catch (e) {
469
- if (e instanceof RedirectionError) return {
470
- layers: [],
471
- redirect: typeof e.page === "string" ? e.page : this.href(e.page),
486
+ if (e instanceof Redirection) return this.createRedirectionLayer(e.page, {
472
487
  pathname,
473
488
  search
474
- };
489
+ });
475
490
  this.log.error(e);
476
491
  it.error = e;
477
492
  break;
@@ -487,9 +502,21 @@ var PageDescriptorProvider = class {
487
502
  acc += it.route.path ? this.compile(it.route.path, params) : "";
488
503
  const path = acc.replace(/\/+/, "/");
489
504
  const localErrorHandler = this.getErrorHandler(it.route);
490
- if (localErrorHandler) request.onError = localErrorHandler;
491
- if (it.error) {
492
- let element$1 = await request.onError(it.error);
505
+ if (localErrorHandler) {
506
+ const onErrorParent = request.onError;
507
+ request.onError = (error, context$1) => {
508
+ const result = localErrorHandler(error, context$1);
509
+ if (result === void 0) return onErrorParent(error, context$1);
510
+ return result;
511
+ };
512
+ }
513
+ if (it.error) try {
514
+ let element$1 = await request.onError(it.error, request);
515
+ if (element$1 === void 0) throw it.error;
516
+ if (element$1 instanceof Redirection) return this.createRedirectionLayer(element$1.page, {
517
+ pathname,
518
+ search
519
+ });
493
520
  if (element$1 === null) element$1 = this.renderError(it.error);
494
521
  layers.push({
495
522
  props,
@@ -503,6 +530,12 @@ var PageDescriptorProvider = class {
503
530
  route: it.route
504
531
  });
505
532
  break;
533
+ } catch (e) {
534
+ if (e instanceof Redirection) return this.createRedirectionLayer(e.page, {
535
+ pathname,
536
+ search
537
+ });
538
+ throw e;
506
539
  }
507
540
  const element = await this.createElement(it.route, {
508
541
  ...props,
@@ -526,6 +559,14 @@ var PageDescriptorProvider = class {
526
559
  search
527
560
  };
528
561
  }
562
+ createRedirectionLayer(href, context) {
563
+ return {
564
+ layers: [],
565
+ redirect: typeof href === "string" ? href : this.href(href),
566
+ pathname: context.pathname,
567
+ search: context.search
568
+ };
569
+ }
529
570
  getErrorHandler(route) {
530
571
  if (route.errorHandler) return route.errorHandler;
531
572
  let parent = route.parent;
@@ -581,6 +622,7 @@ var PageDescriptorProvider = class {
581
622
  let hasNotFoundHandler = false;
582
623
  const pages = this.alepha.descriptors($page);
583
624
  const hasParent = (it) => {
625
+ if (it.options.parent) return true;
584
626
  for (const page of pages) {
585
627
  const children = page.options.children ? Array.isArray(page.options.children) ? page.options.children : page.options.children() : [];
586
628
  if (children.includes(it)) return true;
@@ -596,7 +638,7 @@ var PageDescriptorProvider = class {
596
638
  name: "notFound",
597
639
  cache: true,
598
640
  component: NotFoundPage,
599
- afterHandler: ({ reply }) => {
641
+ onServerResponse: ({ reply }) => {
600
642
  reply.status = 404;
601
643
  }
602
644
  });
@@ -604,6 +646,12 @@ var PageDescriptorProvider = class {
604
646
  });
605
647
  map(pages, target) {
606
648
  const children = target.options.children ? Array.isArray(target.options.children) ? target.options.children : target.options.children() : [];
649
+ const getChildrenFromParent = (it) => {
650
+ const children$1 = [];
651
+ for (const page of pages) if (page.options.parent === it) children$1.push(page);
652
+ return children$1;
653
+ };
654
+ children.push(...getChildrenFromParent(target));
607
655
  return {
608
656
  ...target.options,
609
657
  name: target.name,
@@ -725,6 +773,10 @@ var BrowserRouterProvider = class extends __alepha_router.RouterProvider {
725
773
  options.state.pathname = state.pathname;
726
774
  options.state.search = state.search;
727
775
  }
776
+ if (options.previous) for (let i = 0; i < options.previous.length; i++) {
777
+ const layer = options.previous[i];
778
+ if (state.layers[i]?.name !== layer.name) this.pageDescriptorProvider.page(layer.name)?.onLeave?.();
779
+ }
728
780
  await this.alepha.emit("react:transition:end", {
729
781
  state: options.state,
730
782
  context
@@ -794,15 +846,11 @@ var ReactBrowserProvider = class {
794
846
  }
795
847
  async go(url, options = {}) {
796
848
  const result = await this.render({ url });
797
- if (result.context.url.pathname !== url) {
798
- this.pushState(result.context.url.pathname);
849
+ if (result.context.url.pathname + result.context.url.search !== url) {
850
+ this.pushState(result.context.url.pathname + result.context.url.search);
799
851
  return;
800
852
  }
801
- if (options.replace) {
802
- this.pushState(url);
803
- return;
804
- }
805
- this.pushState(url);
853
+ this.pushState(url, options.replace);
806
854
  }
807
855
  async render(options = {}) {
808
856
  const previous = options.previous ?? this.state.layers;
@@ -831,7 +879,13 @@ var ReactBrowserProvider = class {
831
879
  handler: async () => {
832
880
  const hydration = this.getHydrationState();
833
881
  const previous = hydration?.layers ?? [];
834
- if (hydration?.links) for (const link of hydration.links.links) this.client.pushLink(link);
882
+ if (hydration) {
883
+ for (const [key, value] of Object.entries(hydration)) if (key !== "layers" && key !== "links") this.alepha.state(key, value);
884
+ }
885
+ if (hydration?.links) for (const link of hydration.links.links) this.client.pushLink({
886
+ ...link,
887
+ prefix: hydration.links.prefix
888
+ });
835
889
  const { context } = await this.render({ previous });
836
890
  await this.alepha.emit("react:browser:render", {
837
891
  state: this.state,
@@ -959,6 +1013,7 @@ var ReactServerProvider = class {
959
1013
  html: (0, react_dom_server.renderToString)(this.pageDescriptorProvider.root(state, context))
960
1014
  };
961
1015
  const html = this.renderToHtml(this.template ?? "", state, context, options.hydration);
1016
+ if (html instanceof Redirection) throw new Error("Redirection is not supported in this context");
962
1017
  const result = {
963
1018
  context,
964
1019
  state,
@@ -973,6 +1028,7 @@ var ReactServerProvider = class {
973
1028
  const { url, reply, query, params } = serverRequest;
974
1029
  const template = await templateLoader();
975
1030
  if (!template) throw new Error("Template not found");
1031
+ this.log.trace("Rendering page", { name: page.name });
976
1032
  const context = {
977
1033
  url,
978
1034
  params,
@@ -998,6 +1054,10 @@ var ReactServerProvider = class {
998
1054
  }
999
1055
  target = target.parent;
1000
1056
  }
1057
+ await this.alepha.emit("react:transition:begin", {
1058
+ request: serverRequest,
1059
+ context
1060
+ });
1001
1061
  await this.alepha.emit("react:server:render:begin", {
1002
1062
  request: serverRequest,
1003
1063
  context
@@ -1012,14 +1072,20 @@ var ReactServerProvider = class {
1012
1072
  reply.headers.expires = "0";
1013
1073
  if (page.cache && serverRequest.user) delete context.links;
1014
1074
  const html = this.renderToHtml(template, state, context);
1015
- await this.alepha.emit("react:server:render:end", {
1075
+ if (html instanceof Redirection) {
1076
+ reply.redirect(typeof html.page === "string" ? html.page : this.pageDescriptorProvider.href(html.page));
1077
+ return;
1078
+ }
1079
+ const event = {
1016
1080
  request: serverRequest,
1017
1081
  context,
1018
1082
  state,
1019
1083
  html
1020
- });
1021
- page.afterHandler?.(serverRequest);
1022
- return html;
1084
+ };
1085
+ await this.alepha.emit("react:server:render:end", event);
1086
+ page.onServerResponse?.(serverRequest);
1087
+ this.log.trace("Page rendered", { name: page.name });
1088
+ return event.html;
1023
1089
  };
1024
1090
  }
1025
1091
  renderToHtml(template, state, context, hydration = true) {
@@ -1030,20 +1096,23 @@ var ReactServerProvider = class {
1030
1096
  app = (0, react_dom_server.renderToString)(element);
1031
1097
  } catch (error) {
1032
1098
  this.log.error("Error during SSR", error);
1033
- app = (0, react_dom_server.renderToString)(context.onError(error));
1099
+ const element$1 = context.onError(error, context);
1100
+ if (element$1 instanceof Redirection) return element$1;
1101
+ app = (0, react_dom_server.renderToString)(element$1);
1034
1102
  }
1035
1103
  this.serverTimingProvider.endTiming("renderToString");
1036
1104
  const response = { html: template };
1037
1105
  if (hydration) {
1106
+ const { request, context: context$1,...rest } = this.alepha.context.als?.getStore() ?? {};
1038
1107
  const hydrationData = {
1039
- links: context.links,
1108
+ ...rest,
1040
1109
  layers: state.layers.map((it) => ({
1041
1110
  ...it,
1042
1111
  error: it.error ? {
1043
1112
  ...it.error,
1044
1113
  name: it.error.name,
1045
1114
  message: it.error.message,
1046
- stack: it.error.stack
1115
+ stack: !this.alepha.isProduction() ? it.error.stack : void 0
1047
1116
  } : void 0,
1048
1117
  index: void 0,
1049
1118
  path: void 0,
@@ -1063,24 +1132,34 @@ var ReactServerProvider = class {
1063
1132
  else {
1064
1133
  const bodyOpenTag = /<body([^>]*)>/i;
1065
1134
  if (bodyOpenTag.test(response.html)) response.html = response.html.replace(bodyOpenTag, (match) => {
1066
- return `${match}\n<div id="${this.env.REACT_ROOT_ID}">${app}</div>`;
1135
+ return `${match}<div id="${this.env.REACT_ROOT_ID}">${app}</div>`;
1067
1136
  });
1068
1137
  }
1069
1138
  const bodyCloseTagRegex = /<\/body>/i;
1070
- if (bodyCloseTagRegex.test(response.html)) response.html = response.html.replace(bodyCloseTagRegex, `${script}\n</body>`);
1139
+ if (bodyCloseTagRegex.test(response.html)) response.html = response.html.replace(bodyCloseTagRegex, `${script}</body>`);
1071
1140
  }
1072
1141
  };
1073
1142
 
1074
1143
  //#endregion
1075
1144
  //#region src/hooks/RouterHookApi.ts
1076
1145
  var RouterHookApi = class {
1077
- constructor(pages, context, state, layer, browser) {
1146
+ constructor(pages, context, state, layer, pageApi, browser) {
1078
1147
  this.pages = pages;
1079
1148
  this.context = context;
1080
1149
  this.state = state;
1081
1150
  this.layer = layer;
1151
+ this.pageApi = pageApi;
1082
1152
  this.browser = browser;
1083
1153
  }
1154
+ path(name, config = {}) {
1155
+ return this.pageApi.pathname(name, {
1156
+ params: {
1157
+ ...this.context.params,
1158
+ ...config.params
1159
+ },
1160
+ query: config.query
1161
+ });
1162
+ }
1084
1163
  getURL() {
1085
1164
  if (!this.browser) return this.context.url;
1086
1165
  return new URL(this.location.href);
@@ -1122,23 +1201,23 @@ var RouterHookApi = class {
1122
1201
  }
1123
1202
  async go(path, options) {
1124
1203
  for (const page of this.pages) if (page.name === path) {
1125
- path = page.path ?? "";
1126
- break;
1204
+ await this.browser?.go(this.path(path, options), options);
1205
+ return;
1127
1206
  }
1128
- await this.browser?.go(this.createHref(path, this.layer, options), options);
1207
+ await this.browser?.go(path, options);
1129
1208
  }
1130
1209
  anchor(path, options = {}) {
1210
+ let href = path;
1131
1211
  for (const page of this.pages) if (page.name === path) {
1132
- path = page.path ?? "";
1212
+ href = this.path(path, options);
1133
1213
  break;
1134
1214
  }
1135
- const href = this.createHref(path, this.layer, options);
1136
1215
  return {
1137
1216
  href,
1138
1217
  onClick: (ev) => {
1139
1218
  ev.stopPropagation();
1140
1219
  ev.preventDefault();
1141
- this.go(path, options).catch(console.error);
1220
+ this.go(href, options).catch(console.error);
1142
1221
  }
1143
1222
  };
1144
1223
  }
@@ -1167,27 +1246,18 @@ const useRouter = () => {
1167
1246
  const pages = (0, react.useMemo)(() => {
1168
1247
  return alepha.inject(PageDescriptorProvider).getPages();
1169
1248
  }, []);
1170
- return (0, react.useMemo)(() => new RouterHookApi(pages, ctx.context, ctx.state, layer, alepha.isBrowser() ? alepha.inject(ReactBrowserProvider) : void 0), [layer]);
1249
+ return (0, react.useMemo)(() => new RouterHookApi(pages, ctx.context, ctx.state, layer, alepha.inject(PageDescriptorProvider), alepha.isBrowser() ? alepha.inject(ReactBrowserProvider) : void 0), [layer]);
1171
1250
  };
1172
1251
 
1173
1252
  //#endregion
1174
1253
  //#region src/components/Link.tsx
1175
1254
  const Link = (props) => {
1176
- react.default.useContext(RouterContext);
1177
1255
  const router = useRouter();
1178
- const to = typeof props.to === "string" ? props.to : props.to.options.path;
1179
- if (!to) return null;
1180
- const can = typeof props.to === "string" ? void 0 : props.to.options.can;
1181
- if (can && !can()) return null;
1182
- const name = typeof props.to === "string" ? void 0 : props.to.options.name;
1183
- const anchorProps = {
1184
- ...props,
1185
- to: void 0
1186
- };
1256
+ const { to,...anchorProps } = props;
1187
1257
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
1188
1258
  ...router.anchor(to),
1189
1259
  ...anchorProps,
1190
- children: props.children ?? name
1260
+ children: props.children
1191
1261
  });
1192
1262
  };
1193
1263
  var Link_default = Link;
@@ -1199,22 +1269,21 @@ const useActive = (path) => {
1199
1269
  const ctx = (0, react.useContext)(RouterContext);
1200
1270
  const layer = (0, react.useContext)(RouterLayerContext);
1201
1271
  if (!ctx || !layer) throw new Error("useRouter must be used within a RouterProvider");
1202
- let name;
1203
- if (typeof path === "object" && path.options.name) name = path.options.name;
1204
1272
  const [current, setCurrent] = (0, react.useState)(ctx.state.pathname);
1205
- const href = (0, react.useMemo)(() => router.createHref(path, layer), [path, layer]);
1273
+ const href = (0, react.useMemo)(() => router.createHref(path ?? "", layer), [path, layer]);
1206
1274
  const [isPending, setPending] = (0, react.useState)(false);
1207
- const isActive = current === href;
1208
- useRouterEvents({ onEnd: ({ state }) => setCurrent(state.pathname) });
1275
+ const isActive = current === href || current === `${href}/` || `${current}/` === href;
1276
+ useRouterEvents({ onEnd: ({ state }) => {
1277
+ path && setCurrent(state.pathname);
1278
+ } }, [path]);
1209
1279
  return {
1210
- name,
1211
1280
  isPending,
1212
1281
  isActive,
1213
1282
  anchorProps: {
1214
1283
  href,
1215
1284
  onClick: (ev) => {
1216
- ev.stopPropagation();
1217
- ev.preventDefault();
1285
+ ev?.stopPropagation();
1286
+ ev?.preventDefault();
1218
1287
  if (isActive) return;
1219
1288
  if (isPending) return;
1220
1289
  setPending(true);
@@ -1233,9 +1302,36 @@ const useInject = (service) => {
1233
1302
  return (0, react.useMemo)(() => alepha.inject(service), []);
1234
1303
  };
1235
1304
 
1305
+ //#endregion
1306
+ //#region src/hooks/useStore.ts
1307
+ /**
1308
+ * Hook to access and mutate the Alepha state.
1309
+ */
1310
+ const useStore = (key, defaultValue) => {
1311
+ const alepha = useAlepha();
1312
+ (0, react.useMemo)(() => {
1313
+ if (defaultValue != null && alepha.state(key) == null) alepha.state(key, defaultValue);
1314
+ }, [defaultValue]);
1315
+ const [state, setState] = (0, react.useState)(alepha.state(key));
1316
+ (0, react.useEffect)(() => {
1317
+ if (!alepha.isBrowser()) return;
1318
+ return alepha.on("state:mutate", (ev) => {
1319
+ if (ev.key === key) setState(ev.value);
1320
+ });
1321
+ }, []);
1322
+ if (!alepha.isBrowser()) {
1323
+ const value = alepha.context.get(key);
1324
+ if (value !== null) return [value, (_) => {}];
1325
+ }
1326
+ return [state, (value) => {
1327
+ alepha.state(key, value);
1328
+ }];
1329
+ };
1330
+
1236
1331
  //#endregion
1237
1332
  //#region src/hooks/useClient.ts
1238
1333
  const useClient = (_scope) => {
1334
+ useStore("user");
1239
1335
  return useInject(__alepha_server_links.LinkProvider).client();
1240
1336
  };
1241
1337
 
@@ -1318,29 +1414,6 @@ const ssrSchemaLoading = (alepha, name) => {
1318
1414
  return { loading: true };
1319
1415
  };
1320
1416
 
1321
- //#endregion
1322
- //#region src/hooks/useStore.ts
1323
- /**
1324
- * Hook to access and mutate the Alepha state.
1325
- */
1326
- const useStore = (key) => {
1327
- const alepha = useAlepha();
1328
- const [state, setState] = (0, react.useState)(alepha.state(key));
1329
- (0, react.useEffect)(() => {
1330
- if (!alepha.isBrowser()) return;
1331
- return alepha.on("state:mutate", (ev) => {
1332
- if (ev.key === key) setState(ev.value);
1333
- });
1334
- }, []);
1335
- if (!alepha.isBrowser()) {
1336
- const value = alepha.context.get(key);
1337
- if (value !== null) return [value, (_) => {}];
1338
- }
1339
- return [state, (value) => {
1340
- alepha.state(key, value);
1341
- }];
1342
- };
1343
-
1344
1417
  //#endregion
1345
1418
  //#region src/index.ts
1346
1419
  /**
@@ -1377,7 +1450,7 @@ exports.PageDescriptor = PageDescriptor;
1377
1450
  exports.PageDescriptorProvider = PageDescriptorProvider;
1378
1451
  exports.ReactBrowserProvider = ReactBrowserProvider;
1379
1452
  exports.ReactServerProvider = ReactServerProvider;
1380
- exports.RedirectionError = RedirectionError;
1453
+ exports.Redirection = Redirection;
1381
1454
  exports.RouterContext = RouterContext;
1382
1455
  exports.RouterHookApi = RouterHookApi;
1383
1456
  exports.RouterLayerContext = RouterLayerContext;