@alepha/react 0.7.0 → 0.7.1

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.
Files changed (35) hide show
  1. package/README.md +1 -1
  2. package/dist/index.browser.cjs +21 -21
  3. package/dist/index.browser.js +2 -3
  4. package/dist/index.cjs +151 -83
  5. package/dist/index.d.ts +360 -205
  6. package/dist/index.js +129 -62
  7. package/dist/{useActive-DjpZBEuB.cjs → useRouterState-AdK-XeM2.cjs} +270 -81
  8. package/dist/{useActive-BX41CqY8.js → useRouterState-qoMq7Y9J.js} +272 -84
  9. package/package.json +11 -10
  10. package/src/components/ClientOnly.tsx +35 -0
  11. package/src/components/ErrorBoundary.tsx +1 -1
  12. package/src/components/ErrorViewer.tsx +161 -0
  13. package/src/components/Link.tsx +9 -3
  14. package/src/components/NestedView.tsx +18 -3
  15. package/src/descriptors/$page.ts +139 -30
  16. package/src/errors/RedirectionError.ts +4 -1
  17. package/src/hooks/RouterHookApi.ts +42 -5
  18. package/src/hooks/useAlepha.ts +12 -0
  19. package/src/hooks/useClient.ts +8 -6
  20. package/src/hooks/useInject.ts +2 -2
  21. package/src/hooks/useQueryParams.ts +1 -1
  22. package/src/hooks/useRouter.ts +6 -0
  23. package/src/index.browser.ts +1 -1
  24. package/src/index.shared.ts +11 -5
  25. package/src/index.ts +3 -4
  26. package/src/providers/BrowserRouterProvider.ts +1 -1
  27. package/src/providers/PageDescriptorProvider.ts +72 -21
  28. package/src/providers/ReactBrowserProvider.ts +5 -8
  29. package/src/providers/ReactServerProvider.ts +197 -80
  30. package/dist/index.browser.cjs.map +0 -1
  31. package/dist/index.browser.js.map +0 -1
  32. package/dist/index.cjs.map +0 -1
  33. package/dist/index.js.map +0 -1
  34. package/dist/useActive-BX41CqY8.js.map +0 -1
  35. package/dist/useActive-DjpZBEuB.cjs.map +0 -1
@@ -28,27 +28,161 @@ const $page = (options) => {
28
28
  [core.OPTIONS]: options,
29
29
  render: () => {
30
30
  throw new core.NotImplementedError(KEY);
31
- },
32
- go: () => {
33
- throw new core.NotImplementedError(KEY);
34
- },
35
- createAnchorProps: () => {
36
- throw new core.NotImplementedError(KEY);
37
- },
38
- can: () => {
39
- if (options.can) {
40
- return options.can();
41
- }
42
- return true;
43
31
  }
44
32
  };
45
33
  };
46
34
  $page[core.KIND] = KEY;
47
35
 
36
+ const ClientOnly = (props) => {
37
+ const [mounted, setMounted] = React.useState(false);
38
+ React.useEffect(() => setMounted(true), []);
39
+ if (props.disabled) {
40
+ return props.children;
41
+ }
42
+ return mounted ? props.children : props.fallback;
43
+ };
44
+
48
45
  const RouterContext = React.createContext(
49
46
  void 0
50
47
  );
51
48
 
