@alepha/react 0.9.4 → 0.9.5

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
@@ -33,7 +33,6 @@ const node_path = __toESM(require("node:path"));
33
33
  const __alepha_server_static = __toESM(require("@alepha/server-static"));
34
34
  const react_dom_server = __toESM(require("react-dom/server"));
35
35
  const __alepha_datetime = __toESM(require("@alepha/datetime"));
36
- const react_dom_client = __toESM(require("react-dom/client"));
37
36
  const __alepha_router = __toESM(require("@alepha/router"));
38
37
 
39
38
  //#region src/descriptors/$page.ts
@@ -58,7 +57,10 @@ var PageDescriptor = class extends __alepha_core.Descriptor {
58
57
  * Only valid for server-side rendering, it will throw an error if called on the client-side.
59
58
  */
60
59
  async render(options) {
61
- throw new Error("render method is not implemented in this environment");
60
+ throw new __alepha_core.AlephaError("render() method is not implemented in this environment");
61
+ }
62
+ async fetch(options) {
63
+ throw new __alepha_core.AlephaError("fetch() method is not implemented in this environment");
62
64
  }
63
65
  match(url) {
64
66
  return false;
@@ -87,7 +89,6 @@ const ClientOnly = (props) => {
87
89
  if (props.disabled) return props.children;
88
90
  return mounted ? props.children : props.fallback;
89
91
  };
90
- var ClientOnly_default = ClientOnly;
91
92
 
92
93
  //#endregion
93
94
  //#region src/components/ErrorViewer.tsx
@@ -195,7 +196,6 @@ const ErrorViewer = ({ error, alepha }) => {
195
196
  })] })]
196
197
  });
197
198
  };
198
- var ErrorViewer_default = ErrorViewer;
199
199
  const ErrorViewerProduction = () => {
200
200
  const styles = {
201
201
  container: {
@@ -289,19 +289,55 @@ const useRouterEvents = (opts = {}, deps = []) => {
289
289
  const alepha = useAlepha();
290
290
  (0, react.useEffect)(() => {
291
291
  if (!alepha.isBrowser()) return;
292
+ const cb = (callback) => {
293
+ if (typeof callback === "function") return { callback };
294
+ return callback;
295
+ };
292
296
  const subs = [];
293
297
  const onBegin = opts.onBegin;
294
298
  const onEnd = opts.onEnd;
295
299
  const onError = opts.onError;
296
- if (onBegin) subs.push(alepha.on("react:transition:begin", { callback: onBegin }));
297
- if (onEnd) subs.push(alepha.on("react:transition:end", { callback: onEnd }));
298
- if (onError) subs.push(alepha.on("react:transition:error", { callback: onError }));
300
+ 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)));
299
305
  return () => {
300
306
  for (const sub of subs) sub();
301
307
  };
302
308
  }, deps);
303
309
  };
304
310
 
311
+ //#endregion
312
+ //#region src/hooks/useStore.ts
313
+ /**
314
+ * Hook to access and mutate the Alepha state.
315
+ */
316
+ const useStore = (key, defaultValue) => {
317
+ const alepha = useAlepha();
318
+ (0, react.useMemo)(() => {
319
+ if (defaultValue != null && alepha.state(key) == null) alepha.state(key, defaultValue);
320
+ }, [defaultValue]);
321
+ const [state, setState] = (0, react.useState)(alepha.state(key));
322
+ (0, react.useEffect)(() => {
323
+ if (!alepha.isBrowser()) return;
324
+ return alepha.on("state:mutate", (ev) => {
325
+ if (ev.key === key) setState(ev.value);
326
+ });
327
+ }, []);
328
+ return [state, (value) => {
329
+ alepha.state(key, value);
330
+ }];
331
+ };
332
+
333
+ //#endregion
334
+ //#region src/hooks/useRouterState.ts
335
+ const useRouterState = () => {
336
+ const [state] = useStore("react.router.state");
337
+ if (!state) throw new __alepha_core.AlephaError("Missing react router state");
338
+ return state;
339
+ };
340
+
305
341
  //#endregion
306
342
  //#region src/components/ErrorBoundary.tsx
307
343
  /**
@@ -331,7 +367,6 @@ var ErrorBoundary = class extends react.default.Component {
331
367
  return this.props.children;
332
368
  }
333
369
  };
334
- var ErrorBoundary_default = ErrorBoundary;
335
370
 
336
371
  //#endregion
337
372
  //#region src/components/NestedView.tsx
@@ -342,7 +377,7 @@ var ErrorBoundary_default = ErrorBoundary;
342
377
  *
343
378
  * @example
344
379
  * ```tsx
345
- * import { NestedView } from "@alepha/react";
380
+ * import { NestedView } from "alepha/react";
346
381
  *
347
382
  * class App {
348
383
  * parent = $page({
@@ -357,17 +392,69 @@ var ErrorBoundary_default = ErrorBoundary;
357
392
  * ```
358
393
  */
