@stone-js/use-react 0.2.0 → 0.3.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/LICENSE CHANGED
@@ -1,6 +1,6 @@
1
1
  MIT License
2
2
 
3
- Copyright (c) 2025 Stone.js
3
+ Copyright © 2026 Stone Foundation
4
4
 
5
5
  Permission is hereby granted, free of charge, to any person obtaining a copy
6
6
  of this software and associated documentation files (the "Software"), to deal
package/README.md CHANGED
@@ -3,7 +3,7 @@
3
3
  [![npm](https://img.shields.io/npm/l/@stone-js/use-react)](https://opensource.org/licenses/MIT)
4
4
  [![npm](https://img.shields.io/npm/v/@stone-js/use-react)](https://www.npmjs.com/package/@stone-js/use-react)
5
5
  [![npm](https://img.shields.io/npm/dm/@stone-js/use-react)](https://www.npmjs.com/package/@stone-js/use-react)
6
- ![Maintenance](https://img.shields.io/maintenance/yes/2025)
6
+ ![Maintenance](https://img.shields.io/maintenance/yes/2026)
7
7
  [![Build Status](https://github.com/stone-foundation/stone-js-use-react/actions/workflows/main.yml/badge.svg)](https://github.com/stone-foundation/stone-js-use-react/actions/workflows/main.yml)
8
8
  [![Publish Package to npmjs](https://github.com/stone-foundation/stone-js-use-react/actions/workflows/release.yml/badge.svg)](https://github.com/stone-foundation/stone-js-use-react/actions/workflows/release.yml)
9
9
  [![Quality Gate Status](https://sonarcloud.io/api/project_badges/measure?project=stone-foundation_stone-js-use-react&metric=alert_status)](https://sonarcloud.io/summary/new_code?id=stone-foundation_stone-js-use-react)
package/dist/browser.js CHANGED
@@ -1,17 +1,11 @@
1
- import { createContext, StrictMode, useContext, useState, useEffect } from 'react';
2
- import { isNotEmpty, isEmpty, InitializationError, isMetaClassModule, isMetaFactoryModule, isFunctionModule, isObjectLikeModule, isFunction, mergeBlueprints, stoneBlueprint, classDecoratorLegacyWrapper, setMetadata, methodDecoratorLegacyWrapper, addMetadata, LIFECYCLE_HOOK_KEY, hasMetadata, getMetadata, isMatchedAdapter, Logger, addBlueprint } from '@stone-js/core';
1
+ import { isNotEmpty, isEmpty, InitializationError, isMetaClassModule, isMetaFactoryModule, isObjectLikeModule, isFunction, isFunctionModule, mergeBlueprints, stoneBlueprint, Logger, classDecoratorLegacyWrapper, setMetadata, methodDecoratorLegacyWrapper, addMetadata, LIFECYCLE_HOOK_KEY, hasMetadata, getMetadata, isMatchedAdapter, addBlueprint } from '@stone-js/core';
3
2
  import { jsx, jsxs, Fragment } from 'react/jsx-runtime';
4
3
  import { renderToString } from 'react-dom/server';
5
- import { createRoot, hydrateRoot } from 'react-dom/client';
4
+ import { createContext, StrictMode, useContext, useMemo, useState, useEffect } from 'react';
5
+ import { hydrateRoot, createRoot } from 'react-dom/client';
6
6
  import { Config } from '@stone-js/config';
7
7
  import { GET, NAVIGATION_EVENT, Router, RouteEvent } from '@stone-js/router';
8
- import { OutgoingBrowserResponse, RedirectBrowserResponse } from '@stone-js/browser-core';
9
-
10
- /**
11
- * Stone context.
12
- * Usefull to pass data to the components.
13
- */
14
- const StoneContext = createContext({});
8
+ import { RedirectBrowserResponse, OutgoingBrowserResponse } from '@stone-js/browser-core';
15
9
 
16
10
  /**
17
11
  * Stone DOM Attribute.
@@ -246,6 +240,12 @@ const STONE_SNAPSHOT = '__STONE_SNAPSHOT__';
246
240
  */
247
241
  const STONE_PAGE_EVENT_OUTLET = 'stone:inject:react-page:outlet';
248
242
 
243
+ /**
244
+ * Stone context.
245
+ * Usefull to pass data to the components.
246
+ */
247
+ const StoneContext = createContext({});
248
+
249
249
  /**
250
250
  * Provides a scoped `StoneContext` for its children within a strict mode.
251
251
  *
@@ -912,33 +912,6 @@ class UseReactServiceProvider {
912
912
  */
913
913
  const MetaUseReactServiceProvider = { module: UseReactServiceProvider, isClass: true };
914
914
 
915
- /**
916
- * Utility function to define an adapter error page.
917
- *
918
- * @param module - The adapter error page module.
919
- * @param options - Optional adapter error page options.
920
- * @returns The UseReactBlueprint.
921
- */
922
- function defineAdapterErrorPage(module, options) {
923
- const error = options?.error ?? 'default';
924
- const adapterErrorPages = Object.fromEntries([error].flat().map((err) => [
925
- err,
926
- {
927
- ...options,
928
- module,
929
- error: err,
930
- isFactory: options?.isClass !== true
931
- }
932
- ]));
933
- return {
934
- stone: {
935
- useReact: {
936
- adapterErrorPages
937
- }
938
- }
939
- };
940
- }
941
-
942
915
  /**
943
916
  * Default blueprint for a React-based Stone.js application.
944
917
  *
@@ -993,6 +966,33 @@ function defineStoneReactApp(moduleOrOptions = {}, optionsOrBlueprints, maybeBlu
993
966
  return mergeBlueprints(stoneBlueprint, internalUseReactBlueprint, ...blueprints, { stone: stonePart });
994
967
  }
995
968
 
969
+ /**
970
+ * Utility function to define an adapter error page.
971
+ *
972
+ * @param module - The adapter error page module.
973
+ * @param options - Optional adapter error page options.
974
+ * @returns The UseReactBlueprint.
975
+ */
976
+ function defineAdapterErrorPage(module, options) {
977
+ const error = options?.error ?? 'default';
978
+ const adapterErrorPages = Object.fromEntries([error].flat().map((err) => [
979
+ err,
980
+ {
981
+ ...options,
982
+ module,
983
+ error: err,
984
+ isFactory: options?.isClass !== true
985
+ }
986
+ ]));
987
+ return {
988
+ stone: {
989
+ useReact: {
990
+ adapterErrorPages
991
+ }
992
+ }
993
+ };
994
+ }
995
+
996
996
  /**
997
997
  * Utility function to define a page.
998
998
  *
@@ -1073,6 +1073,73 @@ function defineErrorPage(module, options) {
1073
1073
  };
1074
1074
  }
1075
1075
 
1076
+ /**
1077
+ * Class representing an UseReactBrowserErrorHandler.
1078
+ *
1079
+ * Adapter level error handler for React applications.
1080
+ */
1081
+ class UseReactBrowserErrorHandler {
1082
+ logger;
1083
+ blueprint;
1084
+ /**
1085
+ * Create an UseReactBrowserErrorHandler.
1086
+ *
1087
+ * @param options - UseReactBrowserErrorHandler options.
1088
+ */
1089
+ constructor({ blueprint }) {
1090
+ this.blueprint = blueprint;
1091
+ this.logger = Logger.getInstance();
1092
+ }
1093
+ /**
1094
+ * Handle an error.
1095
+ *
1096
+ * @param error - The error to handle.
1097
+ * @param context - The context of the adapter.
1098
+ * @returns The raw response.
1099
+ */
1100
+ async handle(error, context) {
1101
+ this.logger.error(error.message, { error });
1102
+ return context
1103
+ .rawResponseBuilder
1104
+ .add('render', async () => await this.renderError(error, context));
1105
+ }
1106
+ /**
1107
+ * Get the error body.
1108
+ *
1109
+ * @param error - The error to handle.
1110
+ * @returns The error body.
1111
+ */
1112
+ async renderError(error, context) {
1113
+ const app = await buildAdapterErrorComponent(this.blueprint, context, error.statusCode ?? 500, error);
1114
+ // Render the component
1115
+ renderReactApp(app, this.blueprint);
1116
+ }
1117
+ }
1118
+
1119
+ /**
1120
+ * Create an UseReact response.
1121
+ *
1122
+ * @param options - The options for creating the response.
1123
+ * @returns The React response.
1124
+ */
1125
+ const reactResponse = (options) => {
1126
+ if (isNotEmpty(options) &&
1127
+ (isNotEmpty(options.url) ||
1128
+ (isNotEmpty(options.content) && isNotEmpty(options.content.redirect)))) {
1129
+ return reactRedirectResponse(options);
1130
+ }
1131
+ return OutgoingBrowserResponse.create(options);
1132
+ };
1133
+ /**
1134
+ * Create an UseReact redirect response.
1135
+ *
1136
+ * @param options - The options for creating the response.
1137
+ * @returns The React redirect response.
1138
+ */
1139
+ const reactRedirectResponse = (options) => {
1140
+ return RedirectBrowserResponse.create({ statusCode: 302, ...options });
1141
+ };
1142
+
1076
1143
  /**
1077
1144
  * Constants are defined here to prevent Circular dependency between modules
1078
1145
  * This pattern must be applied to all Stone libraries or third party libraries.
@@ -1285,65 +1352,6 @@ const Snapshot = (name) => {
1285
1352
  });
1286
1353
  };
1287
1354
 
1288
- /**
1289
- * Create an UseReact response.
1290
- *
1291
- * @param options - The options for creating the response.
1292
- * @returns The React response.
1293
- */
1294
- const reactResponse = (options) => {
1295
- if (isNotEmpty(options) &&
1296
- (isNotEmpty(options.url) ||
1297
- (isNotEmpty(options.content) && isNotEmpty(options.content.redirect)))) {
1298
- return reactRedirectResponse(options);
1299
- }
1300
- return OutgoingBrowserResponse.create(options);
1301
- };
1302
- /**
1303
- * Create an UseReact redirect response.
1304
- *
1305
- * @param options - The options for creating the response.
1306
- * @returns The React redirect response.
1307
- */
1308
- const reactRedirectResponse = (options) => {
1309
- return RedirectBrowserResponse.create({ statusCode: 302, ...options });
1310
- };
1311
-
1312
- /**
1313
- * Sets the error handler for the React adapter and registers error pages.
1314
- *
1315
- * @param errorHandler - The error handler to set for the React adapter.
1316
- * @param context - The blueprint context containing modules and blueprint.
1317
- * @returns The updated blueprint context with the error handler and error pages set.
1318
- */
1319
- function setUseReactAdapterErrorHandler(errorHandler, context) {
1320
- context
1321
- .blueprint
1322
- .set('stone.adapter.errorHandlers.default', { module: errorHandler, isClass: true });
1323
- context
1324
- .modules
1325
- .filter(module => hasMetadata(module, REACT_ADAPTER_ERROR_PAGE_KEY))
1326
- .forEach(module => {
1327
- const { error, layout, adapterAlias, platform } = getMetadata(module, REACT_ADAPTER_ERROR_PAGE_KEY, { error: 'default' });
1328
- if (isMatchedAdapter(context.blueprint, platform, adapterAlias)) {
1329
- Array(error).flat().forEach(name => {
1330
- context
1331
- .blueprint
1332
- .set(`stone.useReact.adapterErrorPages.${name}`, { isClass: true, layout, module });
1333
- });
1334
- }
1335
- });
1336
- // Process both eager and lazy loaded error pages
1337
- Object
1338
- .keys(context.blueprint.get('stone.useReact.adapterErrorPages', {}))
1339
- .forEach((name) => {
1340
- context
1341
- .blueprint
1342
- .set(`stone.adapter.errorHandlers.${name}`, { module: errorHandler, isClass: true });
1343
- });
1344
- return context;
1345
- }
1346
-
1347
1355
  /**
1348
1356
  * Blueprint middleware to dynamically set lifecycle hooks for react.
1349
1357
  *
@@ -1476,46 +1484,38 @@ async function SetUseReactEventHandlerMiddleware(context, next) {
1476
1484
  }
1477
1485
 
1478
1486
  /**
1479
- * Class representing an UseReactBrowserErrorHandler.
1487
+ * Sets the error handler for the React adapter and registers error pages.
1480
1488
  *
1481
- * Adapter level error handler for React applications.
1489
+ * @param errorHandler - The error handler to set for the React adapter.
1490
+ * @param context - The blueprint context containing modules and blueprint.
1491
+ * @returns The updated blueprint context with the error handler and error pages set.
1482
1492
  */
1483
- class UseReactBrowserErrorHandler {
1484
- logger;
1485
- blueprint;
1486
- /**
1487
- * Create an UseReactBrowserErrorHandler.
1488
- *
1489
- * @param options - UseReactBrowserErrorHandler options.
1490
- */
1491
- constructor({ blueprint }) {
1492
- this.blueprint = blueprint;
1493
- this.logger = Logger.getInstance();
1494
- }
1495
- /**
1496
- * Handle an error.
1497
- *
1498
- * @param error - The error to handle.
1499
- * @param context - The context of the adapter.
1500
- * @returns The raw response.
1501
- */
1502
- async handle(error, context) {
1503
- this.logger.error(error.message, { error });
1504
- return context
1505
- .rawResponseBuilder
1506
- .add('render', async () => await this.renderError(error, context));
1507
- }
1508
- /**
1509
- * Get the error body.
1510
- *
1511
- * @param error - The error to handle.
1512
- * @returns The error body.
1513
- */
1514
- async renderError(error, context) {
1515
- const app = await buildAdapterErrorComponent(this.blueprint, context, error.statusCode ?? 500, error);
1516
- // Render the component
1517
- renderReactApp(app, this.blueprint);
1518
- }
1493
+ function setUseReactAdapterErrorHandler(errorHandler, context) {
1494
+ context
1495
+ .blueprint
1496
+ .set('stone.adapter.errorHandlers.default', { module: errorHandler, isClass: true });
1497
+ context
1498
+ .modules
1499
+ .filter(module => hasMetadata(module, REACT_ADAPTER_ERROR_PAGE_KEY))
1500
+ .forEach(module => {
1501
+ const { error, layout, adapterAlias, platform } = getMetadata(module, REACT_ADAPTER_ERROR_PAGE_KEY, { error: 'default' });
1502
+ if (isMatchedAdapter(context.blueprint, platform, adapterAlias)) {
1503
+ Array(error).flat().forEach(name => {
1504
+ context
1505
+ .blueprint
1506
+ .set(`stone.useReact.adapterErrorPages.${name}`, { isClass: true, layout, module });
1507
+ });
1508
+ }
1509
+ });
1510
+ // Process both eager and lazy loaded error pages
1511
+ Object
1512
+ .keys(context.blueprint.get('stone.useReact.adapterErrorPages', {}))
1513
+ .forEach((name) => {
1514
+ context
1515
+ .blueprint
1516
+ .set(`stone.adapter.errorHandlers.${name}`, { module: errorHandler, isClass: true });
1517
+ });
1518
+ return context;
1519
1519
  }
1520
1520
 
1521
1521
  /**
@@ -1715,16 +1715,26 @@ const StoneClient = ({ children }) => {
1715
1715
  /**
1716
1716
  * Internal link component using Stone.js router.
1717
1717
  */
1718
- const InternalLink = ({ to, href, noRel, children, className, defaultNav, selectedClass = 'selected', ariaCurrentValue = 'page', rel = 'noopener noreferrer' }) => {
1718
+ const StoneLink = ({ to, href, noRel, external, children, ariaCurrentValue = 'page', selectedClass = 'selected', ...rest }) => {
1719
+ const isExternal = external === true;
1720
+ const shouldHandleNav = !isExternal && isNotEmpty(to);
1719
1721
  const router = useContext(StoneContext).container.resolve(Router);
1720
- const path = isObjectLikeModule(to) ? router.generate(to) : to ?? href;
1722
+ const path = useMemo(() => {
1723
+ return isObjectLikeModule(to) ? router.generate(to) : to ?? href ?? '#';
1724
+ }, [to, href, router]);
1721
1725
  const [currentRoute, setCurrentRoute] = useState(router.getCurrentRoute());
1722
1726
  const selectedClassName = currentRoute?.path === path ? selectedClass : undefined;
1723
- const elemClassName = [className, selectedClassName].filter(Boolean).join(' ').trim();
1727
+ const elemClassName = [rest.className, selectedClassName].filter(Boolean).join(' ').trim();
1724
1728
  const handleClick = (event) => {
1729
+ rest.onClick?.(event);
1730
+ if (event.defaultPrevented || isExternal)
1731
+ return;
1725
1732
  event.preventDefault();
1726
- router.navigate(to ?? '');
1733
+ isNotEmpty(to) && router.navigate(to);
1727
1734
  };
1735
+ if (isEmpty(to) && isEmpty(href)) {
1736
+ Logger.warn('StoneLink: missing "to" or "href"');
1737
+ }
1728
1738
  useEffect(() => {
1729
1739
  const routerEventHandler = (event) => {
1730
1740
  setCurrentRoute(event.get('route'));
@@ -1734,19 +1744,13 @@ const InternalLink = ({ to, href, noRel, children, className, defaultNav, select
1734
1744
  router.off(RouteEvent.ROUTED, routerEventHandler);
1735
1745
  };
1736
1746
  }, [router]);
1737
- return defaultNav === true
1738
- ? (jsx("a", { href: path, className: elemClassName, "aria-current": ariaCurrentValue, rel: noRel !== undefined ? undefined : rel, children: children }))
1739
- : (jsx("button", { onClick: handleClick, className: elemClassName, "aria-current": ariaCurrentValue, rel: noRel !== undefined ? undefined : rel, children: children }));
1740
- };
1741
- /**
1742
- * External link component rendering a regular <a> tag.
1743
- */
1744
- const ExternalLink = ({ to, href, noRel, target, children, className, ariaCurrentValue = 'page', rel = 'noopener noreferrer' }) => (jsx("a", { target: target, className: className, "aria-current": ariaCurrentValue, rel: noRel !== undefined ? undefined : rel, href: typeof to === 'string' ? to : href, children: children }));
1745
- /**
1746
- * Main StoneLink component delegating to internal or external versions.
1747
- */
1748
- const StoneLink = (props) => {
1749
- return props.external === true ? jsx(ExternalLink, { ...props }) : jsx(InternalLink, { ...props });
1747
+ return (
1748
+ // eslint-disable-next-line react/jsx-no-target-blank
1749
+ jsx("a", { ...rest, href: path, className: elemClassName, target: isExternal ? '_blank' : rest.target, "aria-current": isNotEmpty(selectedClassName) ? ariaCurrentValue : undefined, rel: noRel === true
1750
+ ? undefined
1751
+ : isExternal
1752
+ ? 'noopener noreferrer'
1753
+ : rest.rel, onClick: shouldHandleNav ? handleClick : rest.onClick, children: children }));
1750
1754
  };
1751
1755
 
1752
1756
  /**
@@ -1761,7 +1765,7 @@ const StoneLink = (props) => {
1761
1765
  * @param options - The options to create the Stone Outlet.
1762
1766
  * @returns The Stone Outlet component.
1763
1767
  */
1764
- const StoneOutlet = ({ children }) => {
1768
+ const StoneOutlet = ({ children, ...rest }) => {
1765
1769
  const [currentView, setCurrentView] = useState(children);
1766
1770
  useEffect(() => {
1767
1771
  const eventName = STONE_PAGE_EVENT_OUTLET;
@@ -1773,7 +1777,7 @@ const StoneOutlet = ({ children }) => {
1773
1777
  window.addEventListener(eventName, handleEvent);
1774
1778
  return () => window.removeEventListener(eventName, handleEvent);
1775
1779
  }, []);
1776
- return jsx("div", { "data-stone-outlet": 'true', children: currentView });
1780
+ return jsx("div", { ...rest, "data-stone-outlet": 'true', children: currentView });
1777
1781
  };
1778
1782
 
1779
1783
  /**