49
+ const useAlepha = () => {
50
+ const routerContext = React.useContext(RouterContext);
51
+ if (!routerContext) {
52
+ throw new Error("useAlepha must be used within a RouterProvider");
53
+ }
54
+ return routerContext.alepha;
55
+ };
56
+
57
+ const ErrorViewer = ({ error }) => {
58
+ const [expanded, setExpanded] = React.useState(false);
59
+ const isProduction = useAlepha().isProduction();
60
+ if (isProduction) {
61
+ return /* @__PURE__ */ jsxRuntime.jsx(ErrorViewerProduction, {});
62
+ }
63
+ const stackLines = error.stack?.split("\n") ?? [];
64
+ const previewLines = stackLines.slice(0, 5);
65
+ const hiddenLineCount = stackLines.length - previewLines.length;
66
+ const copyToClipboard = (text) => {
67
+ navigator.clipboard.writeText(text).catch((err) => {
68
+ console.error("Clipboard error:", err);
69
+ });
70
+ };
71
+ const styles = {
72
+ container: {
73
+ padding: "24px",
74
+ backgroundColor: "#FEF2F2",
75
+ color: "#7F1D1D",
76
+ border: "1px solid #FECACA",
77
+ borderRadius: "16px",
78
+ boxShadow: "0 8px 24px rgba(0,0,0,0.05)",
79
+ fontFamily: "monospace",
80
+ maxWidth: "768px",
81
+ margin: "40px auto"
82
+ },
83
+ heading: {
84
+ fontSize: "20px",
85
+ fontWeight: "bold",
86
+ marginBottom: "4px"
87
+ },
88
+ name: {
89
+ fontSize: "16px",
90
+ fontWeight: 600
91
+ },
92
+ message: {
93
+ fontSize: "14px",
94
+ marginBottom: "16px"
95
+ },
96
+ sectionHeader: {
97
+ display: "flex",
98
+ justifyContent: "space-between",
99
+ alignItems: "center",
100
+ fontSize: "12px",
101
+ marginBottom: "4px",
102
+ color: "#991B1B"
103
+ },
104
+ copyButton: {
105
+ fontSize: "12px",
106
+ color: "#DC2626",
107
+ background: "none",
108
+ border: "none",
109
+ cursor: "pointer",
110
+ textDecoration: "underline"
111
+ },
112
+ stackContainer: {
113
+ backgroundColor: "#FEE2E2",
114
+ padding: "12px",
115
+ borderRadius: "8px",
116
+ fontSize: "13px",
117
+ lineHeight: "1.4",
118
+ overflowX: "auto",
119
+ whiteSpace: "pre-wrap"
120
+ },
121
+ expandLine: {
122
+ color: "#F87171",
123
+ cursor: "pointer",
124
+ marginTop: "8px"
125
+ }
126
+ };
127
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.container, children: [
128
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
129
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles.heading, children: "\u{1F525} Error" }),
130
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles.name, children: error.name }),
131
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles.message, children: error.message })
132
+ ] }),
133
+ stackLines.length > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { children: [
134
+ /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.sectionHeader, children: [
135
+ /* @__PURE__ */ jsxRuntime.jsx("span", { children: "Stack trace" }),
136
+ /* @__PURE__ */ jsxRuntime.jsx(
137
+ "button",
138
+ {
139
+ onClick: () => copyToClipboard(error.stack),
140
+ style: styles.copyButton,
141
+ children: "Copy all"
142
+ }
143
+ )
144
+ ] }),
145
+ /* @__PURE__ */ jsxRuntime.jsxs("pre", { style: styles.stackContainer, children: [
146
+ (expanded ? stackLines : previewLines).map((line, i) => /* @__PURE__ */ jsxRuntime.jsx("div", { children: line }, i)),
147
+ !expanded && hiddenLineCount > 0 && /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.expandLine, onClick: () => setExpanded(true), children: [
148
+ "+ ",
149
+ hiddenLineCount,
150
+ " more lines..."
151
+ ] })
152
+ ] })
153
+ ] })
154
+ ] });
155
+ };
156
+ const ErrorViewerProduction = () => {
157
+ const styles = {
158
+ container: {
159
+ padding: "24px",
160
+ backgroundColor: "#FEF2F2",
161
+ color: "#7F1D1D",
162
+ border: "1px solid #FECACA",
163
+ borderRadius: "16px",
164
+ boxShadow: "0 8px 24px rgba(0,0,0,0.05)",
165
+ fontFamily: "monospace",
166
+ maxWidth: "768px",
167
+ margin: "40px auto",
168
+ textAlign: "center"
169
+ },
170
+ heading: {
171
+ fontSize: "20px",
172
+ fontWeight: "bold",
173
+ marginBottom: "8px"
174
+ },
175
+ message: {
176
+ fontSize: "14px",
177
+ opacity: 0.85
178
+ }
179
+ };
180
+ return /* @__PURE__ */ jsxRuntime.jsxs("div", { style: styles.container, children: [
181
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles.heading, children: "\u{1F6A8} An error occurred" }),
182
+ /* @__PURE__ */ jsxRuntime.jsx("div", { style: styles.message, children: "Something went wrong. Please try again later." })
183
+ ] });
184
+ };
185
+
52
186
  const RouterLayerContext = React.createContext(void 0);
