@alepha/react 0.9.4 → 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 +101 -7
- package/dist/index.browser.js +290 -86
- package/dist/index.browser.js.map +1 -1
- package/dist/index.cjs +352 -110
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +321 -183
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +318 -180
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +352 -110
- package/dist/index.js.map +1 -1
- package/package.json +17 -14
- package/src/components/Link.tsx +2 -5
- package/src/components/NestedView.tsx +159 -16
- package/src/descriptors/$page.ts +169 -1
- package/src/hooks/useActive.ts +0 -1
- package/src/hooks/useAlepha.ts +1 -1
- package/src/hooks/useQueryParams.ts +9 -5
- package/src/hooks/useRouterEvents.ts +27 -19
- package/src/hooks/useStore.ts +5 -5
- package/src/index.browser.ts +3 -0
- package/src/index.ts +6 -1
- package/src/providers/ReactBrowserProvider.ts +21 -16
- package/src/providers/ReactBrowserRendererProvider.ts +22 -0
- package/src/providers/ReactBrowserRouterProvider.ts +11 -6
- package/src/providers/ReactPageProvider.ts +45 -1
- package/src/providers/ReactServerProvider.ts +105 -38
- package/src/services/ReactRouter.ts +6 -9
package/dist/index.cjs
CHANGED
|
@@ -33,12 +33,96 @@ 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
|
|
40
39
|
/**
|
|
41
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
|
+
* ```
|
|
42
126
|
*/
|
|
43
127
|
const $page = (options) => {
|
|
44
128
|
return (0, __alepha_core.createDescriptor)(PageDescriptor, options);
|
|
@@ -58,7 +142,10 @@ var PageDescriptor = class extends __alepha_core.Descriptor {
|
|
|
58
142
|
* Only valid for server-side rendering, it will throw an error if called on the client-side.
|
|
59
143
|
*/
|
|
60
144
|
async render(options) {
|
|
61
|
-
throw new
|
|
145
|
+
throw new __alepha_core.AlephaError("render() method is not implemented in this environment");
|
|
146
|
+
}
|
|
147
|
+
async fetch(options) {
|
|
148
|
+
throw new __alepha_core.AlephaError("fetch() method is not implemented in this environment");
|
|
62
149
|
}
|
|
63
150
|
match(url) {
|
|
64
151
|
return false;
|
|
@@ -87,7 +174,6 @@ const ClientOnly = (props) => {
|
|
|
87
174
|
if (props.disabled) return props.children;
|
|
88
175
|
return mounted ? props.children : props.fallback;
|
|
89
176
|
};
|
|
90
|
-
var ClientOnly_default = ClientOnly;
|
|
91
177
|
|
|
92
178
|
//#endregion
|
|
93
179
|
//#region src/components/ErrorViewer.tsx
|
|
@@ -195,7 +281,6 @@ const ErrorViewer = ({ error, alepha }) => {
|
|
|
195
281
|
})] })]
|
|
196
282
|
});
|
|
197
283
|
};
|
|
198
|
-
var ErrorViewer_default = ErrorViewer;
|
|
199
284
|
const ErrorViewerProduction = () => {
|
|
200
285
|
const styles = {
|
|
201
286
|
container: {
|
|
@@ -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 = () => {
|
|
@@ -289,19 +374,55 @@ const useRouterEvents = (opts = {}, deps = []) => {
|
|
|
289
374
|
const alepha = useAlepha();
|
|
290
375
|
(0, react.useEffect)(() => {
|
|
291
376
|
if (!alepha.isBrowser()) return;
|
|
377
|
+
const cb = (callback) => {
|
|
378
|
+
if (typeof callback === "function") return { callback };
|
|
379
|
+
return callback;
|
|
380
|
+
};
|
|
292
381
|
const subs = [];
|
|
293
382
|
const onBegin = opts.onBegin;
|
|
294
383
|
const onEnd = opts.onEnd;
|
|
295
384
|
const onError = opts.onError;
|
|
296
|
-
|
|
297
|
-
if (
|
|
298
|
-
if (
|
|
385
|
+
const onSuccess = opts.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)));
|
|
299
390
|
return () => {
|
|
300
391
|
for (const sub of subs) sub();
|
|
301
392
|
};
|
|
302
393
|
}, deps);
|
|
303
394
|
};
|
|
304
395
|
|
|
396
|
+
//#endregion
|
|
397
|
+
//#region src/hooks/useStore.ts
|
|
398
|
+
/**
|
|
399
|
+
* Hook to access and mutate the Alepha state.
|
|
400
|
+
*/
|
|
401
|
+
const useStore = (key, defaultValue) => {
|
|
402
|
+
const alepha = useAlepha();
|
|
403
|
+
(0, react.useMemo)(() => {
|
|
404
|
+
if (defaultValue != null && alepha.state.get(key) == null) alepha.state.set(key, defaultValue);
|
|
405
|
+
}, [defaultValue]);
|
|
406
|
+
const [state, setState] = (0, react.useState)(alepha.state.get(key));
|
|
407
|
+
(0, react.useEffect)(() => {
|
|
408
|
+
if (!alepha.isBrowser()) return;
|
|
409
|
+
return alepha.events.on("state:mutate", (ev) => {
|
|
410
|
+
if (ev.key === key) setState(ev.value);
|
|
411
|
+
});
|
|
412
|
+
}, []);
|
|
413
|
+
return [state, (value) => {
|
|
414
|
+
alepha.state.set(key, value);
|
|
415
|
+
}];
|
|
416
|
+
};
|
|
417
|
+
|
|
418
|
+
//#endregion
|
|
419
|
+
//#region src/hooks/useRouterState.ts
|
|
420
|
+
const useRouterState = () => {
|
|
421
|
+
const [state] = useStore("react.router.state");
|
|
422
|
+
if (!state) throw new __alepha_core.AlephaError("Missing react router state");
|
|
423
|
+
return state;
|
|
424
|
+
};
|
|
425
|
+
|
|
305
426
|
//#endregion
|
|
306
427
|
//#region src/components/ErrorBoundary.tsx
|
|
307
428
|
/**
|
|
@@ -331,7 +452,6 @@ var ErrorBoundary = class extends react.default.Component {
|
|
|
331
452
|
return this.props.children;
|
|
332
453
|
}
|
|
333
454
|
};
|
|
334
|
-
var ErrorBoundary_default = ErrorBoundary;
|
|
335
455
|
|
|
336
456
|
//#endregion
|
|
337
457
|
//#region src/components/NestedView.tsx
|
|
@@ -342,7 +462,7 @@ var ErrorBoundary_default = ErrorBoundary;
|
|
|
342
462
|
*
|
|
343
463
|
* @example
|
|
344
464
|
* ```tsx
|
|
345
|
-
* import { NestedView } from "
|
|
465
|
+
* import { NestedView } from "alepha/react";
|
|
346
466
|
*
|
|
347
467
|
* class App {
|
|
348
468
|
* parent = $page({
|
|
@@ -357,17 +477,69 @@ var ErrorBoundary_default = ErrorBoundary;
|
|
|
357
477
|
* ```
|
|
358
478
|
*/
|
|
359
479
|
const NestedView = (props) => {
|
|
360
|
-
const
|
|
361
|
-
const
|
|
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.");
|
|
480
|
+
const index = (0, react.use)(RouterLayerContext)?.index ?? 0;
|
|
481
|
+
const state = useRouterState();
|
|
365
482
|
const [view, setView] = (0, react.useState)(state.layers[index]?.element);
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
370
|
-
|
|
483
|
+
const [animation, setAnimation] = (0, react.useState)("");
|
|
484
|
+
const animationExitDuration = (0, react.useRef)(0);
|
|
485
|
+
const animationExitNow = (0, react.useRef)(0);
|
|
486
|
+
useRouterEvents({
|
|
487
|
+
onBegin: async ({ previous, state: state$1 }) => {
|
|
488
|
+
const layer = previous.layers[index];
|
|
489
|
+
if (`${state$1.url.pathname}/`.startsWith(`${layer?.path}/`)) return;
|
|
490
|
+
const animationExit = parseAnimation(layer.route?.animation, state$1, "exit");
|
|
491
|
+
if (animationExit) {
|
|
492
|
+
const duration = animationExit.duration || 200;
|
|
493
|
+
animationExitNow.current = Date.now();
|
|
494
|
+
animationExitDuration.current = duration;
|
|
495
|
+
setAnimation(animationExit.animation);
|
|
496
|
+
} else {
|
|
497
|
+
animationExitNow.current = 0;
|
|
498
|
+
animationExitDuration.current = 0;
|
|
499
|
+
setAnimation("");
|
|
500
|
+
}
|
|
501
|
+
},
|
|
502
|
+
onEnd: async ({ state: state$1 }) => {
|
|
503
|
+
const layer = state$1.layers[index];
|
|
504
|
+
if (animationExitNow.current) {
|
|
505
|
+
const duration = animationExitDuration.current;
|
|
506
|
+
const diff = Date.now() - animationExitNow.current;
|
|
507
|
+
if (diff < duration) await new Promise((resolve) => setTimeout(resolve, duration - diff));
|
|
508
|
+
}
|
|
509
|
+
if (!layer?.cache) {
|
|
510
|
+
setView(layer?.element);
|
|
511
|
+
const animationEnter = parseAnimation(layer?.route?.animation, state$1, "enter");
|
|
512
|
+
if (animationEnter) setAnimation(animationEnter.animation);
|
|
513
|
+
else setAnimation("");
|
|
514
|
+
}
|
|
515
|
+
}
|
|
516
|
+
}, []);
|
|
517
|
+
let element = view ?? props.children ?? null;
|
|
518
|
+
if (animation) element = /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
519
|
+
style: {
|
|
520
|
+
display: "flex",
|
|
521
|
+
flex: 1,
|
|
522
|
+
height: "100%",
|
|
523
|
+
width: "100%",
|
|
524
|
+
position: "relative",
|
|
525
|
+
overflow: "hidden"
|
|
526
|
+
},
|
|
527
|
+
children: /* @__PURE__ */ (0, react_jsx_runtime.jsx)("div", {
|
|
528
|
+
style: {
|
|
529
|
+
height: "100%",
|
|
530
|
+
width: "100%",
|
|
531
|
+
display: "flex",
|
|
532
|
+
animation
|
|
533
|
+
},
|
|
534
|
+
children: element
|
|
535
|
+
})
|
|
536
|
+
});
|
|
537
|
+
if (props.errorBoundary === false) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(react_jsx_runtime.Fragment, { children: element });
|
|
538
|
+
if (props.errorBoundary) return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBoundary, {
|
|
539
|
+
fallback: props.errorBoundary,
|
|
540
|
+
children: element
|
|
541
|
+
});
|
|
542
|
+
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)(ErrorBoundary, {
|
|
371
543
|
fallback: (error) => {
|
|
372
544
|
const result = state.onError(error, state);
|
|
373
545
|
if (result instanceof Redirection) return "Redirection inside ErrorBoundary is not allowed.";
|
|
@@ -376,7 +548,37 @@ const NestedView = (props) => {
|
|
|
376
548
|
children: element
|
|
377
549
|
});
|
|
378
550
|
};
|
|
379
|
-
var NestedView_default = NestedView;
|
|
551
|
+
var NestedView_default = (0, react.memo)(NestedView);
|
|
552
|
+
function parseAnimation(animationLike, state, type = "enter") {
|
|
553
|
+
if (!animationLike) return void 0;
|
|
554
|
+
const DEFAULT_DURATION = 300;
|
|
555
|
+
const animation = typeof animationLike === "function" ? animationLike(state) : animationLike;
|
|
556
|
+
if (typeof animation === "string") {
|
|
557
|
+
if (type === "exit") return;
|
|
558
|
+
return {
|
|
559
|
+
duration: DEFAULT_DURATION,
|
|
560
|
+
animation: `${DEFAULT_DURATION}ms ${animation}`
|
|
561
|
+
};
|
|
562
|
+
}
|
|
563
|
+
if (typeof animation === "object") {
|
|
564
|
+
const anim = animation[type];
|
|
565
|
+
const duration = typeof anim === "object" ? anim.duration ?? DEFAULT_DURATION : DEFAULT_DURATION;
|
|
566
|
+
const name = typeof anim === "object" ? anim.name : anim;
|
|
567
|
+
if (type === "exit") {
|
|
568
|
+
const timing$1 = typeof anim === "object" ? anim.timing ?? "" : "";
|
|
569
|
+
return {
|
|
570
|
+
duration,
|
|
571
|
+
animation: `${duration}ms ${timing$1} ${name}`
|
|
572
|
+
};
|
|
573
|
+
}
|
|
574
|
+
const timing = typeof anim === "object" ? anim.timing ?? "" : "";
|
|
575
|
+
return {
|
|
576
|
+
duration,
|
|
577
|
+
animation: `${duration}ms ${timing} ${name}`
|
|
578
|
+
};
|
|
579
|
+
}
|
|
580
|
+
return void 0;
|
|
581
|
+
}
|
|
380
582
|
|
|
381
583
|
//#endregion
|
|
382
584
|
//#region src/components/NotFound.tsx
|
|
@@ -442,6 +644,14 @@ var ReactPageProvider = class {
|
|
|
442
644
|
if (this.env.REACT_STRICT_MODE) return (0, react.createElement)(react.StrictMode, {}, root);
|
|
443
645
|
return root;
|
|
444
646
|
}
|
|
647
|
+
convertStringObjectToObject = (schema, value) => {
|
|
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 {
|
|
650
|
+
value[key] = this.alepha.parse(schema.properties[key], decodeURIComponent(value[key]));
|
|
651
|
+
} catch (e) {}
|
|
652
|
+
}
|
|
653
|
+
return value;
|
|
654
|
+
};
|
|
445
655
|
/**
|
|
446
656
|
* Create a new RouterState based on a given route and request.
|
|
447
657
|
* This method resolves the layers for the route, applying any query and params schemas defined in the route.
|
|
@@ -461,6 +671,7 @@ var ReactPageProvider = class {
|
|
|
461
671
|
const route$1 = it.route;
|
|
462
672
|
const config = {};
|
|
463
673
|
try {
|
|
674
|
+
this.convertStringObjectToObject(route$1.schema?.query, state.query);
|
|
464
675
|
config.query = route$1.schema?.query ? this.alepha.parse(route$1.schema.query, state.query) : {};
|
|
465
676
|
} catch (e) {
|
|
466
677
|
it.error = e;
|
|
@@ -587,6 +798,7 @@ var ReactPageProvider = class {
|
|
|
587
798
|
}
|
|
588
799
|
}
|
|
589
800
|
async createElement(page, props) {
|
|
801
|
+
if (page.lazy && page.component) this.log.warn(`Page ${page.name} has both lazy and component options, lazy will be used`);
|
|
590
802
|
if (page.lazy) {
|
|
591
803
|
const component = await page.lazy();
|
|
592
804
|
return (0, react.createElement)(component.default, props);
|
|
@@ -595,7 +807,7 @@ var ReactPageProvider = class {
|
|
|
595
807
|
return void 0;
|
|
596
808
|
}
|
|
597
809
|
renderError(error) {
|
|
598
|
-
return (0, react.createElement)(
|
|
810
|
+
return (0, react.createElement)(ErrorViewer, {
|
|
599
811
|
error,
|
|
600
812
|
alepha: this.alepha
|
|
601
813
|
});
|
|
@@ -621,7 +833,7 @@ var ReactPageProvider = class {
|
|
|
621
833
|
}
|
|
622
834
|
renderView(index, path, view, page) {
|
|
623
835
|
view ??= this.renderEmptyView();
|
|
624
|
-
const element = page.client ? (0, react.createElement)(
|
|
836
|
+
const element = page.client ? (0, react.createElement)(ClientOnly, typeof page.client === "object" ? page.client : {}, view) : view;
|
|
625
837
|
return (0, react.createElement)(RouterLayerContext.Provider, { value: {
|
|
626
838
|
index,
|
|
627
839
|
path
|
|
@@ -715,18 +927,36 @@ var ReactServerProvider = class {
|
|
|
715
927
|
log = (0, __alepha_logger.$logger)();
|
|
716
928
|
alepha = (0, __alepha_core.$inject)(__alepha_core.Alepha);
|
|
717
929
|
pageApi = (0, __alepha_core.$inject)(ReactPageProvider);
|
|
930
|
+
serverProvider = (0, __alepha_core.$inject)(__alepha_server.ServerProvider);
|
|
718
931
|
serverStaticProvider = (0, __alepha_core.$inject)(__alepha_server_static.ServerStaticProvider);
|
|
719
932
|
serverRouterProvider = (0, __alepha_core.$inject)(__alepha_server.ServerRouterProvider);
|
|
720
933
|
serverTimingProvider = (0, __alepha_core.$inject)(__alepha_server.ServerTimingProvider);
|
|
721
934
|
env = (0, __alepha_core.$env)(envSchema$1);
|
|
722
935
|
ROOT_DIV_REGEX = new RegExp(`<div([^>]*)\\s+id=["']${this.env.REACT_ROOT_ID}["']([^>]*)>(.*?)<\\/div>`, "is");
|
|
936
|
+
preprocessedTemplate = null;
|
|
723
937
|
onConfigure = (0, __alepha_core.$hook)({
|
|
724
938
|
on: "configure",
|
|
725
939
|
handler: async () => {
|
|
726
940
|
const pages = this.alepha.descriptors($page);
|
|
727
941
|
const ssrEnabled = pages.length > 0 && this.env.REACT_SSR_ENABLED !== false;
|
|
728
|
-
this.alepha.state("react.server.ssr", ssrEnabled);
|
|
729
|
-
for (const page of pages)
|
|
942
|
+
this.alepha.state.set("react.server.ssr", ssrEnabled);
|
|
943
|
+
for (const page of pages) {
|
|
944
|
+
page.render = this.createRenderFunction(page.name);
|
|
945
|
+
page.fetch = async (options) => {
|
|
946
|
+
const response = await fetch(`${this.serverProvider.hostname}/${page.pathname(options)}`);
|
|
947
|
+
const html = await response.text();
|
|
948
|
+
if (options?.html) return {
|
|
949
|
+
html,
|
|
950
|
+
response
|
|
951
|
+
};
|
|
952
|
+
const match = html.match(this.ROOT_DIV_REGEX);
|
|
953
|
+
if (match) return {
|
|
954
|
+
html: match[3],
|
|
955
|
+
response
|
|
956
|
+
};
|
|
957
|
+
throw new __alepha_core.AlephaError("Invalid HTML response");
|
|
958
|
+
};
|
|
959
|
+
}
|
|
730
960
|
if (this.alepha.isServerless() === "vite") {
|
|
731
961
|
await this.configureVite(ssrEnabled);
|
|
732
962
|
return;
|
|
@@ -765,6 +995,8 @@ var ReactServerProvider = class {
|
|
|
765
995
|
return this.alepha.env.REACT_SERVER_TEMPLATE ?? "<!DOCTYPE html><html lang='en'><head></head><body></body></html>";
|
|
766
996
|
}
|
|
767
997
|
async registerPages(templateLoader) {
|
|
998
|
+
const template = await templateLoader();
|
|
999
|
+
if (template) this.preprocessedTemplate = this.preprocessTemplate(template);
|
|
768
1000
|
for (const page of this.pageApi.getPages()) {
|
|
769
1001
|
if (page.children?.length) continue;
|
|
770
1002
|
this.log.debug(`+ ${page.match} -> ${page.name}`);
|
|
@@ -806,27 +1038,37 @@ var ReactServerProvider = class {
|
|
|
806
1038
|
params: options.params ?? {},
|
|
807
1039
|
query: options.query ?? {},
|
|
808
1040
|
onError: () => null,
|
|
809
|
-
layers: []
|
|
1041
|
+
layers: [],
|
|
1042
|
+
meta: {}
|
|
810
1043
|
};
|
|
811
1044
|
const state = entry;
|
|
812
1045
|
this.log.trace("Rendering", { url });
|
|
813
|
-
await this.alepha.emit("react:server:render:begin", { state });
|
|
1046
|
+
await this.alepha.events.emit("react:server:render:begin", { state });
|
|
814
1047
|
const { redirect } = await this.pageApi.createLayers(page, state);
|
|
815
|
-
if (redirect)
|
|
1048
|
+
if (redirect) return {
|
|
1049
|
+
state,
|
|
1050
|
+
html: "",
|
|
1051
|
+
redirect
|
|
1052
|
+
};
|
|
816
1053
|
if (!withIndex && !options.html) {
|
|
817
|
-
this.alepha.state("react.router.state", state);
|
|
1054
|
+
this.alepha.state.set("react.router.state", state);
|
|
818
1055
|
return {
|
|
819
1056
|
state,
|
|
820
1057
|
html: (0, react_dom_server.renderToString)(this.pageApi.root(state))
|
|
821
1058
|
};
|
|
822
1059
|
}
|
|
823
|
-
const
|
|
824
|
-
|
|
1060
|
+
const template = this.template ?? "";
|
|
1061
|
+
const html = this.renderToHtml(template, state, options.hydration);
|
|
1062
|
+
if (html instanceof Redirection) return {
|
|
1063
|
+
state,
|
|
1064
|
+
html: "",
|
|
1065
|
+
redirect
|
|
1066
|
+
};
|
|
825
1067
|
const result = {
|
|
826
1068
|
state,
|
|
827
1069
|
html
|
|
828
1070
|
};
|
|
829
|
-
await this.alepha.emit("react:server:render:end", result);
|
|
1071
|
+
await this.alepha.events.emit("react:server:render:end", result);
|
|
830
1072
|
return result;
|
|
831
1073
|
};
|
|
832
1074
|
}
|
|
@@ -844,7 +1086,7 @@ var ReactServerProvider = class {
|
|
|
844
1086
|
layers: []
|
|
845
1087
|
};
|
|
846
1088
|
const state = entry;
|
|
847
|
-
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({
|
|
848
1090
|
user: serverRequest.user,
|
|
849
1091
|
authorization: serverRequest.headers.authorization
|
|
850
1092
|
}));
|
|
@@ -857,7 +1099,7 @@ var ReactServerProvider = class {
|
|
|
857
1099
|
}
|
|
858
1100
|
target = target.parent;
|
|
859
1101
|
}
|
|
860
|
-
await this.alepha.emit("react:server:render:begin", {
|
|
1102
|
+
await this.alepha.events.emit("react:server:render:begin", {
|
|
861
1103
|
request: serverRequest,
|
|
862
1104
|
state
|
|
863
1105
|
});
|
|
@@ -879,7 +1121,7 @@ var ReactServerProvider = class {
|
|
|
879
1121
|
state,
|
|
880
1122
|
html
|
|
881
1123
|
};
|
|
882
|
-
await this.alepha.emit("react:server:render:end", event);
|
|
1124
|
+
await this.alepha.events.emit("react:server:render:end", event);
|
|
883
1125
|
route.onServerResponse?.(serverRequest);
|
|
884
1126
|
this.log.trace("Page rendered", { name: route.name });
|
|
885
1127
|
return event.html;
|
|
@@ -887,7 +1129,7 @@ var ReactServerProvider = class {
|
|
|
887
1129
|
}
|
|
888
1130
|
renderToHtml(template, state, hydration = true) {
|
|
889
1131
|
const element = this.pageApi.root(state);
|
|
890
|
-
this.alepha.state("react.router.state", state);
|
|
1132
|
+
this.alepha.state.set("react.router.state", state);
|
|
891
1133
|
this.serverTimingProvider.beginTiming("renderToString");
|
|
892
1134
|
let app = "";
|
|
893
1135
|
try {
|
|
@@ -925,18 +1167,48 @@ var ReactServerProvider = class {
|
|
|
925
1167
|
}
|
|
926
1168
|
return response.html;
|
|
927
1169
|
}
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
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
|
+
};
|
|
1188
|
+
}
|
|
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
|
+
};
|
|
937
1201
|
}
|
|
938
|
-
|
|
939
|
-
|
|
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;
|
|
940
1212
|
}
|
|
941
1213
|
};
|
|
942
1214
|
|
|
@@ -958,17 +1230,21 @@ var ReactBrowserRouterProvider = class extends __alepha_router.RouterProvider {
|
|
|
958
1230
|
});
|
|
959
1231
|
}
|
|
960
1232
|
});
|
|
961
|
-
async transition(url, previous = []) {
|
|
1233
|
+
async transition(url, previous = [], meta = {}) {
|
|
962
1234
|
const { pathname, search } = url;
|
|
963
1235
|
const entry = {
|
|
964
1236
|
url,
|
|
965
1237
|
query: {},
|
|
966
1238
|
params: {},
|
|
967
1239
|
layers: [],
|
|
968
|
-
onError: () => null
|
|
1240
|
+
onError: () => null,
|
|
1241
|
+
meta
|
|
969
1242
|
};
|
|
970
1243
|
const state = entry;
|
|
971
|
-
await this.alepha.emit("react:transition:begin", {
|
|
1244
|
+
await this.alepha.events.emit("react:transition:begin", {
|
|
1245
|
+
previous: this.alepha.state.get("react.router.state"),
|
|
1246
|
+
state
|
|
1247
|
+
});
|
|
972
1248
|
try {
|
|
973
1249
|
const { route, params } = this.match(pathname);
|
|
974
1250
|
const query = {};
|
|
@@ -985,7 +1261,7 @@ var ReactBrowserRouterProvider = class extends __alepha_router.RouterProvider {
|
|
|
985
1261
|
index: 0,
|
|
986
1262
|
path: "/"
|
|
987
1263
|
});
|
|
988
|
-
await this.alepha.emit("react:transition:success", { state });
|
|
1264
|
+
await this.alepha.events.emit("react:transition:success", { state });
|
|
989
1265
|
} catch (e) {
|
|
990
1266
|
this.log.error("Transition has failed", e);
|
|
991
1267
|
state.layers = [{
|
|
@@ -994,7 +1270,7 @@ var ReactBrowserRouterProvider = class extends __alepha_router.RouterProvider {
|
|
|
994
1270
|
index: 0,
|
|
995
1271
|
path: "/"
|
|
996
1272
|
}];
|
|
997
|
-
await this.alepha.emit("react:transition:error", {
|
|
1273
|
+
await this.alepha.events.emit("react:transition:error", {
|
|
998
1274
|
error: e,
|
|
999
1275
|
state
|
|
1000
1276
|
});
|
|
@@ -1003,8 +1279,8 @@ var ReactBrowserRouterProvider = class extends __alepha_router.RouterProvider {
|
|
|
1003
1279
|
const layer = previous[i];
|
|
1004
1280
|
if (state.layers[i]?.name !== layer.name) this.pageApi.page(layer.name)?.onLeave?.();
|
|
1005
1281
|
}
|
|
1006
|
-
|
|
1007
|
-
this.alepha.
|
|
1282
|
+
this.alepha.state.set("react.router.state", state);
|
|
1283
|
+
await this.alepha.events.emit("react:transition:end", { state });
|
|
1008
1284
|
}
|
|
1009
1285
|
root(state) {
|
|
1010
1286
|
return this.pageApi.root(state);
|
|
@@ -1021,7 +1297,6 @@ var ReactBrowserProvider = class {
|
|
|
1021
1297
|
alepha = (0, __alepha_core.$inject)(__alepha_core.Alepha);
|
|
1022
1298
|
router = (0, __alepha_core.$inject)(ReactBrowserRouterProvider);
|
|
1023
1299
|
dateTimeProvider = (0, __alepha_core.$inject)(__alepha_datetime.DateTimeProvider);
|
|
1024
|
-
root;
|
|
1025
1300
|
options = { scrollRestoration: "top" };
|
|
1026
1301
|
getRootElement() {
|
|
1027
1302
|
const root = this.document.getElementById(this.env.REACT_ROOT_ID);
|
|
@@ -1033,7 +1308,7 @@ var ReactBrowserProvider = class {
|
|
|
1033
1308
|
}
|
|
1034
1309
|
transitioning;
|
|
1035
1310
|
get state() {
|
|
1036
|
-
return this.alepha.state("react.router.state");
|
|
1311
|
+
return this.alepha.state.get("react.router.state");
|
|
1037
1312
|
}
|
|
1038
1313
|
/**
|
|
1039
1314
|
* Accessor for Document DOM API.
|
|
@@ -1097,7 +1372,8 @@ var ReactBrowserProvider = class {
|
|
|
1097
1372
|
});
|
|
1098
1373
|
await this.render({
|
|
1099
1374
|
url,
|
|
1100
|
-
previous: options.force ? [] : this.state.layers
|
|
1375
|
+
previous: options.force ? [] : this.state.layers,
|
|
1376
|
+
meta: options.meta
|
|
1101
1377
|
});
|
|
1102
1378
|
if (this.state.url.pathname + this.state.url.search !== url) {
|
|
1103
1379
|
this.pushState(this.state.url.pathname + this.state.url.search);
|
|
@@ -1114,7 +1390,7 @@ var ReactBrowserProvider = class {
|
|
|
1114
1390
|
from: this.state?.url.pathname
|
|
1115
1391
|
};
|
|
1116
1392
|
this.log.debug("Transitioning...", { to: url });
|
|
1117
|
-
const redirect = await this.router.transition(new URL(`http://localhost${url}`), previous);
|
|
1393
|
+
const redirect = await this.router.transition(new URL(`http://localhost${url}`), previous, options.meta);
|
|
1118
1394
|
if (redirect) {
|
|
1119
1395
|
this.log.info("Redirecting to", { redirect });
|
|
1120
1396
|
return await this.render({ url: redirect });
|
|
@@ -1136,7 +1412,7 @@ var ReactBrowserProvider = class {
|
|
|
1136
1412
|
onTransitionEnd = (0, __alepha_core.$hook)({
|
|
1137
1413
|
on: "react:transition:end",
|
|
1138
1414
|
handler: () => {
|
|
1139
|
-
if (this.options.scrollRestoration === "top" && typeof window !== "undefined") {
|
|
1415
|
+
if (this.options.scrollRestoration === "top" && typeof window !== "undefined" && !this.alepha.isTest()) {
|
|
1140
1416
|
this.log.trace("Restoring scroll position to top");
|
|
1141
1417
|
window.scrollTo(0, 0);
|
|
1142
1418
|
}
|
|
@@ -1148,18 +1424,16 @@ var ReactBrowserProvider = class {
|
|
|
1148
1424
|
const hydration = this.getHydrationState();
|
|
1149
1425
|
const previous = hydration?.layers ?? [];
|
|
1150
1426
|
if (hydration) {
|
|
1151
|
-
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);
|
|
1152
1428
|
}
|
|
1153
1429
|
await this.render({ previous });
|
|
1154
1430
|
const element = this.router.root(this.state);
|
|
1155
|
-
|
|
1156
|
-
|
|
1157
|
-
this.
|
|
1158
|
-
|
|
1159
|
-
|
|
1160
|
-
|
|
1161
|
-
this.log.info("Created root element");
|
|
1162
|
-
}
|
|
1431
|
+
await this.alepha.events.emit("react:browser:render", {
|
|
1432
|
+
element,
|
|
1433
|
+
root: this.getRootElement(),
|
|
1434
|
+
hydration,
|
|
1435
|
+
state: this.state
|
|
1436
|
+
});
|
|
1163
1437
|
window.addEventListener("popstate", () => {
|
|
1164
1438
|
if (this.base + this.state.url.pathname === this.location.pathname) return;
|
|
1165
1439
|
this.log.debug("Popstate event triggered - rendering new state", { url: this.location.pathname + this.location.search });
|
|
@@ -1175,7 +1449,7 @@ var ReactRouter = class {
|
|
|
1175
1449
|
alepha = (0, __alepha_core.$inject)(__alepha_core.Alepha);
|
|
1176
1450
|
pageApi = (0, __alepha_core.$inject)(ReactPageProvider);
|
|
1177
1451
|
get state() {
|
|
1178
|
-
return this.alepha.state("react.router.state");
|
|
1452
|
+
return this.alepha.state.get("react.router.state");
|
|
1179
1453
|
}
|
|
1180
1454
|
get pages() {
|
|
1181
1455
|
return this.pageApi.getPages();
|
|
@@ -1298,44 +1572,12 @@ const useRouter = () => {
|
|
|
1298
1572
|
//#region src/components/Link.tsx
|
|
1299
1573
|
const Link = (props) => {
|
|
1300
1574
|
const router = useRouter();
|
|
1301
|
-
const { to,...anchorProps } = props;
|
|
1302
1575
|
return /* @__PURE__ */ (0, react_jsx_runtime.jsx)("a", {
|
|
1303
|
-
...
|
|
1304
|
-
...
|
|
1576
|
+
...props,
|
|
1577
|
+
...router.anchor(props.href),
|
|
1305
1578
|
children: props.children
|
|
1306
1579
|
});
|
|
1307
1580
|
};
|
|
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
1581
|
|
|
1340
1582
|
//#endregion
|
|
1341
1583
|
//#region src/hooks/useActive.ts
|
|
@@ -1393,7 +1635,7 @@ const useQueryParams = (schema, options = {}) => {
|
|
|
1393
1635
|
const key = options.key ?? "q";
|
|
1394
1636
|
const router = useRouter();
|
|
1395
1637
|
const querystring = router.query[key];
|
|
1396
|
-
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]));
|
|
1397
1639
|
(0, react.useEffect)(() => {
|
|
1398
1640
|
setQueryParams(decode(alepha, schema, querystring));
|
|
1399
1641
|
}, [querystring]);
|
|
@@ -1413,8 +1655,8 @@ const encode = (alepha, schema, data) => {
|
|
|
1413
1655
|
const decode = (alepha, schema, data) => {
|
|
1414
1656
|
try {
|
|
1415
1657
|
return alepha.parse(schema, JSON.parse(atob(decodeURIComponent(data))));
|
|
1416
|
-
} catch
|
|
1417
|
-
return
|
|
1658
|
+
} catch {
|
|
1659
|
+
return;
|
|
1418
1660
|
}
|
|
1419
1661
|
};
|
|
1420
1662
|
|
|
@@ -1480,10 +1722,10 @@ const AlephaReact = (0, __alepha_core.$module)({
|
|
|
1480
1722
|
exports.$page = $page;
|
|
1481
1723
|
exports.AlephaContext = AlephaContext;
|
|
1482
1724
|
exports.AlephaReact = AlephaReact;
|
|
1483
|
-
exports.ClientOnly =
|
|
1484
|
-
exports.ErrorBoundary =
|
|
1485
|
-
exports.ErrorViewer =
|
|
1486
|
-
exports.Link =
|
|
1725
|
+
exports.ClientOnly = ClientOnly;
|
|
1726
|
+
exports.ErrorBoundary = ErrorBoundary;
|
|
1727
|
+
exports.ErrorViewer = ErrorViewer;
|
|
1728
|
+
exports.Link = Link;
|
|
1487
1729
|
exports.NestedView = NestedView_default;
|
|
1488
1730
|
exports.NotFound = NotFoundPage;
|
|
1489
1731
|
exports.PageDescriptor = PageDescriptor;
|