@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.js
CHANGED
|
@@ -1,21 +1,105 @@
|
|
|
1
1
|
import { $env, $hook, $inject, $module, Alepha, AlephaError, Descriptor, KIND, createDescriptor, t } from "@alepha/core";
|
|
2
|
-
import { AlephaServer, HttpClient, ServerRouterProvider, ServerTimingProvider } from "@alepha/server";
|
|
2
|
+
import { AlephaServer, HttpClient, ServerProvider, ServerRouterProvider, ServerTimingProvider } from "@alepha/server";
|
|
3
3
|
import { AlephaServerCache } from "@alepha/server-cache";
|
|
4
4
|
import { AlephaServerLinks, LinkProvider, ServerLinksProvider } from "@alepha/server-links";
|
|
5
5
|
import { $logger } from "@alepha/logger";
|
|
6
|
-
import React, { StrictMode, createContext, createElement, useContext, useEffect, useMemo, useState } from "react";
|
|
7
|
-
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
|
+
import React, { StrictMode, createContext, createElement, memo, use, useContext, useEffect, useMemo, useRef, useState } from "react";
|
|
7
|
+
import { Fragment, jsx, jsxs } from "react/jsx-runtime";
|
|
8
8
|
import { existsSync } from "node:fs";
|
|
9
9
|
import { join } from "node:path";
|
|
10
10
|
import { ServerStaticProvider } from "@alepha/server-static";
|
|
11
11
|
import { renderToString } from "react-dom/server";
|
|
12
12
|
import { DateTimeProvider } from "@alepha/datetime";
|
|
13
|
-
import { createRoot, hydrateRoot } from "react-dom/client";
|
|
14
13
|
import { RouterProvider } from "@alepha/router";
|
|
15
14
|
|
|
16
15
|
//#region src/descriptors/$page.ts
|
|
17
16
|
/**
|
|
18
17
|
* Main descriptor for defining a React route in the application.
|
|
18
|
+
*
|
|
19
|
+
* The $page descriptor is the core building block for creating type-safe, SSR-enabled React routes.
|
|
20
|
+
* It provides a declarative way to define pages with powerful features:
|
|
21
|
+
*
|
|
22
|
+
* **Routing & Navigation**
|
|
23
|
+
* - URL pattern matching with parameters (e.g., `/users/:id`)
|
|
24
|
+
* - Nested routing with parent-child relationships
|
|
25
|
+
* - Type-safe URL parameter and query string validation
|
|
26
|
+
*
|
|
27
|
+
* **Data Loading**
|
|
28
|
+
* - Server-side data fetching with the `resolve` function
|
|
29
|
+
* - Automatic serialization and hydration for SSR
|
|
30
|
+
* - Access to request context, URL params, and parent data
|
|
31
|
+
*
|
|
32
|
+
* **Component Loading**
|
|
33
|
+
* - Direct component rendering or lazy loading for code splitting
|
|
34
|
+
* - Client-only rendering when browser APIs are needed
|
|
35
|
+
* - Automatic fallback handling during hydration
|
|
36
|
+
*
|
|
37
|
+
* **Performance Optimization**
|
|
38
|
+
* - Static generation for pre-rendered pages at build time
|
|
39
|
+
* - Server-side caching with configurable TTL and providers
|
|
40
|
+
* - Code splitting through lazy component loading
|
|
41
|
+
*
|
|
42
|
+
* **Error Handling**
|
|
43
|
+
* - Custom error handlers with support for redirects
|
|
44
|
+
* - Hierarchical error handling (child → parent)
|
|
45
|
+
* - HTTP status code handling (404, 401, etc.)
|
|
46
|
+
*
|
|
47
|
+
* **Page Animations**
|
|
48
|
+
* - CSS-based enter/exit animations
|
|
49
|
+
* - Dynamic animations based on page state
|
|
50
|
+
* - Custom timing and easing functions
|
|
51
|
+
*
|
|
52
|
+
* **Lifecycle Management**
|
|
53
|
+
* - Server response hooks for headers and status codes
|
|
54
|
+
* - Page leave handlers for cleanup (browser only)
|
|
55
|
+
* - Permission-based access control
|
|
56
|
+
*
|
|
57
|
+
* @example Simple page with data fetching
|
|
58
|
+
* ```typescript
|
|
59
|
+
* const userProfile = $page({
|
|
60
|
+
* path: "/users/:id",
|
|
61
|
+
* schema: {
|
|
62
|
+
* params: t.object({ id: t.int() }),
|
|
63
|
+
* query: t.object({ tab: t.optional(t.string()) })
|
|
64
|
+
* },
|
|
65
|
+
* resolve: async ({ params }) => {
|
|
66
|
+
* const user = await userApi.getUser(params.id);
|
|
67
|
+
* return { user };
|
|
68
|
+
* },
|
|
69
|
+
* lazy: () => import("./UserProfile.tsx")
|
|
70
|
+
* });
|
|
71
|
+
* ```
|
|
72
|
+
*
|
|
73
|
+
* @example Nested routing with error handling
|
|
74
|
+
* ```typescript
|
|
75
|
+
* const projectSection = $page({
|
|
76
|
+
* path: "/projects/:id",
|
|
77
|
+
* children: () => [projectBoard, projectSettings],
|
|
78
|
+
* resolve: async ({ params }) => {
|
|
79
|
+
* const project = await projectApi.get(params.id);
|
|
80
|
+
* return { project };
|
|
81
|
+
* },
|
|
82
|
+
* errorHandler: (error) => {
|
|
83
|
+
* if (HttpError.is(error, 404)) {
|
|
84
|
+
* return <ProjectNotFound />;
|
|
85
|
+
* }
|
|
86
|
+
* }
|
|
87
|
+
* });
|
|
88
|
+
* ```
|
|
89
|
+
*
|
|
90
|
+
* @example Static generation with caching
|
|
91
|
+
* ```typescript
|
|
92
|
+
* const blogPost = $page({
|
|
93
|
+
* path: "/blog/:slug",
|
|
94
|
+
* static: {
|
|
95
|
+
* entries: posts.map(p => ({ params: { slug: p.slug } }))
|
|
96
|
+
* },
|
|
97
|
+
* resolve: async ({ params }) => {
|
|
98
|
+
* const post = await loadPost(params.slug);
|
|
99
|
+
* return { post };
|
|
100
|
+
* }
|
|
101
|
+
* });
|
|
102
|
+
* ```
|
|
19
103
|
*/
|
|
20
104
|
const $page = (options) => {
|
|
21
105
|
return createDescriptor(PageDescriptor, options);
|
|
@@ -35,7 +119,10 @@ var PageDescriptor = class extends Descriptor {
|
|
|
35
119
|
* Only valid for server-side rendering, it will throw an error if called on the client-side.
|
|
36
120
|
*/
|
|
37
121
|
async render(options) {
|
|
38
|
-
throw new
|
|
122
|
+
throw new AlephaError("render() method is not implemented in this environment");
|
|
123
|
+
}
|
|
124
|
+
async fetch(options) {
|
|
125
|
+
throw new AlephaError("fetch() method is not implemented in this environment");
|
|
39
126
|
}
|
|
40
127
|
match(url) {
|
|
41
128
|
return false;
|
|
@@ -64,7 +151,6 @@ const ClientOnly = (props) => {
|
|
|
64
151
|
if (props.disabled) return props.children;
|
|
65
152
|
return mounted ? props.children : props.fallback;
|
|
66
153
|
};
|
|
67
|
-
var ClientOnly_default = ClientOnly;
|
|
68
154
|
|
|
69
155
|
//#endregion
|
|
70
156
|
//#region src/components/ErrorViewer.tsx
|
|
@@ -172,7 +258,6 @@ const ErrorViewer = ({ error, alepha }) => {
|
|
|
172
258
|
})] })]
|
|
173
259
|
});
|
|
174
260
|
};
|
|
175
|
-
var ErrorViewer_default = ErrorViewer;
|
|
176
261
|
const ErrorViewerProduction = () => {
|
|
177
262
|
const styles = {
|
|
178
263
|
container: {
|
|
@@ -248,7 +333,7 @@ const AlephaContext = createContext(void 0);
|
|
|
248
333
|
*
|
|
249
334
|
* - alepha.state() for state management
|
|
250
335
|
* - alepha.inject() for dependency injection
|
|
251
|
-
* - alepha.emit() for event handling
|
|
336
|
+
* - alepha.events.emit() for event handling
|
|
252
337
|
* etc...
|
|
253
338
|
*/
|
|
254
339
|
const useAlepha = () => {
|
|
@@ -266,19 +351,55 @@ const useRouterEvents = (opts = {}, deps = []) => {
|
|
|
266
351
|
const alepha = useAlepha();
|
|
267
352
|
useEffect(() => {
|
|
268
353
|
if (!alepha.isBrowser()) return;
|
|
354
|
+
const cb = (callback) => {
|
|
355
|
+
if (typeof callback === "function") return { callback };
|
|
356
|
+
return callback;
|
|
357
|
+
};
|
|
269
358
|
const subs = [];
|
|
270
359
|
const onBegin = opts.onBegin;
|
|
271
360
|
const onEnd = opts.onEnd;
|
|
272
361
|
const onError = opts.onError;
|
|
273
|
-
|
|
274
|
-
if (
|
|
275
|
-
if (
|
|
362
|
+
const onSuccess = opts.onSuccess;
|
|
363
|
+
if (onBegin) subs.push(alepha.events.on("react:transition:begin", cb(onBegin)));
|
|
364
|
+
if (onEnd) subs.push(alepha.events.on("react:transition:end", cb(onEnd)));
|
|
365
|
+
if (onError) subs.push(alepha.events.on("react:transition:error", cb(onError)));
|
|
366
|
+
if (onSuccess) subs.push(alepha.events.on("react:transition:success", cb(onSuccess)));
|
|
276
367
|
return () => {
|
|
277
368
|
for (const sub of subs) sub();
|
|
278
369
|
};
|
|
279
370
|
}, deps);
|
|
280
371
|
};
|
|
281
372
|
|
|
373
|
+
//#endregion
|
|
374
|
+
//#region src/hooks/useStore.ts
|
|
375
|
+
/**
|
|
376
|
+
* Hook to access and mutate the Alepha state.
|
|
377
|
+
*/
|
|
378
|
+
const useStore = (key, defaultValue) => {
|
|
379
|
+
const alepha = useAlepha();
|
|
380
|
+
useMemo(() => {
|
|
381
|
+
if (defaultValue != null && alepha.state.get(key) == null) alepha.state.set(key, defaultValue);
|
|
382
|
+
}, [defaultValue]);
|
|
383
|
+
const [state, setState] = useState(alepha.state.get(key));
|
|
384
|
+
useEffect(() => {
|
|
385
|
+
if (!alepha.isBrowser()) return;
|
|
386
|
+
return alepha.events.on("state:mutate", (ev) => {
|
|
387
|
+
if (ev.key === key) setState(ev.value);
|
|
388
|
+
});
|
|
389
|
+
}, []);
|
|
390
|
+
return [state, (value) => {
|
|
391
|
+
alepha.state.set(key, value);
|
|
392
|
+
}];
|
|
393
|
+
};
|
|
394
|
+
|
|
395
|
+
//#endregion
|
|
396
|
+
//#region src/hooks/useRouterState.ts
|
|
397
|
+
const useRouterState = () => {
|
|
398
|
+
const [state] = useStore("react.router.state");
|
|
399
|
+
if (!state) throw new AlephaError("Missing react router state");
|
|
400
|
+
return state;
|
|
401
|
+
};
|
|
402
|
+
|
|
282
403
|
//#endregion
|
|
283
404
|
//#region src/components/ErrorBoundary.tsx
|
|
284
405
|
/**
|
|
@@ -308,7 +429,6 @@ var ErrorBoundary = class extends React.Component {
|
|
|
308
429
|
return this.props.children;
|
|
309
430
|
}
|
|
310
431
|
};
|
|
311
|
-
var ErrorBoundary_default = ErrorBoundary;
|
|
312
432
|
|
|
313
433
|
//#endregion
|
|
314
434
|
//#region src/components/NestedView.tsx
|
|
@@ -319,7 +439,7 @@ var ErrorBoundary_default = ErrorBoundary;
|
|
|
319
439
|
*
|
|
320
440
|
* @example
|
|
321
441
|
* ```tsx
|
|
322
|
-
* import { NestedView } from "
|
|
442
|
+
* import { NestedView } from "alepha/react";
|
|
323
443
|
*
|
|
324
444
|
* class App {
|
|
325
445
|
* parent = $page({
|
|
@@ -334,17 +454,69 @@ var ErrorBoundary_default = ErrorBoundary;
|
|
|
334
454
|
* ```
|
|
335
455
|
*/
|
|
336
456
|
const NestedView = (props) => {
|
|
337
|
-
const
|
|
338
|
-
const
|
|
339
|
-
const alepha = useAlepha();
|
|
340
|
-
const state = alepha.state("react.router.state");
|
|
341
|
-
if (!state) throw new Error("<NestedView/> must be used inside a RouterLayerContext.");
|
|
457
|
+
const index = use(RouterLayerContext)?.index ?? 0;
|
|
458
|
+
const state = useRouterState();
|
|
342
459
|
const [view, setView] = useState(state.layers[index]?.element);
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
|
|
460
|
+
const [animation, setAnimation] = useState("");
|
|
461
|
+
const animationExitDuration = useRef(0);
|
|
462
|
+
const animationExitNow = useRef(0);
|
|
463
|
+
useRouterEvents({
|
|
464
|
+
onBegin: async ({ previous, state: state$1 }) => {
|
|
465
|
+
const layer = previous.layers[index];
|
|
466
|
+
if (`${state$1.url.pathname}/`.startsWith(`${layer?.path}/`)) return;
|
|
467
|
+
const animationExit = parseAnimation(layer.route?.animation, state$1, "exit");
|
|
468
|
+
if (animationExit) {
|
|
469
|
+
const duration = animationExit.duration || 200;
|
|
470
|
+
animationExitNow.current = Date.now();
|
|
471
|
+
animationExitDuration.current = duration;
|
|
472
|
+
setAnimation(animationExit.animation);
|
|
473
|
+
} else {
|
|
474
|
+
animationExitNow.current = 0;
|
|
475
|
+
animationExitDuration.current = 0;
|
|
476
|
+
setAnimation("");
|
|
477
|
+
}
|
|
478
|
+
},
|
|
479
|
+
onEnd: async ({ state: state$1 }) => {
|
|
480
|
+
const layer = state$1.layers[index];
|
|
481
|
+
if (animationExitNow.current) {
|
|
482
|
+
const duration = animationExitDuration.current;
|
|
483
|
+
const diff = Date.now() - animationExitNow.current;
|
|
484
|
+
if (diff < duration) await new Promise((resolve) => setTimeout(resolve, duration - diff));
|
|
485
|
+
}
|
|
486
|
+
if (!layer?.cache) {
|
|
487
|
+
setView(layer?.element);
|
|
488
|
+
const animationEnter = parseAnimation(layer?.route?.animation, state$1, "enter");
|
|
489
|
+
if (animationEnter) setAnimation(animationEnter.animation);
|
|
490
|
+
else setAnimation("");
|
|
491
|
+
}
|
|
492
|
+
}
|
|
493
|
+
}, []);
|
|
494
|
+
let element = view ?? props.children ?? null;
|
|
495
|
+
if (animation) element = /* @__PURE__ */ jsx("div", {
|
|
496
|
+
style: {
|
|
497
|
+
display: "flex",
|
|
498
|
+
flex: 1,
|
|
499
|
+
height: "100%",
|
|
500
|
+
width: "100%",
|
|
501
|
+
position: "relative",
|
|
502
|
+
overflow: "hidden"
|
|
503
|
+
},
|
|
504
|
+
children: /* @__PURE__ */ jsx("div", {
|
|
505
|
+
style: {
|
|
506
|
+
height: "100%",
|
|
507
|
+
width: "100%",
|
|
508
|
+
display: "flex",
|
|
509
|
+
animation
|
|
510
|
+
},
|
|
511
|
+
children: element
|
|
512
|
+
})
|
|
513
|
+
});
|
|
514
|
+
if (props.errorBoundary === false) return /* @__PURE__ */ jsx(Fragment, { children: element });
|
|
515
|
+
if (props.errorBoundary) return /* @__PURE__ */ jsx(ErrorBoundary, {
|
|
516
|
+
fallback: props.errorBoundary,
|
|
517
|
+
children: element
|
|
518
|
+
});
|
|
519
|
+
return /* @__PURE__ */ jsx(ErrorBoundary, {
|
|
348
520
|
fallback: (error) => {
|
|
349
521
|
const result = state.onError(error, state);
|
|
350
522
|
if (result instanceof Redirection) return "Redirection inside ErrorBoundary is not allowed.";
|
|
@@ -353,7 +525,37 @@ const NestedView = (props) => {
|
|
|
353
525
|
children: element
|
|
354
526
|
});
|
|
355
527
|
};
|
|
356
|
-
var NestedView_default = NestedView;
|
|
528
|
+
var NestedView_default = memo(NestedView);
|
|
529
|
+
function parseAnimation(animationLike, state, type = "enter") {
|
|
530
|
+
if (!animationLike) return void 0;
|
|
531
|
+
const DEFAULT_DURATION = 300;
|
|
532
|
+
const animation = typeof animationLike === "function" ? animationLike(state) : animationLike;
|
|
533
|
+
if (typeof animation === "string") {
|
|
534
|
+
if (type === "exit") return;
|
|
535
|
+
return {
|
|
536
|
+
duration: DEFAULT_DURATION,
|
|
537
|
+
animation: `${DEFAULT_DURATION}ms ${animation}`
|
|
538
|
+
};
|
|
539
|
+
}
|
|
540
|
+
if (typeof animation === "object") {
|
|
541
|
+
const anim = animation[type];
|
|
542
|
+
const duration = typeof anim === "object" ? anim.duration ?? DEFAULT_DURATION : DEFAULT_DURATION;
|
|
543
|
+
const name = typeof anim === "object" ? anim.name : anim;
|
|
544
|
+
if (type === "exit") {
|
|
545
|
+
const timing$1 = typeof anim === "object" ? anim.timing ?? "" : "";
|
|
546
|
+
return {
|
|
547
|
+
duration,
|
|
548
|
+
animation: `${duration}ms ${timing$1} ${name}`
|
|
549
|
+
};
|
|
550
|
+
}
|
|
551
|
+
const timing = typeof anim === "object" ? anim.timing ?? "" : "";
|
|
552
|
+
return {
|
|
553
|
+
duration,
|
|
554
|
+
animation: `${duration}ms ${timing} ${name}`
|
|
555
|
+
};
|
|
556
|
+
}
|
|
557
|
+
return void 0;
|
|
558
|
+
}
|
|
357
559
|
|
|
358
560
|
//#endregion
|
|
359
561
|
//#region src/components/NotFound.tsx
|
|
@@ -419,6 +621,14 @@ var ReactPageProvider = class {
|
|
|
419
621
|
if (this.env.REACT_STRICT_MODE) return createElement(StrictMode, {}, root);
|
|
420
622
|
return root;
|
|
421
623
|
}
|
|
624
|
+
convertStringObjectToObject = (schema, value) => {
|
|
625
|
+
if (t.schema.isObject(schema) && typeof value === "object") {
|
|
626
|
+
for (const key in schema.properties) if (t.schema.isObject(schema.properties[key]) && typeof value[key] === "string") try {
|
|
627
|
+
value[key] = this.alepha.parse(schema.properties[key], decodeURIComponent(value[key]));
|
|
628
|
+
} catch (e) {}
|
|
629
|
+
}
|
|
630
|
+
return value;
|
|
631
|
+
};
|
|
422
632
|
/**
|
|
423
633
|
* Create a new RouterState based on a given route and request.
|
|
424
634
|
* This method resolves the layers for the route, applying any query and params schemas defined in the route.
|
|
@@ -438,6 +648,7 @@ var ReactPageProvider = class {
|
|
|
438
648
|
const route$1 = it.route;
|
|
439
649
|
const config = {};
|
|
440
650
|
try {
|
|
651
|
+
this.convertStringObjectToObject(route$1.schema?.query, state.query);
|
|
441
652
|
config.query = route$1.schema?.query ? this.alepha.parse(route$1.schema.query, state.query) : {};
|
|
442
653
|
} catch (e) {
|
|
443
654
|
it.error = e;
|
|
@@ -564,6 +775,7 @@ var ReactPageProvider = class {
|
|
|
564
775
|
}
|
|
565
776
|
}
|
|
566
777
|
async createElement(page, props) {
|
|
778
|
+
if (page.lazy && page.component) this.log.warn(`Page ${page.name} has both lazy and component options, lazy will be used`);
|
|
567
779
|
if (page.lazy) {
|
|
568
780
|
const component = await page.lazy();
|
|
569
781
|
return createElement(component.default, props);
|
|
@@ -572,7 +784,7 @@ var ReactPageProvider = class {
|
|
|
572
784
|
return void 0;
|
|
573
785
|
}
|
|
574
786
|
renderError(error) {
|
|
575
|
-
return createElement(
|
|
787
|
+
return createElement(ErrorViewer, {
|
|
576
788
|
error,
|
|
577
789
|
alepha: this.alepha
|
|
578
790
|
});
|
|
@@ -598,7 +810,7 @@ var ReactPageProvider = class {
|
|
|
598
810
|
}
|
|
599
811
|
renderView(index, path, view, page) {
|
|
600
812
|
view ??= this.renderEmptyView();
|
|
601
|
-
const element = page.client ? createElement(
|
|
813
|
+
const element = page.client ? createElement(ClientOnly, typeof page.client === "object" ? page.client : {}, view) : view;
|
|
602
814
|
return createElement(RouterLayerContext.Provider, { value: {
|
|
603
815
|
index,
|
|
604
816
|
path
|
|
@@ -692,18 +904,36 @@ var ReactServerProvider = class {
|
|
|
692
904
|
log = $logger();
|
|
693
905
|
alepha = $inject(Alepha);
|
|
694
906
|
pageApi = $inject(ReactPageProvider);
|
|
907
|
+
serverProvider = $inject(ServerProvider);
|
|
695
908
|
serverStaticProvider = $inject(ServerStaticProvider);
|
|
696
909
|
serverRouterProvider = $inject(ServerRouterProvider);
|
|
697
910
|
serverTimingProvider = $inject(ServerTimingProvider);
|
|
698
911
|
env = $env(envSchema$1);
|
|
699
912
|
ROOT_DIV_REGEX = new RegExp(`<div([^>]*)\\s+id=["']${this.env.REACT_ROOT_ID}["']([^>]*)>(.*?)<\\/div>`, "is");
|
|
913
|
+
preprocessedTemplate = null;
|
|
700
914
|
onConfigure = $hook({
|
|
701
915
|
on: "configure",
|
|
702
916
|
handler: async () => {
|
|
703
917
|
const pages = this.alepha.descriptors($page);
|
|
704
918
|
const ssrEnabled = pages.length > 0 && this.env.REACT_SSR_ENABLED !== false;
|
|
705
|
-
this.alepha.state("react.server.ssr", ssrEnabled);
|
|
706
|
-
for (const page of pages)
|
|
919
|
+
this.alepha.state.set("react.server.ssr", ssrEnabled);
|
|
920
|
+
for (const page of pages) {
|
|
921
|
+
page.render = this.createRenderFunction(page.name);
|
|
922
|
+
page.fetch = async (options) => {
|
|
923
|
+
const response = await fetch(`${this.serverProvider.hostname}/${page.pathname(options)}`);
|
|
924
|
+
const html = await response.text();
|
|
925
|
+
if (options?.html) return {
|
|
926
|
+
html,
|
|
927
|
+
response
|
|
928
|
+
};
|
|
929
|
+
const match = html.match(this.ROOT_DIV_REGEX);
|
|
930
|
+
if (match) return {
|
|
931
|
+
html: match[3],
|
|
932
|
+
response
|
|
933
|
+
};
|
|
934
|
+
throw new AlephaError("Invalid HTML response");
|
|
935
|
+
};
|
|
936
|
+
}
|
|
707
937
|
if (this.alepha.isServerless() === "vite") {
|
|
708
938
|
await this.configureVite(ssrEnabled);
|
|
709
939
|
return;
|
|
@@ -742,6 +972,8 @@ var ReactServerProvider = class {
|
|
|
742
972
|
return this.alepha.env.REACT_SERVER_TEMPLATE ?? "<!DOCTYPE html><html lang='en'><head></head><body></body></html>";
|
|
743
973
|
}
|
|
744
974
|
async registerPages(templateLoader) {
|
|
975
|
+
const template = await templateLoader();
|
|
976
|
+
if (template) this.preprocessedTemplate = this.preprocessTemplate(template);
|
|
745
977
|
for (const page of this.pageApi.getPages()) {
|
|
746
978
|
if (page.children?.length) continue;
|
|
747
979
|
this.log.debug(`+ ${page.match} -> ${page.name}`);
|
|
@@ -783,27 +1015,37 @@ var ReactServerProvider = class {
|
|
|
783
1015
|
params: options.params ?? {},
|
|
784
1016
|
query: options.query ?? {},
|
|
785
1017
|
onError: () => null,
|
|
786
|
-
layers: []
|
|
1018
|
+
layers: [],
|
|
1019
|
+
meta: {}
|
|
787
1020
|
};
|
|
788
1021
|
const state = entry;
|
|
789
1022
|
this.log.trace("Rendering", { url });
|
|
790
|
-
await this.alepha.emit("react:server:render:begin", { state });
|
|
1023
|
+
await this.alepha.events.emit("react:server:render:begin", { state });
|
|
791
1024
|
const { redirect } = await this.pageApi.createLayers(page, state);
|
|
792
|
-
if (redirect)
|
|
1025
|
+
if (redirect) return {
|
|
1026
|
+
state,
|
|
1027
|
+
html: "",
|
|
1028
|
+
redirect
|
|
1029
|
+
};
|
|
793
1030
|
if (!withIndex && !options.html) {
|
|
794
|
-
this.alepha.state("react.router.state", state);
|
|
1031
|
+
this.alepha.state.set("react.router.state", state);
|
|
795
1032
|
return {
|
|
796
1033
|
state,
|
|
797
1034
|
html: renderToString(this.pageApi.root(state))
|
|
798
1035
|
};
|
|
799
1036
|
}
|
|
800
|
-
const
|
|
801
|
-
|
|
1037
|
+
const template = this.template ?? "";
|
|
1038
|
+
const html = this.renderToHtml(template, state, options.hydration);
|
|
1039
|
+
if (html instanceof Redirection) return {
|
|
1040
|
+
state,
|
|
1041
|
+
html: "",
|
|
1042
|
+
redirect
|
|
1043
|
+
};
|
|
802
1044
|
const result = {
|
|
803
1045
|
state,
|
|
804
1046
|
html
|
|
805
1047
|
};
|
|
806
|
-
await this.alepha.emit("react:server:render:end", result);
|
|
1048
|
+
await this.alepha.events.emit("react:server:render:end", result);
|
|
807
1049
|
return result;
|
|
808
1050
|
};
|
|
809
1051
|
}
|
|
@@ -821,7 +1063,7 @@ var ReactServerProvider = class {
|
|
|
821
1063
|
layers: []
|
|
822
1064
|
};
|
|
823
1065
|
const state = entry;
|
|
824
|
-
if (this.alepha.has(ServerLinksProvider)) this.alepha.state("api", await this.alepha.inject(ServerLinksProvider).getUserApiLinks({
|
|
1066
|
+
if (this.alepha.has(ServerLinksProvider)) this.alepha.state.set("api", await this.alepha.inject(ServerLinksProvider).getUserApiLinks({
|
|
825
1067
|
user: serverRequest.user,
|
|
826
1068
|
authorization: serverRequest.headers.authorization
|
|
827
1069
|
}));
|
|
@@ -834,7 +1076,7 @@ var ReactServerProvider = class {
|
|
|
834
1076
|
}
|
|
835
1077
|
target = target.parent;
|
|
836
1078
|
}
|
|
837
|
-
await this.alepha.emit("react:server:render:begin", {
|
|
1079
|
+
await this.alepha.events.emit("react:server:render:begin", {
|
|
838
1080
|
request: serverRequest,
|
|
839
1081
|
state
|
|
840
1082
|
});
|
|
@@ -856,7 +1098,7 @@ var ReactServerProvider = class {
|
|
|
856
1098
|
state,
|
|
857
1099
|
html
|
|
858
1100
|
};
|
|
859
|
-
await this.alepha.emit("react:server:render:end", event);
|
|
1101
|
+
await this.alepha.events.emit("react:server:render:end", event);
|
|
860
1102
|
route.onServerResponse?.(serverRequest);
|
|
861
1103
|
this.log.trace("Page rendered", { name: route.name });
|
|
862
1104
|
return event.html;
|
|
@@ -864,7 +1106,7 @@ var ReactServerProvider = class {
|
|
|
864
1106
|
}
|
|
865
1107
|
renderToHtml(template, state, hydration = true) {
|
|
866
1108
|
const element = this.pageApi.root(state);
|
|
867
|
-
this.alepha.state("react.router.state", state);
|
|
1109
|
+
this.alepha.state.set("react.router.state", state);
|
|
868
1110
|
this.serverTimingProvider.beginTiming("renderToString");
|
|
869
1111
|
let app = "";
|
|
870
1112
|
try {
|
|
@@ -902,18 +1144,48 @@ var ReactServerProvider = class {
|
|
|
902
1144
|
}
|
|
903
1145
|
return response.html;
|
|
904
1146
|
}
|
|
905
|
-
|
|
906
|
-
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
912
|
-
|
|
913
|
-
|
|
1147
|
+
preprocessTemplate(template) {
|
|
1148
|
+
const bodyCloseMatch = template.match(/<\/body>/i);
|
|
1149
|
+
const bodyCloseIndex = bodyCloseMatch?.index ?? template.length;
|
|
1150
|
+
const beforeScript = template.substring(0, bodyCloseIndex);
|
|
1151
|
+
const afterScript = template.substring(bodyCloseIndex);
|
|
1152
|
+
const rootDivMatch = beforeScript.match(this.ROOT_DIV_REGEX);
|
|
1153
|
+
if (rootDivMatch) {
|
|
1154
|
+
const beforeDiv = beforeScript.substring(0, rootDivMatch.index);
|
|
1155
|
+
const afterDivStart = rootDivMatch.index + rootDivMatch[0].length;
|
|
1156
|
+
const afterDiv = beforeScript.substring(afterDivStart);
|
|
1157
|
+
const beforeApp = `${beforeDiv}<div${rootDivMatch[1]} id="${this.env.REACT_ROOT_ID}"${rootDivMatch[2]}>`;
|
|
1158
|
+
const afterApp = `</div>${afterDiv}`;
|
|
1159
|
+
return {
|
|
1160
|
+
beforeApp,
|
|
1161
|
+
afterApp,
|
|
1162
|
+
beforeScript: "",
|
|
1163
|
+
afterScript
|
|
1164
|
+
};
|
|
1165
|
+
}
|
|
1166
|
+
const bodyMatch = beforeScript.match(/<body([^>]*)>/i);
|
|
1167
|
+
if (bodyMatch) {
|
|
1168
|
+
const beforeBody = beforeScript.substring(0, bodyMatch.index + bodyMatch[0].length);
|
|
1169
|
+
const afterBody = beforeScript.substring(bodyMatch.index + bodyMatch[0].length);
|
|
1170
|
+
const beforeApp = `${beforeBody}<div id="${this.env.REACT_ROOT_ID}">`;
|
|
1171
|
+
const afterApp = `</div>${afterBody}`;
|
|
1172
|
+
return {
|
|
1173
|
+
beforeApp,
|
|
1174
|
+
afterApp,
|
|
1175
|
+
beforeScript: "",
|
|
1176
|
+
afterScript
|
|
1177
|
+
};
|
|
914
1178
|
}
|
|
915
|
-
|
|
916
|
-
|
|
1179
|
+
return {
|
|
1180
|
+
beforeApp: `<div id="${this.env.REACT_ROOT_ID}">`,
|
|
1181
|
+
afterApp: `</div>`,
|
|
1182
|
+
beforeScript,
|
|
1183
|
+
afterScript
|
|
1184
|
+
};
|
|
1185
|
+
}
|
|
1186
|
+
fillTemplate(response, app, script) {
|
|
1187
|
+
if (!this.preprocessedTemplate) this.preprocessedTemplate = this.preprocessTemplate(response.html);
|
|
1188
|
+
response.html = this.preprocessedTemplate.beforeApp + app + this.preprocessedTemplate.afterApp + script + this.preprocessedTemplate.afterScript;
|
|
917
1189
|
}
|
|
918
1190
|
};
|
|
919
1191
|
|
|
@@ -935,17 +1207,21 @@ var ReactBrowserRouterProvider = class extends RouterProvider {
|
|
|
935
1207
|
});
|
|
936
1208
|
}
|
|
937
1209
|
});
|
|
938
|
-
async transition(url, previous = []) {
|
|
1210
|
+
async transition(url, previous = [], meta = {}) {
|
|
939
1211
|
const { pathname, search } = url;
|
|
940
1212
|
const entry = {
|
|
941
1213
|
url,
|
|
942
1214
|
query: {},
|
|
943
1215
|
params: {},
|
|
944
1216
|
layers: [],
|
|
945
|
-
onError: () => null
|
|
1217
|
+
onError: () => null,
|
|
1218
|
+
meta
|
|
946
1219
|
};
|
|
947
1220
|
const state = entry;
|
|
948
|
-
await this.alepha.emit("react:transition:begin", {
|
|
1221
|
+
await this.alepha.events.emit("react:transition:begin", {
|
|
1222
|
+
previous: this.alepha.state.get("react.router.state"),
|
|
1223
|
+
state
|
|
1224
|
+
});
|
|
949
1225
|
try {
|
|
950
1226
|
const { route, params } = this.match(pathname);
|
|
951
1227
|
const query = {};
|
|
@@ -962,7 +1238,7 @@ var ReactBrowserRouterProvider = class extends RouterProvider {
|
|
|
962
1238
|
index: 0,
|
|
963
1239
|
path: "/"
|
|
964
1240
|
});
|
|
965
|
-
await this.alepha.emit("react:transition:success", { state });
|
|
1241
|
+
await this.alepha.events.emit("react:transition:success", { state });
|
|
966
1242
|
} catch (e) {
|
|
967
1243
|
this.log.error("Transition has failed", e);
|
|
968
1244
|
state.layers = [{
|
|
@@ -971,7 +1247,7 @@ var ReactBrowserRouterProvider = class extends RouterProvider {
|
|
|
971
1247
|
index: 0,
|
|
972
1248
|
path: "/"
|
|
973
1249
|
}];
|
|
974
|
-
await this.alepha.emit("react:transition:error", {
|
|
1250
|
+
await this.alepha.events.emit("react:transition:error", {
|
|
975
1251
|
error: e,
|
|
976
1252
|
state
|
|
977
1253
|
});
|
|
@@ -980,8 +1256,8 @@ var ReactBrowserRouterProvider = class extends RouterProvider {
|
|
|
980
1256
|
const layer = previous[i];
|
|
981
1257
|
if (state.layers[i]?.name !== layer.name) this.pageApi.page(layer.name)?.onLeave?.();
|
|
982
1258
|
}
|
|
983
|
-
|
|
984
|
-
this.alepha.
|
|
1259
|
+
this.alepha.state.set("react.router.state", state);
|
|
1260
|
+
await this.alepha.events.emit("react:transition:end", { state });
|
|
985
1261
|
}
|
|
986
1262
|
root(state) {
|
|
987
1263
|
return this.pageApi.root(state);
|
|
@@ -998,7 +1274,6 @@ var ReactBrowserProvider = class {
|
|
|
998
1274
|
alepha = $inject(Alepha);
|
|
999
1275
|
router = $inject(ReactBrowserRouterProvider);
|
|
1000
1276
|
dateTimeProvider = $inject(DateTimeProvider);
|
|
1001
|
-
root;
|
|
1002
1277
|
options = { scrollRestoration: "top" };
|
|
1003
1278
|
getRootElement() {
|
|
1004
1279
|
const root = this.document.getElementById(this.env.REACT_ROOT_ID);
|
|
@@ -1010,7 +1285,7 @@ var ReactBrowserProvider = class {
|
|
|
1010
1285
|
}
|
|
1011
1286
|
transitioning;
|
|
1012
1287
|
get state() {
|
|
1013
|
-
return this.alepha.state("react.router.state");
|
|
1288
|
+
return this.alepha.state.get("react.router.state");
|
|
1014
1289
|
}
|
|
1015
1290
|
/**
|
|
1016
1291
|
* Accessor for Document DOM API.
|
|
@@ -1074,7 +1349,8 @@ var ReactBrowserProvider = class {
|
|
|
1074
1349
|
});
|
|
1075
1350
|
await this.render({
|
|
1076
1351
|
url,
|
|
1077
|
-
previous: options.force ? [] : this.state.layers
|
|
1352
|
+
previous: options.force ? [] : this.state.layers,
|
|
1353
|
+
meta: options.meta
|
|
1078
1354
|
});
|
|
1079
1355
|
if (this.state.url.pathname + this.state.url.search !== url) {
|
|
1080
1356
|
this.pushState(this.state.url.pathname + this.state.url.search);
|
|
@@ -1091,7 +1367,7 @@ var ReactBrowserProvider = class {
|
|
|
1091
1367
|
from: this.state?.url.pathname
|
|
1092
1368
|
};
|
|
1093
1369
|
this.log.debug("Transitioning...", { to: url });
|
|
1094
|
-
const redirect = await this.router.transition(new URL(`http://localhost${url}`), previous);
|
|
1370
|
+
const redirect = await this.router.transition(new URL(`http://localhost${url}`), previous, options.meta);
|
|
1095
1371
|
if (redirect) {
|
|
1096
1372
|
this.log.info("Redirecting to", { redirect });
|
|
1097
1373
|
return await this.render({ url: redirect });
|
|
@@ -1113,7 +1389,7 @@ var ReactBrowserProvider = class {
|
|
|
1113
1389
|
onTransitionEnd = $hook({
|
|
1114
1390
|
on: "react:transition:end",
|
|
1115
1391
|
handler: () => {
|
|
1116
|
-
if (this.options.scrollRestoration === "top" && typeof window !== "undefined") {
|
|
1392
|
+
if (this.options.scrollRestoration === "top" && typeof window !== "undefined" && !this.alepha.isTest()) {
|
|
1117
1393
|
this.log.trace("Restoring scroll position to top");
|
|
1118
1394
|
window.scrollTo(0, 0);
|
|
1119
1395
|
}
|
|
@@ -1125,18 +1401,16 @@ var ReactBrowserProvider = class {
|
|
|
1125
1401
|
const hydration = this.getHydrationState();
|
|
1126
1402
|
const previous = hydration?.layers ?? [];
|
|
1127
1403
|
if (hydration) {
|
|
1128
|
-
for (const [key, value] of Object.entries(hydration)) if (key !== "layers") this.alepha.state(key, value);
|
|
1404
|
+
for (const [key, value] of Object.entries(hydration)) if (key !== "layers") this.alepha.state.set(key, value);
|
|
1129
1405
|
}
|
|
1130
1406
|
await this.render({ previous });
|
|
1131
1407
|
const element = this.router.root(this.state);
|
|
1132
|
-
|
|
1133
|
-
|
|
1134
|
-
this.
|
|
1135
|
-
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
this.log.info("Created root element");
|
|
1139
|
-
}
|
|
1408
|
+
await this.alepha.events.emit("react:browser:render", {
|
|
1409
|
+
element,
|
|
1410
|
+
root: this.getRootElement(),
|
|
1411
|
+
hydration,
|
|
1412
|
+
state: this.state
|
|
1413
|
+
});
|
|
1140
1414
|
window.addEventListener("popstate", () => {
|
|
1141
1415
|
if (this.base + this.state.url.pathname === this.location.pathname) return;
|
|
1142
1416
|
this.log.debug("Popstate event triggered - rendering new state", { url: this.location.pathname + this.location.search });
|
|
@@ -1152,7 +1426,7 @@ var ReactRouter = class {
|
|
|
1152
1426
|
alepha = $inject(Alepha);
|
|
1153
1427
|
pageApi = $inject(ReactPageProvider);
|
|
1154
1428
|
get state() {
|
|
1155
|
-
return this.alepha.state("react.router.state");
|
|
1429
|
+
return this.alepha.state.get("react.router.state");
|
|
1156
1430
|
}
|
|
1157
1431
|
get pages() {
|
|
1158
1432
|
return this.pageApi.getPages();
|
|
@@ -1275,44 +1549,12 @@ const useRouter = () => {
|
|
|
1275
1549
|
//#region src/components/Link.tsx
|
|
1276
1550
|
const Link = (props) => {
|
|
1277
1551
|
const router = useRouter();
|
|
1278
|
-
const { to,...anchorProps } = props;
|
|
1279
1552
|
return /* @__PURE__ */ jsx("a", {
|
|
1280
|
-
...
|
|
1281
|
-
...
|
|
1553
|
+
...props,
|
|
1554
|
+
...router.anchor(props.href),
|
|
1282
1555
|
children: props.children
|
|
1283
1556
|
});
|
|
1284
1557
|
};
|
|
1285
|
-
var Link_default = Link;
|
|
1286
|
-
|
|
1287
|
-
//#endregion
|
|
1288
|
-
//#region src/hooks/useStore.ts
|
|
1289
|
-
/**
|
|
1290
|
-
* Hook to access and mutate the Alepha state.
|
|
1291
|
-
*/
|
|
1292
|
-
const useStore = (key, defaultValue) => {
|
|
1293
|
-
const alepha = useAlepha();
|
|
1294
|
-
useMemo(() => {
|
|
1295
|
-
if (defaultValue != null && alepha.state(key) == null) alepha.state(key, defaultValue);
|
|
1296
|
-
}, [defaultValue]);
|
|
1297
|
-
const [state, setState] = useState(alepha.state(key));
|
|
1298
|
-
useEffect(() => {
|
|
1299
|
-
if (!alepha.isBrowser()) return;
|
|
1300
|
-
return alepha.on("state:mutate", (ev) => {
|
|
1301
|
-
if (ev.key === key) setState(ev.value);
|
|
1302
|
-
});
|
|
1303
|
-
}, []);
|
|
1304
|
-
return [state, (value) => {
|
|
1305
|
-
alepha.state(key, value);
|
|
1306
|
-
}];
|
|
1307
|
-
};
|
|
1308
|
-
|
|
1309
|
-
//#endregion
|
|
1310
|
-
//#region src/hooks/useRouterState.ts
|
|
1311
|
-
const useRouterState = () => {
|
|
1312
|
-
const [state] = useStore("react.router.state");
|
|
1313
|
-
if (!state) throw new AlephaError("Missing react router state");
|
|
1314
|
-
return state;
|
|
1315
|
-
};
|
|
1316
1558
|
|
|
1317
1559
|
//#endregion
|
|
1318
1560
|
//#region src/hooks/useActive.ts
|
|
@@ -1370,7 +1612,7 @@ const useQueryParams = (schema, options = {}) => {
|
|
|
1370
1612
|
const key = options.key ?? "q";
|
|
1371
1613
|
const router = useRouter();
|
|
1372
1614
|
const querystring = router.query[key];
|
|
1373
|
-
const [queryParams, setQueryParams] = useState(decode(alepha, schema, router.query[key]));
|
|
1615
|
+
const [queryParams = {}, setQueryParams] = useState(decode(alepha, schema, router.query[key]));
|
|
1374
1616
|
useEffect(() => {
|
|
1375
1617
|
setQueryParams(decode(alepha, schema, querystring));
|
|
1376
1618
|
}, [querystring]);
|
|
@@ -1390,8 +1632,8 @@ const encode = (alepha, schema, data) => {
|
|
|
1390
1632
|
const decode = (alepha, schema, data) => {
|
|
1391
1633
|
try {
|
|
1392
1634
|
return alepha.parse(schema, JSON.parse(atob(decodeURIComponent(data))));
|
|
1393
|
-
} catch
|
|
1394
|
-
return
|
|
1635
|
+
} catch {
|
|
1636
|
+
return;
|
|
1395
1637
|
}
|
|
1396
1638
|
};
|
|
1397
1639
|
|
|
@@ -1454,5 +1696,5 @@ const AlephaReact = $module({
|
|
|
1454
1696
|
});
|
|
1455
1697
|
|
|
1456
1698
|
//#endregion
|
|
1457
|
-
export { $page, AlephaContext, AlephaReact,
|
|
1699
|
+
export { $page, AlephaContext, AlephaReact, ClientOnly, ErrorBoundary, ErrorViewer, Link, NestedView_default as NestedView, NotFoundPage as NotFound, PageDescriptor, ReactBrowserProvider, ReactPageProvider, ReactRouter, ReactServerProvider, Redirection, RouterLayerContext, isPageRoute, ssrSchemaLoading, useActive, useAlepha, useClient, useInject, useQueryParams, useRouter, useRouterEvents, useRouterState, useSchema, useStore };
|
|
1458
1700
|
//# sourceMappingURL=index.js.map
|