@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/dist/index.cjs CHANGED
@@ -24,11 +24,11 @@ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__ge
24
24
  const __alepha_core = __toESM(require("@alepha/core"));
25
25
  const __alepha_server = __toESM(require("@alepha/server"));
26
26
  const __alepha_server_cache = __toESM(require("@alepha/server-cache"));
27
+ const __alepha_server_links = __toESM(require("@alepha/server-links"));
27
28
  const react = __toESM(require("react"));
28
29
  const react_jsx_runtime = __toESM(require("react/jsx-runtime"));
29
30
  const node_fs = __toESM(require("node:fs"));
30
31
  const node_path = __toESM(require("node:path"));
31
- const __alepha_server_links = __toESM(require("@alepha/server-links"));
32
32
  const __alepha_server_static = __toESM(require("@alepha/server-static"));
33
33
  const react_dom_server = __toESM(require("react-dom/server"));
34
34
  const __alepha_router = __toESM(require("@alepha/router"));
@@ -40,11 +40,6 @@ const KEY = "PAGE";
40
40
  */
41
41
  const $page = (options) => {
42
42
  (0, __alepha_core.__descriptor)(KEY);
43
- if (options.children) for (const child of options.children) child[__alepha_core.OPTIONS].parent = { [__alepha_core.OPTIONS]: options };
44
- if (options.parent) {
45
- options.parent[__alepha_core.OPTIONS].children ??= [];
46
- options.parent[__alepha_core.OPTIONS].children.push({ [__alepha_core.OPTIONS]: options });
47
- }
48
43
  return {
49
44
  [__alepha_core.KIND]: KEY,
50
45
  [__alepha_core.OPTIONS]: options,
@@ -319,7 +314,7 @@ const NestedView = (props) => {
319
314
  const index = layer?.index ?? 0;
320
315
  const [view, setView] = (0, react.useState)(app?.state.layers[index]?.element);
321
316
  useRouterEvents({ onEnd: ({ state }) => {
322
- setView(state.layers[index]?.element);
317
+ if (!state.layers[index]?.cache) setView(state.layers[index]?.element);
323
318
  } }, [app]);
324
319
  if (!app) throw new Error("NestedView must be used within a RouterContext.");
325
320
  const element = view ?? props.children ?? null;
@@ -417,19 +412,18 @@ var PageDescriptorProvider = class {
417
412
  const route$1 = it.route;
418
413
  const config = {};
419
414
  try {
420
- config.query = route$1.schema?.query ? this.alepha.parse(route$1.schema.query, request.query) : request.query;
415
+ config.query = route$1.schema?.query ? this.alepha.parse(route$1.schema.query, request.query) : {};
421
416
  } catch (e) {
422
417
  it.error = e;
423
418
  break;
424
419
  }
425
420
  try {
426
- config.params = route$1.schema?.params ? this.alepha.parse(route$1.schema.params, request.params) : request.params;
421
+ config.params = route$1.schema?.params ? this.alepha.parse(route$1.schema.params, request.params) : {};
427
422
  } catch (e) {
428
423
  it.error = e;
429
424
  break;
430
425
  }
431
426
  it.config = { ...config };
432
- if (!route$1.resolve) continue;
433
427
  const previous = request.previous;
434
428
  if (previous?.[i] && !forceRefresh && previous[i].name === route$1.name) {
435
429
  const url = (str) => str ? str.replace(/\/\/+/g, "/") : "/";
@@ -444,6 +438,7 @@ var PageDescriptorProvider = class {
444
438
  if (prev === curr) {
445
439
  it.props = previous[i].props;
446
440
  it.error = previous[i].error;
441
+ it.cache = true;
447
442
  context = {
448
443
  ...context,
449
444
  ...it.props
@@ -452,6 +447,7 @@ var PageDescriptorProvider = class {
452
447
  }
453
448
  forceRefresh = true;
454
449
  }
450
+ if (!route$1.resolve) continue;
455
451
  try {
456
452
  const props = await route$1.resolve?.({
457
453
  ...request,
@@ -498,7 +494,7 @@ var PageDescriptorProvider = class {
498
494
  element: this.renderView(i + 1, path, element$1, it.route),
499
495
  index: i + 1,
500
496
  path,
501
- route
497
+ route: it.route
502
498
  });
503
499
  break;
504
500
  }
@@ -514,7 +510,8 @@ var PageDescriptorProvider = class {
514
510
  element: this.renderView(i + 1, path, element, it.route),
515
511
  index: i + 1,
516
512
  path,
517
- route
513
+ route: it.route,
514
+ cache: it.cache
518
515
  });
519
516
  }
520
517
  return {
@@ -570,14 +567,20 @@ var PageDescriptorProvider = class {
570
567
  } }, element);
571
568
  }
572
569
  configure = (0, __alepha_core.$hook)({
573
- name: "configure",
570
+ on: "configure",
574
571
  handler: () => {
575
572
  let hasNotFoundHandler = false;
576
573
  const pages = this.alepha.getDescriptorValues($page);
574
+ const hasParent = (it) => {
575
+ for (const page of pages) {
576
+ const children = page.value[__alepha_core.OPTIONS].children ? Array.isArray(page.value[__alepha_core.OPTIONS].children) ? page.value[__alepha_core.OPTIONS].children : page.value[__alepha_core.OPTIONS].children() : [];
577
+ if (children.includes(it)) return true;
578
+ }
579
+ };
577
580
  for (const { value, key } of pages) value[__alepha_core.OPTIONS].name ??= key;
578
581
  for (const { value } of pages) {
579
- if (value[__alepha_core.OPTIONS].parent) continue;
580
582
  if (value[__alepha_core.OPTIONS].path === "/*") hasNotFoundHandler = true;
583
+ if (hasParent(value)) continue;
581
584
  this.add(this.map(pages, value));
582
585
  }
583
586
  if (!hasNotFoundHandler && pages.length > 0) this.add({
@@ -592,7 +595,7 @@ var PageDescriptorProvider = class {
592
595
  }
593
596
  });
594
597
  map(pages, target) {
595
- const children = target[__alepha_core.OPTIONS].children ?? [];
598
+ const children = target[__alepha_core.OPTIONS].children ? Array.isArray(target[__alepha_core.OPTIONS].children) ? target[__alepha_core.OPTIONS].children : target[__alepha_core.OPTIONS].children() : [];
596
599
  return {
597
600
  ...target[__alepha_core.OPTIONS],
598
601
  parent: void 0,
@@ -649,11 +652,11 @@ var ReactServerProvider = class {
649
652
  env = (0, __alepha_core.$inject)(envSchema);
650
653
  ROOT_DIV_REGEX = new RegExp(`<div([^>]*)\\s+id=["']${this.env.REACT_ROOT_ID}["']([^>]*)>(.*?)<\\/div>`, "is");
651
654
  onConfigure = (0, __alepha_core.$hook)({
652
- name: "configure",
655
+ on: "configure",
653
656
  handler: async () => {
654
657
  const pages = this.alepha.getDescriptorValues($page);
655
658
  const ssrEnabled = pages.length > 0 && this.env.REACT_SSR_ENABLED !== false;
656
- this.alepha.state("ReactServerProvider.ssr", ssrEnabled);
659
+ this.alepha.state("react.server.ssr", ssrEnabled);
657
660
  for (const { key, instance, value } of pages) {
658
661
  const name = value[__alepha_core.OPTIONS].name ?? key;
659
662
  instance[key].render = this.createRenderFunction(name);
@@ -693,7 +696,7 @@ var ReactServerProvider = class {
693
696
  }
694
697
  });
695
698
  get template() {
696
- return this.alepha.state("ReactServerProvider.template") ?? "<!DOCTYPE html><html lang='en'><head></head><body></body></html>";
699
+ return this.alepha.state("react.server.template") ?? "<!DOCTYPE html><html lang='en'><head></head><body></body></html>";
697
700
  }
698
701
  async registerPages(templateLoader) {
699
702
  for (const page of this.pageDescriptorProvider.getPages()) {
@@ -868,7 +871,7 @@ var BrowserRouterProvider = class extends __alepha_router.RouterProvider {
868
871
  this.pageDescriptorProvider.add(entry);
869
872
  }
870
873
  configure = (0, __alepha_core.$hook)({
871
- name: "configure",
874
+ on: "configure",
872
875
  handler: async () => {
873
876
  for (const page of this.pageDescriptorProvider.getPages()) if (page.component || page.lazy) this.push({
874
877
  path: page.match,
@@ -974,8 +977,17 @@ var ReactBrowserProvider = class {
974
977
  get history() {
975
978
  return window.history;
976
979
  }
980
+ get location() {
981
+ return window.location;
982
+ }
977
983
  get url() {
978
- return window.location.pathname + window.location.search;
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);
979
991
  }
980
992
  async invalidate(props) {
981
993
  const previous = [];
@@ -1001,14 +1013,14 @@ var ReactBrowserProvider = class {
1001
1013
  async go(url, options = {}) {
1002
1014
  const result = await this.render({ url });
1003
1015
  if (result.context.url.pathname !== url) {
1004
- this.history.replaceState({}, "", result.context.url.pathname);
1016
+ this.pushState(result.context.url.pathname);
1005
1017
  return;
1006
1018
  }
1007
1019
  if (options.replace) {
1008
- this.history.replaceState({}, "", url);
1020
+ this.pushState(url);
1009
1021
  return;
1010
1022
  }
1011
- this.history.pushState({}, "", url);
1023
+ this.pushState(url);
1012
1024
  }
1013
1025
  async render(options = {}) {
1014
1026
  const previous = options.previous ?? this.state.layers;
@@ -1033,7 +1045,7 @@ var ReactBrowserProvider = class {
1033
1045
  }
1034
1046
  }
1035
1047
  ready = (0, __alepha_core.$hook)({
1036
- name: "ready",
1048
+ on: "ready",
1037
1049
  handler: async () => {
1038
1050
  const hydration = this.getHydrationState();
1039
1051
  const previous = hydration?.layers ?? [];
@@ -1045,6 +1057,7 @@ var ReactBrowserProvider = class {
1045
1057
  hydration
1046
1058
  });
1047
1059
  window.addEventListener("popstate", () => {
1060
+ if (this.state.pathname === location.pathname) return;
1048
1061
  this.render();
1049
1062
  });
1050
1063
  }
@@ -1054,12 +1067,21 @@ var ReactBrowserProvider = class {
1054
1067
  //#endregion
1055
1068
  //#region src/hooks/RouterHookApi.ts
1056
1069
  var RouterHookApi = class {
1057
- constructor(pages, state, layer, browser) {
1070
+ constructor(pages, context, state, layer, browser) {
1058
1071
  this.pages = pages;
1072
+ this.context = context;
1059
1073
  this.state = state;
1060
1074
  this.layer = layer;
1061
1075
  this.browser = browser;
1062
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
+ }
1063
1085
  get current() {
1064
1086
  return this.state;
1065
1087
  }
@@ -1137,7 +1159,7 @@ const useRouter = () => {
1137
1159
  const pages = (0, react.useMemo)(() => {
1138
1160
  return ctx.alepha.get(PageDescriptorProvider).getPages();
1139
1161
  }, []);
1140
- 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]);
1141
1163
  };
1142
1164
 
1143
1165
  //#endregion
@@ -1257,17 +1279,139 @@ const useRouterState = () => {
1257
1279
  //#endregion
1258
1280
  //#region src/index.ts
1259
1281
  /**
1260
- * Alepha React Module
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
1261
1296
  *
1262
- * Alepha React Module contains a router for client-side navigation and server-side rendering.
1263
- * Routes can be defined using the `$page` descriptor.
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
+ * ```
1336
+ *
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
+ * ```
1264
1408
  *
1265
1409
  * @see {@link $page}
1266
1410
  * @module alepha.react
1267
1411
  */
1268
1412
  var AlephaReact = class {
1269
1413
  name = "alepha.react";
1270
- $services = (alepha) => alepha.with(__alepha_server.AlephaServer).with(__alepha_server_cache.AlephaServerCache).with(ReactServerProvider).with(PageDescriptorProvider);
1414
+ $services = (alepha) => alepha.with(__alepha_server.AlephaServer).with(__alepha_server_cache.AlephaServerCache).with(__alepha_server_links.AlephaServerLinks).with(ReactServerProvider).with(PageDescriptorProvider);
1271
1415
  };
1272
1416
  (0, __alepha_core.__bind)($page, AlephaReact);
1273
1417
 
@@ -1278,6 +1422,7 @@ exports.ClientOnly = ClientOnly_default;
1278
1422
  exports.ErrorBoundary = ErrorBoundary_default;
1279
1423
  exports.Link = Link_default;
1280
1424
  exports.NestedView = NestedView_default;
1425
+ exports.NotFound = NotFoundPage;
1281
1426
  exports.PageDescriptorProvider = PageDescriptorProvider;
1282
1427
  exports.ReactBrowserProvider = ReactBrowserProvider;
1283
1428
  exports.ReactServerProvider = ReactServerProvider;