53
187
 
54
188
  const useRouterEvents = (opts = {}, deps = []) => {
@@ -146,14 +280,19 @@ const NestedView = (props) => {
146
280
  };
147
281
 
148
282
  class RedirectionError extends Error {
283
+ page;
149
284
  constructor(page) {
150
285
  super("Redirection");
151
286
  this.page = page;
152
287
  }
153
288
  }
154
289
 
290
+ const envSchema$1 = core.t.object({
291
+ REACT_STRICT_MODE: core.t.boolean({ default: true })
292
+ });
155
293
  class PageDescriptorProvider {
156
294
  log = core.$logger();
295
+ env = core.$inject(envSchema$1);
157
296
  alepha = core.$inject(core.Alepha);
158
297
  pages = [];
159
298
  getPages() {
@@ -167,8 +306,25 @@ class PageDescriptorProvider {
167
306
  }
168
307
  throw new Error(`Page ${name} not found`);
169
308
  }
309
+ url(name, options = {}) {
310
+ const page = this.page(name);
311
+ if (!page) {
312
+ throw new Error(`Page ${name} not found`);
313
+ }
314
+ let url = page.path ?? "";
315
+ let parent = page.parent;
316
+ while (parent) {
317
+ url = `${parent.path ?? ""}/${url}`;
318
+ parent = parent.parent;
319
+ }
320
+ url = this.compile(url, options.params ?? {});
321
+ return new URL(
322
+ url.replace(/\/\/+/g, "/") || "/",
323
+ options.base ?? `http://localhost`
324
+ );
325
+ }
170
326
  root(state, context) {
171
- return React.createElement(
327
+ const root = React.createElement(
172
328
  RouterContext.Provider,
173
329
  {
174
330
  value: {
@@ -179,13 +335,17 @@ class PageDescriptorProvider {
179
335
  },
180
336
  React.createElement(NestedView, {}, state.layers[0]?.element)
181
337
  );
338
+ if (this.env.REACT_STRICT_MODE) {
339
+ return React.createElement(React.StrictMode, {}, root);
340
+ }
341
+ return root;
182
342
  }
183
343
  async createLayers(route, request) {
184
344
  const { pathname, search } = request.url;
185
345
  const layers = [];
186
346
  let context = {};
187
347
  const stack = [{ route }];
188
- let onError = this.renderError;
348
+ request.onError = (error) => this.renderError(error);
189
349
  let parent = route.parent;
190
350
  while (parent) {
191
351
  stack.unshift({ route: parent });
@@ -285,23 +445,26 @@ class PageDescriptorProvider {
285
445
  const path = acc.replace(/\/+/, "/");
286
446
  const localErrorHandler = this.getErrorHandler(it.route);
287
447
  if (localErrorHandler) {
288
- onError = localErrorHandler;
448
+ request.onError = localErrorHandler;
289
449
  }
290
450
  if (it.error) {
291
- const element = await onError(it.error);
451
+ let element2 = await request.onError(it.error);
452
+ if (element2 === null) {
453
+ element2 = this.renderError(it.error);
454
+ }
292
455
  layers.push({
293
456
  props,
294
457
  error: it.error,
295
458
  name: it.route.name,
296
459
  part: it.route.path,
297
460
  config: it.config,
298
- element: this.renderView(i + 1, path, element),
461
+ element: this.renderView(i + 1, path, element2, it.route),
299
462
  index: i + 1,
300
463
  path
301
464
  });
302
465
  break;
303
466
  }
304
- const layer = await this.createElement(it.route, {
467
+ const element = await this.createElement(it.route, {
305
468
  ...props,
306
469
  ...context
307
470
  });
@@ -310,7 +473,7 @@ class PageDescriptorProvider {
310
473
  props,
311
474
  part: it.route.path,
312
475
  config: it.config,
313
- element: this.renderView(i + 1, path, layer),
476
+ element: this.renderView(i + 1, path, element, it.route),
314
477
  index: i + 1,
315
478
  path
316
479
  });
@@ -366,8 +529,8 @@ class PageDescriptorProvider {
366
529
  ctx.head.meta = [...ctx.head.meta ?? [], ...head.meta ?? []];
367
530
  }
368
531
  }
369
- renderError(e) {
370
- return React.createElement("pre", { style: { overflow: "auto" } }, `${e.stack}`);
532
+ renderError(error) {
533
+ return React.createElement(ErrorViewer, { error });
371
534
  }
372
535
  renderEmptyView() {
373
536
  return React.createElement(NestedView, {});
@@ -392,7 +555,13 @@ class PageDescriptorProvider {
392
555
  }
393
556
  return path;
394
557
  }
395
- renderView(index, path, view = this.renderEmptyView()) {
558
+ renderView(index, path, view, page) {
559
+ view ??= this.renderEmptyView();
560
+ const element = page.client ? React.createElement(
561
+ ClientOnly,
562
+ typeof page.client === "object" ? page.client : {},
563
+ view
564
+ ) : view;
396
565
  return React.createElement(
397
566
  RouterLayerContext.Provider,
398
567
  {
@@ -401,7 +570,7 @@ class PageDescriptorProvider {
401
570
  path
402
571
  }
403
572
  },
404
- view
573
+ element
405
574
  );
406
575
  }
407
576
  configure = core.$hook({
@@ -410,6 +579,8 @@ class PageDescriptorProvider {
410
579
  const pages = this.alepha.getDescriptorValues($page);
411
580
  for (const { value, key } of pages) {
412
581
  value[core.OPTIONS].name ??= key;
582
+ }
583
+ for (const { value } of pages) {
413
584
  if (value[core.OPTIONS].parent) {
414
585
  continue;
415
586
  }
@@ -419,11 +590,6 @@ class PageDescriptorProvider {
419
590
  });
420
591
  map(pages, target) {
421
592
  const children = target[core.OPTIONS].children ?? [];
422
- for (const it of pages) {
423
- if (it.value[core.OPTIONS].parent === target) {
424
- children.push(it.value);
425
- }
426
- }
427
593
  return {
428
594
  ...target[core.OPTIONS],
429
595
  parent: void 0,
@@ -735,7 +901,7 @@ class ReactBrowserProvider {
735
901
  const hydration = this.getHydrationState();
736
902
  const previous = hydration?.layers ?? [];
737
903
  if (hydration?.links) {
738
- for (const link of hydration.links) {
904
+ for (const link of hydration.links.links) {
739
905
  this.client.pushLink(link);
740
906
  }
741
907
  }
@@ -771,7 +937,8 @@ class ReactBrowserProvider {
771
937
  }
772
938
 
773
939
  class RouterHookApi {
774
- constructor(state, layer, browser) {
940
+ constructor(pages, state, layer, browser) {
941
+ this.pages = pages;
775
942
  this.state = state;
776
943
  this.layer = layer;
777
944
  this.browser = browser;
@@ -825,23 +992,40 @@ class RouterHookApi {
825
992
  * @param pathname
826
993
  * @param layer
827
994
  */
828
- createHref(pathname, layer = this.layer) {
995
+ createHref(pathname, layer = this.layer, options = {}) {
829
996
  if (typeof pathname === "object") {
830
997
  pathname = pathname.options.path ?? "";
831
998
  }
999
+ if (options.params) {
1000
+ for (const [key, value] of Object.entries(options.params)) {
1001
+ pathname = pathname.replace(`:${key}`, String(value));
1002
+ }
1003
+ }
832
1004
  return pathname.startsWith("/") ? pathname : `${layer.path}/${pathname}`.replace(/\/\/+/g, "/");
833
1005
  }
834
1006
  async go(path, options) {
835
- await this.browser?.go(this.createHref(path, this.layer), options);
1007
+ for (const page of this.pages) {
1008
+ if (page.name === path) {
1009
+ path = page.path ?? "";
1010
+ break;
1011
+ }
1012
+ }
1013
+ await this.browser?.go(this.createHref(path, this.layer, options), options);
836
1014
  }
837
- anchor(path) {
838
- const href = this.createHref(path, this.layer);
1015
+ anchor(path, options = {}) {
1016
+ for (const page of this.pages) {
1017
+ if (page.name === path) {
1018
+ path = page.path ?? "";
1019
+ break;
1020
+ }
1021
+ }
1022
+ const href = this.createHref(path, this.layer, options);
839
1023
  return {
840
1024
  href,
841
1025
  onClick: (ev) => {
842
1026
  ev.stopPropagation();
843
1027
  ev.preventDefault();
844
- this.go(path).catch(console.error);
1028
+ this.go(path, options).catch(console.error);
845
1029
  }
846
1030
  };
847
1031
  }
@@ -869,8 +1053,12 @@ const useRouter = () => {
869
1053
  if (!ctx || !layer) {
870
1054
  throw new Error("useRouter must be used within a RouterProvider");
871
1055
  }
1056
+ const pages = React.useMemo(() => {
1057
+ return ctx.alepha.get(PageDescriptorProvider).getPages();
1058
+ }, []);
872
1059
  return React.useMemo(
873
1060
  () => new RouterHookApi(
1061
+ pages,
874
1062
  ctx.state,
875
1063
  layer,
876
1064
  ctx.alepha.isBrowser() ? ctx.alepha.get(ReactBrowserProvider) : void 0
@@ -881,6 +1069,7 @@ const useRouter = () => {
881
1069
 
882
1070
  const Link = (props) => {
883
1071
  React.useContext(RouterContext);
1072
+ const router = useRouter();
884
1073
  const to = typeof props.to === "string" ? props.to : props.to[core.OPTIONS].path;
885
1074
  if (!to) {
886
1075
  return null;
@@ -890,8 +1079,49 @@ const Link = (props) => {
890
1079
  return null;
891
1080
  }
892
1081
  const name = typeof props.to === "string" ? void 0 : props.to[core.OPTIONS].name;
1082
+ const anchorProps = {
1083
+ ...props,
1084
+ to: void 0
1085
+ };
1086
+ return /* @__PURE__ */ jsxRuntime.jsx("a", { ...router.anchor(to), ...anchorProps, children: props.children ?? name });
1087
+ };
1088
+
1089
+ const useActive = (path) => {
893
1090
  const router = useRouter();
894
- return /* @__PURE__ */ jsxRuntime.jsx("a", { ...router.anchor(to), ...props, children: props.children ?? name });
1091
+ const ctx = React.useContext(RouterContext);
1092
+ const layer = React.useContext(RouterLayerContext);
1093
+ if (!ctx || !layer) {
1094
+ throw new Error("useRouter must be used within a RouterProvider");
1095
+ }
1096
+ let name;
1097
+ if (typeof path === "object" && path.options.name) {
1098
+ name = path.options.name;
1099
+ }
1100
+ const [current, setCurrent] = React.useState(ctx.state.pathname);
1101
+ const href = React.useMemo(() => router.createHref(path, layer), [path, layer]);
1102
+ const [isPending, setPending] = React.useState(false);
1103
+ const isActive = current === href;
1104
+ useRouterEvents({
1105
+ onEnd: ({ state }) => setCurrent(state.pathname)
1106
+ });
1107
+ return {
1108
+ name,
1109
+ isPending,
1110
+ isActive,
1111
+ anchorProps: {
1112
+ href,
1113
+ onClick: (ev) => {
1114
+ ev.stopPropagation();
1115
+ ev.preventDefault();
1116
+ if (isActive) return;
1117
+ if (isPending) return;
1118
+ setPending(true);
1119
+ router.go(href).then(() => {
1120
+ setPending(false);
1121
+ });
1122
+ }
1123
+ }
1124
+ };
895
1125
  };
896
1126
 
897
1127
  const useInject = (clazz) => {
@@ -902,10 +1132,7 @@ const useInject = (clazz) => {
902
1132
  return React.useMemo(() => ctx.alepha.get(clazz), []);
903
1133
  };
904
1134
 
905
- const useClient = () => {
906
- return useInject(server.HttpClient);
907
- };
908
- const useApi = () => {
1135
+ const useClient = (_scope) => {
909
1136
  return useInject(server.HttpClient).of();
910
1137
  };
911
1138
 
@@ -939,7 +1166,7 @@ const encode = (alepha, schema, data) => {
939
1166
  const decode = (alepha, schema, data) => {
940
1167
  try {
941
1168
  return alepha.parse(schema, JSON.parse(atob(decodeURIComponent(data))));
942
- } catch (error) {
1169
+ } catch (_error) {
943
1170
  return {};
944
1171
  }
945
1172
  };
@@ -957,46 +1184,9 @@ const useRouterState = () => {
957
1184
  return state;
958
1185
  };
959
1186
 
960
- const useActive = (path) => {
961
- const router = useRouter();
962
- const ctx = React.useContext(RouterContext);
963
- const layer = React.useContext(RouterLayerContext);
964
- if (!ctx || !layer) {
965
- throw new Error("useRouter must be used within a RouterProvider");
966
- }
967
- let name;
968
- if (typeof path === "object" && path.options.name) {
969
- name = path.options.name;
970
- }
971
- const [current, setCurrent] = React.useState(ctx.state.pathname);
972
- const href = React.useMemo(() => router.createHref(path, layer), [path, layer]);
973
- const [isPending, setPending] = React.useState(false);
974
- const isActive = current === href;
975
- useRouterEvents({
976
- onEnd: ({ state }) => setCurrent(state.pathname)
977
- });
978
- return {
979
- name,
980
- isPending,
981
- isActive,
982
- anchorProps: {
983
- href,
984
- onClick: (ev) => {
985
- ev.stopPropagation();
986
- ev.preventDefault();
987
- if (isActive) return;
988
- if (isPending) return;
989
- setPending(true);
990
- router.go(href).then(() => {
991
- setPending(false);
992
- });
993
- }
994
- }
995
- };
996
- };
997
-
998
1187
  exports.$page = $page;
999
1188
  exports.BrowserRouterProvider = BrowserRouterProvider;
1189
+ exports.ClientOnly = ClientOnly;
1000
1190
  exports.ErrorBoundary = ErrorBoundary;
1001
1191
  exports.Link = Link;
1002
1192
  exports.NestedView = NestedView;
@@ -1008,11 +1198,10 @@ exports.RouterHookApi = RouterHookApi;
1008
1198
  exports.RouterLayerContext = RouterLayerContext;
1009
1199
  exports.isPageRoute = isPageRoute;
1010
1200
  exports.useActive = useActive;
1011
- exports.useApi = useApi;
1201
+ exports.useAlepha = useAlepha;
1012
1202
  exports.useClient = useClient;
1013
1203
  exports.useInject = useInject;
1014
1204
  exports.useQueryParams = useQueryParams;
1015
1205
  exports.useRouter = useRouter;
1016
1206
  exports.useRouterEvents = useRouterEvents;
1017
1207
  exports.useRouterState = useRouterState;
1018
- //# sourceMappingURL=useActive-DjpZBEuB.cjs.map