@alepha/react 0.7.7 → 0.8.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.
- package/README.md +153 -1
- package/dist/index.browser.js +61 -26
- package/dist/index.browser.js.map +1 -1
- package/dist/index.cjs +175 -30
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +198 -41
- package/dist/index.d.cts.map +1 -0
- package/dist/index.d.ts +197 -40
- package/dist/index.d.ts.map +1 -0
- package/dist/index.js +180 -31
- package/dist/index.js.map +1 -1
- package/package.json +8 -7
- package/src/components/NestedView.tsx +3 -1
- package/src/descriptors/$page.ts +50 -42
- package/src/hooks/RouterHookApi.ts +17 -0
- package/src/hooks/useRouter.ts +1 -0
- package/src/index.browser.ts +4 -0
- package/src/index.shared.ts +1 -0
- package/src/index.ts +127 -3
- package/src/providers/BrowserRouterProvider.ts +1 -1
- package/src/providers/PageDescriptorProvider.ts +39 -23
- package/src/providers/ReactBrowserProvider.ts +38 -5
- package/src/providers/ReactBrowserRenderer.ts +21 -1
- package/src/providers/ReactServerProvider.ts +5 -5
- package/dist/index.browser.d.ts +0 -523
package/dist/index.js
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import { $hook, $inject, $logger, Alepha, KIND, NotImplementedError, OPTIONS, __bind, __descriptor, t } from "@alepha/core";
|
|
2
2
|
import { AlephaServer, ServerRouterProvider, ServerTimingProvider, apiLinksResponseSchema } from "@alepha/server";
|
|
3
3
|
import { AlephaServerCache } from "@alepha/server-cache";
|
|
4
|
+
import { AlephaServerLinks, LinkProvider, ServerLinksProvider } from "@alepha/server-links";
|
|
4
5
|
import React, { StrictMode, createContext, createElement, useContext, useEffect, useMemo, useState } from "react";
|
|
5
6
|
import { jsx, jsxs } from "react/jsx-runtime";
|
|
6
7
|
import { existsSync } from "node:fs";
|
|
7
8
|
import { join } from "node:path";
|
|
8
|
-
import { LinkProvider, ServerLinksProvider } from "@alepha/server-links";
|
|
9
9
|
import { ServerStaticProvider } from "@alepha/server-static";
|
|
10
10
|
import { renderToString } from "react-dom/server";
|
|
11
11
|
import { RouterProvider } from "@alepha/router";
|
|
@@ -17,11 +17,6 @@ const KEY = "PAGE";
|
|
|
17
17
|
*/
|
|
18
18
|
const $page = (options) => {
|
|
19
19
|
__descriptor(KEY);
|
|
20
|
-
if (options.children) for (const child of options.children) child[OPTIONS].parent = { [OPTIONS]: options };
|
|
21
|
-
if (options.parent) {
|
|
22
|
-
options.parent[OPTIONS].children ??= [];
|
|
23
|
-
options.parent[OPTIONS].children.push({ [OPTIONS]: options });
|
|
24
|
-
}
|
|
25
20
|
return {
|
|
26
21
|
[KIND]: KEY,
|
|
27
22
|
[OPTIONS]: options,
|
|
@@ -296,7 +291,7 @@ const NestedView = (props) => {
|
|
|
296
291
|
const index = layer?.index ?? 0;
|
|
297
292
|
const [view, setView] = useState(app?.state.layers[index]?.element);
|
|
298
293
|
useRouterEvents({ onEnd: ({ state }) => {
|
|
299
|
-
setView(state.layers[index]?.element);
|
|
294
|
+
if (!state.layers[index]?.cache) setView(state.layers[index]?.element);
|
|
300
295
|
} }, [app]);
|
|
301
296
|
if (!app) throw new Error("NestedView must be used within a RouterContext.");
|
|
302
297
|
const element = view ?? props.children ?? null;
|
|
@@ -394,19 +389,18 @@ var PageDescriptorProvider = class {
|
|
|
394
389
|
const route$1 = it.route;
|
|
395
390
|
const config = {};
|
|
396
391
|
try {
|
|
397
|
-
config.query = route$1.schema?.query ? this.alepha.parse(route$1.schema.query, request.query) :
|
|
392
|
+
config.query = route$1.schema?.query ? this.alepha.parse(route$1.schema.query, request.query) : {};
|
|
398
393
|
} catch (e) {
|
|
399
394
|
it.error = e;
|
|
400
395
|
break;
|
|
401
396
|
}
|
|
402
397
|
try {
|
|
403
|
-
config.params = route$1.schema?.params ? this.alepha.parse(route$1.schema.params, request.params) :
|
|
398
|
+
config.params = route$1.schema?.params ? this.alepha.parse(route$1.schema.params, request.params) : {};
|
|
404
399
|
} catch (e) {
|
|
405
400
|
it.error = e;
|
|
406
401
|
break;
|
|
407
402
|
}
|
|
408
403
|
it.config = { ...config };
|
|
409
|
-
if (!route$1.resolve) continue;
|
|
410
404
|
const previous = request.previous;
|
|
411
405
|
if (previous?.[i] && !forceRefresh && previous[i].name === route$1.name) {
|
|
412
406
|
const url = (str) => str ? str.replace(/\/\/+/g, "/") : "/";
|
|
@@ -421,6 +415,7 @@ var PageDescriptorProvider = class {
|
|
|
421
415
|
if (prev === curr) {
|
|
422
416
|
it.props = previous[i].props;
|
|
423
417
|
it.error = previous[i].error;
|
|
418
|
+
it.cache = true;
|
|
424
419
|
context = {
|
|
425
420
|
...context,
|
|
426
421
|
...it.props
|
|
@@ -429,6 +424,7 @@ var PageDescriptorProvider = class {
|
|
|
429
424
|
}
|
|
430
425
|
forceRefresh = true;
|
|
431
426
|
}
|
|
427
|
+
if (!route$1.resolve) continue;
|
|
432
428
|
try {
|
|
433
429
|
const props = await route$1.resolve?.({
|
|
434
430
|
...request,
|
|
@@ -475,7 +471,7 @@ var PageDescriptorProvider = class {
|
|
|
475
471
|
element: this.renderView(i + 1, path, element$1, it.route),
|
|
476
472
|
index: i + 1,
|
|
477
473
|
path,
|
|
478
|
-
route
|
|
474
|
+
route: it.route
|
|
479
475
|
});
|
|
480
476
|
break;
|
|
481
477
|
}
|
|
@@ -491,7 +487,8 @@ var PageDescriptorProvider = class {
|
|
|
491
487
|
element: this.renderView(i + 1, path, element, it.route),
|
|
492
488
|
index: i + 1,
|
|
493
489
|
path,
|
|
494
|
-
route
|
|
490
|
+
route: it.route,
|
|
491
|
+
cache: it.cache
|
|
495
492
|
});
|
|
496
493
|
}
|
|
497
494
|
return {
|
|
@@ -547,14 +544,20 @@ var PageDescriptorProvider = class {
|
|
|
547
544
|
} }, element);
|
|
548
545
|
}
|
|
549
546
|
configure = $hook({
|
|
550
|
-
|
|
547
|
+
on: "configure",
|
|
551
548
|
handler: () => {
|
|
552
549
|
let hasNotFoundHandler = false;
|
|
553
550
|
const pages = this.alepha.getDescriptorValues($page);
|
|
551
|
+
const hasParent = (it) => {
|
|
552
|
+
for (const page of pages) {
|
|
553
|
+
const children = page.value[OPTIONS].children ? Array.isArray(page.value[OPTIONS].children) ? page.value[OPTIONS].children : page.value[OPTIONS].children() : [];
|
|
554
|
+
if (children.includes(it)) return true;
|
|
555
|
+
}
|
|
556
|
+
};
|
|
554
557
|
for (const { value, key } of pages) value[OPTIONS].name ??= key;
|
|
555
558
|
for (const { value } of pages) {
|
|
556
|
-
if (value[OPTIONS].parent) continue;
|
|
557
559
|
if (value[OPTIONS].path === "/*") hasNotFoundHandler = true;
|
|
560
|
+
if (hasParent(value)) continue;
|
|
558
561
|
this.add(this.map(pages, value));
|
|
559
562
|
}
|
|
560
563
|
if (!hasNotFoundHandler && pages.length > 0) this.add({
|
|
@@ -569,7 +572,7 @@ var PageDescriptorProvider = class {
|
|
|
569
572
|
}
|
|
570
573
|
});
|
|
571
574
|
map(pages, target) {
|
|
572
|
-
const children = target[OPTIONS].children
|
|
575
|
+
const children = target[OPTIONS].children ? Array.isArray(target[OPTIONS].children) ? target[OPTIONS].children : target[OPTIONS].children() : [];
|
|
573
576
|
return {
|
|
574
577
|
...target[OPTIONS],
|
|
575
578
|
parent: void 0,
|
|
@@ -626,11 +629,11 @@ var ReactServerProvider = class {
|
|
|
626
629
|
env = $inject(envSchema);
|
|
627
630
|
ROOT_DIV_REGEX = new RegExp(`<div([^>]*)\\s+id=["']${this.env.REACT_ROOT_ID}["']([^>]*)>(.*?)<\\/div>`, "is");
|
|
628
631
|
onConfigure = $hook({
|
|
629
|
-
|
|
632
|
+
on: "configure",
|
|
630
633
|
handler: async () => {
|
|
631
634
|
const pages = this.alepha.getDescriptorValues($page);
|
|
632
635
|
const ssrEnabled = pages.length > 0 && this.env.REACT_SSR_ENABLED !== false;
|
|
633
|
-
this.alepha.state("
|
|
636
|
+
this.alepha.state("react.server.ssr", ssrEnabled);
|
|
634
637
|
for (const { key, instance, value } of pages) {
|
|
635
638
|
const name = value[OPTIONS].name ?? key;
|
|
636
639
|
instance[key].render = this.createRenderFunction(name);
|
|
@@ -670,7 +673,7 @@ var ReactServerProvider = class {
|
|
|
670
673
|
}
|
|
671
674
|
});
|
|
672
675
|
get template() {
|
|
673
|
-
return this.alepha.state("
|
|
676
|
+
return this.alepha.state("react.server.template") ?? "<!DOCTYPE html><html lang='en'><head></head><body></body></html>";
|
|
674
677
|
}
|
|
675
678
|
async registerPages(templateLoader) {
|
|
676
679
|
for (const page of this.pageDescriptorProvider.getPages()) {
|
|
@@ -845,7 +848,7 @@ var BrowserRouterProvider = class extends RouterProvider {
|
|
|
845
848
|
this.pageDescriptorProvider.add(entry);
|
|
846
849
|
}
|
|
847
850
|
configure = $hook({
|
|
848
|
-
|
|
851
|
+
on: "configure",
|
|
849
852
|
handler: async () => {
|
|
850
853
|
for (const page of this.pageDescriptorProvider.getPages()) if (page.component || page.lazy) this.push({
|
|
851
854
|
path: page.match,
|
|
@@ -951,8 +954,22 @@ var ReactBrowserProvider = class {
|
|
|
951
954
|
get history() {
|
|
952
955
|
return window.history;
|
|
953
956
|
}
|
|
957
|
+
get location() {
|
|
958
|
+
return window.location;
|
|
959
|
+
}
|
|
954
960
|
get url() {
|
|
955
|
-
|
|
961
|
+
let url = this.location.pathname + this.location.search;
|
|
962
|
+
if (import.meta?.env?.BASE_URL) {
|
|
963
|
+
url = url.replace(import.meta.env?.BASE_URL, "");
|
|
964
|
+
if (!url.startsWith("/")) url = `/${url}`;
|
|
965
|
+
}
|
|
966
|
+
return url;
|
|
967
|
+
}
|
|
968
|
+
pushState(url, replace) {
|
|
969
|
+
let path = url;
|
|
970
|
+
if (import.meta?.env?.BASE_URL) path = (import.meta.env?.BASE_URL + path).replaceAll("//", "/");
|
|
971
|
+
if (replace) this.history.replaceState({}, "", path);
|
|
972
|
+
else this.history.pushState({}, "", path);
|
|
956
973
|
}
|
|
957
974
|
async invalidate(props) {
|
|
958
975
|
const previous = [];
|
|
@@ -978,14 +995,14 @@ var ReactBrowserProvider = class {
|
|
|
978
995
|
async go(url, options = {}) {
|
|
979
996
|
const result = await this.render({ url });
|
|
980
997
|
if (result.context.url.pathname !== url) {
|
|
981
|
-
this.
|
|
998
|
+
this.pushState(result.context.url.pathname);
|
|
982
999
|
return;
|
|
983
1000
|
}
|
|
984
1001
|
if (options.replace) {
|
|
985
|
-
this.
|
|
1002
|
+
this.pushState(url);
|
|
986
1003
|
return;
|
|
987
1004
|
}
|
|
988
|
-
this.
|
|
1005
|
+
this.pushState(url);
|
|
989
1006
|
}
|
|
990
1007
|
async render(options = {}) {
|
|
991
1008
|
const previous = options.previous ?? this.state.layers;
|
|
@@ -1010,7 +1027,7 @@ var ReactBrowserProvider = class {
|
|
|
1010
1027
|
}
|
|
1011
1028
|
}
|
|
1012
1029
|
ready = $hook({
|
|
1013
|
-
|
|
1030
|
+
on: "ready",
|
|
1014
1031
|
handler: async () => {
|
|
1015
1032
|
const hydration = this.getHydrationState();
|
|
1016
1033
|
const previous = hydration?.layers ?? [];
|
|
@@ -1022,6 +1039,7 @@ var ReactBrowserProvider = class {
|
|
|
1022
1039
|
hydration
|
|
1023
1040
|
});
|
|
1024
1041
|
window.addEventListener("popstate", () => {
|
|
1042
|
+
if (this.state.pathname === location.pathname) return;
|
|
1025
1043
|
this.render();
|
|
1026
1044
|
});
|
|
1027
1045
|
}
|
|
@@ -1031,12 +1049,21 @@ var ReactBrowserProvider = class {
|
|
|
1031
1049
|
//#endregion
|
|
1032
1050
|
//#region src/hooks/RouterHookApi.ts
|
|
1033
1051
|
var RouterHookApi = class {
|
|
1034
|
-
constructor(pages, state, layer, browser) {
|
|
1052
|
+
constructor(pages, context, state, layer, browser) {
|
|
1035
1053
|
this.pages = pages;
|
|
1054
|
+
this.context = context;
|
|
1036
1055
|
this.state = state;
|
|
1037
1056
|
this.layer = layer;
|
|
1038
1057
|
this.browser = browser;
|
|
1039
1058
|
}
|
|
1059
|
+
getURL() {
|
|
1060
|
+
if (!this.browser) return this.context.url;
|
|
1061
|
+
return new URL(this.location.href);
|
|
1062
|
+
}
|
|
1063
|
+
get location() {
|
|
1064
|
+
if (!this.browser) throw new Error("Browser is required");
|
|
1065
|
+
return this.browser.location;
|
|
1066
|
+
}
|
|
1040
1067
|
get current() {
|
|
1041
1068
|
return this.state;
|
|
1042
1069
|
}
|
|
@@ -1114,7 +1141,7 @@ const useRouter = () => {
|
|
|
1114
1141
|
const pages = useMemo(() => {
|
|
1115
1142
|
return ctx.alepha.get(PageDescriptorProvider).getPages();
|
|
1116
1143
|
}, []);
|
|
1117
|
-
return useMemo(() => new RouterHookApi(pages, ctx.state, layer, ctx.alepha.isBrowser() ? ctx.alepha.get(ReactBrowserProvider) : void 0), [layer]);
|
|
1144
|
+
return useMemo(() => new RouterHookApi(pages, ctx.context, ctx.state, layer, ctx.alepha.isBrowser() ? ctx.alepha.get(ReactBrowserProvider) : void 0), [layer]);
|
|
1118
1145
|
};
|
|
1119
1146
|
|
|
1120
1147
|
//#endregion
|
|
@@ -1234,20 +1261,142 @@ const useRouterState = () => {
|
|
|
1234
1261
|
//#endregion
|
|
1235
1262
|
//#region src/index.ts
|
|
1236
1263
|
/**
|
|
1237
|
-
*
|
|
1264
|
+
* Provides full-stack React development with declarative routing, server-side rendering, and client-side hydration.
|
|
1265
|
+
*
|
|
1266
|
+
* The React module enables building modern React applications using the `$page` descriptor on class properties.
|
|
1267
|
+
* It delivers seamless server-side rendering, automatic code splitting, and client-side navigation with full
|
|
1268
|
+
* type safety and schema validation for route parameters and data.
|
|
1269
|
+
*
|
|
1270
|
+
* **Key Features:**
|
|
1271
|
+
* - Declarative page definition with `$page` descriptor
|
|
1272
|
+
* - Server-side rendering (SSR) with automatic hydration
|
|
1273
|
+
* - Type-safe routing with parameter validation
|
|
1274
|
+
* - Schema-based data resolution and validation
|
|
1275
|
+
* - SEO-friendly meta tag management
|
|
1276
|
+
* - Automatic code splitting and lazy loading
|
|
1277
|
+
* - Client-side navigation with browser history
|
|
1278
|
+
*
|
|
1279
|
+
* **Basic Usage:**
|
|
1280
|
+
* ```ts
|
|
1281
|
+
* import { Alepha, run, t } from "alepha";
|
|
1282
|
+
* import { AlephaReact, $page } from "alepha/react";
|
|
1283
|
+
*
|
|
1284
|
+
* class AppRoutes {
|
|
1285
|
+
* // Home page
|
|
1286
|
+
* home = $page({
|
|
1287
|
+
* path: "/",
|
|
1288
|
+
* component: () => (
|
|
1289
|
+
* <div>
|
|
1290
|
+
* <h1>Welcome to Alepha</h1>
|
|
1291
|
+
* <p>Build amazing React applications!</p>
|
|
1292
|
+
* </div>
|
|
1293
|
+
* ),
|
|
1294
|
+
* });
|
|
1295
|
+
*
|
|
1296
|
+
* // About page with meta tags
|
|
1297
|
+
* about = $page({
|
|
1298
|
+
* path: "/about",
|
|
1299
|
+
* head: {
|
|
1300
|
+
* title: "About Us",
|
|
1301
|
+
* description: "Learn more about our mission",
|
|
1302
|
+
* },
|
|
1303
|
+
* component: () => (
|
|
1304
|
+
* <div>
|
|
1305
|
+
* <h1>About Us</h1>
|
|
1306
|
+
* <p>Learn more about our mission.</p>
|
|
1307
|
+
* </div>
|
|
1308
|
+
* ),
|
|
1309
|
+
* });
|
|
1310
|
+
* }
|
|
1311
|
+
*
|
|
1312
|
+
* const alepha = Alepha.create()
|
|
1313
|
+
* .with(AlephaReact)
|
|
1314
|
+
* .with(AppRoutes);
|
|
1315
|
+
*
|
|
1316
|
+
* run(alepha);
|
|
1317
|
+
* ```
|
|
1318
|
+
*
|
|
1319
|
+
* **Dynamic Routes with Parameters:**
|
|
1320
|
+
* ```tsx
|
|
1321
|
+
* class UserRoutes {
|
|
1322
|
+
* userProfile = $page({
|
|
1323
|
+
* path: "/users/:id",
|
|
1324
|
+
* schema: {
|
|
1325
|
+
* params: t.object({
|
|
1326
|
+
* id: t.string(),
|
|
1327
|
+
* }),
|
|
1328
|
+
* },
|
|
1329
|
+
* resolve: async ({ params }) => {
|
|
1330
|
+
* // Fetch user data server-side
|
|
1331
|
+
* const user = await getUserById(params.id);
|
|
1332
|
+
* return { user };
|
|
1333
|
+
* },
|
|
1334
|
+
* head: ({ user }) => ({
|
|
1335
|
+
* title: `${user.name} - Profile`,
|
|
1336
|
+
* description: `View ${user.name}'s profile`,
|
|
1337
|
+
* }),
|
|
1338
|
+
* component: ({ user }) => (
|
|
1339
|
+
* <div>
|
|
1340
|
+
* <h1>{user.name}</h1>
|
|
1341
|
+
* <p>Email: {user.email}</p>
|
|
1342
|
+
* </div>
|
|
1343
|
+
* ),
|
|
1344
|
+
* });
|
|
1238
1345
|
*
|
|
1239
|
-
*
|
|
1240
|
-
*
|
|
1346
|
+
* userSettings = $page({
|
|
1347
|
+
* path: "/users/:id/settings",
|
|
1348
|
+
* schema: {
|
|
1349
|
+
* params: t.object({
|
|
1350
|
+
* id: t.string(),
|
|
1351
|
+
* }),
|
|
1352
|
+
* },
|
|
1353
|
+
* component: ({ params }) => (
|
|
1354
|
+
* <UserSettings userId={params.id} />
|
|
1355
|
+
* ),
|
|
1356
|
+
* });
|
|
1357
|
+
* }
|
|
1358
|
+
* ```
|
|
1359
|
+
*
|
|
1360
|
+
* **Static Generation:**
|
|
1361
|
+
* ```tsx
|
|
1362
|
+
* class BlogRoutes {
|
|
1363
|
+
* blogPost = $page({
|
|
1364
|
+
* path: "/blog/:slug",
|
|
1365
|
+
* schema: {
|
|
1366
|
+
* params: t.object({
|
|
1367
|
+
* slug: t.string(),
|
|
1368
|
+
* }),
|
|
1369
|
+
* },
|
|
1370
|
+
* static: {
|
|
1371
|
+
* entries: [
|
|
1372
|
+
* { params: { slug: "getting-started" } },
|
|
1373
|
+
* { params: { slug: "advanced-features" } },
|
|
1374
|
+
* { params: { slug: "deployment" } },
|
|
1375
|
+
* ],
|
|
1376
|
+
* },
|
|
1377
|
+
* resolve: ({ params }) => {
|
|
1378
|
+
* const post = getBlogPost(params.slug);
|
|
1379
|
+
* return { post };
|
|
1380
|
+
* },
|
|
1381
|
+
* component: ({ post }) => (
|
|
1382
|
+
* <article>
|
|
1383
|
+
* <h1>{post.title}</h1>
|
|
1384
|
+
* <div>{post.content}</div>
|
|
1385
|
+
* </article>
|
|
1386
|
+
* ),
|
|
1387
|
+
* });
|
|
1388
|
+
* }
|
|
1389
|
+
* ```
|
|
1241
1390
|
*
|
|
1242
1391
|
* @see {@link $page}
|
|
1243
1392
|
* @module alepha.react
|
|
1244
1393
|
*/
|
|
1245
1394
|
var AlephaReact = class {
|
|
1246
1395
|
name = "alepha.react";
|
|
1247
|
-
$services = (alepha) => alepha.with(AlephaServer).with(AlephaServerCache).with(ReactServerProvider).with(PageDescriptorProvider);
|
|
1396
|
+
$services = (alepha) => alepha.with(AlephaServer).with(AlephaServerCache).with(AlephaServerLinks).with(ReactServerProvider).with(PageDescriptorProvider);
|
|
1248
1397
|
};
|
|
1249
1398
|
__bind($page, AlephaReact);
|
|
1250
1399
|
|
|
1251
1400
|
//#endregion
|
|
1252
|
-
export { $page, AlephaReact, ClientOnly_default as ClientOnly, ErrorBoundary_default as ErrorBoundary, Link_default as Link, NestedView_default as NestedView, PageDescriptorProvider, ReactBrowserProvider, ReactServerProvider, RedirectionError, RouterContext, RouterHookApi, RouterLayerContext, isPageRoute, useActive, useAlepha, useClient, useInject, useQueryParams, useRouter, useRouterEvents, useRouterState };
|
|
1401
|
+
export { $page, AlephaReact, ClientOnly_default as ClientOnly, ErrorBoundary_default as ErrorBoundary, Link_default as Link, NestedView_default as NestedView, NotFoundPage as NotFound, PageDescriptorProvider, ReactBrowserProvider, ReactServerProvider, RedirectionError, RouterContext, RouterHookApi, RouterLayerContext, isPageRoute, useActive, useAlepha, useClient, useInject, useQueryParams, useRouter, useRouterEvents, useRouterState };
|
|
1253
1402
|
//# sourceMappingURL=index.js.map
|