359
394
  const NestedView = (props) => {
360
- const layer = (0, react.useContext)(RouterLayerContext);
361
- const index = layer?.index ?? 0;
362
- const alepha = useAlepha();
363
- const state = alepha.state("react.router.state");
364
- if (!state) throw new Error("<NestedView/> must be used inside a RouterLayerContext.");
395
+ const index = (0, react.use)(RouterLayerContext)?.index ?? 0;
396
+ const state = useRouterState();
365
397
  const [view, setView] = (0, react.useState)(state.layers[index]?.element);
366
- useRouterEvents({ onEnd: ({ state: state$1 }) => {
367
- if (!state$1.layers[index]?.cache) setView(state$1.layers[index]?.element);
368
- } }, []);
369
- const element = view ?? props.children ?? null;
370
- return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBoundary_default, {
398
+ const [animation, setAnimation] = (0, react.useState)("");
399
+ const animationExitDuration = (0, react.useRef)(0);
400
+ const animationExitNow = (0, react.useRef)(0);
401
+ useRouterEvents({
402
+ onBegin: async ({ previous, state: state$1 }) => {
403
+ const layer = previous.layers[index];
404
+ if (`${state$1.url.pathname}/`.startsWith(`${layer?.path}/`)) return;
405
+ const animationExit = parseAnimation(layer.route?.animation, state$1, "exit");
406
+ if (animationExit) {
407
+ const duration = animationExit.duration || 200;
408
+ animationExitNow.current = Date.now();
409
+ animationExitDuration.current = duration;
410
+ setAnimation(animationExit.animation);
411
+ } else {
412
+ animationExitNow.current = 0;
413
+ animationExitDuration.current = 0;
414
+ setAnimation("");
415
+ }
416
+ },
417
+ onEnd: async ({ state: state$1 }) => {
418
+ const layer = state$1.layers[index];
419
+ if (animationExitNow.current) {
420
+ const duration = animationExitDuration.current;
421
+ const diff = Date.now() - animationExitNow.current;
422
+ if (diff < duration) await new Promise((resolve) => setTimeout(resolve, duration - diff));
423
+ }
424
+ if (!layer?.cache) {
425
+ setView(layer?.element);
426
+ const animationEnter = parseAnimation(layer?.route?.animation, state$1, "enter");
427
+ if (animationEnter) setAnimation(animationEnter.animation);
428
+ else setAnimation("");
429
+ }
430
+ }
431
+ }, []);
432
+ let element = view ?? props.children ?? null;
433
+ if (animation) element = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
434
+ style: {
435
+ display: "flex",
436
+ flex: 1,
437
+ height: "100%",
438
+ width: "100%",
439
+ position: "relative",
440
+ overflow: "hidden"
441
+ },
442
+ children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
443
+ style: {
444
+ height: "100%",
445
+ width: "100%",
446
+ display: "flex",
447
+ animation
448
+ },
449
+ children: element
450
+ })
451
+ });
452
+ if (props.errorBoundary === false) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: element });
453
+ if (props.errorBoundary) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBoundary, {
454
+ fallback: props.errorBoundary,
455
+ children: element
456
+ });
457
+ return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBoundary, {
371
458
  fallback: (error) => {
372
459
  const result = state.onError(error, state);
373
460
  if (result instanceof Redirection) return "Redirection inside ErrorBoundary is not allowed.";
@@ -376,7 +463,37 @@ const NestedView = (props) => {
376
463
  children: element
377
464
  });
378
465
  };
379
- var NestedView_default = NestedView;
466
+ var NestedView_default = (0, react.memo)(NestedView);
467
+ function parseAnimation(animationLike, state, type = "enter") {
468
+ if (!animationLike) return void 0;
469
+ const DEFAULT_DURATION = 300;
470
+ const animation = typeof animationLike === "function" ? animationLike(state) : animationLike;
471
+ if (typeof animation === "string") {
472
+ if (type === "exit") return;
473
+ return {
474
+ duration: DEFAULT_DURATION,
475
+ animation: `${DEFAULT_DURATION}ms ${animation}`
476
+ };
477
+ }
478
+ if (typeof animation === "object") {
479
+ const anim = animation[type];
480
+ const duration = typeof anim === "object" ? anim.duration ?? DEFAULT_DURATION : DEFAULT_DURATION;
481
+ const name = typeof anim === "object" ? anim.name : anim;
482
+ if (type === "exit") {
483
+ const timing$1 = typeof anim === "object" ? anim.timing ?? "" : "";
484
+ return {
485
+ duration,
486
+ animation: `${duration}ms ${timing$1} ${name}`
487
+ };
488
+ }
489
+ const timing = typeof anim === "object" ? anim.timing ?? "" : "";
490
+ return {
491
+ duration,
492
+ animation: `${duration}ms ${timing} ${name}`
493
+ };
494
+ }
495
+ return void 0;
496
+ }
380
497
 
