@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.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) : 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) : 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
- name: "configure",
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
- name: "configure",
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("ReactServerProvider.ssr", ssrEnabled);
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("ReactServerProvider.template") ?? "<!DOCTYPE html><html lang='en'><head></head><body></body></html>";
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
- name: "configure",
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
- return window.location.pathname + window.location.search;
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.history.replaceState({}, "", result.context.url.pathname);
998
+ this.pushState(result.context.url.pathname);
982
999
  return;
983
1000
  }
984
1001
  if (options.replace) {
985
- this.history.replaceState({}, "", url);
1002
+ this.pushState(url);
986
1003
  return;
987
1004
  }
988
- this.history.pushState({}, "", url);
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
- name: "ready",
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
- * Alepha React Module
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
- * Alepha React Module contains a router for client-side navigation and server-side rendering.
1240
- * Routes can be defined using the `$page` descriptor.
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