@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/README.md +85 -3
- package/dist/index.browser.js +110 -25
- package/dist/index.browser.js.map +1 -1
- package/dist/index.cjs +163 -44
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +122 -29
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +100 -7
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +164 -45
- package/dist/index.js.map +1 -1
- package/package.json +13 -12
- package/src/descriptors/$page.ts +85 -0
- package/src/hooks/useAlepha.ts +1 -1
- package/src/hooks/useQueryParams.ts +9 -5
- package/src/hooks/useRouterEvents.ts +4 -4
- package/src/hooks/useStore.ts +5 -5
- package/src/providers/ReactBrowserProvider.ts +3 -3
- package/src/providers/ReactBrowserRouterProvider.ts +6 -6
- package/src/providers/ReactPageProvider.ts +2 -3
- package/src/providers/ReactServerProvider.ts +83 -35
- package/src/services/ReactRouter.ts +1 -1
package/README.md
CHANGED
|
@@ -34,14 +34,96 @@ run(alepha);
|
|
|
34
34
|
|
|
35
35
|
### Descriptors
|
|
36
36
|
|
|
37
|
-
Descriptors are functions that define and configure various aspects of your application. They follow the convention of starting with
|
|
37
|
+
Descriptors are functions that define and configure various aspects of your application. They follow the convention of starting with ` $ ` and return configured descriptor instances.
|
|
38
38
|
|
|
39
|
-
For more details, see the [Descriptors documentation](
|
|
39
|
+
For more details, see the [Descriptors documentation](/docs/descriptors).
|
|
40
40
|
|
|
41
41
|
#### $page()
|
|
42
42
|
|
|
43
43
|
Main descriptor for defining a React route in the application.
|
|
44
44
|
|
|
45
|
+
The $page descriptor is the core building block for creating type-safe, SSR-enabled React routes.
|
|
46
|
+
It provides a declarative way to define pages with powerful features:
|
|
47
|
+
|
|
48
|
+
**Routing & Navigation**
|
|
49
|
+
- URL pattern matching with parameters (e.g., `/users/:id`)
|
|
50
|
+
- Nested routing with parent-child relationships
|
|
51
|
+
- Type-safe URL parameter and query string validation
|
|
52
|
+
|
|
53
|
+
**Data Loading**
|
|
54
|
+
- Server-side data fetching with the `resolve` function
|
|
55
|
+
- Automatic serialization and hydration for SSR
|
|
56
|
+
- Access to request context, URL params, and parent data
|
|
57
|
+
|
|
58
|
+
**Component Loading**
|
|
59
|
+
- Direct component rendering or lazy loading for code splitting
|
|
60
|
+
- Client-only rendering when browser APIs are needed
|
|
61
|
+
- Automatic fallback handling during hydration
|
|
62
|
+
|
|
63
|
+
**Performance Optimization**
|
|
64
|
+
- Static generation for pre-rendered pages at build time
|
|
65
|
+
- Server-side caching with configurable TTL and providers
|
|
66
|
+
- Code splitting through lazy component loading
|
|
67
|
+
|
|
68
|
+
**Error Handling**
|
|
69
|
+
- Custom error handlers with support for redirects
|
|
70
|
+
- Hierarchical error handling (child → parent)
|
|
71
|
+
- HTTP status code handling (404, 401, etc.)
|
|
72
|
+
|
|
73
|
+
**Page Animations**
|
|
74
|
+
- CSS-based enter/exit animations
|
|
75
|
+
- Dynamic animations based on page state
|
|
76
|
+
- Custom timing and easing functions
|
|
77
|
+
|
|
78
|
+
**Lifecycle Management**
|
|
79
|
+
- Server response hooks for headers and status codes
|
|
80
|
+
- Page leave handlers for cleanup (browser only)
|
|
81
|
+
- Permission-based access control
|
|
82
|
+
|
|
83
|
+
```typescript
|
|
84
|
+
const userProfile = $page({
|
|
85
|
+
path: "/users/:id",
|
|
86
|
+
schema: {
|
|
87
|
+
params: t.object({ id: t.int() }),
|
|
88
|
+
query: t.object({ tab: t.optional(t.string()) })
|
|
89
|
+
},
|
|
90
|
+
resolve: async ({ params }) => {
|
|
91
|
+
const user = await userApi.getUser(params.id);
|
|
92
|
+
return { user };
|
|
93
|
+
},
|
|
94
|
+
lazy: () => import("./UserProfile.tsx")
|
|
95
|
+
});
|
|
96
|
+
```
|
|
97
|
+
|
|
98
|
+
```typescript
|
|
99
|
+
const projectSection = $page({
|
|
100
|
+
path: "/projects/:id",
|
|
101
|
+
children: () => [projectBoard, projectSettings],
|
|
102
|
+
resolve: async ({ params }) => {
|
|
103
|
+
const project = await projectApi.get(params.id);
|
|
104
|
+
return { project };
|
|
105
|
+
},
|
|
106
|
+
errorHandler: (error) => {
|
|
107
|
+
if (HttpError.is(error, 404)) {
|
|
108
|
+
return <ProjectNotFound />;
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
});
|
|
112
|
+
```
|
|
113
|
+
|
|
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
|
+
```
|
|
126
|
+
|
|
45
127
|
### Hooks
|
|
46
128
|
|
|
47
129
|
Hooks provide a way to tap into various lifecycle events and extend functionality. They follow the convention of starting with `use` and return configured hook instances.
|
|
@@ -56,7 +138,7 @@ With Alepha, you can access the core functionalities of the framework:
|
|
|
56
138
|
|
|
57
139
|
- alepha.state() for state management
|
|
58
140
|
- alepha.inject() for dependency injection
|
|
59
|
-
- alepha.emit() for event handling
|
|
141
|
+
- alepha.events.emit() for event handling
|
|
60
142
|
etc...
|
|
61
143
|
|
|
62
144
|
#### useClient()
|
package/dist/index.browser.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import { $env, $hook, $inject, $module, Alepha, AlephaError, Descriptor, KIND,
|
|
1
|
+
import { $env, $hook, $inject, $module, Alepha, AlephaError, Descriptor, KIND, createDescriptor, t } from "@alepha/core";
|
|
2
2
|
import { AlephaServer, HttpClient } from "@alepha/server";
|
|
3
3
|
import { AlephaServerLinks, LinkProvider } from "@alepha/server-links";
|
|
4
4
|
import { DateTimeProvider } from "@alepha/datetime";
|
|
@@ -11,6 +11,91 @@ import { createRoot, hydrateRoot } from "react-dom/client";
|
|
|
11
11
|
//#region src/descriptors/$page.ts
|
|
12
12
|
/**
|
|
13
13
|
* Main descriptor for defining a React route in the application.
|
|
14
|
+
*
|
|
15
|
+
* The $page descriptor is the core building block for creating type-safe, SSR-enabled React routes.
|
|
16
|
+
* It provides a declarative way to define pages with powerful features:
|
|
17
|
+
*
|
|
18
|
+
* **Routing & Navigation**
|
|
19
|
+
* - URL pattern matching with parameters (e.g., `/users/:id`)
|
|
20
|
+
* - Nested routing with parent-child relationships
|
|
21
|
+
* - Type-safe URL parameter and query string validation
|
|
22
|
+
*
|
|
23
|
+
* **Data Loading**
|
|
24
|
+
* - Server-side data fetching with the `resolve` function
|
|
25
|
+
* - Automatic serialization and hydration for SSR
|
|
26
|
+
* - Access to request context, URL params, and parent data
|
|
27
|
+
*
|
|
28
|
+
* **Component Loading**
|
|
29
|
+
* - Direct component rendering or lazy loading for code splitting
|
|
30
|
+
* - Client-only rendering when browser APIs are needed
|
|
31
|
+
* - Automatic fallback handling during hydration
|
|
32
|
+
*
|
|
33
|
+
* **Performance Optimization**
|
|
34
|
+
* - Static generation for pre-rendered pages at build time
|
|
35
|
+
* - Server-side caching with configurable TTL and providers
|
|
36
|
+
* - Code splitting through lazy component loading
|
|
37
|
+
*
|
|
38
|
+
* **Error Handling**
|
|
39
|
+
* - Custom error handlers with support for redirects
|
|
40
|
+
* - Hierarchical error handling (child → parent)
|
|
41
|
+
* - HTTP status code handling (404, 401, etc.)
|
|
42
|
+
*
|
|
43
|
+
* **Page Animations**
|
|
44
|
+
* - CSS-based enter/exit animations
|
|
45
|
+
* - Dynamic animations based on page state
|
|
46
|
+
* - Custom timing and easing functions
|
|
47
|
+
*
|
|
48
|
+
* **Lifecycle Management**
|
|
49
|
+
* - Server response hooks for headers and status codes
|
|
50
|
+
* - Page leave handlers for cleanup (browser only)
|
|
51
|
+
* - Permission-based access control
|
|
52
|
+
*
|
|
53
|
+
* @example Simple page with data fetching
|
|
54
|
+
* ```typescript
|
|
55
|
+
* const userProfile = $page({
|
|
56
|
+
* path: "/users/:id",
|
|
57
|
+
* schema: {
|
|
58
|
+
* params: t.object({ id: t.int() }),
|
|
59
|
+
* query: t.object({ tab: t.optional(t.string()) })
|
|
60
|
+
* },
|
|
61
|
+
* resolve: async ({ params }) => {
|
|
62
|
+
* const user = await userApi.getUser(params.id);
|
|
63
|
+
* return { user };
|
|
64
|
+
* },
|
|
65
|
+
* lazy: () => import("./UserProfile.tsx")
|
|
66
|
+
* });
|
|
67
|
+
* ```
|
|
68
|
+
*
|
|
69
|
+
* @example Nested routing with error handling
|
|
70
|
+
* ```typescript
|
|
71
|
+
* const projectSection = $page({
|
|
72
|
+
* path: "/projects/:id",
|
|
73
|
+
* children: () => [projectBoard, projectSettings],
|
|
74
|
+
* resolve: async ({ params }) => {
|
|
75
|
+
* const project = await projectApi.get(params.id);
|
|
76
|
+
* return { project };
|
|
77
|
+
* },
|
|
78
|
+
* errorHandler: (error) => {
|
|
79
|
+
* if (HttpError.is(error, 404)) {
|
|
80
|
+
* return <ProjectNotFound />;
|
|
81
|
+
* }
|
|
82
|
+
* }
|
|
83
|
+
* });
|
|
84
|
+
* ```
|
|
85
|
+
*
|
|
86
|
+
* @example Static generation with caching
|
|
87
|
+
* ```typescript
|
|
88
|
+
* const blogPost = $page({
|
|
89
|
+
* path: "/blog/:slug",
|
|
90
|
+
* static: {
|
|
91
|
+
* entries: posts.map(p => ({ params: { slug: p.slug } }))
|
|
92
|
+
* },
|
|
93
|
+
* resolve: async ({ params }) => {
|
|
94
|
+
* const post = await loadPost(params.slug);
|
|
95
|
+
* return { post };
|
|
96
|
+
* }
|
|
97
|
+
* });
|
|
98
|
+
* ```
|
|
14
99
|
*/
|
|
15
100
|
const $page = (options) => {
|
|
16
101
|
return createDescriptor(PageDescriptor, options);
|
|
@@ -269,7 +354,7 @@ const AlephaContext = createContext(void 0);
|
|
|
269
354
|
*
|
|
270
355
|
* - alepha.state() for state management
|
|
271
356
|
* - alepha.inject() for dependency injection
|
|
272
|
-
* - alepha.emit() for event handling
|
|
357
|
+
* - alepha.events.emit() for event handling
|
|
273
358
|
* etc...
|
|
274
359
|
*/
|
|
275
360
|
const useAlepha = () => {
|
|
@@ -296,10 +381,10 @@ const useRouterEvents = (opts = {}, deps = []) => {
|
|
|
296
381
|
const onEnd = opts.onEnd;
|
|
297
382
|
const onError = opts.onError;
|
|
298
383
|
const onSuccess = opts.onSuccess;
|
|
299
|
-
if (onBegin) subs.push(alepha.on("react:transition:begin", cb(onBegin)));
|
|
300
|
-
if (onEnd) subs.push(alepha.on("react:transition:end", cb(onEnd)));
|
|
301
|
-
if (onError) subs.push(alepha.on("react:transition:error", cb(onError)));
|
|
302
|
-
if (onSuccess) subs.push(alepha.on("react:transition:success", cb(onSuccess)));
|
|
384
|
+
if (onBegin) subs.push(alepha.events.on("react:transition:begin", cb(onBegin)));
|
|
385
|
+
if (onEnd) subs.push(alepha.events.on("react:transition:end", cb(onEnd)));
|
|
386
|
+
if (onError) subs.push(alepha.events.on("react:transition:error", cb(onError)));
|
|
387
|
+
if (onSuccess) subs.push(alepha.events.on("react:transition:success", cb(onSuccess)));
|
|
303
388
|
return () => {
|
|
304
389
|
for (const sub of subs) sub();
|
|
305
390
|
};
|
|
@@ -314,17 +399,17 @@ const useRouterEvents = (opts = {}, deps = []) => {
|
|
|
314
399
|
const useStore = (key, defaultValue) => {
|
|
315
400
|
const alepha = useAlepha();
|
|
316
401
|
useMemo(() => {
|
|
317
|
-
if (defaultValue != null && alepha.state(key) == null) alepha.state(key, defaultValue);
|
|
402
|
+
if (defaultValue != null && alepha.state.get(key) == null) alepha.state.set(key, defaultValue);
|
|
318
403
|
}, [defaultValue]);
|
|
319
|
-
const [state, setState] = useState(alepha.state(key));
|
|
404
|
+
const [state, setState] = useState(alepha.state.get(key));
|
|
320
405
|
useEffect(() => {
|
|
321
406
|
if (!alepha.isBrowser()) return;
|
|
322
|
-
return alepha.on("state:mutate", (ev) => {
|
|
407
|
+
return alepha.events.on("state:mutate", (ev) => {
|
|
323
408
|
if (ev.key === key) setState(ev.value);
|
|
324
409
|
});
|
|
325
410
|
}, []);
|
|
326
411
|
return [state, (value) => {
|
|
327
|
-
alepha.state(key, value);
|
|
412
|
+
alepha.state.set(key, value);
|
|
328
413
|
}];
|
|
329
414
|
};
|
|
330
415
|
|
|
@@ -533,8 +618,8 @@ var ReactPageProvider = class {
|
|
|
533
618
|
return root;
|
|
534
619
|
}
|
|
535
620
|
convertStringObjectToObject = (schema, value) => {
|
|
536
|
-
if (
|
|
537
|
-
for (const key in schema.properties) if (
|
|
621
|
+
if (t.schema.isObject(schema) && typeof value === "object") {
|
|
622
|
+
for (const key in schema.properties) if (t.schema.isObject(schema.properties[key]) && typeof value[key] === "string") try {
|
|
538
623
|
value[key] = this.alepha.parse(schema.properties[key], decodeURIComponent(value[key]));
|
|
539
624
|
} catch (e) {}
|
|
540
625
|
}
|
|
@@ -831,8 +916,8 @@ var ReactBrowserRouterProvider = class extends RouterProvider {
|
|
|
831
916
|
meta
|
|
832
917
|
};
|
|
833
918
|
const state = entry;
|
|
834
|
-
await this.alepha.emit("react:transition:begin", {
|
|
835
|
-
previous: this.alepha.state("react.router.state"),
|
|
919
|
+
await this.alepha.events.emit("react:transition:begin", {
|
|
920
|
+
previous: this.alepha.state.get("react.router.state"),
|
|
836
921
|
state
|
|
837
922
|
});
|
|
838
923
|
try {
|
|
@@ -851,7 +936,7 @@ var ReactBrowserRouterProvider = class extends RouterProvider {
|
|
|
851
936
|
index: 0,
|
|
852
937
|
path: "/"
|
|
853
938
|
});
|
|
854
|
-
await this.alepha.emit("react:transition:success", { state });
|
|
939
|
+
await this.alepha.events.emit("react:transition:success", { state });
|
|
855
940
|
} catch (e) {
|
|
856
941
|
this.log.error("Transition has failed", e);
|
|
857
942
|
state.layers = [{
|
|
@@ -860,7 +945,7 @@ var ReactBrowserRouterProvider = class extends RouterProvider {
|
|
|
860
945
|
index: 0,
|
|
861
946
|
path: "/"
|
|
862
947
|
}];
|
|
863
|
-
await this.alepha.emit("react:transition:error", {
|
|
948
|
+
await this.alepha.events.emit("react:transition:error", {
|
|
864
949
|
error: e,
|
|
865
950
|
state
|
|
866
951
|
});
|
|
@@ -869,8 +954,8 @@ var ReactBrowserRouterProvider = class extends RouterProvider {
|
|
|
869
954
|
const layer = previous[i];
|
|
870
955
|
if (state.layers[i]?.name !== layer.name) this.pageApi.page(layer.name)?.onLeave?.();
|
|
871
956
|
}
|
|
872
|
-
this.alepha.state("react.router.state", state);
|
|
873
|
-
await this.alepha.emit("react:transition:end", { state });
|
|
957
|
+
this.alepha.state.set("react.router.state", state);
|
|
958
|
+
await this.alepha.events.emit("react:transition:end", { state });
|
|
874
959
|
}
|
|
875
960
|
root(state) {
|
|
876
961
|
return this.pageApi.root(state);
|
|
@@ -898,7 +983,7 @@ var ReactBrowserProvider = class {
|
|
|
898
983
|
}
|
|
899
984
|
transitioning;
|
|
900
985
|
get state() {
|
|
901
|
-
return this.alepha.state("react.router.state");
|
|
986
|
+
return this.alepha.state.get("react.router.state");
|
|
902
987
|
}
|
|
903
988
|
/**
|
|
904
989
|
* Accessor for Document DOM API.
|
|
@@ -1014,11 +1099,11 @@ var ReactBrowserProvider = class {
|
|
|
1014
1099
|
const hydration = this.getHydrationState();
|
|
1015
1100
|
const previous = hydration?.layers ?? [];
|
|
1016
1101
|
if (hydration) {
|
|
1017
|
-
for (const [key, value] of Object.entries(hydration)) if (key !== "layers") this.alepha.state(key, value);
|
|
1102
|
+
for (const [key, value] of Object.entries(hydration)) if (key !== "layers") this.alepha.state.set(key, value);
|
|
1018
1103
|
}
|
|
1019
1104
|
await this.render({ previous });
|
|
1020
1105
|
const element = this.router.root(this.state);
|
|
1021
|
-
await this.alepha.emit("react:browser:render", {
|
|
1106
|
+
await this.alepha.events.emit("react:browser:render", {
|
|
1022
1107
|
element,
|
|
1023
1108
|
root: this.getRootElement(),
|
|
1024
1109
|
hydration,
|
|
@@ -1059,7 +1144,7 @@ var ReactRouter = class {
|
|
|
1059
1144
|
alepha = $inject(Alepha);
|
|
1060
1145
|
pageApi = $inject(ReactPageProvider);
|
|
1061
1146
|
get state() {
|
|
1062
|
-
return this.alepha.state("react.router.state");
|
|
1147
|
+
return this.alepha.state.get("react.router.state");
|
|
1063
1148
|
}
|
|
1064
1149
|
get pages() {
|
|
1065
1150
|
return this.pageApi.getPages();
|
|
@@ -1245,7 +1330,7 @@ const useQueryParams = (schema, options = {}) => {
|
|
|
1245
1330
|
const key = options.key ?? "q";
|
|
1246
1331
|
const router = useRouter();
|
|
1247
1332
|
const querystring = router.query[key];
|
|
1248
|
-
const [queryParams, setQueryParams] = useState(decode(alepha, schema, router.query[key]));
|
|
1333
|
+
const [queryParams = {}, setQueryParams] = useState(decode(alepha, schema, router.query[key]));
|
|
1249
1334
|
useEffect(() => {
|
|
1250
1335
|
setQueryParams(decode(alepha, schema, querystring));
|
|
1251
1336
|
}, [querystring]);
|
|
@@ -1265,8 +1350,8 @@ const encode = (alepha, schema, data) => {
|
|
|
1265
1350
|
const decode = (alepha, schema, data) => {
|
|
1266
1351
|
try {
|
|
1267
1352
|
return alepha.parse(schema, JSON.parse(atob(decodeURIComponent(data))));
|
|
1268
|
-
} catch
|
|
1269
|
-
return
|
|
1353
|
+
} catch {
|
|
1354
|
+
return;
|
|
1270
1355
|
}
|
|
1271
1356
|
};
|
|
1272
1357
|
|