381
498
  //#endregion
382
499
  //#region src/components/NotFound.tsx
@@ -442,6 +559,14 @@ var ReactPageProvider = class {
442
559
  if (this.env.REACT_STRICT_MODE) return (0, react.createElement)(react.StrictMode, {}, root);
443
560
  return root;
444
561
  }
562
+ 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 {
565
+ value[key] = this.alepha.parse(schema.properties[key], decodeURIComponent(value[key]));
566
+ } catch (e) {}
567
+ }
568
+ return value;
569
+ };
445
570
  /**
446
571
  * Create a new RouterState based on a given route and request.
447
572
  * This method resolves the layers for the route, applying any query and params schemas defined in the route.
@@ -461,6 +586,7 @@ var ReactPageProvider = class {
461
586
  const route$1 = it.route;
462
587
  const config = {};
463
588
  try {
589
+ this.convertStringObjectToObject(route$1.schema?.query, state.query);
464
590
  config.query = route$1.schema?.query ? this.alepha.parse(route$1.schema.query, state.query) : {};
465
591
  } catch (e) {
466
592
  it.error = e;
@@ -587,6 +713,7 @@ var ReactPageProvider = class {
587
713
  }
588
714
  }
589
715
  async createElement(page, props) {
716
+ if (page.lazy && page.component) this.log.warn(`Page ${page.name} has both lazy and component options, lazy will be used`);
590
717
  if (page.lazy) {
591
718
  const component = await page.lazy();
592
719
  return (0, react.createElement)(component.default, props);
@@ -595,7 +722,7 @@ var ReactPageProvider = class {
595
722
  return void 0;
596
723
  }
597
724
  renderError(error) {
598
- return (0, react.createElement)(ErrorViewer_default, {
725
+ return (0, react.createElement)(ErrorViewer, {
599
726
  error,
600
727
  alepha: this.alepha
601
728
  });
@@ -621,7 +748,7 @@ var ReactPageProvider = class {
621
748
  }
622
749
  renderView(index, path, view, page) {
623
750
  view ??= this.renderEmptyView();
624
- const element = page.client ? (0, react.createElement)(ClientOnly_default, typeof page.client === "object" ? page.client : {}, view) : view;
751
+ const element = page.client ? (0, react.createElement)(ClientOnly, typeof page.client === "object" ? page.client : {}, view) : view;
625
752
  return (0, react.createElement)(RouterLayerContext.Provider, { value: {
626
753
  index,
627
754
  path
@@ -715,6 +842,7 @@ var ReactServerProvider = class {
715
842
  log = (0, __alepha_logger.$logger)();
716
843
  alepha = (0, __alepha_core.$inject)(__alepha_core.Alepha);
717
844
  pageApi = (0, __alepha_core.$inject)(ReactPageProvider);
845
+ serverProvider = (0, __alepha_core.$inject)(__alepha_server.ServerProvider);
718
846
  serverStaticProvider = (0, __alepha_core.$inject)(__alepha_server_static.ServerStaticProvider);
719
847
  serverRouterProvider = (0, __alepha_core.$inject)(__alepha_server.ServerRouterProvider);
720
848
  serverTimingProvider = (0, __alepha_core.$inject)(__alepha_server.ServerTimingProvider);
@@ -726,7 +854,23 @@ var ReactServerProvider = class {
726
854
  const pages = this.alepha.descriptors($page);
727
855
  const ssrEnabled = pages.length > 0 && this.env.REACT_SSR_ENABLED !== false;
728
856
  this.alepha.state("react.server.ssr", ssrEnabled);
729
- for (const page of pages) page.render = this.createRenderFunction(page.name);
857
+ for (const page of pages) {
858
+ page.render = this.createRenderFunction(page.name);
859
+ page.fetch = async (options) => {
860
+ const response = await fetch(`${this.serverProvider.hostname}/${page.pathname(options)}`);
861
+ const html = await response.text();
862
+ if (options?.html) return {
863
+ html,
864
+ response
865
+ };
866
+ const match = html.match(this.ROOT_DIV_REGEX);
867
+ if (match) return {
868
+ html: match[3],
869
+ response
870
+ };
871
+ throw new __alepha_core.AlephaError("Invalid HTML response");
872
+ };
873
+ }
730
874
  if (this.alepha.isServerless() === "vite") {
731
875
  await this.configureVite(ssrEnabled);
732
876
  return;
@@ -806,13 +950,18 @@ var ReactServerProvider = class {
806
950
  params: options.params ?? {},
807
951
  query: options.query ?? {},
808
952
  onError: () => null,
809
- layers: []
953
+ layers: [],
954
+ meta: {}
810
955
  };
811
956
  const state = entry;
812
957
  this.log.trace("Rendering", { url });
813
958
  await this.alepha.emit("react:server:render:begin", { state });
814
959
  const { redirect } = await this.pageApi.createLayers(page, state);
815
- if (redirect) throw new __alepha_core.AlephaError("Redirection is not supported in this context");
960
+ if (redirect) return {
961
+ state,
962
+ html: "",
963
+ redirect
964
+ };
816
965
  if (!withIndex && !options.html) {
817
966
  this.alepha.state("react.router.state", state);
818
967
  return {
@@ -821,7 +970,11 @@ var ReactServerProvider = class {
821
970
  };
822
971
  }
823
972
  const html = this.renderToHtml(this.template ?? "", state, options.hydration);
824
- if (html instanceof Redirection) throw new Error("Redirection is not supported in this context");
973
+ if (html instanceof Redirection) return {
974
+ state,
975
+ html: "",
976
+ redirect
977
+ };
825
978
  const result = {
826
979
  state,
827
980
  html
@@ -958,17 +1111,21 @@ var ReactBrowserRouterProvider = class extends __alepha_router.RouterProvider {
958
1111
  });
959
1112
  }
960
1113
  });
961
- async transition(url, previous = []) {
1114
+ async transition(url, previous = [], meta = {}) {
962
1115
  const { pathname, search } = url;
963
1116
  const entry = {
964
1117
  url,
965
1118
  query: {},
966
1119
  params: {},
967
1120
  layers: [],
968
- onError: () => null
1121
+ onError: () => null,
1122
+ meta
969
1123
  };
970
1124
  const state = entry;
971
- await this.alepha.emit("react:transition:begin", { state });
1125
+ await this.alepha.emit("react:transition:begin", {
1126
+ previous: this.alepha.state("react.router.state"),
1127
+ state
1128
+ });
972
1129
  try {
973
1130
  const { route, params } = this.match(pathname);
974
1131
  const query = {};
@@ -1003,8 +1160,8 @@ var ReactBrowserRouterProvider = class extends __alepha_router.RouterProvider {
1003
1160
  const layer = previous[i];
1004
1161
  if (state.layers[i]?.name !== layer.name) this.pageApi.page(layer.name)?.onLeave?.();
1005
1162
  }
1006
- await this.alepha.emit("react:transition:end", { state });
1007
1163
  this.alepha.state("react.router.state", state);
1164
+ await this.alepha.emit("react:transition:end", { state });
1008
1165
  }
1009
1166
  root(state) {
1010
1167
  return this.pageApi.root(state);
@@ -1021,7 +1178,6 @@ var ReactBrowserProvider = class {
1021
1178
  alepha = (0, __alepha_core.$inject)(__alepha_core.Alepha);
1022
1179
  router = (0, __alepha_core.$inject)(ReactBrowserRouterProvider);
1023
1180
  dateTimeProvider = (0, __alepha_core.$inject)(__alepha_datetime.DateTimeProvider);
1024
- root;
1025
1181
  options = { scrollRestoration: "top" };
1026
1182
  getRootElement() {
1027
1183
  const root = this.document.getElementById(this.env.REACT_ROOT_ID);
@@ -1097,7 +1253,8 @@ var ReactBrowserProvider = class {
1097
1253
  });
1098
1254
  await this.render({
1099
1255
  url,
1100
- previous: options.force ? [] : this.state.layers
1256
+ previous: options.force ? [] : this.state.layers,
1257
+ meta: options.meta
1101
1258
  });
1102
1259
  if (this.state.url.pathname + this.state.url.search !== url) {
1103
1260
  this.pushState(this.state.url.pathname + this.state.url.search);
@@ -1114,7 +1271,7 @@ var ReactBrowserProvider = class {
1114
1271
  from: this.state?.url.pathname
1115
1272
  };
1116
1273
  this.log.debug("Transitioning...", { to: url });
1117
- const redirect = await this.router.transition(new URL(`http://localhost${url}`), previous);
1274
+ const redirect = await this.router.transition(new URL(`http://localhost${url}`), previous, options.meta);
1118
1275
  if (redirect) {
1119
1276
  this.log.info("Redirecting to", { redirect });
1120
1277
  return await this.render({ url: redirect });
@@ -1136,7 +1293,7 @@ var ReactBrowserProvider = class {
1136
1293
  onTransitionEnd = (0, __alepha_core.$hook)({
1137
1294
  on: "react:transition:end",
1138
1295
  handler: () => {
1139
- if (this.options.scrollRestoration === "top" && typeof window !== "undefined") {
1296
+ if (this.options.scrollRestoration === "top" && typeof window !== "undefined" && !this.alepha.isTest()) {
1140
1297
  this.log.trace("Restoring scroll position to top");
1141
1298
  window.scrollTo(0, 0);
1142
1299
  }
@@ -1152,14 +1309,12 @@ var ReactBrowserProvider = class {
1152
1309
  }
1153
1310
  await this.render({ previous });
1154
1311
  const element = this.router.root(this.state);
1155
- if (hydration?.layers) {
1156
- this.root = (0, react_dom_client.hydrateRoot)(this.getRootElement(), element);
1157
- this.log.info("Hydrated root element");
1158
- } else {
1159
- this.root ??= (0, react_dom_client.createRoot)(this.getRootElement());
1160
- this.root.render(element);
1161
- this.log.info("Created root element");
1162
- }
1312
+ await this.alepha.emit("react:browser:render", {
1313
+ element,
1314
+ root: this.getRootElement(),
1315
+ hydration,
1316
+ state: this.state
1317
+ });
1163
1318
  window.addEventListener("popstate", () => {
1164
1319
  if (this.base + this.state.url.pathname === this.location.pathname) return;
1165
1320
  this.log.debug("Popstate event triggered - rendering new state", { url: this.location.pathname + this.location.search });
@@ -1298,44 +1453,12 @@ const useRouter = () => {
1298
1453
  //#region src/components/Link.tsx
1299
1454
  const Link = (props) => {
1300
1455
  const router = useRouter();
1301
- const { to,...anchorProps } = props;
1302
1456
  return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
1303
- ...router.anchor(to),
1304
- ...anchorProps,
1457
+ ...props,
1458
+ ...router.anchor(props.href),
1305
1459
  children: props.children
1306
1460
  });
1307
1461
  };
1308
- var Link_default = Link;
1309
-
1310
- //#endregion
1311
- //#region src/hooks/useStore.ts
1312
- /**
1313
- * Hook to access and mutate the Alepha state.
1314
- */
1315
- const useStore = (key, defaultValue) => {
1316
- const alepha = useAlepha();
1317
- (0, react.useMemo)(() => {
1318
- if (defaultValue != null && alepha.state(key) == null) alepha.state(key, defaultValue);
1319
- }, [defaultValue]);
1320
- const [state, setState] = (0, react.useState)(alepha.state(key));
1321
- (0, react.useEffect)(() => {
1322
- if (!alepha.isBrowser()) return;
1323
- return alepha.on("state:mutate", (ev) => {
1324
- if (ev.key === key) setState(ev.value);
1325
- });
1326
- }, []);
1327
- return [state, (value) => {
1328
- alepha.state(key, value);
1329
- }];
1330
- };
1331
-
1332
- //#endregion
1333
- //#region src/hooks/useRouterState.ts
1334
- const useRouterState = () => {
1335
- const [state] = useStore("react.router.state");
1336
- if (!state) throw new __alepha_core.AlephaError("Missing react router state");
1337
- return state;
1338
- };
1339
1462
 
1340
1463
  //#endregion
1341
1464
  //#region src/hooks/useActive.ts
@@ -1480,10 +1603,10 @@ const AlephaReact = (0, __alepha_core.$module)({
1480
1603
  exports.$page = $page;
1481
1604
  exports.AlephaContext = AlephaContext;
1482
1605
  exports.AlephaReact = AlephaReact;
1483
- exports.ClientOnly = ClientOnly_default;
1484
- exports.ErrorBoundary = ErrorBoundary_default;
1485
- exports.ErrorViewer = ErrorViewer_default;
1486
- exports.Link = Link_default;
1606
+ exports.ClientOnly = ClientOnly;
1607
+ exports.ErrorBoundary = ErrorBoundary;
1608
+ exports.ErrorViewer = ErrorViewer;
1609
+ exports.Link = Link;
1487
1610
  exports.NestedView = NestedView_default;
1488
1611
  exports.NotFound = NotFoundPage;
1489
1612
  exports.PageDescriptor = PageDescriptor;