@alepha/react 0.8.0 → 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 +47 -14
- package/dist/index.browser.js.map +1 -1
- package/dist/index.cjs +162 -18
- package/dist/index.cjs.map +1 -1
- package/dist/index.d.cts +163 -29
- package/dist/index.d.cts.map +1 -1
- package/dist/index.d.ts +157 -23
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +167 -19
- package/dist/index.js.map +1 -1
- package/package.json +8 -7
- package/src/components/NestedView.tsx +3 -1
- package/src/descriptors/$page.ts +40 -40
- package/src/hooks/RouterHookApi.ts +17 -0
- package/src/hooks/useRouter.ts +1 -0
- package/src/index.shared.ts +1 -0
- package/src/index.ts +125 -3
- package/src/providers/PageDescriptorProvider.ts +18 -13
- package/src/providers/ReactBrowserProvider.ts +37 -4
- package/src/providers/ReactBrowserRenderer.ts +20 -0
- package/src/providers/ReactServerProvider.ts +4 -4
package/dist/index.cjs
CHANGED
|
@@ -314,7 +314,7 @@ const NestedView = (props) => {
|
|
|
314
314
|
const index = layer?.index ?? 0;
|
|
315
315
|
const [view, setView] = (0, react.useState)(app?.state.layers[index]?.element);
|
|
316
316
|
useRouterEvents({ onEnd: ({ state }) => {
|
|
317
|
-
setView(state.layers[index]?.element);
|
|
317
|
+
if (!state.layers[index]?.cache) setView(state.layers[index]?.element);
|
|
318
318
|
} }, [app]);
|
|
319
319
|
if (!app) throw new Error("NestedView must be used within a RouterContext.");
|
|
320
320
|
const element = view ?? props.children ?? null;
|
|
@@ -412,19 +412,18 @@ var PageDescriptorProvider = class {
|
|
|
412
412
|
const route$1 = it.route;
|
|
413
413
|
const config = {};
|
|
414
414
|
try {
|
|
415
|
-
config.query = route$1.schema?.query ? this.alepha.parse(route$1.schema.query, request.query) :
|
|
415
|
+
config.query = route$1.schema?.query ? this.alepha.parse(route$1.schema.query, request.query) : {};
|
|
416
416
|
} catch (e) {
|
|
417
417
|
it.error = e;
|
|
418
418
|
break;
|
|
419
419
|
}
|
|
420
420
|
try {
|
|
421
|
-
config.params = route$1.schema?.params ? this.alepha.parse(route$1.schema.params, request.params) :
|
|
421
|
+
config.params = route$1.schema?.params ? this.alepha.parse(route$1.schema.params, request.params) : {};
|
|
422
422
|
} catch (e) {
|
|
423
423
|
it.error = e;
|
|
424
424
|
break;
|
|
425
425
|
}
|
|
426
426
|
it.config = { ...config };
|
|
427
|
-
if (!route$1.resolve) continue;
|
|
428
427
|
const previous = request.previous;
|
|
429
428
|
if (previous?.[i] && !forceRefresh && previous[i].name === route$1.name) {
|
|
430
429
|
const url = (str) => str ? str.replace(/\/\/+/g, "/") : "/";
|
|
@@ -439,6 +438,7 @@ var PageDescriptorProvider = class {
|
|
|
439
438
|
if (prev === curr) {
|
|
440
439
|
it.props = previous[i].props;
|
|
441
440
|
it.error = previous[i].error;
|
|
441
|
+
it.cache = true;
|
|
442
442
|
context = {
|
|
443
443
|
...context,
|
|
444
444
|
...it.props
|
|
@@ -447,6 +447,7 @@ var PageDescriptorProvider = class {
|
|
|
447
447
|
}
|
|
448
448
|
forceRefresh = true;
|
|
449
449
|
}
|
|
450
|
+
if (!route$1.resolve) continue;
|
|
450
451
|
try {
|
|
451
452
|
const props = await route$1.resolve?.({
|
|
452
453
|
...request,
|
|
@@ -493,7 +494,7 @@ var PageDescriptorProvider = class {
|
|
|
493
494
|
element: this.renderView(i + 1, path, element$1, it.route),
|
|
494
495
|
index: i + 1,
|
|
495
496
|
path,
|
|
496
|
-
route
|
|
497
|
+
route: it.route
|
|
497
498
|
});
|
|
498
499
|
break;
|
|
499
500
|
}
|
|
@@ -509,7 +510,8 @@ var PageDescriptorProvider = class {
|
|
|
509
510
|
element: this.renderView(i + 1, path, element, it.route),
|
|
510
511
|
index: i + 1,
|
|
511
512
|
path,
|
|
512
|
-
route
|
|
513
|
+
route: it.route,
|
|
514
|
+
cache: it.cache
|
|
513
515
|
});
|
|
514
516
|
}
|
|
515
517
|
return {
|
|
@@ -577,8 +579,8 @@ var PageDescriptorProvider = class {
|
|
|
577
579
|
};
|
|
578
580
|
for (const { value, key } of pages) value[__alepha_core.OPTIONS].name ??= key;
|
|
579
581
|
for (const { value } of pages) {
|
|
580
|
-
if (hasParent(value)) continue;
|
|
581
582
|
if (value[__alepha_core.OPTIONS].path === "/*") hasNotFoundHandler = true;
|
|
583
|
+
if (hasParent(value)) continue;
|
|
582
584
|
this.add(this.map(pages, value));
|
|
583
585
|
}
|
|
584
586
|
if (!hasNotFoundHandler && pages.length > 0) this.add({
|
|
@@ -654,7 +656,7 @@ var ReactServerProvider = class {
|
|
|
654
656
|
handler: async () => {
|
|
655
657
|
const pages = this.alepha.getDescriptorValues($page);
|
|
656
658
|
const ssrEnabled = pages.length > 0 && this.env.REACT_SSR_ENABLED !== false;
|
|
657
|
-
this.alepha.state("
|
|
659
|
+
this.alepha.state("react.server.ssr", ssrEnabled);
|
|
658
660
|
for (const { key, instance, value } of pages) {
|
|
659
661
|
const name = value[__alepha_core.OPTIONS].name ?? key;
|
|
660
662
|
instance[key].render = this.createRenderFunction(name);
|
|
@@ -694,7 +696,7 @@ var ReactServerProvider = class {
|
|
|
694
696
|
}
|
|
695
697
|
});
|
|
696
698
|
get template() {
|
|
697
|
-
return this.alepha.state("
|
|
699
|
+
return this.alepha.state("react.server.template") ?? "<!DOCTYPE html><html lang='en'><head></head><body></body></html>";
|
|
698
700
|
}
|
|
699
701
|
async registerPages(templateLoader) {
|
|
700
702
|
for (const page of this.pageDescriptorProvider.getPages()) {
|
|
@@ -975,8 +977,17 @@ var ReactBrowserProvider = class {
|
|
|
975
977
|
get history() {
|
|
976
978
|
return window.history;
|
|
977
979
|
}
|
|
980
|
+
get location() {
|
|
981
|
+
return window.location;
|
|
982
|
+
}
|
|
978
983
|
get url() {
|
|
979
|
-
|
|
984
|
+
let url = this.location.pathname + this.location.search;
|
|
985
|
+
return url;
|
|
986
|
+
}
|
|
987
|
+
pushState(url, replace) {
|
|
988
|
+
let path = url;
|
|
989
|
+
if (replace) this.history.replaceState({}, "", path);
|
|
990
|
+
else this.history.pushState({}, "", path);
|
|
980
991
|
}
|
|
981
992
|
async invalidate(props) {
|
|
982
993
|
const previous = [];
|
|
@@ -1002,14 +1013,14 @@ var ReactBrowserProvider = class {
|
|
|
1002
1013
|
async go(url, options = {}) {
|
|
1003
1014
|
const result = await this.render({ url });
|
|
1004
1015
|
if (result.context.url.pathname !== url) {
|
|
1005
|
-
this.
|
|
1016
|
+
this.pushState(result.context.url.pathname);
|
|
1006
1017
|
return;
|
|
1007
1018
|
}
|
|
1008
1019
|
if (options.replace) {
|
|
1009
|
-
this.
|
|
1020
|
+
this.pushState(url);
|
|
1010
1021
|
return;
|
|
1011
1022
|
}
|
|
1012
|
-
this.
|
|
1023
|
+
this.pushState(url);
|
|
1013
1024
|
}
|
|
1014
1025
|
async render(options = {}) {
|
|
1015
1026
|
const previous = options.previous ?? this.state.layers;
|
|
@@ -1046,6 +1057,7 @@ var ReactBrowserProvider = class {
|
|
|
1046
1057
|
hydration
|
|
1047
1058
|
});
|
|
1048
1059
|
window.addEventListener("popstate", () => {
|
|
1060
|
+
if (this.state.pathname === location.pathname) return;
|
|
1049
1061
|
this.render();
|
|
1050
1062
|
});
|
|
1051
1063
|
}
|
|
@@ -1055,12 +1067,21 @@ var ReactBrowserProvider = class {
|
|
|
1055
1067
|
//#endregion
|
|
1056
1068
|
//#region src/hooks/RouterHookApi.ts
|
|
1057
1069
|
var RouterHookApi = class {
|
|
1058
|
-
constructor(pages, state, layer, browser) {
|
|
1070
|
+
constructor(pages, context, state, layer, browser) {
|
|
1059
1071
|
this.pages = pages;
|
|
1072
|
+
this.context = context;
|
|
1060
1073
|
this.state = state;
|
|
1061
1074
|
this.layer = layer;
|
|
1062
1075
|
this.browser = browser;
|
|
1063
1076
|
}
|
|
1077
|
+
getURL() {
|
|
1078
|
+
if (!this.browser) return this.context.url;
|
|
1079
|
+
return new URL(this.location.href);
|
|
1080
|
+
}
|
|
1081
|
+
get location() {
|
|
1082
|
+
if (!this.browser) throw new Error("Browser is required");
|
|
1083
|
+
return this.browser.location;
|
|
1084
|
+
}
|
|
1064
1085
|
get current() {
|
|
1065
1086
|
return this.state;
|
|
1066
1087
|
}
|
|
@@ -1138,7 +1159,7 @@ const useRouter = () => {
|
|
|
1138
1159
|
const pages = (0, react.useMemo)(() => {
|
|
1139
1160
|
return ctx.alepha.get(PageDescriptorProvider).getPages();
|
|
1140
1161
|
}, []);
|
|
1141
|
-
return (0, react.useMemo)(() => new RouterHookApi(pages, ctx.state, layer, ctx.alepha.isBrowser() ? ctx.alepha.get(ReactBrowserProvider) : void 0), [layer]);
|
|
1162
|
+
return (0, react.useMemo)(() => new RouterHookApi(pages, ctx.context, ctx.state, layer, ctx.alepha.isBrowser() ? ctx.alepha.get(ReactBrowserProvider) : void 0), [layer]);
|
|
1142
1163
|
};
|
|
1143
1164
|
|
|
1144
1165
|
//#endregion
|
|
@@ -1258,10 +1279,132 @@ const useRouterState = () => {
|
|
|
1258
1279
|
//#endregion
|
|
1259
1280
|
//#region src/index.ts
|
|
1260
1281
|
/**
|
|
1261
|
-
*
|
|
1282
|
+
* Provides full-stack React development with declarative routing, server-side rendering, and client-side hydration.
|
|
1283
|
+
*
|
|
1284
|
+
* The React module enables building modern React applications using the `$page` descriptor on class properties.
|
|
1285
|
+
* It delivers seamless server-side rendering, automatic code splitting, and client-side navigation with full
|
|
1286
|
+
* type safety and schema validation for route parameters and data.
|
|
1287
|
+
*
|
|
1288
|
+
* **Key Features:**
|
|
1289
|
+
* - Declarative page definition with `$page` descriptor
|
|
1290
|
+
* - Server-side rendering (SSR) with automatic hydration
|
|
1291
|
+
* - Type-safe routing with parameter validation
|
|
1292
|
+
* - Schema-based data resolution and validation
|
|
1293
|
+
* - SEO-friendly meta tag management
|
|
1294
|
+
* - Automatic code splitting and lazy loading
|
|
1295
|
+
* - Client-side navigation with browser history
|
|
1296
|
+
*
|
|
1297
|
+
* **Basic Usage:**
|
|
1298
|
+
* ```ts
|
|
1299
|
+
* import { Alepha, run, t } from "alepha";
|
|
1300
|
+
* import { AlephaReact, $page } from "alepha/react";
|
|
1301
|
+
*
|
|
1302
|
+
* class AppRoutes {
|
|
1303
|
+
* // Home page
|
|
1304
|
+
* home = $page({
|
|
1305
|
+
* path: "/",
|
|
1306
|
+
* component: () => (
|
|
1307
|
+
* <div>
|
|
1308
|
+
* <h1>Welcome to Alepha</h1>
|
|
1309
|
+
* <p>Build amazing React applications!</p>
|
|
1310
|
+
* </div>
|
|
1311
|
+
* ),
|
|
1312
|
+
* });
|
|
1313
|
+
*
|
|
1314
|
+
* // About page with meta tags
|
|
1315
|
+
* about = $page({
|
|
1316
|
+
* path: "/about",
|
|
1317
|
+
* head: {
|
|
1318
|
+
* title: "About Us",
|
|
1319
|
+
* description: "Learn more about our mission",
|
|
1320
|
+
* },
|
|
1321
|
+
* component: () => (
|
|
1322
|
+
* <div>
|
|
1323
|
+
* <h1>About Us</h1>
|
|
1324
|
+
* <p>Learn more about our mission.</p>
|
|
1325
|
+
* </div>
|
|
1326
|
+
* ),
|
|
1327
|
+
* });
|
|
1328
|
+
* }
|
|
1329
|
+
*
|
|
1330
|
+
* const alepha = Alepha.create()
|
|
1331
|
+
* .with(AlephaReact)
|
|
1332
|
+
* .with(AppRoutes);
|
|
1333
|
+
*
|
|
1334
|
+
* run(alepha);
|
|
1335
|
+
* ```
|
|
1262
1336
|
*
|
|
1263
|
-
*
|
|
1264
|
-
*
|
|
1337
|
+
* **Dynamic Routes with Parameters:**
|
|
1338
|
+
* ```tsx
|
|
1339
|
+
* class UserRoutes {
|
|
1340
|
+
* userProfile = $page({
|
|
1341
|
+
* path: "/users/:id",
|
|
1342
|
+
* schema: {
|
|
1343
|
+
* params: t.object({
|
|
1344
|
+
* id: t.string(),
|
|
1345
|
+
* }),
|
|
1346
|
+
* },
|
|
1347
|
+
* resolve: async ({ params }) => {
|
|
1348
|
+
* // Fetch user data server-side
|
|
1349
|
+
* const user = await getUserById(params.id);
|
|
1350
|
+
* return { user };
|
|
1351
|
+
* },
|
|
1352
|
+
* head: ({ user }) => ({
|
|
1353
|
+
* title: `${user.name} - Profile`,
|
|
1354
|
+
* description: `View ${user.name}'s profile`,
|
|
1355
|
+
* }),
|
|
1356
|
+
* component: ({ user }) => (
|
|
1357
|
+
* <div>
|
|
1358
|
+
* <h1>{user.name}</h1>
|
|
1359
|
+
* <p>Email: {user.email}</p>
|
|
1360
|
+
* </div>
|
|
1361
|
+
* ),
|
|
1362
|
+
* });
|
|
1363
|
+
*
|
|
1364
|
+
* userSettings = $page({
|
|
1365
|
+
* path: "/users/:id/settings",
|
|
1366
|
+
* schema: {
|
|
1367
|
+
* params: t.object({
|
|
1368
|
+
* id: t.string(),
|
|
1369
|
+
* }),
|
|
1370
|
+
* },
|
|
1371
|
+
* component: ({ params }) => (
|
|
1372
|
+
* <UserSettings userId={params.id} />
|
|
1373
|
+
* ),
|
|
1374
|
+
* });
|
|
1375
|
+
* }
|
|
1376
|
+
* ```
|
|
1377
|
+
*
|
|
1378
|
+
* **Static Generation:**
|
|
1379
|
+
* ```tsx
|
|
1380
|
+
* class BlogRoutes {
|
|
1381
|
+
* blogPost = $page({
|
|
1382
|
+
* path: "/blog/:slug",
|
|
1383
|
+
* schema: {
|
|
1384
|
+
* params: t.object({
|
|
1385
|
+
* slug: t.string(),
|
|
1386
|
+
* }),
|
|
1387
|
+
* },
|
|
1388
|
+
* static: {
|
|
1389
|
+
* entries: [
|
|
1390
|
+
* { params: { slug: "getting-started" } },
|
|
1391
|
+
* { params: { slug: "advanced-features" } },
|
|
1392
|
+
* { params: { slug: "deployment" } },
|
|
1393
|
+
* ],
|
|
1394
|
+
* },
|
|
1395
|
+
* resolve: ({ params }) => {
|
|
1396
|
+
* const post = getBlogPost(params.slug);
|
|
1397
|
+
* return { post };
|
|
1398
|
+
* },
|
|
1399
|
+
* component: ({ post }) => (
|
|
1400
|
+
* <article>
|
|
1401
|
+
* <h1>{post.title}</h1>
|
|
1402
|
+
* <div>{post.content}</div>
|
|
1403
|
+
* </article>
|
|
1404
|
+
* ),
|
|
1405
|
+
* });
|
|
1406
|
+
* }
|
|
1407
|
+
* ```
|
|
1265
1408
|
*
|
|
1266
1409
|
* @see {@link $page}
|
|
1267
1410
|
* @module alepha.react
|
|
@@ -1279,6 +1422,7 @@ exports.ClientOnly = ClientOnly_default;
|
|
|
1279
1422
|
exports.ErrorBoundary = ErrorBoundary_default;
|
|
1280
1423
|
exports.Link = Link_default;
|
|
1281
1424
|
exports.NestedView = NestedView_default;
|
|
1425
|
+
exports.NotFound = NotFoundPage;
|
|
1282
1426
|
exports.PageDescriptorProvider = PageDescriptorProvider;
|
|
1283
1427
|
exports.ReactBrowserProvider = ReactBrowserProvider;
|
|
1284
1428
|
exports.ReactServerProvider = ReactServerProvider;
|